#!/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