Files
arah2/.planning/phases/08.1-contraction-hierarchies-routing-backend-v3-the-lever-that-cl/08.1-02-PLAN.md
T
araharah-executor 67026b9483 docs(08.1): fix plan task ownership and broken verify gates
- 08.1-01 task 2: re-pin graph.test.mjs in-memory G-cases (G1/G3/G8/G9/G10/G11)
  and astar.test.mjs in-memory/ALT cases; suspend only committed-fixture G7 and
  A1/A2/A4/A5/A6 (the runner auto-discovers all suites and parses no argv, so
  task 2's verify could never exit 0 otherwise)
- 08.1-01 task 3: own un-suspension of G7 and A1/A2/A4/A5/A6 after the ARG3
  fixture regeneration
- 08.1-01 task 1: drop the cosmetic --test-name-pattern flag (ignored by the
  hand-rolled runner; full suite runs instead)
- 08.1-02 task 2: use grep -cF for the rollout decodeGraph call-site gate
  (BRE form exited 2 with 'Unmatched ( or \\(')
- 08.1-02 task 1: replace the no-op '|| test $? -eq 1' landmark gate with a
  'test "$(...)" -eq 0' count assertion that can actually fail
2026-10-26 00:54:01 +08:00

312 lines
21 KiB
Markdown

---
phase: 08.1-contraction-hierarchies-routing-backend-v3-the-lever-that-cl
plan: 02
type: execute
wave: 2
depends_on:
- 08.1-01
files_modified:
- routing/scripts/build-road-graph.mjs
- routing/scripts/measure-route-cpu.mjs
- routing/scripts/measure-optimality-gate.mjs
- routing/scripts/measure-astar-oracle-sweep.mjs
- scripts/lib/budget-gates.mjs
- scripts/test/budget-audit.test.mjs
- routing/test/rollout.test.mjs
autonomous: true
requirements:
- D-01
- D-02
- D-03
- D-08
- D-09
- D-13
must_haves:
truths:
- "The production graph build runs CH ordering, contraction, and shortcut encoding and prints measured bytes, shortcut ratio, witness counts, and wall-clock without inventing a fixed payload ceiling."
- "The existing optimality gate and 200-pair seeded Dijkstra-oracle sweep run unchanged against the ARG3 binary and report zero optimality violations."
- "The local route-CPU gate prints the CH p95 against the unchanged 8 ms product budget and is the only SC-1 closing measurement."
- "The drift guard is re-anchored to approximately two times the measured CH p95 only when the c3 class retires, and the constant plus its audit mirror move together."
- "The Worker and upload path remain format-agnostic: one decode call, no version branch, no format token in the upload script."
artifacts:
- path: "routing/scripts/build-road-graph.mjs"
provides: "Build-time CH preprocessing stage and measured ARG3 statistics print"
- path: "routing/scripts/measure-route-cpu.mjs"
provides: "Unchanged local 25-route and extended 100-route CH CPU measurement gates"
- path: "scripts/lib/budget-gates.mjs"
provides: "CPU verdict gate with D-03 drift-guard constant and CH regime wording"
- path: "scripts/test/budget-audit.test.mjs"
provides: "CPU3 mirror vector pinned to the CH verdict"
key_links:
- from: "routing/scripts/build-road-graph.mjs"
to: "routing/src/contraction.ts"
via: "contractGraph over the compacted directed edges, then encodeGraph ch block"
pattern: "contractGraph|encodeGraph"
- from: "routing/scripts/measure-route-cpu.mjs"
to: "routing/src/astar.ts"
via: "routeAstar now delegates to the CH query"
pattern: "routeAstar|astar"
- from: "scripts/lib/budget-gates.mjs"
to: "scripts/test/budget-audit.test.mjs"
via: "ROUTE_CPU_DRIFT_GUARD_MS and its CPU3 evidence mirror"
pattern: "ROUTE_CPU_DRIFT_GUARD_MS|CPU3"
---
<objective>
Wire the ARG3 binary into the real build and measurement campaign, then close SC-1 from measured local evidence with an honest retire-or-shrink verdict.
Purpose: The CH implementation is only useful if the production binary carries the hierarchy and the same evidence class that failed Phase 8 measures whether it succeeded. D-01 forbids retiring the c3 class on failing numbers; D-02 forbids substituting a deployed probe.
Output: CH build stage, measured ARG3 build, re-run optimality/oracle/CPU gates, and the tightened or deliberately unchanged drift guard.
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08.1-contraction-hierarchies-routing-backend-v3-the-lever-that-cl/08.1-CONTEXT.md
@.planning/phases/08.1-contraction-hierarchies-routing-backend-v3-the-lever-that-cl/08.1-RESEARCH.md
@.planning/phases/08.1-contraction-hierarchies-routing-backend-v3-the-lever-that-cl/08.1-PATTERNS.md
@.planning/phases/08.1-contraction-hierarchies-routing-backend-v3-the-lever-that-cl/08.1-VALIDATION.md
@routing/scripts/build-road-graph.mjs
@routing/scripts/measure-route-cpu.mjs
@routing/scripts/measure-optimality-gate.mjs
@routing/scripts/measure-astar-oracle-sweep.mjs
@scripts/lib/budget-gates.mjs
@scripts/test/budget-audit.test.mjs
@routing/test/rollout.test.mjs
</context>
<interfaces>
Contracts consumed from Plan 01:
```typescript
// routing/src/contraction.ts
export function contractGraph(input: ContractionInput): ContractionResult;
// ContractionResult.stats: { shortcutCount, ratioPerNode, witnessSearches, witnessBudgetHits }
```
Measurement constants to preserve or re-anchor:
```javascript
// routing/scripts/measure-route-cpu.mjs
const P95_BUDGET_MS = 8; // MUST remain exactly 8 and appear exactly once
const ROUTE_COUNT = 25;
const EXTENDED_PAIR_COUNT = 100;
// routing/scripts/measure-optimality-gate.mjs
const OPTIMALITY_EPSILON_PCT = 0.1;
const REQUIRED_BASELINE_ARG_VERSION = 1; // the frozen record is immutable
// routing/scripts/measure-astar-oracle-sweep.mjs
const SWEEP_PAIR_COUNT = 200;
const SWEEP_SEED = 0x08f1;
const ORACLE_EPSILON_S = 1e-6;
// scripts/lib/budget-gates.mjs
export const P95_BUDGET_MS = 8;
export const ROUTE_CPU_DRIFT_GUARD_MS = <ceil(2 * measured CH p95)>; // only if c3 retires
```
</interfaces>
<tasks>
<task type="auto" tdd="true">
<name>task 1: replace the ALT build stage with measured CH preprocessing</name>
<files>routing/scripts/build-road-graph.mjs</files>
<read_first>
- routing/scripts/build-road-graph.mjs
- routing/src/contraction.ts
- routing/src/graph.ts
- routing/test/fixtures/build-mini-graph.mjs
- .planning/phases/08.1-contraction-hierarchies-routing-backend-v3-the-lever-that-cl/08.1-RESEARCH.md
</read_first>
<behavior>
- The build no longer imports landmark.ts and has no --landmarks option.
- The build calls contractGraph and encodes the CH block through encodeGraph.
- The build prints contracted node count, shortcut count, ratio per node, witness search count, witness budget hits, CH-section bytes, total bytes, and wall-clock.
- The existing 1-80 MB envelope gate still fails before any write; no new fixed payload ceiling exists.
</behavior>
<action>
Remove the `--landmarks` argument parsing, `LANDMARK_COUNT_MIN`, `LANDMARK_COUNT_MAX`, `NODES_PER_LANDMARK`, `LANDMARK_MIN_COVERAGE_FRACTION`, and `LANDMARK_COUNT_OVERRIDE_MAX` constants and their usage.
Remove the ALT preprocessing block and replace it with one CH stage. Import `contractGraph` from `../src/contraction.ts`. Project the compacted directed edges into `{ from, to, seconds, flags }` and call `contractGraph({ nodeCount: cLat.length, edges })`. If `contractGraph` throws, fail with a message naming the stage and the thrown reason, then exit 1 before any write. Pass the resulting CH data to `encodeGraph` through the `ch:` input field, never a `landmarks:` field.
Measure the build wall-clock with `process.hrtime.bigint()` around the CH stage and the encode. Print a `CH stats:` block containing exactly these labeled measurements:
- `contracted nodes: <N>`
- `shortcuts: <S> (<ratio> per node)`
- `witness searches: <W> (<H> hit the settle budget)`
- `chRank bytes: <4*N>`
- `shortcut bytes: <12*S>`
- `build wall-clock: <ms> ms`
- `total binary: <MB> (<bytes> B)`
Do not add an ARG3 byte ceiling. Keep the existing enforced `MIN_BINARY_BYTES` floor and `MAX_BINARY_BYTES` ceiling unchanged. Keep collect-then-write semantics: any gate failure exits 1 with the previous binary untouched.
Update `--check <file>` so it prints CH facts instead of landmark facts: nodeCount, edgeCount, nameCount, maxSpeedKmh, bbox, bytes, chShortcutCount, shortcut count/ratio, and rank length. It must not import landmark.ts.
Update the script header's usage text to remove `--landmarks` and describe the CH stage.
</action>
<verify>
<automated>node --check routing/scripts/build-road-graph.mjs &amp;&amp; test "$(grep -v '^[[:space:]]*\*' routing/scripts/build-road-graph.mjs | grep -v '^[[:space:]]*//' | grep -c 'landmark\|--landmarks')" -eq 0</automated>
</verify>
<verify_note>
The landmark-absence gate uses `test "$(... grep -c ...)" -eq 0` rather than `grep -c ... || test $? -eq 1`. The `||` form is a no-op gate: `grep -c` exits 0 when it FINDS matches (it prints the count), so `|| test $? -eq 1` never fires and the overall command exits 0 even with every landmark token still present. The `test ... -eq 0` form fails when the count is non-zero — verified: it exits 1 against the current file (39 landmark matches) and exits 0 against a clean file.
</verify_note>
<acceptance_criteria>
- `grep -v '^[[:space:]]*\*' routing/scripts/build-road-graph.mjs | grep -v '^[[:space:]]*//' | grep -c 'landmark'` equals 0.
- `grep -v '^[[:space:]]*\*' routing/scripts/build-road-graph.mjs | grep -v '^[[:space:]]*//' | grep -c 'contractGraph'` is at least 1.
- `grep -v '^[[:space:]]*\*' routing/scripts/build-road-graph.mjs | grep -v '^[[:space:]]*//' | grep -c 'build wall-clock\|witness searches\|shortcuts:'` is at least 1.
- `node routing/scripts/build-road-graph.mjs --check routing/test/fixtures/mini-graph.bin` exits 0 and prints `chShortcutCount`.
</acceptance_criteria>
<done>The production build emits ARG3 through the one CH module and prints every D-08/D-09 measurement without a payload ceiling.</done>
</task>
<task type="auto" tdd="true">
<name>task 2: re-pin the existing gates for ARG3 and preserve the one-path rollout locks</name>
<files>routing/scripts/measure-optimality-gate.mjs, routing/scripts/measure-astar-oracle-sweep.mjs, routing/scripts/measure-route-cpu.mjs, routing/test/rollout.test.mjs</files>
<read_first>
- routing/scripts/measure-optimality-gate.mjs
- routing/scripts/measure-astar-oracle-sweep.mjs
- routing/scripts/measure-route-cpu.mjs
- routing/test/rollout.test.mjs
- routing/src/astar.ts
- .planning/phases/08.1-contraction-hierarchies-routing-backend-v3-the-lever-that-cl/08.1-VALIDATION.md
</read_first>
<behavior>
- The optimality gate still rejects a baseline whose argVersion is not 1 and still mirrors the record's 0.1 percent epsilon.
- The oracle sweep still runs 200 compared pairs with seed 0x08f1 and epsilon 1e-6, classifying SUBOPTIMAL, ORACLE-MISMATCH, and class breaks.
- The CPU gate still measures 25 routes plus an optional 100-route extended run against P95_BUDGET_MS 8 and prints p95.
- The upload script and Worker remain format-agnostic, and the ARG3 magic is added to their banned-token lists.
</behavior>
<action>
In `routing/scripts/measure-optimality-gate.mjs`, keep `OPTIMALITY_EPSILON_PCT = 0.1` and `REQUIRED_BASELINE_ARG_VERSION = 1` unchanged. Change only the decode-failure message so it says the current binary must be ARG3 and names the rebuild command `node routing/scripts/build-road-graph.mjs`. Do not edit `routing/test/fixtures/alt-baseline.json`; it is immutable.
In `routing/scripts/measure-astar-oracle-sweep.mjs`, keep `SWEEP_PAIR_COUNT = 200`, `SWEEP_SEED = 0x08f1`, `SWEEP_OVERSAMPLE_FACTOR = 3`, `SWEEP_MIN_PAIR_KM = 12`, and `ORACLE_EPSILON_S = 1e-6` unchanged. Update only the graph-missing and decode-failure wording to name ARG3. Keep the symmetric comparison and the three violation classes.
In `routing/scripts/measure-route-cpu.mjs`, keep `P95_BUDGET_MS = 8`, `ROUTE_COUNT = 25`, `SAMPLE_SEED = 0x06d4`, `EXTENDED_PAIR_COUNT = 100`, and `EXTENDED_SEED = 0x08a1` unchanged. Update the LADDER HISTORY comment with a CH rung line once the measured CH numbers are available. Do not change the measurement logic or the warm-up law. The `P95_BUDGET_MS = 8` string must remain present exactly once in this file.
In `routing/test/rollout.test.mjs`, add the ARG3 magic `0x41524733` to RO1's banned-token list and to RO2's banned-token list. Keep the Worker pinned to exactly one `decodeGraph` call, no `GRAPH_VERSION`, no graph magic, and no `=== 2`. Keep the upload script's banned-token list free of format awareness.
</action>
<verify>
<automated>npm run routing:test &amp;&amp; test "$(grep -c 'P95_BUDGET_MS = 8' routing/scripts/measure-route-cpu.mjs)" -eq 1 &amp;&amp; test "$(grep -cF 'const calls = code.match(/decodeGraph\(/g)' routing/test/rollout.test.mjs)" -eq 1</automated>
</verify>
<verify_note>
The rollout assertion uses `grep -cF` (fixed-string) on purpose. The BRE form `grep -c 'const calls = code.match(/decodeGraph\(/g)'` is a group-open and exits 2 with `grep: Unmatched ( or \(` — the gate would fail on a correct implementation. `grep -cF` treats the pattern as a literal string and returns 1 on the current file (the one decodeGraph call site in `routing/test/rollout.test.mjs`), so `-eq 1` is a real assertion.
</verify_note>
<acceptance_criteria>
- `grep -c 'P95_BUDGET_MS = 8' routing/scripts/measure-route-cpu.mjs` equals 1.
- `grep -c 'SWEEP_PAIR_COUNT = 200' routing/scripts/measure-astar-oracle-sweep.mjs` equals 1.
- `grep -c 'SWEEP_SEED = 0x08f1' routing/scripts/measure-astar-oracle-sweep.mjs` equals 1.
- `grep -c 'ORACLE_EPSILON_S = 1e-6' routing/scripts/measure-astar-oracle-sweep.mjs` equals 1.
- `grep -c 'REQUIRED_BASELINE_ARG_VERSION = 1' routing/scripts/measure-optimality-gate.mjs` equals 1.
- `grep -c '0x41524733' routing/test/rollout.test.mjs` is at least 2.
- `npm run routing:test` exits 0.
</acceptance_criteria>
<done>The D-13 gates remain unchanged in substance, the failure messages honestly name ARG3, and the Worker/upload one-path laws cover the new magic.</done>
</task>
<task type="auto">
<name>task 3: run the maintainer measurement campaign and re-anchor the drift guard only on a green verdict</name>
<files>scripts/lib/budget-gates.mjs, scripts/test/budget-audit.test.mjs</files>
<read_first>
- scripts/lib/budget-gates.mjs
- scripts/test/budget-audit.test.mjs
- routing/scripts/measure-route-cpu.mjs
- routing/scripts/measure-optimality-gate.mjs
- routing/scripts/measure-astar-oracle-sweep.mjs
- docs/BUDGETS.md
- docs/KNOWN-LIMITATIONS.md
</read_first>
<behavior>
- The built ARG3 binary decodes and the optimality gate reports zero D03A and zero D03B violations.
- The 200-pair oracle sweep reports 0 SUBOPTIMAL, 0 ORACLE-MISMATCH, and 0 class breaks.
- The local CPU gate prints the CH p95 for both the 25-route and 100-route extended samples.
- If measured CH p95 is below 8 ms, the drift guard becomes ceil(2 * measured p95) and the CPU3 mirror moves in the same commit.
- If measured CH p95 is at or above 8 ms, the c3 class remains, the drift guard is not tightened, and the evidence says the class stays.
</behavior>
<action>
Run the maintainer campaign in this exact order and preserve the printed output in the plan summary:
1. `node routing/scripts/build-road-graph.mjs`
2. `node routing/scripts/measure-optimality-gate.mjs`
3. `node routing/scripts/measure-astar-oracle-sweep.mjs`
4. `node routing/scripts/measure-route-cpu.mjs`
5. `node routing/scripts/measure-route-cpu.mjs --extended`
6. `node scripts/test/budget-audit.test.mjs`
The local gate is the sole SC-1 evidence per D-02. Do not add a deployed-Worker CPU probe. Do not change `P95_BUDGET_MS = 8`; do not change `OPTIMALITY_EPSILON_PCT = 0.1`; do not change `ORACLE_EPSILON_S = 1e-6`; do not loosen any gate to manufacture a pass.
Apply D-01/D-03 conditionally on the printed numbers:
- If the 25-route CH p95 is strictly below 8 ms, the c3 class retires. Set `ROUTE_CPU_DRIFT_GUARD_MS` in `scripts/lib/budget-gates.mjs` to `Math.ceil(2 * measuredChP95Ms)` as a literal integer, and update its provenance comment to name the measured CH p95, the date, and the host. In the same commit, update the CPU3 vector in `scripts/test/budget-audit.test.mjs`: it must assert the CH verdict wording and must no longer require the literal `EXECUTED in Phase 8` phrase. Keep the `p95 <number> ms`, `8 ms`, `10 ms free-plan ceiling`, and `GATE FAIL` assertions when the child still exits 1 for an over-budget regression.
- If the 25-route CH p95 is at or above 8 ms, keep `ROUTE_CPU_DRIFT_GUARD_MS` at its current value and keep the c3 class. Rewrite only the budget-gates verdict prose so it names the executed-and-measured CH result and states that the class remains because retirement fires only on a passing measurement. Update the CPU3 mirror regex in the same commit to the new wording.
In both branches, the three-branch verdict structure in `runCpuLadderGate` stays: uncharted regression beyond the drift guard, over-budget-within-band, and within-budget. Keep the honest NOT-RUN behavior for a missing binary.
</action>
<verify>
<automated>node routing/scripts/measure-optimality-gate.mjs &amp;&amp; node routing/scripts/measure-astar-oracle-sweep.mjs &amp;&amp; node scripts/test/budget-audit.test.mjs</automated>
</verify>
<acceptance_criteria>
- `node routing/scripts/measure-optimality-gate.mjs` prints `GATE PASS` and `0 violation(s)` for both D03A and D03B.
- `node routing/scripts/measure-astar-oracle-sweep.mjs` prints `GATE PASS` and `0 SUBOPTIMAL`, `0 ORACLE-MISMATCH`, `0 class-breaks`.
- `node routing/scripts/measure-route-cpu.mjs` prints a `p95: <number> ms` line; the number is recorded verbatim in the plan summary.
- `grep -c 'P95_BUDGET_MS = 8' scripts/lib/budget-gates.mjs` equals 1.
- `node scripts/test/budget-audit.test.mjs` exits 0.
- The summary states explicitly whether the c3 class retired and, if it did, the exact new `ROUTE_CPU_DRIFT_GUARD_MS` value; if it did not, the summary states that the class remains.
</acceptance_criteria>
<done>The measured CH verdict exists, the same local evidence class decides SC-1, and the drift guard plus its mirror move together only when the numbers justify retirement.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Overpass response -> build pipeline | External map data enters the graph builder; malformed or hostile data must fail the build before a binary is written. |
| Built ARG3 binary -> measurement gates | The gitignored local binary is the artifact under test; a corrupt or stale binary must make gates fail honestly rather than pass. |
| Gate output -> budget verdict | Measured p95 and guard constants decide whether the c3 class retires; a mistaken or stale number could retire the class on failing evidence. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-08.1-10 | Tampering / DoS | Overpass-derived graph entering contraction | mitigate | The existing build validates extracted ways, node ids, finite coordinates, and non-empty graphs before contraction; any `contractGraph` throw fails before collect-then-write, leaving the previous binary untouched. |
| T-08.1-11 | Tampering | Stale or corrupt local binary passing a gate | mitigate | `decodeGraph` returns null for non-ARG3 bytes and every measurement script fails on null; the gates never substitute a missing measurement for a pass. |
| T-08.1-12 | Repudiation | Retiring c3 on failing numbers | mitigate | D-01 is encoded in the conditional action: the guard is re-anchored only when the printed 25-route p95 is strictly below 8 ms; the measured value and verdict must be quoted in the plan summary and later record surfaces. |
| T-08.1-13 | Tampering | Constant-mirror drift between guard and audit vector | mitigate | `ROUTE_CPU_DRIFT_GUARD_MS` and the CPU3 evidence mirror are changed in the same commit; CPU2 still pins `P95_BUDGET_MS = 8` exactly once. |
| T-08.1-14 | DoS | Unbounded CH build consuming the maintainer machine | mitigate | Witness searches are bounded by distance and settle count; the build prints wall-clock and budget-hit counts; D-09 treats an over-run as an honest measurement, not a hidden failure. |
| T-08.1-15 | DoS | Search CPU regression after the re-anchor | mitigate | The tightened drift guard is approximately 2x the measured CH p95, so a regression that consumes the headroom fails the route-cpu gate; the 8 ms product budget remains the binding pass bar. |
</threat_model>
<verification>
Run the full suite and the maintainer campaign:
```bash
npm run routing:test && npm test -w bot && npm run routing:typecheck && npm run bot:typecheck && npm run web:typecheck
node scripts/test/budget-audit.test.mjs
node routing/scripts/build-road-graph.mjs
node routing/scripts/measure-optimality-gate.mjs
node routing/scripts/measure-astar-oracle-sweep.mjs
node routing/scripts/measure-route-cpu.mjs
node routing/scripts/measure-route-cpu.mjs --extended
```
Expected: every suite green; optimality and oracle gates green; both CPU p95 values printed and recorded; the drift guard either tightened to ceil(2 x measured p95) with its mirror updated, or deliberately unchanged with the class remaining.
</verification>
<success_criteria>
- The real build produces ARG3 with measured CH statistics and no invented byte ceiling.
- The D-03 optimality gate and 200-pair oracle sweep pass against the ARG3 binary without threshold changes.
- The local CPU gate prints the CH p95 for both sample widths.
- The c3 class is retired only when the measured p95 is below 8 ms; otherwise it remains with an honest record.
- The drift guard and its audit mirror agree, and the Worker/upload path remains format-agnostic.
</success_criteria>
<output>
After completion, create `.planning/phases/08.1-contraction-hierarchies-routing-backend-v3-the-lever-that-cl/08.1-02-SUMMARY.md`
</output>