--- 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: 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" --- 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`. @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md @.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 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). task 1: Preserve ARG1 fixture bytes as the rejection vector routing/test/fixtures/mini-graph-arg1.bin - 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) 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. 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" - `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`. ARG1 bytes frozen under a new committed name, provenance documented, original fixture untouched. task 2: Baseline recorder + the committed pre-ALT record (measure FIRST) routing/scripts/measure-optimality-baseline.mjs, routing/test/fixtures/alt-baseline.json - .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) 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: s reachable|unreachable|unsnappable`) and a summary (routed count, mean/max seconds); write RECORD only after all rows: ```json { "recordedAt": "", "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": , "nodeCount": , "edgeCount": , "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. 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)" - `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). The committed record pins 100 coordinate-identified pairs with pre-ALT seconds + verdict classes; reproducible without the old binary. task 3: --extended flag on the CPU gate (default mode byte-compatible) routing/scripts/measure-route-cpu.mjs - 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) 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. 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 - `node routing/scripts/measure-route-cpu.mjs --extended` prints ≥ 90 ` xNNN: ms` per-pair lines (snap-miss/unreachable pairs excepted, reported separately), a `p95: 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`). D-04's extended verdict exists behind an opt-in flag; the audited zero-arg ladder contract is untouched and its suite is green. ## 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 | - `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. 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. After completion, create `.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-01-SUMMARY.md`