Files

19 KiB
Raw Permalink Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
08-alt-landmark-preprocessing-routing-backend-v2 01 execute 1
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
true
SC3
SC4
truths artifacts key_links
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)
path provides
routing/scripts/measure-optimality-baseline.mjs exported pair-generation + measurement core; CLI records the baseline
path provides
routing/test/fixtures/alt-baseline.json committed pre-ALT baseline record — the D-03 comparison anchor
path provides
routing/test/fixtures/mini-graph-arg1.bin ARG1 bytes preserved (byte-identical to the pre-phase mini-graph.bin)
path provides
routing/scripts/measure-route-cpu.mjs --extended opt-in flag; default mode byte-compatible
from to via pattern
routing/scripts/measure-optimality-baseline.mjs routing/data/kl-roadgraph.v1.bin readFileSync → toArrayBuffer → decodeGraph → snapToGraph → routeAstar decodeGraph
from to via pattern
routing/test/fixtures/alt-baseline.json pairs pinned by origin/destination lat/lng record schema — never node indices (Pitfall 2) "origin"\s*:\s*{\s*"lat"
from to via pattern
routing/scripts/measure-route-cpu.mjs (--extended) routing/scripts/measure-optimality-baseline.mjs import { generateCrossMetroPairs } — ONE pair-set definition, two consumers 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.

<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_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

From routing/scripts/measure-route-cpu.mjs (lines 50–64, 136–185):

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:

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):

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):

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:

{
  "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. 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)" <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> 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 <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> D-04's extended verdict exists behind an opt-in flag; the audited zero-arg ladder contract is untouched and its suite is green.

<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>
- `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.

<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>

After completion, create `.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-01-SUMMARY.md`