Files

246 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 08-alt-landmark-preprocessing-routing-backend-v2
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- routing/scripts/measure-optimality-baseline.mjs
- routing/test/fixtures/alt-baseline.json
- routing/test/fixtures/mini-graph-arg1.bin
- routing/scripts/measure-route-cpu.mjs
autonomous: true
requirements: [SC3, SC4]
must_haves:
truths:
- "The pre-ALT optimality baseline (100 coordinate-pinned cross-metro pairs: seconds + verdict class) is committed BEFORE the ARG2 codec lands — reproducible from the record alone, never from the old binary (Pitfall 1)"
- "The 25-route CI CPU gate runs with zero arguments exactly as today — stdout anchor lines `p95: <n> ms (...)` and `GATE (PASS|FAIL): ...` unchanged, so scripts/lib/budget-gates.mjs keeps passing"
- "An opt-in `--extended` flag measures ~100 seeded coordinate-pinned cross-metro pairs and prints its own p95 + gate verdict (D-04)"
- "The current committed ARG1 fixture bytes are preserved verbatim as `routing/test/fixtures/mini-graph-arg1.bin` — the future decode-rejection vector (Plan 02 consumes it)"
artifacts:
- path: "routing/scripts/measure-optimality-baseline.mjs"
provides: "exported pair-generation + measurement core; CLI records the baseline"
- path: "routing/test/fixtures/alt-baseline.json"
provides: "committed pre-ALT baseline record — the D-03 comparison anchor"
- path: "routing/test/fixtures/mini-graph-arg1.bin"
provides: "ARG1 bytes preserved (byte-identical to the pre-phase mini-graph.bin)"
- path: "routing/scripts/measure-route-cpu.mjs"
provides: "--extended opt-in flag; default mode byte-compatible"
key_links:
- from: "routing/scripts/measure-optimality-baseline.mjs"
to: "routing/data/kl-roadgraph.v1.bin"
via: "readFileSync → toArrayBuffer → decodeGraph → snapToGraph → routeAstar"
pattern: "decodeGraph"
- from: "routing/test/fixtures/alt-baseline.json"
to: "pairs pinned by origin/destination lat/lng"
via: "record schema — never node indices (Pitfall 2)"
pattern: "\"origin\"\\s*:\\s*\\{\\s*\"lat\""
- from: "routing/scripts/measure-route-cpu.mjs (--extended)"
to: "routing/scripts/measure-optimality-baseline.mjs"
via: "import { generateCrossMetroPairs } — ONE pair-set definition, two consumers"
pattern: "generateCrossMetroPairs"
---
<objective>
Wave-0 measurement instrumentation: record the pre-ALT optimality baseline FIRST (research Pitfall 1 — once the ARG2 codec lands, old-code+old-binary is unreproducible), preserve the ARG1 fixture bytes as the rejection vector, and add the D-04 extended-sample flag to the CPU gate with its default CLI contract untouched.
Purpose: every later plan (codec, ALT, campaign) compares against THIS committed record; measuring it after the codec change is impossible.
Output: `routing/scripts/measure-optimality-baseline.mjs`, committed `routing/test/fixtures/alt-baseline.json`, `routing/test/fixtures/mini-graph-arg1.bin`, `measure-route-cpu.mjs --extended`.
</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-alt-landmark-preprocessing-routing-backend-v2/08-CONTEXT.md
@.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-RESEARCH.md
@.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-PATTERNS.md
@.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-VALIDATION.md
<interfaces>
<!-- Existing mechanics this plan extends. Extracted 2026-10-25. -->
From routing/scripts/measure-route-cpu.mjs (lines 50–64, 136–185):
```js
const GRAPH_PATH = resolve(here, "..", "data", "kl-roadgraph.v1.bin");
const P95_BUDGET_MS = 8; // 20% margin under the 10 ms ceiling
const ROUTE_COUNT = 25;
const SAMPLE_SEED = 0x06d4;
// prints per-pair ` ${label}: ${ms.toFixed(3)} ms (${km})` then
// `p95: ${p95.toFixed(3)} ms (max ..., mean ..., n=...)` then `GATE PASS|FAIL: ...`
```
From scripts/lib/budget-gates.mjs runCpuLadderGate (lines 78–119) — the CLI contract that MUST keep matching:
```js
out = execFileSync(process.execPath, [script], { ... }); // script run with ZERO args
const p95 = Number(out.match(/^p95: (\d+(?:\.\d+)?) ms/m)?.[1] ?? NaN);
const childGateLine = out.match(/^GATE (?:FAIL|PASS): .*$/m)?.[0] ?? null;
```
(The audit suite `node scripts/test/budget-audit.test.mjs` — 19/19 vectors — also regex-checks measure-route-cpu.mjs source for the `measure-route-cpu` and `P95_BUDGET_MS = 8` tokens. Keep both names/constant verbatim.)
From routing/src (the pure surfaces the recorder reuses):
```ts
export function decodeGraph(buffer: ArrayBuffer): RoadGraph | null; // graph.ts — still ARG1 (v1) during this plan
export function snapToGraph(...); // snap.ts (mjs-wrapper in scripts)
export function routeAstar(graph, originNode, goalNode); // astar.ts — returns engineered seconds + nodes or null
```
graph.bbox: `{ minLat, minLng, maxLat, maxLng }` — the order-INDEPENDENT pair-generation source (Pitfall 2).
</interfaces>
</context>
<tasks>
<task type="auto">
<name>task 1: Preserve ARG1 fixture bytes as the rejection vector</name>
<files>routing/test/fixtures/mini-graph-arg1.bin</files>
<read_first>
- routing/test/fixtures/mini-graph.bin (the committed ARG1 bytes being preserved)
- .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-CONTEXT.md (D-01)
- .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-PATTERNS.md ("build-mini-graph.mjs" section — research requirement that old committed ARG1 bytes are preserved BEFORE the fixture regenerates)
</read_first>
<action>
Copy the CURRENT committed `routing/test/fixtures/mini-graph.bin` byte-for-byte to a NEW file `routing/test/fixtures/mini-graph-arg1.bin`. Do NOT touch `mini-graph.bin` itself (Plan 02 task 3 regenerates it as ARG2). Add a header comment to `routing/test/fixtures/build-mini-graph.mjs`'s docstring (one line only, no behavior change): that `mini-graph-arg1.bin` holds the frozen ARG1-era bytes preserved by Phase 8 plan 08-01 expressly as the v1 decode-rejection vector consumed by the G-suite, and must not be regenerated.
</action>
<verify>
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH" && cmp routing/test/fixtures/mini-graph.bin routing/test/fixtures/mini-graph-arg1.bin && echo "ARG1-VECTOR-IDENTICAL"</automated>
</verify>
<acceptance_criteria>
- `routing/test/fixtures/mini-graph-arg1.bin` exists, is committed to git, and `cmp` vs `mini-graph.bin` reports byte-identical (both still ARG1 at this point in history).
- `mini-graph.bin` itself is byte-unchanged at this commit (`git diff --stat routing/test/fixtures/mini-graph.bin` empty).
- `build-mini-graph.mjs` carries the one-line provenance comment mentioning `mini-graph-arg1.bin`.
</acceptance_criteria>
<done>ARG1 bytes frozen under a new committed name, provenance documented, original fixture untouched.</done>
</task>
<task type="auto">
<name>task 2: Baseline recorder + the committed pre-ALT record (measure FIRST)</name>
<files>routing/scripts/measure-optimality-baseline.mjs, routing/test/fixtures/alt-baseline.json</files>
<read_first>
- .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-RESEARCH.md (Pitfall 1 — baseline cannot be re-measured after ARG2 lands; Pitfall 2 — pin by COORDINATES, never node indices; A8 — f64 JSON round-trip of seconds is lossless)
- .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-PATTERNS.md (route-corpus.mjs exported-core/CLI-guard pattern; measure-route-cpu.mjs run mechanics)
- routing/scripts/route-corpus.mjs lines 60–92 + 320–334 (exported measurement core + `const invoked = process.argv[1] ? pathToFileURL(...)` CLI guard to copy)
- routing/scripts/measure-route-cpu.mjs lines 50–135 (GRAPH_PATH, toArrayBuffer, decodeGraph-null fail, mulberry32)
- routing/scripts/build-road-graph.mjs lines 130–133 (`fail()` print + exit 1 shape)
</read_first>
<action>
Create `routing/scripts/measure-optimality-baseline.mjs` (#!/usr/bin/env node, maintainer-time, NOTE in header: "run BEFORE the ARG2 codec lands — D-03's comparison anchor; Pitfall 1").
Architecture (copy route-corpus.mjs): exported core + CLI guard (`if (invoked === import.meta.url) main();`), named constants at top with doc-comments, `fail(message)` → console.error + exit 1, collect-then-write (record written ONLY after all pairs measured).
Constants (named, no magic):
```js
const GRAPH_PATH = resolve(here, "..", "data", "kl-roadgraph.v1.bin");
const RECORD_PATH = resolve(here, "..", "test", "fixtures", "alt-baseline.json");
const BASELINE_PAIR_COUNT = 100; // D-04: ~100 cross-metro worst-case pairs
const BASELINE_SEED = 0x08a1; // deterministic; the SAME seed --extended reuses
const MIN_PAIR_KM = 12; // straight-line endpoint floor → cross-metro class
```
Exported core:
`export function generateCrossMetroPairs(bbox, { count, minPairKm, seed })` — mulberry32(seed) PRNG (copy the exact mulberry32 from measure-route-cpu.mjs lines 100–108); per candidate pair: draw two independent uniform points inside bbox (`lat = minLat + rand()*(maxLat-minLat)` etc. via the 1e-6-safe pattern); REJECT candidates whose haversine endpoint distance < minPairKm; label `x${String(i).padStart(3,"0")}`; emit `{ pairId, origin:{lat,lng} (rounded to 1e-6 integers /1e6 exact-decimal rule), destination:{lat,lng}, fromHintKm: straight-line km }`. Deterministic: same bbox+seed ⇒ same pairs always.
`export function measureBaselinePairs(graph, pairs, { snapToGraph, routeAstar, metersToKm })` — for each pair: snap both endpoints (honest snap-miss ⇒ verdict `"unsnappable"`, recorded as `seconds: null` — a real measurement, never skipped silently); route with the CURRENT routeAstar (still haversine-potential at record time); record `seconds` as the JSON number routeAstar returns (f64 accumulation, A8-safe) or `null` for honest null; verdict = `"reachable"` if a route object returned, else `"unreachable"`. Return per-pair rows.
`main()`: existsSync(GRAPH_PATH) guard (absent graph ⇒ fail-honest, print the build hint); decodeGraph (null ⇒ fail, "rebuild the binary"); generate + measure; print per-pair one line (` xNNN: <sec>s reachable|unreachable|unsnappable`) and a summary (routed count, mean/max seconds); write RECORD only after all rows:
```json
{
"recordedAt": "<ISO-8601>",
"tool": "routing/scripts/measure-optimality-baseline.mjs",
"purpose": "pre-ALT baseline for the D-03 optimality gate (Phase 8); measured BEFORE the ARG2 codec landed (Pitfall 1)",
"epsilon": { "name": "OPTIMALITY_EPSILON_PCT", "pct": 0.1 },
"graph": { "path": "routing/data/kl-roadgraph.v1.bin", "byteLength": <n>, "nodeCount": <n>, "edgeCount": <n>, "argVersion": 1 },
"pairRules": { "seed": "0x08a1", "count": 100, "minPairKm": 12 },
"pairs": [ { "pairId": "x000", "origin": {"lat": 3.12, "lng": 101.6}, "destination": {"lat": 3.19, "lng": 101.73}, "straightLineKm": 0.0, "seconds": 0.0, "verdict": "reachable" } ]
}
```
The `epsilon` block is DOCUMENTARY here; the gate script (Plan 08-05) asserts its own constant matches this record's value. Refuse to overwrite an existing record ("baseline already recorded — the record is immutable; delete by explicit git revert only"), fail-honest.
</action>
<verify>
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH" && test -s routing/test/fixtures/alt-baseline.json && node -e "const r=require('./routing/test/fixtures/alt-baseline.json'); console.log('pairs:', r.pairs.length, 'verdicts:', JSON.stringify([...new Set(r.pairs.map(p=>p.verdict))]), 'epsilon:', r.epsilon.pct)"</automated>
</verify>
<acceptance_criteria>
- `routing/test/fixtures/alt-baseline.json` committed; `pairs.length === 100`; every pair has BOTH endpoints as `{lat, lng}` objects (grep: `"lat"` present in the origin/destination fields; zero `nodeIndex` keys anywhere).
- `epsilon.pct === 0.1` and `graph.argVersion === 1` in the record.
- The recorder re-run refuses to overwrite (exit 1 with the immutable-record message).
- The CONTINGENCY GUARD: if `routing/data/kl-roadgraph.v1.bin` is absent OR decodes null (already ARG2 — should not happen in Wave 1), the script exits 1 with the honest build/rebuild message and NO record is written or mutated.
- Summary printed to stdout includes the routed-pair count (print-the-measurement law).
</acceptance_criteria>
<done>The committed record pins 100 coordinate-identified pairs with pre-ALT seconds + verdict classes; reproducible without the old binary.</done>
</task>
<task type="auto">
<name>task 3: --extended flag on the CPU gate (default mode byte-compatible)</name>
<files>routing/scripts/measure-route-cpu.mjs</files>
<read_first>
- routing/scripts/measure-route-cpu.mjs IN FULL (185 lines — you are editing it; preserve every existing print shape)
- scripts/lib/budget-gates.mjs lines 78–119 (the zero-arg + regex contract being preserved)
- scripts/test/budget-audit.test.mjs lines 82–130 (source-token checks: `measure-route-cpu`, `P95_BUDGET_MS = 8`)
- .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-RESEARCH.md (D-04; the "constraint to re-verify" — budget-audit compatibility)
</read_first>
<action>
Add an opt-in flag, NOT a rewrite. Named constants joined at the top (doc-comment style matching the file):
```js
/** D-04 extended verdict: ~100 cross-metro coordinate-pinned pairs (maintainer-time). */
const EXTENDED_PAIR_COUNT = 100;
/** Shared pair-set seed — identical to the baseline recorder's (same pairs, two reporters). */
const EXTENDED_SEED = 0x08a1;
/** Cross-metro floor: straight-line endpoint distance below this is NOT a worst-case pair. */
const MIN_PAIR_KM = 12;
```
CLI dispatch at the very top of main-flow: `const extended = process.argv.includes("--extended")`. If extended: import { generateCrossMetroPairs } from "./measure-optimality-baseline.mjs"; build pairs from the decoded graph's bbox + EXTENDED_SEED/MIN_PAIR_KM/EXTENDED_PAIR_COUNT; snap + route with the EXISTING per-pair cpuMs harness — reuse the existing per-pair print line shape (` ${pairId}: ${ms} ms (${km})`) and the existing p95 statistics block VERBATIM (sort, ceil(0.95*n)-1 index, `p95: ... ms (max ...)` line) then an additional final line `GATE PASS:|GATE FAIL: p95 ${p95} ms <=|exceeds ${P95_BUDGET_MS} ms budget — D-04 extended verdict`. Unreachable/snap-miss probes reported separately exactly like the default mode does (count + mean of measured ms, never in the p95 sample). Exit codes: same 1-on-FAIL / 0-on-PASS honest law.
DEFAULT MODE (no flag): behaviour, stdout, exit codes byte-identical to today. Do not rename ROUTE_COUNT/SAMPLE_SEED/P95_BUDGET_MS; do not reorder prints; do not add any new output line to the default path. The only default-mode-visible change allowed: a comment block update in the header noting the D-04 extended flag exists (keeps the GPL line-citation in scripts/lib/source-gates.mjs from drifting semantically — keep lines 3–9's content intact; if adding header lines shifts them, VERIFY `node scripts/test/budget-audit.test.mjs` and `scripts/test/privacy-audit.test.mjs` still pass and adjust nothing else).
Pre-ALT, the --extended run EXPECTS an honest GATE FAIL (that is the c3 evidence); a FAIL exit is a valid measurement in Wave 1 — never paper over it.
</action>
<verify>
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH" && node routing/scripts/measure-route-cpu.mjs --extended 2>&1 | grep -E "^p95: .* ms|^ x[0-9]{3}:|GATE (PASS|FAIL):" | head -6; node scripts/test/budget-audit.test.mjs 2>&1 | tail -1</automated>
</verify>
<acceptance_criteria>
- `node routing/scripts/measure-route-cpu.mjs --extended` prints ≥ 90 ` xNNN: <ms> ms` per-pair lines (snap-miss/unreachable pairs excepted, reported separately), a `p95: <n> ms (...)` line, and a `GATE (PASS|FAIL):` line mentioning "D-04 extended verdict" — exit 1 with GATE FAIL is an acceptable pre-ALT measurement.
- `node routing/scripts/measure-route-cpu.mjs` (no args) still prints the quadrant-sample `p95:` + `GATE` lines with unchanged regex shape (`^p95: (\d+(?:\.\d+)?) ms`).
- `node scripts/test/budget-audit.test.mjs` → final line `BUDGET-AUDIT SUITE: 19/19 vectors ok`.
- grep gate: `grep -c "generateCrossMetroPairs" routing/scripts/measure-route-cpu.mjs` ≥ 1 AND the extended path lives behind `--extended` (grep `-c '"--extended"\|--extended' =≥ 1`).
</acceptance_criteria>
<done>D-04's extended verdict exists behind an opt-in flag; the audited zero-arg ladder contract is untouched and its suite is green.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| maintainer CLI → repo artifact | the recorder writes a committed artifact; garbage-in would poison the D-03 gate |
| graph binary → inpool measurement | the ARG1 binary is trusted-local today, but the recorder must fail-honest on absent/corrupt bytes |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-08-01 | Tampering/DoS | measure-optimality-baseline.mjs | mitigate | absent/corrupt/undecodable graph → `fail()` exit 1 before any measurement; decode null is never retried or papered over |
| T-08-02 | Tampering | routing/test/fixtures/alt-baseline.json | mitigate | record is immutable by construction (refuse-overwrite exit 1); committed — any tamper is visible in git history; the gate mirrors the epsilon constant so a record edit alone cannot fake equality |
| T-08-03 | Information disclosure | baseline/extended print lines | accept | prints carry route seconds + pair ids (`x000`) and km only — no coordinates beyond the committed record's public KL bbox endpoints, consistent with the category-only log law |
| T-08-04 | DoS | --extended runtime | accept | 100 routes × ~40 ms worst-case ≈ seconds, maintainer-time only; bounded by count/pairRules constants |
</threat_model>
<verification>
- `npm run routing:test` still green (no routing/src changes in this plan, but the ARG1 fixture addition must not break existing suites).
- `node scripts/test/budget-audit.test.mjs` → `BUDGET-AUDIT SUITE: 19/19 vectors ok`.
- The committed record exists with 100 coordinate-pinned pairs; `cmp` proves the ARG1 vector byte-identical to `mini-graph.bin` at this commit.
</verification>
<success_criteria>
The pre-ALT baseline is a committed, coordinate-pinned, epsilon-documented JSON artifact produced by a re-runnable tool, measured while old-code+old-binary is still in the tree (Pitfall 1 closed); the D-04 extended verdict exists behind `--extended`; budget-audit's CLI contract is proven intact by its own suite.
</success_criteria>
<output>
After completion, create `.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-01-SUMMARY.md`
</output>