Files
arah2/scripts/lib/budget-gates.mjs
araharah-executor 79a302caf5 fix(08-05): route-cpu gate law re-anchored to the post-ALT measured truth (escalation record — no retirement)
Task 3, re-scoped per the plan's honest-FAIL branch: the CPU verdict FAILED
(the ladder ran to its documented stop), so the c3 class is NOT retired —
the record instead carries the executed-and-measured ALT truth.

- budget-gates.mjs: the verdict law documents the Phase-8 outcome (ALT
  EXECUTED, optimality green, CPU verdict FAIL 152.788/185.692 ms quoted
  with date; L=32 probe measured worse) — the stale 'next-milestone item'
  wording is gone; ROUTE_CPU_DRIFT_GUARD_MS re-anchored 200 → 400 (the
  post-ALT documented band outgrew the old guard; ≈2x the worst shipped-
  config run 185.692 ms); the guard stays a regression trip-wire, the 8 ms
  budget stays and its FAIL still prints every run
- budget-audit.test.mjs: CPU3 pins the new law (evidence must name the
  EXECUTED-in-Phase-8 verdict and must NEVER say 'next-milestone');
  19/19 vectors ok (the count holds — no vector dropped)
- docs/BUDGETS.md: route-cpu row carries the Phase-8 measured numbers +
  date; the c3 section is the honest post-ALT record (what changed, the
  quoted gate lines, the one-directional-Lipschitz wall, the open
  escalation) — staged as selective hunks; the tree's pre-existing
  doc-sync edits in other sections remain uncommitted, untouched

grep law: zero 'next-milestone' hits in budget-gates.mjs + BUDGETS.md;
ROUTE_CPU_DRIFT_GUARD_MS still exported and used.
2026-10-25 20:06:57 +08:00

554 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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,
* re-measured in Phase 8 / 08-05): the full-sample ladder p95 (25 pairs incl.
* the 10 adversarial quadrant worst cases) MEASURED 38–93 ms pre-ALT 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 ALT landmark lever was EXECUTED in Phase 8 (ARG2 binary +
* landmark steering) and MEASURED on 2026-10-25: the optimality gate is green
* (route seconds equal the frozen pre-ALT record within ±0.1%), but the CPU
* verdict FAILED honestly — 25-route p95 152.788 ms, extended 100-pair p95
* 185.692 ms (both far over the 8 ms budget; the L=32 probe measured worse:
* 182.725 / 204.499 ms). The sound-but-slower toTable-only steering is
* mandatory: the fromTable families are one-directional Lipschitz (their
* u-side term needs the reverse edge, which a oneway does not provide) and
* the shared Ikeda average p = (h_f − h_b)/2 is consistent only with
* two-sided families — the c3 class therefore REMAINS the accepted designed
* degradation, and the honest escalation (direction-split potentials with a
* re-proven meet rule, Contraction Hierarchies, or Workers Paid) stays open
* for the maintainer. This gate checks the measured p95 against the
* DOCUMENTED accepted band (a drift guard ≈ 2x the worst documented
* accepted 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 runs top
* at 185.692 ms (the shipped L=24 ARG2 config's extended-100 verdict,
* 2026-10-25); the pre-ALT band topped at 93.6 ms (06-04's worst shared-box
* run) and the L=32 probe measured 204.499 ms. The guard sits at ≈ 2x the
* worst documented shipped-config run — beyond it nothing the escalation
* examined applies: uncharted regression. */
export const ROUTE_CPU_DRIFT_GUARD_MS = 400;
/** 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> ms (<km> km / ...)` line per
* pair, an optional `unreachable — honest null` marker per dead-end probe,
* and the summary `p95: <n> ms (max <n> ms, mean <n> ms, n=<k>)` 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 185.7 ms measured, 2026-10-25)`
: overBudget
? `over the ${P95_BUDGET_MS} ms product budget — the DOCUMENTED, ACCEPTED c3 class (cross-metro honest degradation: 1102 → fail-honest null → router-pending; the ALT landmark lever was EXECUTED in Phase 8 and measured over budget on 2026-10-25 — optimality green, CPU verdict FAIL — so the class remains; the escalation stays open)`
: `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: <CONSTANT>`.
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 = [];
const cwd = opts.cwd ?? REPO_ROOT;
if (!existsSync(path.join(cwd, "package.json"))) {
// Environment drift, not a corpus failure: the corpora are npm scripts —
// without the root manifest they cannot even be spawned (T-07-22 class).
return notRun(id, "package.json is absent in the audit root — the golden corpora cannot be spawned");
}
for (const [corpusId, argv] of commands) {
let res;
try {
res = spawnSync(argv[0], argv.slice(1), {
cwd,
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 };
}