Files
arah2/scripts/budget-audit.mjs
T
araharah-executor bbf003fa5d 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)
2026-10-25 05:05:04 +08:00

135 lines
7.6 KiB
JavaScript
Raw 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-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