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,103 @@
|
||||
// B1 Schritt 1 — Snapshot-Regressionsnetz der heutigen TISAX-Bewertung.
|
||||
//
|
||||
// Hält die Ausgabe von buildControlRows (Reifegradvorschlag, Zielwert, Belegstatus und
|
||||
// offene Punkte je Control) für den Demo-Mandanten als JSON im Repo fest. JEDE spätere
|
||||
// Abweichung (ControlSpec-Umbau, Strategie-Extraktion) ist eine Regression, kein
|
||||
// „ist besser geworden" (Feindesign §7, §7a: Schritt 1 ist die wichtigste Maßnahme).
|
||||
//
|
||||
// Baseline erzeugen/aktualisieren: UPDATE_SNAPSHOT=1 npx tsx scripts/test-framework-assessment.ts
|
||||
// Prüfen (Merge-Gate): npx tsx scripts/test-framework-assessment.ts
|
||||
|
||||
import "dotenv/config";
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { prisma, dbForTenant } from "../src/server/db";
|
||||
import { buildControlRows } from "../src/server/soa-context";
|
||||
import { buildAssessment } from "../src/server/assessment";
|
||||
|
||||
const SNAPSHOT = join(dirname(fileURLToPath(import.meta.url)), "snapshots", "framework-assessment-tisax.json");
|
||||
|
||||
/** Kanonisches JSON (rekursiv sortierte Objekt-Schlüssel) für stabilen Vergleich. */
|
||||
function canonical(v: unknown): unknown {
|
||||
if (Array.isArray(v)) return v.map(canonical);
|
||||
if (v && typeof v === "object") {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const k of Object.keys(v as Record<string, unknown>).sort()) out[k] = canonical((v as Record<string, unknown>)[k]);
|
||||
return out;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
const stable = (v: unknown) => JSON.stringify(canonical(v), null, 2);
|
||||
|
||||
async function build() {
|
||||
const tenant = await prisma.tenant.findFirst({ where: { slug: "demo" }, select: { id: true } });
|
||||
if (!tenant) throw new Error("Demo-Mandant fehlt (prisma/seed.ts ausführen).");
|
||||
const { rows, level } = await buildControlRows(dbForTenant(tenant.id), tenant.id);
|
||||
// Nur die bewertungsrelevanten, deterministischen Felder je Control festhalten.
|
||||
const controls = rows.map((r) => ({
|
||||
control: r.control,
|
||||
target: r.target,
|
||||
confirmed: r.confirmed,
|
||||
suggestion: r.suggestion,
|
||||
evidence: r.evidence,
|
||||
gaps: r.gaps,
|
||||
}));
|
||||
return { level, count: controls.length, controls };
|
||||
}
|
||||
|
||||
/** §7.2: buildAssessment("TISAX") auf dieselbe Snapshot-Form abbilden. */
|
||||
async function buildViaStrategy() {
|
||||
const tenant = await prisma.tenant.findFirst({ where: { slug: "demo" }, select: { id: true } });
|
||||
const { rows } = await buildAssessment(dbForTenant(tenant!.id), tenant!.id, "TISAX");
|
||||
const controls = rows.map((r) => {
|
||||
if (r.verdict.kind !== "maturity") throw new Error("TISAX-Verdict ist nicht 'maturity'.");
|
||||
return { control: r.control, target: r.verdict.target, confirmed: r.verdict.confirmed, suggestion: r.verdict.suggestion, evidence: r.evidence, gaps: r.gaps };
|
||||
});
|
||||
return { level: "AL2", count: controls.length, controls };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const current = await build();
|
||||
|
||||
// Strategie-Äquivalenz (Feindesign §7.2): buildAssessment("TISAX") == buildControlRows.
|
||||
const viaStrategy = await buildViaStrategy();
|
||||
if (stable(viaStrategy.controls) !== stable(current.controls)) {
|
||||
console.error("✗ buildAssessment(\"TISAX\") weicht von buildControlRows ab — die Strategie-Schicht verändert TISAX.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (process.env.UPDATE_SNAPSHOT === "1" || !existsSync(SNAPSHOT)) {
|
||||
mkdirSync(dirname(SNAPSHOT), { recursive: true });
|
||||
writeFileSync(SNAPSHOT, stable(current) + "\n", "utf-8");
|
||||
console.log(`✓ Snapshot ${existsSync(SNAPSHOT) ? "aktualisiert" : "erstellt"}: ${current.count} Controls (Level ${current.level}).`);
|
||||
console.log(" → Baseline committen; künftige Läufe prüfen gegen diesen Stand.");
|
||||
return;
|
||||
}
|
||||
|
||||
const expected = readFileSync(SNAPSHOT, "utf-8").trim();
|
||||
const actual = stable(current).trim();
|
||||
if (expected === actual) {
|
||||
console.log(`✓ TISAX-Snapshot unverändert: ${current.count} Controls, Level ${current.level} — 0 Abweichungen.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Erste abweichende Control-Zeile für die Diagnose zeigen.
|
||||
const exp = JSON.parse(expected) as { controls: { control: string }[] };
|
||||
const expByControl = new Map(exp.controls.map((c) => [c.control, stable(c)]));
|
||||
const diffs: string[] = [];
|
||||
for (const c of current.controls) {
|
||||
const e = expByControl.get(c.control);
|
||||
const a = stable(c);
|
||||
if (e !== a) diffs.push(c.control);
|
||||
}
|
||||
const onlyExpected = exp.controls.map((c) => c.control).filter((id) => !current.controls.some((c) => c.control === id));
|
||||
console.error(`✗ TISAX-Snapshot weicht ab. Betroffene Controls (${diffs.length}): ${diffs.slice(0, 20).join(", ")}`);
|
||||
if (onlyExpected.length) console.error(` Fehlend gegenüber Baseline: ${onlyExpected.join(", ")}`);
|
||||
console.error(" → Wenn die Änderung beabsichtigt ist: UPDATE_SNAPSHOT=1 erneut ausführen. Sonst Regression.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error(e); process.exit(1); })
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user