// b32 — logic: the SAMA v2 verifier. Implements the seven §4 // conformance checks (Sorted, Architecture, Modeled-tests, // Modeled-boundary, Atomic, the Law §1.2, Consistency §3) as pure // functions over an in-memory (profile, files) input. Never reads // the filesystem — the loader (c14_sama_profile + d21 handler) // populates the input map. The shared pure helpers and the parse- // boundary detector live in a31_sama_v2 so this verifier and the // §5 metrics emitter agree by construction. import { PARSE_BOUNDARY_PATTERNS, collectRelativeImports, declaredLayer, findParseBoundaryCallSites, isSamaFile, isTestFile, resolveImport, stripStringsAndComments, type SamaV2Check, type SamaV2Input, type SamaV2Report, type SamaV2Violation, } from "./a31_sama_v2.ts"; // — Check 1: Sorted ------------------------------------------------- // // "Every file carries a profile-recognised prefix; lexicographic // prefix order equals layer order." const checkSorted = (input: SamaV2Input): SamaV2Check => { const violations: SamaV2Violation[] = []; let examined = 0; // Collect (prefix, layer) pairs from the profile. const pairs: Array<{ prefix: string; layer: number }> = []; for (const [k, spec] of Object.entries(input.profile.layers)) { const layer = parseInt(k, 10); for (const sub of spec.sublayers) pairs.push({ prefix: sub.prefix, layer }); } // For any two prefixes with layer(A) < layer(B), A must lex-sort < B. for (let i = 0; i < pairs.length; i++) { for (let j = 0; j < pairs.length; j++) { if (i === j) continue; const a = pairs[i]!; const b = pairs[j]!; if (a.layer < b.layer && a.prefix > b.prefix) { violations.push({ file: a.prefix, detail: `prefix \`${a.prefix}\` (layer ${a.layer}) sorts after \`${b.prefix}\` (layer ${b.layer}) — lex order must equal layer order`, }); } } } // Also count source files whose prefix isn't recognised by any // sublayer. They'd be flagged by Architecture too, but the Sorted // rule needs each file to have a recognised prefix. for (const path of input.files.keys()) { if (!isSamaFile(path)) continue; examined++; if (declaredLayer(path, input.profile) === null) { violations.push({ file: path, detail: "no profile-recognised prefix" }); } } return { id: 1, name: "Sorted", property: "Sorted", passed: violations.length === 0, examined, violations, }; }; // — Check 2: Architecture ------------------------------------------- // // "Every file maps to exactly one canonical layer; no file is // unprefixed or maps to two layers." const checkArchitecture = (input: SamaV2Input): SamaV2Check => { const violations: SamaV2Violation[] = []; let examined = 0; for (const path of input.files.keys()) { if (!isSamaFile(path) && !isTestFile(path)) continue; examined++; const base = path.split("/").pop() ?? path; // Find every profile prefix that matches this filename. Exactly // one is required; zero = unprefixed (caught by Sorted too) but // we surface it here as the canonical "unmapped" failure. const matches: Array<{ layer: number; prefix: string }> = []; for (const [k, spec] of Object.entries(input.profile.layers)) { const layer = parseInt(k, 10); for (const sub of spec.sublayers) { if (base.startsWith(sub.prefix)) matches.push({ layer, prefix: sub.prefix }); } } if (matches.length === 0) { violations.push({ file: path, detail: "unprefixed — does not match any profile prefix" }); } else if (matches.length > 1) { // Two prefixes claim the same file: profile ambiguity. const distinctLayers = new Set(matches.map((m) => m.layer)); if (distinctLayers.size > 1) { violations.push({ file: path, detail: `ambiguous — matches multiple layers: ${matches.map((m) => `${m.prefix}→L${m.layer}`).join(", ")}`, }); } } } return { id: 2, name: "Architecture", property: "Architecture", passed: violations.length === 0, examined, violations, }; }; // — Check 3: Modeled (tests) ---------------------------------------- // // "Every Layer 1 and Layer 2 behavior file has a sibling test file." const checkModeledTests = (input: SamaV2Input): SamaV2Check => { const violations: SamaV2Violation[] = []; let examined = 0; for (const path of input.files.keys()) { if (!isSamaFile(path)) continue; const decl = declaredLayer(path, input.profile); if (!decl) continue; if (decl.layer !== 1 && decl.layer !== 2) continue; examined++; const siblingPath = path.replace(/\.ts$/, ".test.ts"); if (!input.files.has(siblingPath)) { violations.push({ file: path, detail: `no sibling test at \`${siblingPath}\` — Layer ${decl.layer} requires one`, }); } } return { id: 3, name: "Modeled (tests)", property: "Modeled (tests)", passed: violations.length === 0, examined, violations, }; }; // — Check 4: Modeled (boundary) ------------------------------------- // // "External input is parsed only in Layer 2." // // §4.4 is profile-dependent (spec §6). Our profile defines boundary // parsing as `JSON.parse(` of arbitrary input (not constant strings) // or `new URL(` of arbitrary input — i.e. patterns that turn bytes // into typed structures. Platform-provided parsers called *through* // Layer 3 entry handlers (`req.json()`, `req.formData()`, route // params) are treated as delegation to the platform's own Layer 2, // not parsing performed in our Layer 3. The verifier reports any // raw JSON.parse / new URL calls landing outside Layer 2. // // The call-site detector lives in a31_sama_v2 (findParseBoundary- // CallSites). This check consumes its output and groups by // (file, pattern) so the violation list stays at file-pattern // granularity — the same shape pre-refactor. The §5 boundaryRatio // metric consumes the same detector and counts individual call // sites, but does not change this check's verdict. const checkModeledBoundary = (input: SamaV2Input): SamaV2Check => { const violations: SamaV2Violation[] = []; let examined = 0; // Bucket call sites by file → set of patterns observed. const patternsByFile = new Map>(); for (const site of findParseBoundaryCallSites(input.files)) { let s = patternsByFile.get(site.file); if (!s) { s = new Set(); patternsByFile.set(site.file, s); } s.add(site.pattern); } // Iterate files in input order; emit one violation per (file, // pattern) for files outside Layer 2, preserving PARSE_BOUNDARY_- // PATTERNS order. This matches the pre-refactor verdict bit-for-bit. for (const path of input.files.keys()) { if (!isSamaFile(path)) continue; const decl = declaredLayer(path, input.profile); if (!decl) continue; examined++; if (decl.layer === 2) continue; // Layer 2 is the legitimate site. const observed = patternsByFile.get(path); if (!observed) continue; for (const pat of PARSE_BOUNDARY_PATTERNS) { if (observed.has(pat.name)) { violations.push({ file: path, detail: `boundary pattern \`${pat.name}\` found in Layer ${decl.layer} — parsing belongs in Layer 2`, }); } } } return { id: 4, name: "Modeled (boundary)", property: "Modeled (boundary)", passed: violations.length === 0, examined, violations, note: "profile-dependent (spec §4.4): boundary = raw `JSON.parse` / `new URL` outside Layer 2. Platform parsers reached via `req.json()` etc. are treated as delegation to the platform's own Layer 2.", }; }; // — Check 5: Atomic ------------------------------------------------- // // "No file exceeds the line cap (default ~700; profile may lower, // never raise). No barrel re-export files." const ATOMIC_LINE_CAP = 700; const checkAtomic = (input: SamaV2Input): SamaV2Check => { const violations: SamaV2Violation[] = []; let examined = 0; for (const [path, content] of input.files.entries()) { if (!isSamaFile(path) && !isTestFile(path)) continue; examined++; const lines = content.split("\n").length; if (lines > ATOMIC_LINE_CAP) { violations.push({ file: path, detail: `${lines} lines (over the ${ATOMIC_LINE_CAP}-line cap — split per UI/data domain)`, }); } // Barrel detection: a file whose entire body is re-exports. // Heuristic: every non-blank, non-comment line is `export ... from`. const stripped = stripStringsAndComments(content); const codeLines = stripped.split("\n").map((l) => l.trim()).filter((l) => l.length > 0); if (codeLines.length >= 2 && codeLines.every((l) => /^export\s+(\*|\{)/.test(l) && /\bfrom\b/.test(l))) { violations.push({ file: path, detail: "barrel re-export file (all lines are `export … from`)" }); } } return { id: 5, name: "Atomic", property: "Atomic", passed: violations.length === 0, examined, violations, }; }; // — Check 6: The Law (§1.2) ----------------------------------------- // // "Imports always point to a strictly lower layer number — never // upward, never sideways across a higher number, never cyclic." // // Build the import graph from relative-.ts imports, then for each // edge A → B require: layer(B) < layer(A), OR same layer + B's // sublayer index <= A's sublayer index. Also run a DFS cycle detector. const checkLaw = (input: SamaV2Input): SamaV2Check => { const violations: SamaV2Violation[] = []; let examined = 0; // Build adjacency. const adj = new Map(); for (const [path, content] of input.files.entries()) { if (!isSamaFile(path) && !isTestFile(path)) continue; examined++; const out: string[] = []; for (const imp of collectRelativeImports(content)) { const resolved = resolveImport(path, imp); // Only follow edges into known SAMA files (in-tree, in src/). if (input.files.has(resolved)) out.push(resolved); } adj.set(path, out); } // Edge-by-edge layer/sublayer check. for (const [from, outs] of adj.entries()) { const aDecl = declaredLayer(from, input.profile); if (!aDecl) continue; // Unmapped — caught by Architecture. for (const to of outs) { const bDecl = declaredLayer(to, input.profile); if (!bDecl) continue; if (bDecl.layer < aDecl.layer) continue; // strictly lower — OK if (bDecl.layer > aDecl.layer) { violations.push({ file: from, detail: `imports \`${to}\` — Layer ${aDecl.layer} → Layer ${bDecl.layer} (upward, breaks §1.2)`, }); continue; } // Same layer: sublayer ordering. The import target must be in // an earlier-or-equal sublayer slot (spec §2.2: later may import // earlier). if (bDecl.sublayer.index > aDecl.sublayer.index) { violations.push({ file: from, detail: `imports \`${to}\` — same layer ${aDecl.layer} but sublayer order is reversed (${aDecl.sublayer.name} sublayer-index ${aDecl.sublayer.index} → ${bDecl.sublayer.name} sublayer-index ${bDecl.sublayer.index})`, }); } } } // DFS cycle detection on the same graph. const WHITE = 0, GRAY = 1, BLACK = 2; const color = new Map(); for (const k of adj.keys()) color.set(k, WHITE); const cycles: string[][] = []; const stack: string[] = []; const dfs = (node: string): boolean => { color.set(node, GRAY); stack.push(node); for (const next of adj.get(node) ?? []) { const c = color.get(next) ?? WHITE; if (c === GRAY) { const idx = stack.indexOf(next); if (idx !== -1) cycles.push([...stack.slice(idx), next]); return true; } if (c === WHITE && dfs(next)) { // bubble up } } stack.pop(); color.set(node, BLACK); return false; }; for (const k of adj.keys()) if (color.get(k) === WHITE) dfs(k); for (const cyc of cycles) { violations.push({ file: cyc[0] ?? "(unknown)", detail: `import cycle: ${cyc.join(" → ")}`, }); } return { id: 6, name: "Law (§1.2)", property: "Law", passed: violations.length === 0, examined, violations, }; }; // — Check 7: Consistency (§3) --------------------------------------- // // "Verifier FAILS if a file imports from a layer that its declared // layer is not permitted to import." This is the same set of edges // the Law check examines, framed from the file's own perspective: // does the prefix lie about what the file actually does? // // We emit a separate verdict so the report can show both framings. // In a profile where no §1.2 violation exists, §3 also passes by // construction — both are derived from the same edge set. const checkConsistency = (input: SamaV2Input): SamaV2Check => { const violations: SamaV2Violation[] = []; let examined = 0; for (const [path, content] of input.files.entries()) { if (!isSamaFile(path)) continue; const aDecl = declaredLayer(path, input.profile); if (!aDecl) continue; examined++; let ceiling = -1; let ceilingFile: string | null = null; for (const imp of collectRelativeImports(content)) { const resolved = resolveImport(path, imp); const bDecl = declaredLayer(resolved, input.profile); if (!bDecl) continue; if (bDecl.layer > ceiling) { ceiling = bDecl.layer; ceilingFile = resolved; } } // Consistency fails if any import goes to a strictly higher // layer than the file's declared layer. Same-layer with bad // sublayer order is the Law's concern, not Consistency's. if (ceiling > aDecl.layer) { violations.push({ file: path, detail: `declared Layer ${aDecl.layer} (prefix \`${aDecl.sublayer.prefix}\`) but imports reach Layer ${ceiling} via \`${ceilingFile}\` — the prefix claims something the imports contradict`, }); } } return { id: 7, name: "Consistency (§3)", property: "Consistency", passed: violations.length === 0, examined, violations, }; }; // — Orchestrator ---------------------------------------------------- export const verifySamaV2 = (input: SamaV2Input): SamaV2Report => { const checks: SamaV2Check[] = [ checkSorted(input), checkArchitecture(input), checkModeledTests(input), checkModeledBoundary(input), checkAtomic(input), checkLaw(input), checkConsistency(input), ]; // Architecture's examined count is the canonical total — it counts // every file the profile assigns to a layer (or fails to). const examined = checks.find((c) => c.id === 2)?.examined ?? 0; return { profile: input.profile.profile, examined, checks, overallPassed: checks.every((c) => c.passed), }; };