Files
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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>
2026-09-14 11:05:39 +02:00

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;
}