// 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 = {}; for (const k of Object.keys(v as Record).sort()) out[k] = canonical((v as Record)[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());