32 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_have_notes, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_have_notes | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-hardening-private-launch | 04 | execute | 2 |
|
|
true |
|
|
|
Purpose: D-06 — the free-first mandate is only real if the numbers are
MEASURED and PRINTED, not assumed. The repo already has the measuring
instruments (the A* CPU ladder, the BA9 bundle budget, the golden route/search
corpora); this plan CONSOLIDATES them into one command and one checked-in
record, and adds the static-asset cache policy the record needs to reference.
Output: scripts/budget-audit.mjs + scripts/lib/budget-gates.mjs (new),
web/public/_headers (new), the root budget-audit npm script, and
docs/BUDGETS.md (the record).
<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/07-hardening-private-launch/07-CONTEXT.md @.planning/phases/07-hardening-private-launch/07-RESEARCH.md @.planning/phases/07-hardening-private-launch/07-PATTERNS.md The measurement header law — from `routing/scripts/measure-route-cpu.mjs` lines 3–9 (the budget audit inherits this shape): * A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src,
* web/src, or routing/src imports it. The free plan gives a Worker 10 ms of
* CPU per request (Error 1102 beyond it), so the A*-over-the-real-graph cost
* is MEASURED AND PRINTED, never hoped for ("budget gates print their
* measurement" — the BA9 precedent).
The print-then-gate shape — from web/test/build-artifact.test.mjs lines 221–234:
const total = inputs.reduce((sum, f) => sum + gzipSync(readFileSync(f)).length, 0);
console.log(`BA9 route bundle budget: ${total} / 204800 gz bytes`);
assert.ok(total <= 204800, `route payload is ${total} gz bytes — over the 204800-byte (200 KiB) D-11 budget by ${total - 204800}.`);
The gate-fail message with the mitigation ladder — from
routing/scripts/measure-route-cpu.mjs lines 180–185:
if (p95 > P95_BUDGET_MS) {
console.error(`GATE FAIL: p95 ${p95.toFixed(3)} ms exceeds the ${P95_BUDGET_MS} ms budget (A1) — apply the mitigation ladder: (a) … (b) … (c) …`);
process.exit(1);
}
console.log(`GATE PASS: p95 ${p95.toFixed(3)} ms <= ${P95_BUDGET_MS} ms budget — A1 retired for this graph`);
The BA9 input set — from web/test/build-artifact.test.mjs lines 204–209:
const ROUTE_INPUT_DIRS = [
join(WEB, "..", "routing", "src"),
join(WEB, "src", "route"),
join(WEB, "src", "guidance"),
];
const ROUTE_FIXTURES_FILE = join(WEB, "..", "routing", "src", "data", "route-fixtures.json");
The KV write classes (from bot/src/kv/*.ts and bot/src/webhook.ts):
| Class | Write site | Key prefix | TTL constant | TTL |
|---|---|---|---|---|
| session mint | bot/src/kv/sessions.ts mintSession |
session: + chat: |
SESSION_TTL_SECONDS |
900 |
| location pin | bot/src/kv/location-pin.ts |
loc: |
PIN_TTL_SECONDS |
300 |
| guidance envelope | bot/src/kv/guidance-sessions.ts mintGuidanceSession |
guidance: |
GUIDANCE_SESSION_TTL_SECONDS |
7200 |
| cron tick | bot/src/webhook.ts runGuidanceCronTick |
— | — | 0 writes (lists + reads + Telegram sends only) |
The free-plan ceilings (from 07-RESEARCH.md §Pattern 5 — cite this table in the record):
| Resource | Free-plan ceiling |
|---|---|
| Worker HTTP CPU | 10 ms/invocation |
| Cron CPU | 10 ms/invocation |
| Worker subrequests | 50/request |
| Worker requests | 100,000/day |
| Worker memory | 128 MB/isolate |
| Worker bundle | 64 MiB uncompressed |
| Static assets | free and unlimited requests; 20,000 files/version; 25 MiB/file |
| KV reads | 100,000/day |
| KV writes to different keys | 1,000/day |
| KV writes to same key | 1/second |
| KV list requests | 1,000/day |
| KV storage | 1 GB/account and namespace |
| KV operations/invocation | 1,000 |
| Workers Logs Free | 200,000 events/day; 3-day retention |
The static-asset cache rules (from 07-RESEARCH.md §Code Examples — use verbatim):
/assets/*
Cache-Control: public, max-age=31556952, immutable
/
Cache-Control: public, max-age=0, must-revalidate
# Cloudflare Workers static-assets cache policy (07-04, D-06).
# Vite emits content-HASHED files under /assets/* — a hashed URL's bytes can
# never change, so they are immutable for a year. The HTML entry is NOT
# hashed (it points at the current hashed assets) — it must revalidate on
# every visit or a stale entry would point at deleted asset files.
/assets/*
Cache-Control: public, max-age=31556952, immutable
/
Cache-Control: public, max-age=0, must-revalidate
CONSTRAINTS:
- The file is NOT JSON and NOT YAML — it is the
_headersformat (path pattern line, then indentedHeader: valuelines). - The
Cache-Controlvalues are EXACTLY as written above (31556952seconds = 365 days;max-age=0, must-revalidate). - The glyphs, the logo files, and the
/glyphs/*PNGs are NOT hashed by Vite (they are staticpublic/copies) — do NOT give them the immutable rule; leave them on the default policy. Only/assets/*getsimmutable. - Add NO other header rule. No
Set-Cookie, no CSP, noAccess-Control-*(the same-origin policy is the app's design; adding a CORS rule here would widen it).
Then VERIFY the file lands in the build output: run
npm run build -w theme && npm run build -w web and confirm web/dist/_headers
exists. Vite copies public/ verbatim, so it will — but the verification is
mandatory, because a _headers that never reaches dist is a policy that
does not exist.
<acceptance_criteria>
- web/public/_headers exists and contains the literals /assets/*, Cache-Control: public, max-age=31556952, immutable, /, Cache-Control: public, max-age=0, must-revalidate
- grep -c "Set-Cookie\|Access-Control-Allow-Origin" web/public/_headers returns 0
- ls web/dist/_headers succeeds after a build — the policy reaches the served directory
</acceptance_criteria>
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm run build -w theme >/dev/null 2>&1 && npm run build -w web >/dev/null 2>&1 && test -f web/dist/_headers && cat web/dist/_headers && grep -c "immutable" web/dist/_headers
The _headers policy exists in web/public/, reaches web/dist/, and
carries exactly the two rules (hashed assets immutable, HTML revalidating).
It MUST export:
export function runCpuLadderGate(); // id: "route-cpu" — RE-RUNS the existing script
export function runBundleBudgetGate(); // id: "bundle-budget" — BA9's gzip measurement
export function runKvWriteClassGate(); // id: "kv-writes" — write classes + the cron's zero-write law
export function runSubrequestClassGate(); // id: "subrequests" — the per-request-class subrequest inventory
export function runCacheHeaderGate(); // id: "cache-headers" — live headers (or NOT RUN)
export function runGoldenCorpusGate(); // id: "golden-corpus" — RE-RUNS the routing + search golden corpora
runCpuLadderGate() — DO NOT re-implement the A* measurement. Spawn the
existing script as a child process:
node routing/scripts/measure-route-cpu.mjs (cwd = repo root), capture its
stdout. Parse the p95: <n> ms line (the script prints
p95: ${p95.toFixed(3)} ms (max … )). The gate:
- FAIL when the child exits non-zero (the ladder's own gate failed) — the
evidence must carry the child's own
GATE FAILline verbatim (the mitigation ladder text is part of the evidence). - PASS when p95 ≤
P95_BUDGET_MS = 8(the same constant the script uses — 20% under the 10 ms free ceiling). - When the graph binary
routing/data/kl-roadgraph.v1.binis ABSENT, return{ pass: false, notRun: true, evidence: "kl-roadgraph.v1.bin is absent — build it (node routing/scripts/build-road-graph.mjs) and re-run" }(a missing graph is NOT a pass). - The evidence string carries the measured p95, max, mean and n, PLUS the child's LADDER HISTORY context is NOT re-printed — the record cites it.
- ALSO record the free-plan comparison:
10 ms/invocationis the ceiling; the gate printsp95 X ms <= 8 ms budget (10 ms free-plan ceiling).
runBundleBudgetGate() — the BA9 measurement, re-implemented in the
library (the BA9 inputs, the gzip sum, the 204800 byte ceiling). Print the
measurement (route bundle budget: <n> / 204800 gz bytes) — the budget is
VISIBLE on every run (D-11's law). FAIL above the ceiling, with the message
carrying the overage.
- Additionally measure the TOTAL gzipped size of
web/dist/assets/*.jsand print it against a stated ceiling of 500000 bytes (500 KB gz) for the whole app bundle — the app bundle's real number goes in the record; if it exceeds the ceiling, the gate FAILS with the overage. - Also measure the Worker bundle's uncompressed size if
bot/distexists; when it does not, report the number as NOT MEASURED (not as zero).
runKvWriteClassGate() — the write-class inventory. Scan bot/src for
every .put( call site and classify each by the key family it writes
(session: / chat: / loc: / guidance:), recording the file, the line and
the TTL constant referenced. Assert every write references a NAMED constant
(the PG1 law, re-checked here from the budget side).
- THE CRON LAW: read
bot/src/webhook.ts, extract therunGuidanceCronTickfunction body (fromasync function runGuidanceCronTick(to the matching closing brace — a simple brace counter is fine), and assert that body contains NO.put(and NO.delete(. The evidence states the cron write count (0) explicitly. The cron READS (.get,.list) and SENDS to Telegram — those are not KV writes. - Also print the per-class TTL table (family → constant → seconds) by READING
the constants out of the three
bot/src/kv/*.tsfiles with a regex onexport const <NAME> = <n>;— never hard-code the numbers.
runSubrequestClassGate() — the SUBREQUEST inventory (D-06 names
"Worker CPU/subrequests" — the 50-subrequests-per-request free-plan ceiling
must have a number beside it, not just the ceiling). The gate is a
SOURCE-DERIVED per-request-class inventory, and it must say plainly which
numbers are measured and which are not:
- Enumerate the Worker request classes from
bot/src/webhook.ts's route table (read the file; the classes are the request paths the Worker answers — e.g. the session mint, the location pin, the guidance envelope, the route endpoint, the Telegram webhook). - For EACH class, count the EXTERNAL subrequests the code path can make.
Two facts are derivable from the source and MUST be recorded:
- the route endpoint (
/api/route) makes 0 external subrequests — the routing graph is loaded from the bundled KV binding, not fetched, and the A* search is pure CPU; - the cron tick's Telegram send is the ONLY recurring external-subrequest
class — one
fetchto the Telegram Bot API per due guidance envelope. KV operations are NOT subrequests — Cloudflare counts them against the separate KV-operations ceiling (1,000/invocation) — so KV ops are reported in the KV-write gate, and this gate says so explicitly rather than silently omitting them.
- the route endpoint (
- Where a class's subrequest count CANNOT be derived from the source (for
example a count that depends on the number of live envelopes), print
NOT MEASUREDwith the reason — never a zero and never an estimate. - The gate PRINTS the per-class table (class → external subrequests → budget 50/request → headroom) and FAILS if any class exceeds 50.
- HONESTY REQUIREMENT: this gate is a SOURCE-DERIVED inventory, not a live
platform measurement. The gate's evidence string MUST say so verbatim
("source-derived inventory, not a live Workers Logs metric"). The live
Workers Logs subrequest metric is a SEPARATE measurement and is NOT
available outside
--live— when the live path cannot produce it, the row readsnot measured outside --liveand never a fabricated number.
runGoldenCorpusGate() — the golden corpora (SC4.a / SC4.b). The
budget audit ORCHESTRATES the corpora so the ONE command that produces the
budget record also proves the corpora are green (the VALIDATION map names
this gate as the enforcement point for SC4.a / SC4.b):
- Spawn
npm run routing:corpusandnpm run search:corpusas child processes (cwd = repo root), exactly asrunCpuLadderGatespawnsrouting/scripts/measure-route-cpu.mjs— RE-RUN, never re-implemented. - FAIL when either child exits non-zero, carrying that child's own failure output verbatim in the evidence.
- PASS when both exit 0, and the evidence names the two corpora by id.
runCacheHeaderGate() — the live header probe. Re-use the probe from
scripts/lib/cloudflare-probes.mjs (import runAssetHeaderProbe from it — do
NOT fork it) when the credentials/origin are available; otherwise return
{ pass: false, notRun: true, evidence: "live cache-header probe not run — no deployed origin or credentials" }.
Print the observed cache-control for the HTML entry and for a hashed
/assets/* file, and note that CF-Cache-Status is probabilistic per
Cloudflare's own footnote (the evidence quotes it, it does not interpret it).
<acceptance_criteria>
- scripts/lib/budget-gates.mjs exists and exports runCpuLadderGate, runBundleBudgetGate, runKvWriteClassGate, runSubrequestClassGate, runCacheHeaderGate, runGoldenCorpusGate
- grep -c "measure-route-cpu" scripts/lib/budget-gates.mjs returns ≥ 1 (the ladder is RE-RUN, not re-implemented)
- grep -c "P95_BUDGET_MS = 8" scripts/lib/budget-gates.mjs returns ≥ 1
- grep -c "204800" scripts/lib/budget-gates.mjs returns ≥ 1 (the BA9 ceiling)
- grep -c "gzipSync" scripts/lib/budget-gates.mjs returns ≥ 1
- grep -c "runGuidanceCronTick" scripts/lib/budget-gates.mjs returns ≥ 1 (the cron zero-write law is source-proven)
- grep -c "routing:corpus" scripts/lib/budget-gates.mjs returns ≥ 1 and grep -c "search:corpus" scripts/lib/budget-gates.mjs returns ≥ 1 (the golden corpora are RE-RUN)
- grep -c "subrequest" scripts/lib/budget-gates.mjs returns ≥ 1 (the 50/request ceiling has an inventory beside it)
</acceptance_criteria>
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && node --check scripts/lib/budget-gates.mjs && node -e "import('./scripts/lib/budget-gates.mjs').then(async m=>{for(const k of ['runCpuLadderGate','runBundleBudgetGate','runKvWriteClassGate','runSubrequestClassGate','runCacheHeaderGate','runGoldenCorpusGate']){const r=await mk;console.log(r.id+': '+(r.notRun?'NOT RUN':(r.pass?'PASS':'FAIL'))+' — '+String(r.evidence).slice(0,160));}})"
The six budget gates measure and print the CPU p95 (by re-running the
existing ladder), the bundle bytes, the KV write classes with the cron's
zero-write count, the per-class subrequest inventory, the live cache
headers, and the golden corpora (by re-running them) — each with a
NOT-RUN path that is never a pass.
Behaviour:
- Accepts an optional
--liveflag. Without it, the cache-header gate is SKIPPED and reported asNOT RUN (pass --live). - Runs
runCpuLadderGate(),runBundleBudgetGate(),runKvWriteClassGate(),runSubrequestClassGate(),runGoldenCorpusGate(); with--live, alsorunCacheHeaderGate(). - Prints ONE line per gate:
<id>: PASS|FAIL|NOT RUN — <evidence>. - Prints the FREE-TIER COMPARISON TABLE to stdout — every ceiling from
07-RESEARCH.md §Pattern 5 with the CURRENT value beside it where the audit
measured one, and
not measuredwhere it did not. This table is the console form ofdocs/BUDGETS.md. TheWorker subrequests | 50/requestceiling MUST appear in this table with the per-class inventory beside it (never a bare ceiling with no number). - Prints a summary line:
BUDGET AUDIT CLEAN — <n> gate(s) within budget/BUDGET AUDIT FAILED — <n> gate(s) over budget/BUDGET AUDIT NOT RUN — <reason>. - EXIT CODES:
0all required gates passed;1at least one gate FAILED;2environment drift (a required measurement could not run — a missing graph binary, a missing build). A NOT RUN is never a pass. - The output carries NO secret and NO coordinate.
Add to the ROOT package.json scripts:
"budget-audit": "node scripts/budget-audit.mjs",
Create docs/BUDGETS.md — the checked-in budget record (D-06's "one
document"). Follow the docs/CONFIGURATION.md clinical prose style. It MUST
contain:
- A title
# Budgetsand a dated front-matter block naming the measurement date, the commit SHA (git rev-parse --short HEAD), and the measurement host. - A
## Free-plan ceilingstable — EVERY row from 07-RESEARCH.md §Pattern 5, with the SOURCE cited (Cloudflare Workers limits / KV limits / pricing). - A
## Current measurementstable — one row per measurement:| Measurement | Current | Budget | Headroom | Evidence |with rows for: the A* CPU p95 (local ladder), the app bundle gz bytes, the route payload gz bytes, the Worker bundle size, the KV write count per request class (session mint / location pin / guidance envelope / cron = 0), the static asset cache headers, the KV daily write estimate, the subrequests per request class, and the golden-corpus status. The Current column carries the REAL measured number from the run; the Headroom column is the budget minus the current value; the Evidence column names the gate id that produced it. - A
## Subrequests per request classsection — the D-06 subrequest row. It MUST contain a table with one row per Worker request class:| Request class | External subrequests | Budget (50/request) | Headroom | Source |. The table MUST include:- the route endpoint (
/api/route) — 0 external subrequests (the routing graph comes from the bundled KV binding; the A* search is pure CPU) — with the source site cited; - the cron guidance tick — the ONLY recurring external-subrequest class (one Telegram Bot API send per due envelope) — with the source site cited;
- one row for every other Worker request class the route table answers.
A row for KV operations with the note that KV operations are NOT
subrequests — they are billed against the separate KV-operations ceiling
(1,000/invocation) and are tracked in the KV-write row.
Every row whose count cannot be derived from the source MUST read
not measuredwith the reason — never0, never an estimate. The section MUST state, in one sentence, that the subrequest counts are a SOURCE-DERIVED INVENTORY, not a live Workers Logs metric; where the live metric cannot be produced, the record saysnot measured outside --liveand does not substitute a number.
- the route endpoint (
- A
## The cron writes zerosection — one honest paragraph explaining WHY (the tick lists, reads, and sends; it never writes), and naming the source gate that proves it. - A
## Golden corporasection — one short paragraph naming the two corpora (routing:corpus,search:corpus), the gate id that re-runs them (golden-corpus), and the outcome from THIS run. - A
## Cross-metro CPU degradation (the c3 class)section — the honest escalation from Phase 6: short product routes (2–8 km) measure 2–8 ms (inside the budget); cross-metro routes (20–30 km) measured p95 38–93 ms and degrade HONESTLY to the router-pending state; the mitigation ladder is (a) tightenDRIVABLE_CLASSES+ rebuild — rejected on correctness (12/25 pairs unreachable), (b) bidirectional A* — kept (sound, no worse), (c) Workers Paid — rejected ($0 law), and the committed upgrade lever is the ALT landmark preprocessing (a NEXT-MILESTONE item, not this phase). Citerouting/scripts/measure-route-cpu.mjs's LADDER HISTORY and the 06-04 plan summary as the evidence. - A
## Static assetssection — the_headerspolicy and the observed live headers (or an honest "not measured in this run"). - A
## Statementsection — the honest one-paragraph claim, naming which numbers are measured, which are NOT measured (the live Worker CPU metric — the local ladder and the live Workers Logs CPU number are DIFFERENT measurements and neither substitutes for the other), and which are estimated. - A
## Signatureblock with the maintainer's name and date lines left as________________(the human signs at the launch gate).
FILL the tables with the REAL run output — run npm run budget-audit and
paste the printed numbers. Do NOT invent, round optimistically, or copy a
number from another phase's summary.
<acceptance_criteria>
- scripts/budget-audit.mjs exists and contains the literals BUDGET AUDIT CLEAN, BUDGET AUDIT FAILED, BUDGET AUDIT NOT RUN
- grep -c "process.exit" scripts/budget-audit.mjs returns ≥ 3
- grep -c '"budget-audit"' package.json returns exactly 1
- grep -c "runGoldenCorpusGate\|runSubrequestClassGate" scripts/budget-audit.mjs returns ≥ 2 (the CLI runs BOTH new gates)
- docs/BUDGETS.md exists and contains the literals ## Free-plan ceilings, ## Current measurements, ## Subrequests per request class, ## The cron writes zero, ## Golden corpora, ## Cross-metro CPU degradation, ## Static assets, ## Statement, ## Signature
- grep -c "50/request\|50 /request" docs/BUDGETS.md returns ≥ 1 (the subrequest ceiling is stated)
- npm run budget-audit exits 0 on the current tree and prints BUDGET AUDIT CLEAN
</acceptance_criteria>
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm run budget-audit 2>&1 | tail -35 && npm run budget-audit >/dev/null 2>&1; echo "exit=$?"
npm run budget-audit measures and prints every budget with its free-plan
ceiling (including the per-request-class subrequest inventory and the
golden-corpus status), exits 0/1/2 correctly, and docs/BUDGETS.md
carries the record with the real numbers, the subrequests section, the
c3 escalation, and a human signature block.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| The free-plan ceiling → the measurement | The ceilings are documented facts; the current numbers are measurements. A stale ceiling silently mis-states headroom. |
| The cron tick → the KV namespace | The cron is the one code path that runs without a user request; a stray write there burns the 1,000/day KV write budget. |
| Static assets → the browser cache | A wrong cache policy serves a stale HTML entry that points at deleted hashed assets — a broken app for every driver. |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-07-18 | Denial of Service | the cron tick — a KV write in the scheduled path burns the free-plan write budget (1,000/day) | mitigate | runKvWriteClassGate extracts the runGuidanceCronTick body and asserts it contains no .put( and no .delete( — the zero-write law is source-proven, not claimed; the evidence prints the count (0). |
| T-07-19 | Denial of Service | the Worker CPU budget — an A* p95 above 10 ms makes the Worker return Error 1102 | mitigate | The ladder is RE-RUN (not re-implemented) and its p95 is compared to the 8 ms budget; a non-zero child exit FAILS the gate with the child's own mitigation-ladder text. The record additionally states the c3 honest-degradation class for cross-metro routes. |
| T-07-20 | Tampering | web/public/_headers — a stale HTML entry pointing at deleted hashed assets |
mitigate | Only /assets/* (content-hashed, immutable for 31556952 s) gets the immutable rule; the HTML entry is max-age=0, must-revalidate. The live header probe prints the OBSERVED headers as evidence rather than assuming the policy took effect. |
| T-07-21 | Information Disclosure | the budget audit's own output | accept | The audit prints sizes, counts, timings and header values only — no secret, no token, no coordinate. The bot token and webhook secret live only in bot/.dev.vars / Worker secrets and are never read by this tool. |
| T-07-22 | Spoofing | the budget verdict — a missing graph binary or build reported as "within budget" | mitigate | A missing graph binary returns {pass:false, notRun:true} and the CLI maps a NOT RUN to exit code 2 with BUDGET AUDIT NOT RUN — a skip is never a pass. |
| T-07-23b | Spoofing | the subrequest inventory — a fabricated zero beside the 50/request ceiling | mitigate | runSubrequestClassGate derives every class's external-subrequest count from the Worker's own route table and PRINTS the source site; a count that cannot be derived reads not measured with its reason — never 0, never an estimate. The gate's evidence states that the inventory is SOURCE-DERIVED, not a live Workers Logs metric, and the record names the live metric not measured outside --live rather than substituting a number. |
| </threat_model> |
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"
cd /home/C-Fu/araharah
npm run build -w theme && npm run build -w web
npm run budget-audit
npm run budget-audit -- --live
The first must exit 0 with BUDGET AUDIT CLEAN. The second must exit 0 (with
the live header evidence) or exit 2 with BUDGET AUDIT NOT RUN — it must
NEVER exit 0 while reporting a measurement as skipped.
<success_criteria>
web/public/_headersexists and reachesweb/dist/, carrying the immutable hashed-asset rule and the revalidating HTML rule, and nothing else.npm run budget-auditmeasures and prints the A* CPU p95 (by re-running the existing ladder), the bundle bytes, the KV write classes with the cron's zero-write count, the per-request-class subrequest inventory, the golden corpora (by re-running them), and the cache headers, each against a documented free-plan ceiling.- The subrequest row carries a per-request-class inventory (the route endpoint = 0 external subrequests; the cron tick's Telegram send = the only recurring class; KV ops tracked separately against their own ceiling), and any count that cannot be derived from the source reads
not measured— never0, never an estimate. - The exit codes are 0 / 1 / 2 and a NOT RUN is never a pass.
docs/BUDGETS.mdcarries the record with the real numbers, the## Subrequests per request classsection, the## Golden corporasection, the c3 cross-metro degradation class, the cron zero-write explanation, and a human signature block. </success_criteria>