docs(07): revise phase 07 plans — wire the safe-area watch, fold the corpora into budget-audit, add the subrequests row, resolve the research open questions

Checker-issue round 1 on the Phase 7 plan set:

BLOCKER (07-01) — watchSafeAreaChanges and watchFullscreenEvents were created
and unit-tested but never called in production: main.ts:74 was the only
safe-area site and was not in files_modified, and the fullscreen events were
never registered. Insets never re-resolved at runtime on a real device while
every gate stayed green.

  - web/src/main.ts joins 07-01's files_modified; task 4 step 8 wires
    watchSafeAreaChanges(getWebApp()) directly after the boot site's
    applySafeArea(resolveSafeInsets(getWebApp())) line.
  - task 4 step 6 registers watchFullscreenEvents once at the guidance-start
    edge in route.ts, with a deliberately no-op callback (events are
    observability, never correctness).
  - New grep acceptance criteria pin BOTH production call sites; two new
    must_haves key_links name them; T-07-06b now names the boot-site call.

WARNING (07-04 / VALIDATION.md) — the golden corpora were attributed to 07-04
t3, which never ran them. Folded into the budget audit: runGoldenCorpusGate()
re-runs `npm run routing:corpus` and `npm run search:corpus` as child
processes (the same shape runCpuLadderGate already uses), so the ONE
budget-audit command enforces SC4.a/SC4.b. VALIDATION.md rows now name 07-04
t2 and the budget-audit command.

WARNING (07-02 t3) — the landscape-leg snippet omitted the required setup.
The action now says to copy the existing corridor guidance leg VERBATIM
(setup stubGuidanceRoute(page, corridorModel) + guideQuery carrying dest +
fixpair + waitMode/expectGuide) and change ONLY the name, the scheme, and the
viewport; two grep criteria pin the setup and the query.

WARNING (07-04) — D-06/SC4 name subrequests but nothing measured beside the
50/request ceiling. Added runSubrequestClassGate(): a per-request-class
inventory derived from the Worker's route table (engine-A /api/route = 0
external subrequests; the cron tick's Telegram send is the only recurring
class; KV ops tracked separately against their own ceiling). Unmeasurable
counts read "not measured", never 0. docs/BUDGETS.md gains a
"## Subrequests per request class" section and the record states that the
inventory is source-derived, not a live Workers Logs metric.

WARNING (07-RESEARCH.md) — "## Open Questions" is now
"## Open Questions (RESOLVED)" and each of the five items carries its adopted
disposition; Q2/Q3 resolve to "answered by measurement at the launch gate per
D-04/D-06".

INFO (07-VALIDATION.md) — nyquist_compliant flipped to true and the sign-off
is approved 2026-10-25 after the map-row correction.

Waves, dependencies, autonomous flags, and the other three plans are unchanged.
This commit is contained in:
araharah-executor
2026-10-25 02:44:30 +08:00
parent 27b340fa6b
commit a7d8dbf345
6 changed files with 1048 additions and 78 deletions
@@ -21,6 +21,8 @@ must_haves:
- "The A* CPU cost over the real KL graph is MEASURED and PRINTED against the 8 ms budget — never assumed, never hoped for."
- "The shipped route payload's gzipped size is measured and printed against the 200 KiB (204800 byte) budget."
- "Every KV write class is recorded with its TTL — and the cron tick is proven to write ZERO times."
- "The 50-subrequests-per-request ceiling has a per-request-class INVENTORY beside it — every class's external subrequest count is derived from the source, and a count that cannot be derived reads `not measured`, never `0`."
- "The golden route and search corpora are re-run by the SAME `npm run budget-audit` command — so the budget record and the corpora can never disagree."
- "Static assets carry an explicit cache policy: hashed /assets/* are immutable for a year, the HTML entry revalidates."
- "The budget record compares every current number with the documented Cloudflare free-plan ceiling in ONE place."
artifacts:
@@ -28,7 +30,7 @@ must_haves:
provides: "The consolidated budget CLI (npm run budget-audit) — measurement + free-tier comparison + exit codes"
contains: "BUDGET AUDIT"
- path: "scripts/lib/budget-gates.mjs"
provides: "The measurement gates — CPU ladder, bundle bytes, KV write classes, cache headers"
provides: "The measurement gates — CPU ladder, bundle bytes, KV write classes, cache headers, subrequest classes, golden corpora"
contains: "P95_BUDGET_MS"
- path: "web/public/_headers"
provides: "The Workers static-assets cache policy for hashed assets and the HTML entry"
@@ -45,6 +47,14 @@ must_haves:
to: "bot/src/webhook.ts"
via: "the cron tick's zero-write law is proven by scanning scheduled() for .put( — the source half of SC4.e"
pattern: "cron"
- from: "scripts/lib/budget-gates.mjs"
to: "package.json"
via: "the golden corpora (SC4.a/SC4.b) are RE-RUN as child processes — `npm run routing:corpus` and `npm run search:corpus` — so the ONE budget-audit command also enforces them"
pattern: "routing:corpus"
- from: "scripts/lib/budget-gates.mjs"
to: "bot/src/webhook.ts"
via: "the per-request-class subrequest inventory is derived from the Worker's route table — /api/route = 0 external subrequests; the cron tick's Telegram send is the only recurring subrequest class"
pattern: "subrequest"
- from: "web/public/_headers"
to: "web/dist/assets"
via: "the /assets/* rule covers Vite's hashed output; the live probe reads the deployed response headers back"
@@ -241,7 +251,9 @@ 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
@@ -289,6 +301,50 @@ the TTL constant referenced. Assert every write references a NAMED constant
the constants out of the three `bot/src/kv/*.ts` files with a regex on
`export 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 `fetch` to 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.
- 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 MEASURED` with 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
reads `not measured outside --live` and 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:corpus` and `npm run search:corpus` as child
processes (cwd = repo root), exactly as `runCpuLadderGate` spawns
`routing/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
@@ -298,21 +354,24 @@ Print the observed `cache-control` for the HTML entry and for a hashed
Cloudflare's own footnote (the evidence quotes it, it does not interpret it).
</action>
<acceptance_criteria>
- `scripts/lib/budget-gates.mjs` exists and exports `runCpuLadderGate`, `runBundleBudgetGate`, `runKvWriteClassGate`, `runCacheHeaderGate`
- `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>
<verify>
<automated>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','runCacheHeaderGate']){const r=m[k]();console.log(r.id+': '+(r.notRun?'NOT RUN':(r.pass?'PASS':'FAIL'))+' — '+String(r.evidence).slice(0,160));}})"</automated>
<automated>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=m[k]();console.log(r.id+': '+(r.notRun?'NOT RUN':(r.pass?'PASS':'FAIL'))+' — '+String(r.evidence).slice(0,160));}})"</automated>
</verify>
<done>
The four budget gates measure and print the CPU p95 (by re-running the
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, and the live cache headers — each with a NOT-RUN path
that is never a pass.
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.
</done>
</task>
@@ -332,13 +391,16 @@ Create `scripts/budget-audit.mjs` — the orchestrator (the same shape as
Behaviour:
- Accepts an optional `--live` flag. Without it, the cache-header gate is
SKIPPED and reported as `NOT RUN (pass --live)`.
- Runs `runCpuLadderGate()`, `runBundleBudgetGate()`, `runKvWriteClassGate()`;
with `--live`, also `runCacheHeaderGate()`.
- Runs `runCpuLadderGate()`, `runBundleBudgetGate()`, `runKvWriteClassGate()`,
`runSubrequestClassGate()`, `runGoldenCorpusGate()`; with `--live`, also
`runCacheHeaderGate()`.
- 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 measured` where it did not. This table is the
console form of `docs/BUDGETS.md`.
console form of `docs/BUDGETS.md`. The `Worker subrequests | 50/request`
ceiling 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>`.
@@ -366,14 +428,37 @@ contain:
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, and the KV daily write estimate.
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.
4. A `## The cron writes zero` section — one honest paragraph explaining WHY
4. A `## Subrequests per request class` section — 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 measured` with the reason — never `0`, 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 says `not measured outside --live`
and does not substitute a number.
5. A `## The cron writes zero` section — one honest paragraph explaining WHY
(the tick lists, reads, and sends; it never writes), and naming the source
gate that proves it.
5. A `## Cross-metro CPU degradation (the c3 class)` section — the honest
6. A `## Golden corpora` section — 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.
7. 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
@@ -383,15 +468,15 @@ contain:
the ALT landmark preprocessing (a NEXT-MILESTONE item, not this phase).
Cite `routing/scripts/measure-route-cpu.mjs`'s LADDER HISTORY and the
06-04 plan summary as the evidence.
6. A `## Static assets` section — the `_headers` policy and the observed live
8. A `## Static assets` section — the `_headers` policy and the observed live
headers (or an honest "not measured in this run").
7. A `## Statement` section — the honest one-paragraph claim, naming which
9. A `## Statement` section — 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.
8. A `## Signature` block with the maintainer's name and date lines left as
`________________` (the human signs at the launch gate).
10. A `## Signature` block 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
@@ -401,7 +486,9 @@ number from another phase's summary.
- `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
- `docs/BUDGETS.md` exists and contains the literals `## Free-plan ceilings`, `## Current measurements`, `## The cron writes zero`, `## Cross-metro CPU degradation`, `## Static assets`, `## Statement`, `## Signature`
- `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>
<verify>
@@ -409,8 +496,10 @@ number from another phase's summary.
</verify>
<done>
`npm run budget-audit` measures and prints every budget with its free-plan
ceiling, exits 0/1/2 correctly, and `docs/BUDGETS.md` carries the record
with the real numbers, the c3 escalation, and a human signature block.
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.
</done>
</task>
@@ -434,6 +523,7 @@ number from another phase's summary.
| 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>
<verification>
@@ -454,9 +544,10 @@ NEVER exit 0 while reporting a measurement as skipped.
<success_criteria>
- `web/public/_headers` exists and reaches `web/dist/`, carrying the immutable hashed-asset rule and the revalidating HTML rule, and nothing else.
- `npm run budget-audit` measures 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, and the cache headers, each against a documented free-plan ceiling.
- `npm run budget-audit` measures 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` — never `0`, never an estimate.
- The exit codes are 0 / 1 / 2 and a NOT RUN is never a pass.
- `docs/BUDGETS.md` carries the record with the real numbers, the c3 cross-metro degradation class, the cron zero-write explanation, and a human signature block.
- `docs/BUDGETS.md` carries the record with the real numbers, the `## Subrequests per request class` section, the `## Golden corpora` section, the c3 cross-metro degradation class, the cron zero-write explanation, and a human signature block.
</success_criteria>
<output>