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>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user