feat(07-04): the budget-audit CLI + the root npm script — orchestration, free-tier comparison table, 0/1/2 exit codes

- npm run budget-audit: 5 required gates + the --live cache-header probe; one evidence line per gate
- the FREE-TIER COMPARISON table: every documented Cloudflare free-plan ceiling beside the
  current value where measured, `not measured` where not — the console form of docs/BUDGETS.md
- exit 0 CLEAN / 1 FAILED / 2 NOT RUN — a skip is never a pass (T-07-22)
- no secret, no coordinate in the output (D-14's category-only law, applied to budgets)
This commit is contained in:
araharah-executor
2026-10-25 05:05:04 +08:00
parent 8378337578
commit bbf003fa5d
2 changed files with 135 additions and 0 deletions
+1
View File
@@ -17,6 +17,7 @@
"gallery": "npm run gallery --workspace theme", "gallery": "npm run gallery --workspace theme",
"audit": "npm run audit --workspace theme", "audit": "npm run audit --workspace theme",
"privacy-audit": "node scripts/privacy-audit.mjs", "privacy-audit": "node scripts/privacy-audit.mjs",
"budget-audit": "node scripts/budget-audit.mjs",
"capture": "npm run capture --workspace theme", "capture": "npm run capture --workspace theme",
"bot:test": "npm test -w bot", "bot:test": "npm test -w bot",
"bot:typecheck": "npm run typecheck -w bot", "bot:typecheck": "npm run typecheck -w bot",
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env node
/*
* budget-audit.mjs — the ONE 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. GPL-3.0: a fork reruns
* `npm run budget-audit` over its own trees before its FIRST driver — the
* budget record doubles as the fork's free-tier onboarding check.
*
* D-06: the free-first mandate is only real if the numbers are MEASURED and
* PRINTED, not assumed. This CLI runs the six measurement gates, prints one
* evidence line per gate, prints the FREE-TIER COMPARISON TABLE (every
* documented Cloudflare free-plan ceiling beside the current value where the
* audit measured one, `not measured` where it did not), and writes nothing —
* the human record is docs/BUDGETS.md, filled from THIS output.
*
* What it runs, in order (the privacy-audit shape, 07-03):
* runCpuLadderGate() id: route-cpu — RE-RUNS the A* ladder (child process)
* runBundleBudgetGate() id: bundle-budget — BA9 route-payload gz + whole-app bundle gz
* runKvWriteClassGate() id: kv-writes — write classes + the cron ZERO-write law
* runSubrequestClassGate() id: subrequests — the per-request-class subrequest inventory
* runGoldenCorpusGate() id: golden-corpus — RE-RUNS routing:corpus + search:corpus
* [--live] runCacheHeaderGate() id: cache-headers — LIVE cache headers (observed, verbatim)
*
* Exit codes (the privacy-audit contract — no silent passes):
* 0 clean — every required gate PASSed (a gate's pass means its measurement
* is inside its budget, OR matches a documented, decided escalation —
* the accepted c3 class for route-cpu; the record carries both verdicts)
* 1 a gate FAILED — each printed above as its evidence line
* 2 NOT RUN — environment drift (a required measurement could not run: a
* missing graph binary, a missing build, an unreachable/undeployed
* origin). A NOT RUN is NEVER a pass (T-07-22).
*
* The output carries NO secret and NO coordinate: sizes, counts, timings and
* header lines only — the D-14 category-only law, applied to budgets.
*
* PRIVACY_AUDIT_ROOT: repo root override (shared with the privacy audit's
* gates) — the gates are otherwise cwd-INDEPENDENT (module-URL root).
*
* Usage: node scripts/budget-audit.mjs [--live] (the `budget-audit` npm script)
*/
import { runCpuLadderGate, runBundleBudgetGate, runKvWriteClassGate, runSubrequestClassGate, runCacheHeaderGate, runGoldenCorpusGate } from "./lib/budget-gates.mjs";
import { existsSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const REPO_ROOT = process.env.PRIVACY_AUDIT_ROOT ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const live = process.argv.includes("--live");
/** The five required gates — always measured. */
const results = [
await runCpuLadderGate(),
await runBundleBudgetGate(),
await runKvWriteClassGate(),
await runSubrequestClassGate(),
await runGoldenCorpusGate(),
];
if (live) {
results.push(await runCacheHeaderGate());
} else {
// Flag-skip placeholder — visible, honest, and NEVER green: the skip is the
// operator's explicit choice (no --live), excluded from the exit-2 class.
results.push({ id: "cache-headers", pass: false, notRun: true, flagSkip: true, evidence: "(pass --live)" });
}
for (const result of results) {
const status = result.pass ? "PASS" : result.notRun ? "NOT RUN" : "FAIL";
console.log(`${result.id}: ${status} — ${result.evidence}`);
}
/* ————— the FREE-TIER COMPARISON TABLE — the console form of docs/BUDGETS.md ————— */
/** Static-asset file count in web/dist (the 20,000 files/version row's current value). */
function distFileCount() {
const dist = path.join(REPO_ROOT, "web", "dist");
if (!existsSync(dist)) return null;
let n = 0;
const walk = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else n += 1;
}
};
walk(dist);
return n;
}
const routeCpu = results.find((r) => r.id === "route-cpu");
const bundle = results.find((r) => r.id === "bundle-budget");
const kv = results.find((r) => r.id === "kv-writes");
const sq = results.find((r) => r.id === "subrequests");
const distFiles = distFileCount();
console.log("\nFREE-TIER COMPARISON (ceilings: Cloudflare Workers limits / KV limits / pricing —");
console.log("developers.cloudflare.com/workers/platform/limits, /kv/platform/limits, /workers/platform/pricing):");
console.log("| Resource | Free-plan ceiling | Current (this run) |");
console.log("|---|---|---|");
console.log(`| Worker HTTP CPU | 10 ms/invocation | ${routeCpu?.p95 !== undefined ? `ladder p95 ${routeCpu.p95.toFixed(3)} ms (local 25-pair ladder, full sample incl. cross-metro worst cases — the accepted c3 class; live Workers Logs CPU is a DIFFERENT measurement, not measured)` : "not measured"}`);
console.log("| Cron CPU | 10 ms/invocation | not measured (no live cron metric outside --live) |");
console.log(`| Worker subrequests | 50/request | ${sq?.pass ? "per-class inventory printed above (subrequests gate) — /api/route = 0 external; the cron's Telegram send = the only recurring class" : "not measured"}`);
console.log("| Worker requests | 100,000/day | not measured (no live metric) |");
console.log("| Worker memory | 128 MB/isolate | not measured |");
console.log(`| Worker bundle | 64 MiB uncompressed | ${(bundle?.evidence ?? "").includes("NOT MEASURED") ? "NOT MEASURED — bot/dist absent (wrangler builds in memory on deploy)" : "see the bundle-budget gate's Worker bundle row"}`);
console.log(`| Static assets | free unlimited requests; 20,000 files/version; 25 MiB/file | ${distFiles !== null ? `${distFiles} file(s) in web/dist` : "not measured (web/dist absent)"}`);
console.log(`| KV reads | 100,000/day | not measured (derived bound in docs/BUDGETS.md) |`);
console.log(`| KV writes to different keys | 1,000/day | ${kv?.pass ? "4 write site(s), class table above; the daily estimate is derived in docs/BUDGETS.md" : "not measured"}`);
console.log("| KV writes to same key | 1/second | not measured |");
console.log(`| KV list requests | 1,000/day | derived 1,440/day (the 60 s cron × 1 list per tick) — OVER the ceiling: a DISCOVERED FINDING, printed in docs/BUDGETS.md; not silently accepted |`);
console.log("| KV storage | 1 GB/account and namespace | not measured |");
console.log("| KV operations/invocation | 1,000 | not measured |");
console.log("| Workers Logs Free | 200,000 events/day; 3-day retention | not measured |");
/* ————— summary + exit codes ————— */
const failed = results.filter((r) => !r.pass && !r.notRun);
const notRun = results.filter((r) => !r.pass && r.notRun && !r.flagSkip);
const passed = results.filter((r) => r.pass).length;
if (failed.length > 0) {
console.log(`BUDGET AUDIT FAILED — ${failed.length} gate(s) over budget`);
process.exit(1); // violations — each printed above
}
if (notRun.length > 0) {
const reasons = notRun.map((r) => `${r.id}: ${r.evidence}`).join("; ");
console.log(`BUDGET AUDIT NOT RUN — ${reasons} (environment drift; never a pass)`);
process.exit(2); // environment drift — fix the environment and rerun
}
console.log(
`BUDGET AUDIT CLEAN — ${passed} gate(s) within budget ` +
`(printed findings carried to docs/BUDGETS.md: the accepted c3 CPU class and the discovered KV-list over-ceiling)`,
);
process.exit(0); // clean