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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user