From 79a302caf50ff9187da452aa61eccebeb1de9c55 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sun, 25 Oct 2026 20:06:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(08-05):=20route-cpu=20gate=20law=20re-ancho?= =?UTF-8?q?red=20to=20the=20post-ALT=20measured=20truth=20(escalation=20re?= =?UTF-8?q?cord=20=E2=80=94=20no=20retirement)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/BUDGETS.md | 125 +++++++++++++++++++---------- scripts/lib/budget-gates.mjs | 54 ++++++++----- scripts/test/budget-audit.test.mjs | 4 + 3 files changed, 120 insertions(+), 63 deletions(-) diff --git a/docs/BUDGETS.md b/docs/BUDGETS.md index bf17deb..f3be93e 100644 --- a/docs/BUDGETS.md +++ b/docs/BUDGETS.md @@ -55,16 +55,16 @@ rounded away, never hidden in a footnote. | Measurement | Current | Budget | Headroom | Evidence | | --- | --- | --- | --- | --- | -| A* CPU p95 (local ladder, full 25-pair sample) | 83.989 ms (runs today: 67.5–84.0 ms; documented band 38–93 ms) | 8 ms ladder budget (10 ms free ceiling) | **−76.0 ms** — the accepted c3 class (see below) | `route-cpu` | -| Route payload (BA9 gz sum: fixtures + routing/route/guidance sources) | 79,815 gz bytes | 204,800 gz bytes (200 KiB) | +124,985 | `bundle-budget` | -| Whole-app bundle (`web/dist/assets/*.js`, gz-summed) | 479,178 gz bytes (3 hashed assets) | 500,000 gz bytes | +20,822 | `bundle-budget` | +| 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:` + `chat:`), TTL 900 s | 1,000/day | see the daily estimate | `kv-writes` | | KV writes — location pin (chat flow) | 1 write (`loc:`), TTL 300 s | 1,000/day | see the daily estimate | `kv-writes` | | KV writes — guidance envelope (POST /api/guidance) | 1 write (`guidance:`), 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 | **1,440/day derived** (60 s cron × 1 `list` per tick) | 1,000/day | **−440/day — OVER** (discovered finding; see below) | source derivation | +| 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` | @@ -121,52 +121,91 @@ enforcement point for SC4.a / SC4.b): 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 — a DISCOVERED over-ceiling finding (07-04) +## 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 that the launch gate must see: +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. -- **Current:** the cron trigger is `*/1 * * * *` (60 s scan, D-11's window) and every tick - makes exactly one `env.chat_sessions.list(...)` call ⇒ **1,440 list requests/day derived**. +- **Historical (pre-retune):** the cron trigger was `*/1 * * * *` (60 s scan, D-11's + lower window edge) and every tick made exactly one `env.chat_sessions.list(...)` call ⇒ + **1,440 list requests/day derived**. +- **Current (post-retune):** the trigger is `*/2 * * * *` (120 s ticks) in + `bot/wrangler.jsonc` and every tick still makes exactly one `list` call ⇒ **720 list + requests/day derived** — **+280/day headroom under the ceiling**. `CARD_CADENCE_S` in + `bot/src/guidance/cron.ts` is `240` (a card window every 4 minutes; `SCAN_TICK_S` stays + `60` as the window-width comparison, and `240 % 120 = 0` keeps 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:** every UTC day after ~16 h 40 m, the cron's KV list fails — the tick - degrades (the list error surfaces as a waitUntil rejection; no cards fire until UTC - midnight). Benign for a morning driver, not benign for an evening one. -- **Why this plan does not auto-fix it:** the tick spacing is coupled into the due-card - math — `projectTurnCards`' due rule is `nowSec % CARD_CADENCE_S < SCAN_TICK_S` with - `CARD_CADENCE_S = 180` and `SCAN_TICK_S = 60`. Moving the trigger to 120 s (D-11's - window upper edge, 720 lists/day) breaks the divisibility (`180 % 120 ≠ 0`) and retimes - the designed 3-minute card cadence — a product-timing decision, not a config-only - change. -- **The maintainer's options:** (i) retune the pair (e.g. `CARD_CADENCE_S = 240` with a - 120 s tick ⇒ 720 lists/day, cards every 4 min) — a deliberate product decision with - test updates in `bot/test/guidance-cron.test.mjs`; (ii) accept the documented - degradation; (iii) Workers Paid — breaks the $0 law, requires sign-off. This is - recorded as an open decision for the 07-06 launch gate. +- **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 is `nowSec % CARD_CADENCE_S < SCAN_TICK_S`. + At `CARD_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 = 240` with a 120 s tick ⇒ 720 + lists/day, cards every 4 min — with the test updates in + `bot/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-retune `derived 1,440/day … OVER` row — the + gate re-measures the write classes but does not re-derive lists/day from + `bot/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) +## Cross-metro CPU degradation (the c3 class — post-ALT state) -The honest escalation from Phase 6 (plan 06-04), restated as the record's CPU row: +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. -- Short product routes (2–8 km) measure **2–8 ms** on a quiet isolate — inside the 8 ms - ladder budget. The full 25-pair sample (the corpus demo-script pairs plus 10 - quadrant-sampled cross-city worst cases) measures **p95 38–93 ms** across runs — over - budget, and this box's shared-load variance is ≈ 2x. -- The mitigation ladder was exhausted WITHIN the $0 law: (a) tighten `DRIVABLE_CLASSES` + - rebuild — **rejected on correctness** (12/25 pairs unreachable; the dropped classes are - the fine mesh tying landmarks to the arterials), reverted; (b) bidirectional A* — - **kept** (provably optimal, no worse on the p95 class, better on short product routes); - (c) Workers Paid — **rejected** ($5/mo breaks the $0 law; needs maintainer sign-off). -- The DECIDED posture (c3, now): 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 committed upgrade lever is the **ALT landmark preprocessing** - (build-time, still $0 at runtime) — a NEXT-MILESTONE item, not this phase. -- Evidence: `routing/scripts/measure-route-cpu.mjs`'s LADDER HISTORY (in-file) and the - 06-04 plan summary (`.planning/phases/06-turn-by-turn-guidance-live-driving/06-04-SUMMARY.md`). -- The `route-cpu` gate therefore compares the re-run p95 against a 200 ms DRIFT GUARD - (≈ 2x the worst documented run): inside the band = the documented accepted state (the - 8 ms comparison is still printed, headroom negative); beyond it = uncharted regression - and the gate FAILS. +- **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) and `GATE 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 32` + rebuild 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-cpu` gate 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 diff --git a/scripts/lib/budget-gates.mjs b/scripts/lib/budget-gates.mjs index b3697fe..37bfd51 100644 --- a/scripts/lib/budget-gates.mjs +++ b/scripts/lib/budget-gates.mjs @@ -21,20 +21,31 @@ * runCacheHeaderGate() id: cache-headers — the LIVE header probe (re-used from cloudflare-probes.mjs) * runGoldenCorpusGate() id: golden-corpus — RE-RUNS `routing:corpus` + `search:corpus` as child processes * - * The route-cpu verdict law (the c3 class, decided in Phase 6 / 06-04): the - * full-sample ladder p95 (25 pairs incl. the 10 adversarial quadrant worst - * cases) MEASURED 38–93 ms across runs — over the 8 ms product budget, and - * the mitigation ladder was exhausted WITHIN the $0 law ((a) rejected on - * correctness — 12/25 pairs unreachable, (b) bidirectional A* shipped, - * (c) Workers Paid rejected on the $0 mandate). The DECIDED posture is the - * honest degradation: cross-metro CPU exhaust → Error 1102 → fail-honest - * null → the client's router-pending state; the committed upgrade lever is - * ALT landmark preprocessing (next milestone). This gate therefore checks - * the measured p95 against the DOCUMENTED accepted band (a 200 ms drift - * guard ≈ 2x the worst documented run): a p95 inside the band is the - * documented state (the record still prints it against the 8 ms budget with - * negative headroom — nothing is hidden); a p95 beyond it is UNCHARTED - * regression and FAILS. + * The route-cpu verdict law (the c3 class, decided in Phase 6 / 06-04, + * re-measured in Phase 8 / 08-05): the full-sample ladder p95 (25 pairs incl. + * the 10 adversarial quadrant worst cases) MEASURED 38–93 ms pre-ALT across + * runs — over the 8 ms product budget, and the mitigation ladder was + * exhausted WITHIN the $0 law ((a) rejected on correctness — 12/25 pairs + * unreachable, (b) bidirectional A* shipped, (c) Workers Paid rejected on + * the $0 mandate). The DECIDED posture is the honest degradation: cross-metro + * CPU exhaust → Error 1102 → fail-honest null → the client's router-pending + * state. The ALT landmark lever was EXECUTED in Phase 8 (ARG2 binary + + * landmark steering) and MEASURED on 2026-10-25: the optimality gate is green + * (route seconds equal the frozen pre-ALT record within ±0.1%), but the CPU + * verdict FAILED honestly — 25-route p95 152.788 ms, extended 100-pair p95 + * 185.692 ms (both far over the 8 ms budget; the L=32 probe measured worse: + * 182.725 / 204.499 ms). The sound-but-slower toTable-only steering is + * mandatory: the fromTable families are one-directional Lipschitz (their + * u-side term needs the reverse edge, which a oneway does not provide) and + * the shared Ikeda average p = (h_f − h_b)/2 is consistent only with + * two-sided families — the c3 class therefore REMAINS the accepted designed + * degradation, and the honest escalation (direction-split potentials with a + * re-proven meet rule, Contraction Hierarchies, or Workers Paid) stays open + * for the maintainer. This gate checks the measured p95 against the + * DOCUMENTED accepted band (a drift guard ≈ 2x the worst documented + * accepted run): a p95 inside the band is the documented state (the record + * still prints it against the 8 ms budget with negative headroom — nothing + * is hidden); a p95 beyond it is UNCHARTED regression and FAILS. * * Test injectability (the privacy-audit suite precedent): every gate accepts * opts-only path overrides; production callers pass nothing. @@ -55,10 +66,13 @@ export const P95_BUDGET_MS = 8; export const ROUTE_PAYLOAD_BUDGET_BYTES = 204800; /** The whole-app bundle ceiling (D-06's record row): 500 KB gz for ALL of web/dist/assets/*.js. */ export const APP_BUNDLE_BUDGET_BYTES = 500000; -/** The drift guard for the accepted c3 band: the documented accepted p95 tops - * at 93.6 ms (06-04's worst shared-box run); beyond 200 ms nothing the - * escalation examined applies — uncharted regression. */ -export const ROUTE_CPU_DRIFT_GUARD_MS = 200; +/** The drift guard for the accepted c3 band: the documented accepted runs top + * at 185.692 ms (the shipped L=24 ARG2 config's extended-100 verdict, + * 2026-10-25); the pre-ALT band topped at 93.6 ms (06-04's worst shared-box + * run) and the L=32 probe measured 204.499 ms. The guard sits at ≈ 2x the + * worst documented shipped-config run — beyond it nothing the escalation + * examined applies: uncharted regression. */ +export const ROUTE_CPU_DRIFT_GUARD_MS = 400; /** A missing environment is the exit-2 class: pass:false + notRun:true. */ function notRun(id, reason) { @@ -122,9 +136,9 @@ export function runCpuLadderGate(opts = {}) { // state (see the header's verdict law); beyond the drift guard = FAIL. const pass = !uncharted; const verdict = uncharted - ? `UNCHARTED REGRESSION — p95 beyond the ${ROUTE_CPU_DRIFT_GUARD_MS} ms drift guard (the documented accepted c3 band tops at ~93 ms)` + ? `UNCHARTED REGRESSION — p95 beyond the ${ROUTE_CPU_DRIFT_GUARD_MS} ms drift guard (the documented accepted c3 band tops at 185.7 ms measured, 2026-10-25)` : overBudget - ? `over the ${P95_BUDGET_MS} ms product budget — the DOCUMENTED, ACCEPTED c3 class (cross-metro honest degradation: 1102 → fail-honest null → router-pending; mitigation ladder exhausted within the $0 law; the ALT landmark lever is a next-milestone item)` + ? `over the ${P95_BUDGET_MS} ms product budget — the DOCUMENTED, ACCEPTED c3 class (cross-metro honest degradation: 1102 → fail-honest null → router-pending; the ALT landmark lever was EXECUTED in Phase 8 and measured over budget on 2026-10-25 — optimality green, CPU verdict FAIL — so the class remains; the escalation stays open)` : `within the ${P95_BUDGET_MS} ms product budget`; const evidence = `p95 ${p95.toFixed(3)} ms <= 8 ms budget (10 ms free-plan ceiling) — NOT satisfied as stated: ` + diff --git a/scripts/test/budget-audit.test.mjs b/scripts/test/budget-audit.test.mjs index 35f1e9a..3bfbcb8 100644 --- a/scripts/test/budget-audit.test.mjs +++ b/scripts/test/budget-audit.test.mjs @@ -111,6 +111,10 @@ await test("CPU3 on the current tree the ladder runs, the measured p95 is printe assert.match(r.evidence, /10 ms free-plan ceiling/, "the free-plan ceiling must be printed"); assert.match(r.evidence, /GATE FAIL/, "the child's own GATE FAIL line (cross-metro class) is part of the evidence"); assert.match(r.evidence, /c3/, "the accepted c3 escalation class must be named"); + // 08-05 pin: the record carries the POST-ALT truth — the ALT lever was + // executed and measured over budget (never "a next-milestone item" again). + assert.match(r.evidence, /EXECUTED in Phase 8/, "the executed-and-measured ALT verdict must be named"); + assert.doesNotMatch(r.evidence, /next-milestone/, "the stale next-milestone wording must never return"); }); await test("CPU4 a missing graph binary is notRun — NEVER a pass (T-07-22)", async () => {