Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
3.2 KiB
TypeScript
83 lines
3.2 KiB
TypeScript
// Import der Umsetzungshinweise (Story B5-1) aus dem Fachcontent C6 in das globale
|
|
// Modell ImplementationHint. Ein Block je Teilanforderung:
|
|
//
|
|
// ### <id> — [<STUFE>]
|
|
// **Anforderung:** …
|
|
// - **Organisatorisch:** …
|
|
// - **Technisch:** …
|
|
// - **Typische Nachweise:** …
|
|
// - **Vorlage:** L00
|
|
// - **Ressourcen:** …
|
|
// - **AL-Filter:** AL2 + AL3
|
|
//
|
|
// Idempotent (Upsert über reqId). Kein tenantId (globaler Katalog).
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import type { PrismaClient } from "@prisma/client";
|
|
|
|
const C6_PATH = join(__dirname, "..", "docs", "wizard-uebergabe", "02_Fachcontent_C1-C9", "C6_Umsetzungshinweise.md");
|
|
|
|
const field = (block: string, label: string): string => {
|
|
// Feldzeile "- **Label:** Wert" oder "**Label:** Wert" bis Zeilenende.
|
|
const re = new RegExp(`\\*\\*${label}:\\*\\*\\s*(.+)`);
|
|
const m = block.match(re);
|
|
return m ? m[1].trim() : "";
|
|
};
|
|
|
|
const isProcurement = (resources: string): boolean => {
|
|
const r = resources.toLowerCase();
|
|
if (/kein\s+toolbudget|kein\s+budget|ohne\s+budget|keine\s+beschaffung/.test(r)) return false;
|
|
return /beschaffung|toolbudget|budget|lizenz|invest|anschaffung|€|eur\b/.test(r);
|
|
};
|
|
|
|
export interface ParsedHint {
|
|
reqId: string; control: string; stufe: string; requirement: string;
|
|
organisational: string; technical: string; evidence: string;
|
|
template: string | null; resources: string; alFilter: string; procurement: boolean;
|
|
}
|
|
|
|
/** Parst den C6-Text zu Hinweis-Objekten (ohne DB-Zugriff — testbar). */
|
|
export function parseC6Hints(text: string): ParsedHint[] {
|
|
const hints: ParsedHint[] = [];
|
|
// Blöcke ab "### <id> — [STUFE]"; nächster "### "/"## "/"# " beendet den Block.
|
|
const re = /^###\s+(\S+)\s+—\s+\[([^\]]+)\]\s*\n([\s\S]*?)(?=^#{1,3}\s|$(?![\s\S]))/gm;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = re.exec(text)) !== null) {
|
|
const reqId = m[1].trim();
|
|
if (!/^\d+\.\d+\.\d+-/.test(reqId)) continue; // nur Anforderungs-IDs
|
|
const stufe = m[2].trim();
|
|
const block = m[3];
|
|
const resources = field(block, "Ressourcen");
|
|
const template = field(block, "Vorlage") || null;
|
|
hints.push({
|
|
reqId,
|
|
control: reqId.split("-")[0],
|
|
stufe,
|
|
requirement: field(block, "Anforderung"),
|
|
organisational: field(block, "Organisatorisch"),
|
|
technical: field(block, "Technisch"),
|
|
evidence: field(block, "Typische Nachweise"),
|
|
template,
|
|
resources,
|
|
alFilter: field(block, "AL-Filter"),
|
|
procurement: isProcurement(resources),
|
|
});
|
|
}
|
|
return hints;
|
|
}
|
|
|
|
/** Liest C6 und schreibt die Hinweise (Upsert je reqId). Liefert die Anzahl. */
|
|
export async function importImplementationHints(prisma: PrismaClient): Promise<number> {
|
|
const text = readFileSync(C6_PATH, "utf-8");
|
|
const hints = parseC6Hints(text);
|
|
for (const h of hints) {
|
|
await prisma.implementationHint.upsert({
|
|
where: { reqId: h.reqId },
|
|
update: { control: h.control, stufe: h.stufe, requirement: h.requirement, organisational: h.organisational, technical: h.technical, evidence: h.evidence, template: h.template, resources: h.resources, alFilter: h.alFilter, procurement: h.procurement },
|
|
create: h,
|
|
});
|
|
}
|
|
return hints.length;
|
|
}
|