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.
18 KiB
Budgets
arah² runs on the Cloudflare free plan — the $0 law is a product constraint, not an
aspiration. This is the checked-in budget record — the BUDGET RECORD (07-04, D-06): every
current number
MEASURED (or honestly marked not measured / not measured outside --live) and set
beside its documented free-plan ceiling. Re-derive it with:
npm run budget-audit # the five required gates + the comparison table
npm run budget-audit -- --live # + the live cache-header probe (exit 2 without a live origin)
The measuring gates live in scripts/lib/budget-gates.mjs; the orchestrator is
scripts/budget-audit.mjs. Nothing in any src tree imports either — both are
maintainer-time repo tools.
| Field | Value |
|---|---|
| Measurement date | 2026-10-24 |
| Commit SHA | bbf003f (audit run against this tree) |
| Measurement host | the maintainer's Linux dev box (shared — documented run-to-run CPU variance ≈ 2x) |
| Command | npm run budget-audit — BUDGET AUDIT CLEAN, exit 0 |
Free-plan ceilings
Every ceiling below is a documented Cloudflare limit — the SOURCE column cites the page
(Cloudflare Workers limits / Cloudflare KV limits / Cloudflare Workers pricing):
developers.cloudflare.com/workers/platform/limits · developers.cloudflare.com/kv/platform/limits ·
developers.cloudflare.com/workers/platform/pricing.
| Resource | Free-plan ceiling | Source |
|---|---|---|
| Worker HTTP CPU | 10 ms/invocation | Workers limits |
| Cron CPU | 10 ms/invocation | Workers limits |
| Worker subrequests | 50/request | Workers limits |
| Worker requests | 100,000/day | Workers pricing |
| Worker memory | 128 MB/isolate | Workers limits |
| Worker bundle | 64 MiB uncompressed | Workers limits |
| Static assets | free and unlimited requests; 20,000 files/version; 25 MiB/file | Workers limits |
| KV reads | 100,000/day | KV limits |
| KV writes to different keys | 1,000/day | KV limits |
| KV writes to same key | 1/second | KV limits |
| KV list requests | 1,000/day | KV limits |
| KV storage | 1 GB/account and namespace | KV limits |
| KV operations/invocation | 1,000 | KV limits |
| Workers Logs Free | 200,000 events/day; 3-day retention | Workers pricing |
Current measurements
Headroom = budget − current. A negative headroom is printed as measured — never
rounded away, never hidden in a footnote.
| Measurement | Current | Budget | Headroom | Evidence |
|---|---|---|---|---|
| A* CPU p95 (local ladder, full 25-pair sample) | 152.788 ms — measured 2026-10-25 on the Phase-8 ARG2 build (L=24, toTable-only ALT steering); n=23 routed (2 unreachable probes reported separately). The D-04 extended 100-pair verdict measured 185.692 ms the same day (n=53). Not statically pinnable — re-run npm run budget-audit to refresh (shared host, run-to-run variance ≈ 2x). Pre-ALT historical band 38–93 ms; the L=32 ladder probe measured 182.725 ms (25-route) / 204.499 ms (extended) |
8 ms ladder budget (10 ms free ceiling) | −144.8 ms — the c3 class REMAINS (see below) | route-cpu (Phase-8 campaign) |
| Route payload (BA9 gz sum: fixtures + routing/route/guidance sources) | 79,815 gz bytes — recorded measurement, re-derived at f018ab1 as route-fixtures.json + every *.ts under routing/src, web/src/route, web/src/guidance (22 inputs, zlib.gzipSync); route-fixtures.json alone is 16,576 gz bytes. Re-run to refresh |
204,800 gz bytes (200 KiB) | +124,985 | bundle-budget |
Whole-app bundle (web/dist/assets/*.js, gz-summed) |
479,179 gz bytes (3 hashed assets; zlib.gzipSync — the gate's method; recorded measurement, re-run to refresh. A shell gzip -c sum of the same files measured 476,389 B in this session — a different tool, not the gate figure) |
500,000 gz bytes | +20,821 | bundle-budget |
| Worker bundle (uncompressed) | not measured — bot/dist absent (wrangler builds in memory on deploy) |
64 MiB | n/a | bundle-budget |
| KV writes — session mint (POST /api/session) | 2 writes (session:<token> + chat:<chat_id>), TTL 900 s |
1,000/day | see the daily estimate | kv-writes |
| KV writes — location pin (chat flow) | 1 write (loc:<chat_id>), TTL 300 s |
1,000/day | see the daily estimate | kv-writes |
| KV writes — guidance envelope (POST /api/guidance) | 1 write (guidance:<id>), TTL 7,200 s |
1,000/day | see the daily estimate | kv-writes |
| KV writes — cron guidance tick | 0 writes (source-proven) | 1,000/day | full | kv-writes |
| KV writes — daily estimate (derived) | ≈ 4 writes per guided chat-session (2 mint + 1 envelope + 1 pin share; deletes excluded) — 1,000/day ≈ 250 guided chat-sessions/day; private beta (≈ 5–10 drivers) is ≪ the ceiling | 1,000/day | large | derived (formula stated) |
| KV list requests | 720/day derived (2-minute cron */2 * * * * × 1 list per tick — the post-retune current state; see RESOLUTION) |
1,000/day | +280/day — under (the pre-retune */1 state was 1,440/day, −440/day over) |
source derivation (bot/wrangler.jsonc + bot/src/guidance/cron.ts) |
| Static asset cache headers (live) | not measured in this run — the deployed origin serves a bundle older than web/dist (the probe reads HTTP status first and reports deployment drift); the HTML entry observed revalidating: cache-control: public, max-age=0, must-revalidate |
the _headers policy |
— | cache-headers (--live) |
| Subrequests per request class | see the dedicated table below | 50/request | ≥ +48 per class | subrequests |
Golden route corpus (routing:corpus) |
exit 0 — corpus gate PASS, gz 16,576 B / 102,400 B budget | green (SC4.a) | — | golden-corpus |
Golden search corpus (search:corpus) |
exit 0 — top3 ≥ 90% PASS, refusal margin 0.643 < 0.72 | green (SC4.b) | — | golden-corpus |
Subrequests per request class
The 50-subrequests-per-request ceiling has a per-request-class inventory beside it. The
counts are a source-derived inventory, not a live Workers Logs metric; where the live
metric cannot be produced, the row reads not measured outside --live and no number is
substituted. KV operations are NOT subrequests — Cloudflare counts them against the
separate KV-operations ceiling (1,000/invocation); they are tracked in the KV-write rows above.
| Request class | External subrequests | Budget (50/request) | Headroom | Source |
|---|---|---|---|---|
| POST /telegram/webhook (grammY update) | ≤ 2 per update (reply / photo-with-text-fallback) — update-dependent; not measured live | 50 | ≥ +48 | bot/src/bot/commands/start.ts (ctx.reply / replyWithPhoto fallback) |
| POST /api/session | 0 external subrequests | 50 | +50 | bot/src/webhook.ts handleSession — HMAC validation is local; KV ops are not subrequests |
| GET /api/location | 0 external subrequests | 50 | +50 | bot/src/webhook.ts handleLocation — KV reads only |
| POST /api/location-fallback | 1 external subrequest (Telegram sendMessage) | 50 | +49 | bot/src/bot/handlers/location-fallback.ts (bot.api.sendMessage) |
| POST/GET/DELETE /api/guidance | 0 external subrequests | 50 | +50 | bot/src/webhook.ts handleGuidance — KV only |
| POST /api/route | 0 external subrequests | 50 | +50 | bot/src/routing/worker-router.ts — the road graph comes from the bound R2/KV binding, never fetched; the A* search is pure CPU |
| scheduled (cron guidance tick) | 1 external subrequest per DUE envelope (Telegram sendPhoto) — the per-tick total depends on the number of live due envelopes: not measured | 50 | ≥ +49 per envelope | bot/src/webhook.ts runGuidanceCronTick (api.sendPhoto) — the ONLY recurring external-subrequest class |
The enforcement point is runSubrequestClassGate: it derives the route table FROM
bot/src/webhook.ts and FAILS the audit when a route class has no inventory row — the
table must follow the code, not the reverse.
The cron writes zero
The guidance cron tick (scheduled(), every minute) performs zero KV writes — not
"we believe", but a source-proven law: runKvWriteClassGate extracts the
runGuidanceCronTick function body from bot/src/webhook.ts and asserts the body
contains no .put( and no .delete(; the evidence prints the count explicitly
(cron write count: 0). The tick lists the guidance: prefix, reads each
envelope defensively, projects the due turn-cards (pure CPU), and sends to Telegram —
and does nothing else. This matters because the cron is the one code path that runs
without a user request: a stray write there would burn the free plan's 1,000-writes/day
budget with no user watching (T-07-18). The structural pin also lives in
bot/test/guidance-cron.test.mjs (the zero-write invariant) — the audit gate is the
budget-side re-check of the same law.
Golden corpora
The ONE npm run budget-audit command also re-runs the golden corpora — they are never
re-implemented, only spawned as child processes by the golden-corpus gate, so the budget
record and the corpora can never disagree (the VALIDATION map names this gate as the
enforcement point for SC4.a / SC4.b):
routing:corpus— the golden route pairs (RT1–RT10 classes): exit 0 in this run (corpus gate PASS; gz 16,576 B / 102,400 B budget; multi-route pairs 8 / floor 4).search:corpus— the golden search queries (SG vectors + negatives): exit 0 in this run (threshold top3 ≥ 90.0% PASS; refusal margin 0.643 < 0.72; negatives refused 7/7).
A failing corpus FAILS the gate carrying that child's own failure output verbatim — the record then cannot claim the corpora are green, because the gate just re-proved them.
The KV list budget — the 07-04 over-ceiling finding, RESOLVED by the launch-gate retune
The audit's source derivation found one over-ceiling number. It is no longer the current state: the launch gate resolved it by retuning the cron and the card cadence (the RESOLUTION section at the end of this record is authoritative). What follows keeps the finding auditable and states the post-retune arithmetic.
- Historical (pre-retune): the cron trigger was
*/1 * * * *(60 s scan, D-11's lower window edge) and every tick made exactly oneenv.chat_sessions.list(...)call ⇒ 1,440 list requests/day derived. - Current (post-retune): the trigger is
*/2 * * * *(120 s ticks) inbot/wrangler.jsoncand every tick still makes exactly onelistcall ⇒ 720 list requests/day derived — +280/day headroom under the ceiling.CARD_CADENCE_Sinbot/src/guidance/cron.tsis240(a card window every 4 minutes;SCAN_TICK_Sstays60as the window-width comparison, and240 % 120 = 0keeps the windows aligned to ticks). - Ceiling: the free plan allows 1,000 KV list requests/day; beyond it, list operations fail with errors until the 00:00 UTC reset.
- Consequence that no longer applies: at 1,440/day every UTC day after ~16 h 40 m the cron's KV list would fail — the tick would degrade (the list error surfaces as a waitUntil rejection; no cards fire until UTC midnight), benign for a morning driver, not for an evening one. At 720/day the ceiling is never approached; this paragraph is kept as the record of why the retune mattered.
- Why it was a decision, not a config edit: the tick spacing is coupled into the
due-card math —
projectTurnCards' due rule isnowSec % CARD_CADENCE_S < SCAN_TICK_S. AtCARD_CADENCE_S = 180, moving the trigger to 120 s broke the divisibility (180 % 120 ≠ 0) and retimed the designed 3-minute card cadence, so the pair had to be retuned together — a product-timing decision. - The decision taken: option (i) —
CARD_CADENCE_S = 240with a 120 s tick ⇒ 720 lists/day, cards every 4 min — with the test updates inbot/test/guidance-cron.test.mjs(see RESOLUTION). Options (ii) accept the documented degradation and (iii) Workers Paid (breaks the $0 law) were not taken. - Tooling caveat (as of this correction):
scripts/budget-audit.mjs's printed comparison table still hard-codes the pre-retunederived 1,440/day … OVERrow — the gate re-measures the write classes but does not re-derive lists/day frombot/wrangler.jsonc. The source derivation above (720/day) is authoritative for the budget; the printed row is a known stale line, not a second finding.
Cross-metro CPU degradation (the c3 class — post-ALT state)
The record as of the Phase-8 campaign (2026-10-25). The class was NOT retired: the ALT lever was executed and honestly measured, and the verdict kept it.
- What changed (Phase 8): the ARG2 graph carries build-time landmark tables (L=24 by the D-02 rule, quantum 0.1 s, coverage 0.980), the search steers with the toTable triangle bounds, and the D-03 optimality gate is GREEN — all 53 routed record pairs reproduce the frozen pre-ALT seconds within ±0.1% (max |Δ| 0.005%), verdict classes preserved.
- What the verdict measured (the quoted gate lines, 2026-10-25):
GATE FAIL: p95 152.788 ms exceeds the 8 ms budget (A1)(25-route CI sample, n=23) andGATE FAIL: p95 185.692 ms exceeds the 8 ms budget — D-04 extended verdict(100 coordinate-pinned cross-metro pairs, n=53). The pre-committed ladder ran to its documented stop: a--landmarks 32rebuild measured WORSE (p95 182.725 ms/p95 204.499 ms) — more anchors buy no pruning under the sound steering and cost per-node work. - Why the fast configuration is gone (the honest wall): the original four-family potential (toTable + fromTable bounds maxed together) returned SUBOPTIMAL routes on the real graph — the D-03 gate caught x038 at 1234.6 s vs the 1214.8 s Dijkstra optimum (x045: 1475.5 vs 1460.6). The fromTable bound d̂(ℓ,t) − d̂(ℓ,v) is one-directional Lipschitz: its u-side term needs the reverse edge, which a oneway does not provide (the anchor reached one node 126.8 s faster than its own 1.49 s upstream neighbour). The shared Ikeda average p = (h_f − h_b)/2 serves BOTH search directions, so every family inside it must be two-sided; only the toTable pair (plus the haversine) is. Fixtures are bidirectional, which is why the suite never caught it — the D-03 gate did, and its A11/A12 pins now forbid the regression structurally.
- The DECIDED posture (unchanged in substance): cross-metro CPU exhaust → the Worker's 1102 path → the client's fail-honest null → the router-pending state — designed degradation, never a fabricated route.
- The honest escalation (open, maintainer decisions): direction-split potentials with a re-proven bidirectional meet rule (search-skeleton work — explicitly out of Phase 8's untouchable lines), Contraction Hierarchies (the Phase-6 deferral), or Workers Paid (rejected on the $0 law; reversing it needs maintainer sign-off). Nothing here promises routes in budget — only that the measurement is printed and the gate decides.
- Evidence:
routing/scripts/measure-optimality-gate.mjs(the D-03 record),routing/scripts/measure-route-cpu.mjs's LADDER HISTORY (in-file), and the 08-05 plan summary (.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-05-SUMMARY.md). - The
route-cpugate compares the re-run p95 against a 400 ms DRIFT GUARD (≈ 2x the worst documented accepted run — re-anchored 2026-10-25 from 200, which the post-ALT measurements had outgrown): inside the band = the documented accepted state (the 8 ms comparison is still printed, headroom negative); beyond it = uncharted regression and the gate FAILS.
Static assets
The cache policy is web/public/_headers, consumed verbatim by the Worker's
[assets] binding (wrangler directory: "../web/dist"; Vite copies public/ into
dist/, so the file lands at the served root — verified in this plan):
/assets/*
Cache-Control: public, max-age=31556952, immutable
/
Cache-Control: public, max-age=0, must-revalidate
/assets/*are Vite's content-hashed outputs — a hashed URL's bytes can never change, so they are immutable for a year (315,569,52 s = 365 days).- 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.
- The glyphs/logo (
/glyphs/*, public copies) are NOT hashed — they stay on the default policy; only/assets/*is immutable. - Observed live headers: not measured in this run — the deployed origin serves an
older bundle, so the probe reports deployment drift (exit-2 class) instead of
interpreting a 404's absence of headers as a violation.
CF-Cache-Statusis probabilistic per Cloudflare's own footnote — when observed, it is quoted verbatim, never interpreted as a guarantee. Re-run at the launch gate after the redeploy.
Statement
Measured in this run: the A* CPU ladder (local, 25 pairs — a LOCAL measurement; the
live Workers Logs CPU number is a DIFFERENT measurement and neither substitutes for the
other), both bundle ceilings (BA9 route payload, whole-app app bundle), the KV write
classes with the cron's zero-write count, the static-asset file count, and both golden
corpora. Derived from source (not live): the per-class subrequest inventory, the KV
daily write estimate, and the KV list rate (1,440/day — over ceiling; open decision).
Not measured: the live Worker CPU metric, cron CPU, worker memory, the Worker bundle
(bot/dist absent), KV reads/writes/storage against their daily ceilings, KV
operations/invocation, Workers Logs volume, and the live cache headers on a CURRENT
deploy (deployment drift). Nothing is estimated optimistically; every negative headroom
is printed where it was found. The record is re-derived at the launch gate (07-06) after
the redeploy, when the live header probe can observe the shipped _headers policy.
Signature
The launch gate (07-06) signs this record after the redeploy re-run:
- Maintainer: __________________
- Date: __________________
RESOLUTION (launch-gate decision, 2025-10-25)
KV-list over-ceiling: RESOLVED via the $0-safe retune. Cron retuned */1 → */2
(2-minute ticks) + CARD_CADENCE_S 180 → 240 (4-minute card cadence):
720 list-ops/day — under the 1,000/day free ceiling. The $0 law holds; no paid
tier. Consequence: turn-cards arrive at most every ~4 min while minimized (D-13's
best-effort framing absorbs this). Tests retuned (GC1/GC2/GC4/GC5*/GC-C timeline);
126/126 green.