#!/usr/bin/env node /* * budget-gates.mjs — the measurement gates for the consolidated budget audit * (07-04, D-06). * --------------------------------------------------------------------------- * A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src, * web/src, routing/src, or search/src imports it. The free plan gives a Worker * 10 ms of CPU per request (Error 1102 beyond it) and 50 subrequests per * request, so every budget here is MEASURED AND PRINTED, never hoped for * ("budget gates print their measurement" — the BA9 precedent). * * Gate contract (07-RESEARCH Pattern 4): every gate is an async function * returning { id, pass, evidence, violations? } — and every gate carries an * honest NOT-RUN path that is NEVER a pass ({ pass:false, notRun:true }). * * The gates, in audit order: * runCpuLadderGate() id: route-cpu — RE-RUNS routing/scripts/measure-route-cpu.mjs as a child process (never re-implemented) * runBundleBudgetGate() id: bundle-budget — BA9's route-payload gzip sum + the whole-app bundle ceiling * runKvWriteClassGate() id: kv-writes — the write-class inventory + the cron's ZERO-write law (source-proven) * runSubrequestClassGate() id: subrequests — the per-request-class external-subrequest inventory (source-derived) * runCacheHeaderGate() id: cache-headers — the LIVE header probe (re-used from cloudflare-probes.mjs) * runGoldenCorpusGate() id: golden-corpus — RE-RUNS `routing:corpus` + `search:corpus` as child processes * * The route-cpu verdict law (the c3 class, decided in Phase 6 / 06-04): the * full-sample ladder p95 (25 pairs incl. the 10 adversarial quadrant worst * cases) MEASURED 38–93 ms across runs — over the 8 ms product budget, and * the mitigation ladder was exhausted WITHIN the $0 law ((a) rejected on * correctness — 12/25 pairs unreachable, (b) bidirectional A* shipped, * (c) Workers Paid rejected on the $0 mandate). The DECIDED posture is the * honest degradation: cross-metro CPU exhaust → Error 1102 → fail-honest * null → the client's router-pending state; the committed upgrade lever is * ALT landmark preprocessing (next milestone). This gate therefore checks * the measured p95 against the DOCUMENTED accepted band (a 200 ms drift * guard ≈ 2x the worst documented run): a p95 inside the band is the * documented state (the record still prints it against the 8 ms budget with * negative headroom — nothing is hidden); a p95 beyond it is UNCHARTED * regression and FAILS. * * Test injectability (the privacy-audit suite precedent): every gate accepts * opts-only path overrides; production callers pass nothing. */ import { spawnSync, execFileSync } from "node:child_process"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { gzipSync } from "node:zlib"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { runAssetHeaderProbe } from "./cloudflare-probes.mjs"; const MODULE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); const REPO_ROOT = process.env.PRIVACY_AUDIT_ROOT ?? MODULE_ROOT; /** The ladder's own budget constant — 8 ms, 20% under the 10 ms free ceiling. */ export const P95_BUDGET_MS = 8; /** The BA9 route-payload ceiling (D-11): 200 KiB gz. */ export const ROUTE_PAYLOAD_BUDGET_BYTES = 204800; /** The whole-app bundle ceiling (D-06's record row): 500 KB gz for ALL of web/dist/assets/*.js. */ export const APP_BUNDLE_BUDGET_BYTES = 500000; /** The drift guard for the accepted c3 band: the documented accepted p95 tops * at 93.6 ms (06-04's worst shared-box run); beyond 200 ms nothing the * escalation examined applies — uncharted regression. */ export const ROUTE_CPU_DRIFT_GUARD_MS = 200; /** A missing environment is the exit-2 class: pass:false + notRun:true. */ function notRun(id, reason) { return { id, pass: false, notRun: true, evidence: reason }; } /* ══════════════════════════ route-cpu ══════════════════════════ */ /** * RE-RUN the existing A* ladder as a child process and gate its measurement. * The child's stdout carries one ` label: ms ( km / ...)` line per * pair, an optional `unreachable — honest null` marker per dead-end probe, * and the summary `p95: ms (max ms, mean ms, n=)` line. * opts: scriptPath, graphPath, ladderOutput (pre-captured child stdout — * tests), cwd. */ export function runCpuLadderGate(opts = {}) { const id = "route-cpu"; const script = opts.scriptPath ?? path.join(REPO_ROOT, "routing", "scripts", "measure-route-cpu.mjs"); const graphPath = opts.graphPath ?? path.join(REPO_ROOT, "routing", "data", "kl-roadgraph.v1.bin"); if (!existsSync(graphPath)) { return notRun(id, "kl-roadgraph.v1.bin is absent — build it (node routing/scripts/build-road-graph.mjs) and re-run"); } if (!existsSync(script)) return notRun(id, "routing/scripts/measure-route-cpu.mjs is absent — the ladder cannot be re-run"); let out; if (opts.ladderOutput !== undefined) { out = opts.ladderOutput; // test injection — the captured child stdout } else { try { out = execFileSync(process.execPath, [script], { cwd: opts.cwd ?? REPO_ROOT, encoding: "utf8", timeout: 120000, maxBuffer: 1 << 24, stdio: ["ignore", "pipe", "pipe"], }); } catch (err) { // The ladder exits 1 on its own gate failure — its stdout is still the // measurement (the GATE FAIL line prints to stderr and is merged here: // the mitigation-ladder text is part of the evidence). if (err.stdout === undefined) return notRun(id, `the ladder could not run (${err.code === "ETIMEDOUT" ? "timed out" : "spawn failure"})`); out = err.stdout + "\n" + (err.stderr ?? ""); } } // The child's own verdict lines — carried VERBATIM (the mitigation-ladder // text is part of the evidence, per the plan). const childP95Line = out.match(/^p95: .*$/m)?.[0] ?? null; const childGateLine = out.match(/^GATE (?:FAIL|PASS): .*$/m)?.[0] ?? null; const p95 = Number(out.match(/^p95: (\d+(?:\.\d+)?) ms/m)?.[1] ?? NaN); if (!Number.isFinite(p95) || childP95Line === null) { return notRun(id, "the ladder produced no parsable p95 line — the measurement is unavailable, never assumed"); } const overBudget = p95 > P95_BUDGET_MS; const uncharted = p95 > ROUTE_CPU_DRIFT_GUARD_MS; // The verdict: inside the documented accepted c3 band = the documented // state (see the header's verdict law); beyond the drift guard = FAIL. const pass = !uncharted; const verdict = uncharted ? `UNCHARTED REGRESSION — p95 beyond the ${ROUTE_CPU_DRIFT_GUARD_MS} ms drift guard (the documented accepted c3 band tops at ~93 ms)` : overBudget ? `over the ${P95_BUDGET_MS} ms product budget — the DOCUMENTED, ACCEPTED c3 class (cross-metro honest degradation: 1102 → fail-honest null → router-pending; mitigation ladder exhausted within the $0 law; the ALT landmark lever is a next-milestone item)` : `within the ${P95_BUDGET_MS} ms product budget`; const evidence = `p95 ${p95.toFixed(3)} ms <= 8 ms budget (10 ms free-plan ceiling) — NOT satisfied as stated: ` + `${verdict}. Full-sample ladder (corpus + quadrant worst cases): ${childP95Line}. ` + `Child's own gate line: ${childGateLine ?? "(none printed)"}. ` + `LADDER HISTORY context is cited in docs/BUDGETS.md, not re-printed here.`; return { id, pass, evidence, p95 }; } /* ══════════════════════════ bundle-budget ══════════════════════════ */ /** The BA9 input set (web/test/build-artifact.test.mjs lines 204–209). */ function collectTsFiles(dir) { const out = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) out.push(...collectTsFiles(full)); else if (entry.name.endsWith(".ts")) out.push(full); } return out; } /** * The BA9 measurement, re-implemented in the library (the inputs, the gzip * sum, the 204800 ceiling), PLUS the whole-app bundle measurement and the * Worker bundle size (NOT MEASURED when bot/dist is absent — never zero). * opts: distDir, routeDirs, routeFixturesFile, botDistDir. */ export function runBundleBudgetGate(opts = {}) { const id = "bundle-budget"; const webRoot = opts.webRoot ?? path.join(REPO_ROOT, "web"); const distDir = opts.distDir ?? path.join(webRoot, "dist"); const routeDirs = opts.routeDirs ?? [ path.join(REPO_ROOT, "routing", "src"), path.join(webRoot, "src", "route"), path.join(webRoot, "src", "guidance"), ]; const routeFixturesFile = opts.routeFixturesFile ?? path.join(REPO_ROOT, "routing", "src", "data", "route-fixtures.json"); const measurements = []; const violations = []; // — BA9: the route payload's gz bytes (source gz ≈ conservative vs minified+gz). if (!existsSync(routeFixturesFile)) return notRun(id, "route-fixtures.json is absent — the BA9 inputs are incomplete"); const missingDirs = routeDirs.filter((d) => !existsSync(d)); if (missingDirs.length > 0) return notRun(id, `route-payload input dir(s) absent: ${missingDirs.map((d) => path.relative(REPO_ROOT, d)).join(", ")}`); const inputs = [routeFixturesFile, ...routeDirs.flatMap((dir) => collectTsFiles(dir))]; const routePayloadGz = inputs.reduce((sum, f) => sum + gzipSync(readFileSync(f)).length, 0); measurements.push(`route bundle budget: ${routePayloadGz} / ${ROUTE_PAYLOAD_BUDGET_BYTES} gz bytes`); if (routePayloadGz > ROUTE_PAYLOAD_BUDGET_BYTES) { violations.push(`route payload is ${routePayloadGz} gz bytes — over the ${ROUTE_PAYLOAD_BUDGET_BYTES}-byte (200 KiB) D-11 budget by ${routePayloadGz - ROUTE_PAYLOAD_BUDGET_BYTES}; shrink the fixtures (simplified geometry) or the route code before shipping (T-05-12)`); } // — The whole-app bundle: every hashed JS asset in web/dist, gz-summed. if (!existsSync(distDir)) return notRun(id, "web/dist is absent — run npm run build -w web and re-run"); const assetsDir = path.join(distDir, "assets"); if (!existsSync(assetsDir)) return notRun(id, "web/dist/assets is absent — a build without hashed assets is not the shipped shape"); const jsAssets = readdirSync(assetsDir).filter((f) => f.endsWith(".js")); if (jsAssets.length === 0) return notRun(id, "web/dist/assets carries no .js file — the build is broken"); const appBundleGz = jsAssets.reduce((sum, f) => sum + gzipSync(readFileSync(path.join(assetsDir, f))).length, 0); measurements.push(`app bundle budget: ${appBundleGz} / ${APP_BUNDLE_BUDGET_BYTES} gz bytes (${jsAssets.length} hashed js asset(s))`); if (appBundleGz > APP_BUNDLE_BUDGET_BYTES) { violations.push(`app bundle is ${appBundleGz} gz bytes — over the ${APP_BUNDLE_BUDGET_BYTES}-byte (500 KB gz) D-06 record ceiling by ${appBundleGz - APP_BUNDLE_BUDGET_BYTES}`); } // — The Worker bundle's uncompressed size (honest NOT MEASURED, never zero). const botDistDir = opts.botDistDir ?? path.join(REPO_ROOT, "bot", "dist"); if (existsSync(botDistDir)) { // A flat byte-sum over the directory's files (wrangler emits a bundle dir). let workerBytesTotal = 0; const walkSum = (dir) => { for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) walkSum(full); else workerBytesTotal += readFileSync(full).length; } }; walkSum(botDistDir); measurements.push(`Worker bundle: ${workerBytesTotal} uncompressed bytes (bot/dist)`); } else { measurements.push("Worker bundle: NOT MEASURED — bot/dist is absent (wrangler builds in memory on deploy; the number is not fabricated)"); } const evidence = violations.length === 0 ? `${measurements.join(" | ")}` : `${measurements.join(" | ")} — ${violations.length} violation(s): ${violations.join("; ")}`; return { id, pass: violations.length === 0, evidence, violations: violations.length }; } /* ══════════════════════════ kv-writes ══════════════════════════ */ /** The key families (the KV storage contract — shared with cloudflare-probes). */ const KEY_FAMILIES = ["session:", "chat:", "loc:", "guidance:"]; /** * The KV write-class inventory (T-07-18). Three laws: * 1. every `.put(` call site in bot/src writes a key whose family is one of * the four TTL-only families AND carries a NAMED TTL constant on the * same line (the PG1 law, re-checked from the budget side); * 2. the cron tick (`runGuidanceCronTick`'s body) contains NO `.put(` and * NO `.delete(` — the zero-write law is SOURCE-PROVEN, not claimed; * 3. the per-family TTLs are READ from the constants (never hard-coded). * opts: botRoot (defaults REPO_ROOT/bot). */ export function runKvWriteClassGate(opts = {}) { const id = "kv-writes"; const botRoot = opts.botRoot ?? path.join(REPO_ROOT, "bot"); const srcDir = path.join(botRoot, "src"); if (!existsSync(srcDir)) return notRun(id, "bot/src is absent — the write classes cannot be inventoried"); // — The write-class inventory: every .put( call site in bot/src. const putSites = []; const walk = (dir) => { for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) walk(full); else if (entry.name.endsWith(".ts")) { const lines = readFileSync(full, "utf8").split("\n"); lines.forEach((line, i) => { if (line.includes(".put(")) { putSites.push({ file: path.relative(botRoot, full), line: i + 1, text: line.trim() }); } }); } } }; walk(srcDir); const classViolations = []; const classRows = []; // Family ownership by module (the key schemes are each module's own law, // documented in their headers — sessions.ts owns session:/chat:, etc.). // A .put( in a file OUTSIDE the owned modules must carry a family literal // on its own line, or it is a new, unclassified write family. const FILE_FAMILY = (rel) => rel.startsWith("src/kv/sessions.ts") ? "session: + chat:" : rel.startsWith("src/kv/location-pin.ts") ? "loc:" : rel.startsWith("src/kv/guidance-sessions.ts") ? "guidance:" : null; for (const site of putSites) { const owned = FILE_FAMILY(site.file); const literalFamily = KEY_FAMILIES.find((f) => site.text.includes(f)); const family = owned ?? literalFamily; // The named-TTL law: the call line carries `expirationTtl: `. const namedTtl = /expirationTtl:\s*[A-Z][A-Z0-9_]+/.test(site.text); if (family === undefined || family === null) { classViolations.push(`${site.file}:${site.line} — a .put( whose key family is not one of ${KEY_FAMILIES.join("/")} and whose module owns none`); } else { classRows.push(`${site.file}:${site.line} ${family}`); } if (!namedTtl) { classViolations.push(`${site.file}:${site.line} — a .put( WITHOUT a NAMED TTL constant on the same line (PG1)`); } } // — The TTL table, READ from the constants (never hard-coded). const ttlRows = []; const ttlConstants = [ ["SESSION_TTL_SECONDS", "src/kv/sessions.ts", "session: + chat:"], ["PIN_TTL_SECONDS", "src/kv/location-pin.ts", "loc:"], ["GUIDANCE_SESSION_TTL_SECONDS", "src/kv/guidance-sessions.ts", "guidance:"], ]; for (const [name, file, families] of ttlConstants) { const full = path.join(botRoot, file); if (!existsSync(full)) { ttlRows.push(`${name}: NOT FOUND (${file} absent)`); classViolations.push(`${file} is absent — the ${name} constant cannot be read`); continue; } const value = readFileSync(full, "utf8").match(new RegExp(`export const ${name} = (\\d+);`))?.[1]; if (value === undefined) { ttlRows.push(`${name}: NOT FOUND in ${file}`); classViolations.push(`${file} no longer exports ${name} — the TTL table cannot be read from source`); } else { ttlRows.push(`${name} = ${value} (${families})`); } } // — THE CRON LAW: extract runGuidanceCronTick's body and count writes. const webhookPath = path.join(srcDir, "webhook.ts"); let cronWriteCount = null; if (!existsSync(webhookPath)) { classViolations.push("bot/src/webhook.ts is absent — the cron zero-write law cannot be source-proven"); } else { const src = readFileSync(webhookPath, "utf8"); const start = src.indexOf("async function runGuidanceCronTick("); if (start === -1) { classViolations.push("runGuidanceCronTick is absent from bot/src/webhook.ts — the cron law's subject is gone"); } else { // A simple brace counter from the function's first { to its match. let depth = 0; let end = -1; for (let i = src.indexOf("{", start); i < src.length; i++) { if (src[i] === "{") depth += 1; else if (src[i] === "}") { depth -= 1; if (depth === 0) { end = i; break; } } } if (end === -1) { classViolations.push("runGuidanceCronTick's body did not balance — the extraction failed"); } else { const body = src.slice(start, end + 1); cronWriteCount = (body.match(/\.put\(/g) ?? []).length + (body.match(/\.delete\(/g) ?? []).length; if (cronWriteCount !== 0) { classViolations.push(`the cron tick performs ${cronWriteCount} KV write/delete call(s) — the free-plan zero-write law (T-07-18) is violated in runGuidanceCronTick`); } } } } const cronEvidence = cronWriteCount === null ? "cron write count: UNKNOWN (extraction failed)" : `cron write count: ${cronWriteCount} (runGuidanceCronTick scans for .put(/.delete( — zero; the tick lists, reads, and sends only)`; const evidence = classViolations.length === 0 ? `${putSites.length} KV write site(s) [${[...new Set(classRows)].join("; ")}] — every write carries a NAMED TTL constant; ${ttlRows.join("; ")}; ${cronEvidence}` : `${putSites.length} write site(s); ${cronEvidence}; ${classViolations.length} violation(s): ${classViolations.join("; ")}`; return { id, pass: classViolations.length === 0, evidence, violations: classViolations.length }; } /* ══════════════════════════ subrequests ══════════════════════════ */ /** * The per-request-class external-subrequest inventory (T-07-23b). * * HONESTY REQUIREMENT: this gate is a SOURCE-DERIVED inventory, not a live * Workers Logs metric. Each row's count is derived from the Worker's route * table (bot/src/webhook.ts) and the code paths behind it; the live Workers * Logs subrequest metric is NOT available outside --live and when it cannot * be produced the row reads `not measured outside --live` — never a * fabricated number. * * The derived facts (each with its source site): * /telegram/webhook ≤ 2 per update — ctx.reply / replyWithPhoto with a * text fallback (bot/src/bot/commands/start.ts); * the count is update-dependent — not measured live * /api/session 0 external — HMAC validation is local; KV ops are * NOT subrequests (bot/src/webhook.ts handleSession) * /api/location 0 external — KV reads only (handleLocation) * /api/location-fallback 1 external — bot.api.sendMessage * (bot/src/bot/handlers/location-fallback.ts) * /api/guidance 0 external — KV only (handleGuidance) * /api/route 0 external — the graph comes from the R2/KV * binding, never fetched; the A* search is pure * CPU (bot/src/routing/worker-router.ts) * scheduled (cron tick) 1 per DUE envelope (api.sendPhoto, * bot/src/webhook.ts) — the ONLY recurring * external-subrequest class; the per-tick count * depends on the number of live due envelopes — * not measured * * KV operations are NOT subrequests — Cloudflare counts them against the * separate KV-operations ceiling (1,000/invocation) — so they are reported * by the kv-writes gate and this gate SAYS SO explicitly. * * The gate derives the route table FROM webhook.ts (`url.pathname === "…"` * entries) and FAILS on any class the inventory does not cover — the table * must follow the code, not the reverse. * opts: webhookPath, plus per-class overrides for the fixture tests. */ export function runSubrequestClassGate(opts = {}) { const id = "subrequests"; const webhookPath = opts.webhookPath ?? path.join(REPO_ROOT, "bot", "src", "webhook.ts"); if (!existsSync(webhookPath)) return notRun(id, "bot/src/webhook.ts is absent — the route table cannot be derived"); const src = readFileSync(webhookPath, "utf8"); // Derive the route classes from the Worker's own route table. const routes = [...src.matchAll(/url\.pathname === "([^"]+)"/g)].map((m) => m[1]); if (src.includes("scheduled(") || src.includes("runGuidanceCronTick")) routes.push("scheduled (cron tick)"); // The inventory — the source-derived facts, keyed by route class. const INVENTORY = { "/telegram/webhook": { count: "≤ 2 per update (reply / photo-with-text-fallback — update-dependent)", source: "bot/src/bot/commands/start.ts ctx.reply/replyWithPhoto", }, "/api/session": { count: "0 external subrequests", source: "bot/src/webhook.ts handleSession — HMAC validation is local; KV ops are not subrequests" }, "/api/location": { count: "0 external subrequests", source: "bot/src/webhook.ts handleLocation — KV reads only" }, "/api/location-fallback": { count: "1 external subrequest (bot.api.sendMessage)", source: "bot/src/bot/handlers/location-fallback.ts" }, "/api/guidance": { count: "0 external subrequests", source: "bot/src/webhook.ts handleGuidance — KV only" }, "/api/route": { count: "0 external subrequests", source: "bot/src/routing/worker-router.ts — the graph comes from the R2/KV binding, never fetched; the A* search is pure CPU" }, "scheduled (cron tick)": { count: "1 per DUE envelope (api.sendPhoto); the per-tick total depends on the number of live due envelopes — not measured", source: "bot/src/webhook.ts runGuidanceCronTick — the ONLY recurring external-subrequest class", }, }; const violations = []; const rows = []; for (const route of routes) { const row = INVENTORY[route]; if (row === undefined) { violations.push(`uninventoried request class: ${route} — the subrequest inventory must be updated beside the route table`); continue; } rows.push(`${route} → ${row.count} [${row.source}]`); } // A documented inventory row whose route vanished from the table is drift too. for (const route of Object.keys(INVENTORY)) { if (!routes.includes(route)) violations.push(`inventoried class ${route} is no longer in the route table — update the inventory`); } // Re-verify the two MANDATORY derived facts from the source (they are the // record's load-bearing rows — a silent regression would empty them). const fallbackSrc = path.join(REPO_ROOT, "bot", "src", "bot", "handlers", "location-fallback.ts"); if (opts.verifyFacts !== false) { if (existsSync(fallbackSrc) && !readFileSync(fallbackSrc, "utf8").includes("api.sendMessage")) { violations.push("location-fallback no longer calls api.sendMessage — the inventory's 1-subrequest row is stale"); } if (existsSync(webhookPath)) { const tickStart = src.indexOf("async function runGuidanceCronTick("); const routeIdx = src.indexOf('url.pathname === "/api/route"'); if (tickStart !== -1 && !src.slice(tickStart).includes("api.sendPhoto")) { violations.push("the cron tick no longer sends via api.sendPhoto — the only-recurring-class fact is stale"); } if (routeIdx !== -1 && /url\.pathname === "\/api\/route"[\s\S]{0,2000}?\bfetch\(/.test(src.slice(routeIdx, src.indexOf("return new Response(\"Not found\"")))) { violations.push("/api/route now appears to fetch — its 0-external-subrequest fact is stale"); } } } const table = rows.join(" | "); const liveNote = "live Workers Logs subrequest metric: not measured outside --live (and not producible by this gate — see the record)"; const kvNote = "KV operations are NOT subrequests — they count against the separate KV-operations ceiling (1,000/invocation) and are tracked by the kv-writes gate"; const evidence = violations.length === 0 ? `source-derived inventory, not a live Workers Logs metric — ${routes.length} request class(es): ${table} | budget 50/request per class | ${liveNote} | ${kvNote}` : `source-derived inventory; ${violations.length} violation(s): ${violations.join("; ")}`; return { id, pass: violations.length === 0, evidence, violations: violations.length }; } /* ══════════════════════════ cache-headers ══════════════════════════ */ /** * The LIVE header probe — RE-USED from scripts/lib/cloudflare-probes.mjs * (imported, never forked). Without a deployed origin the probe degrades to * notRun. The probe quotes the observed cache-control / cf-cache-status * VERBATIM (cf-cache-status is probabilistic per Cloudflare's own footnote — * quoted, never interpreted as a guarantee). * opts: baseUrl (default: the shared probe's own resolution — MINI_APP_URL / * bot/wrangler.jsonc), forceMissingOrigin (tests). */ export function runCacheHeaderGate(opts = {}) { const id = "cache-headers"; if (opts.forceMissingOrigin || opts.baseUrl === null) { return notRun(id, "live cache-header probe not run — no deployed origin or credentials"); } const result = opts.baseUrl !== undefined ? runAssetHeaderProbe({ baseUrl: opts.baseUrl }) : runAssetHeaderProbe(); // The shared probe carries id "asset-headers"; the budget audit's row is // "cache-headers" — same measurement, one id per audit family. return { ...result, id }; } /* ══════════════════════════ golden-corpus ══════════════════════════ */ /** * RE-RUN the golden route + search corpora as child processes (cwd = repo * root) — RE-RUN, never re-implemented — so the ONE budget-audit command * that produces the record also proves the corpora are green (SC4.a/SC4.b). * opts: commands (Array<[id, argv]> overrides — tests). */ export function runGoldenCorpusGate(opts = {}) { const id = "golden-corpus"; const commands = opts.commands ?? [ ["routing:corpus", ["npm", "run", "routing:corpus"]], ["search:corpus", ["npm", "run", "search:corpus"]], ]; const parts = []; const failures = []; for (const [corpusId, argv] of commands) { let res; try { res = spawnSync(argv[0], argv.slice(1), { cwd: opts.cwd ?? REPO_ROOT, encoding: "utf8", timeout: 300000, maxBuffer: 1 << 26, stdio: ["ignore", "pipe", "pipe"], }); } catch (err) { failures.push(`${corpusId} could not run (${err.code === "ETIMEDOUT" ? "timed out" : "spawn failure"})`); continue; } if (res.status !== 0) { const tail = ((res.stdout ?? "") + (res.stderr ?? "")).trim().split("\n").slice(-3).join(" | "); failures.push(`${corpusId} exited ${res.status ?? "signal"} — its own failure output: ${tail}`); } else { parts.push(`${corpusId} exit 0`); } } const evidence = failures.length === 0 ? `golden corpora re-run by THIS gate (child processes): ${parts.join("; ")} — SC4.a + SC4.b proven by the same command that produces the record` : `${failures.join("; ")}`; return { id, pass: failures.length === 0, evidence, violations: failures.length }; }