Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

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:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
+103
View File
@@ -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());