18 KiB
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 | 04 | execute | 4 |
|
|
true |
|
|
Purpose: the byte payload this plan produces is what the measurement campaign (08-05) gates on; the D-02 scaling rule and the coverage/quantum gates live here so no gate can be skipped at campaign time.
Output: staged build-road-graph.mjs + a real, checked-green ARG2 routing/data/kl-roadgraph.v1.bin on disk.
<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/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.mdStructure to fit (everything is named-constant + fail-honest + collect-then-write already):
- Constants block with rationale doc-comments: lines 56–128 (MAX_BINARY_BYTES 80 MB, EXPECTED_MIN_BYTES 30 MB advisory, MIN_BINARY_BYTES 1 MB, the A1 LADDER HISTORY comment at 80–92 — ALT constants join this block and the ladder comment gains rung (c2)).
function fail(message)→ console.error + exit 1 (lines 130–133).--check <file>mode: lines 137–159 — decodeGraph + stats print + envelope checks, NO network. It currently prints nodeCount/edgeCount/nameCount/maxSpeedKmh/bbox/bytes.- Edge assembly ends at line 331 (
edgesfinal after degree-0 compaction; shape{ from, to, seconds, flags, nameId }). - Encode: line 336
const buffer = encodeGraph({ lat: cLat, lng: cLng, edges, names, maxSpeedKmh: maxSeenKmh }). - Stats gate: lines 339–367 (print-every-stat, then the enforced 1–80 MB gates, then the advisory band WARNING).
- Collect-then-write: lines 369–375 — writeFileSync happens ONLY after every gate passed.
landmark.ts (Plan 08-02 task 1) exports consumed here:
LANDMARK_QUANTUM_S = 0.1;
farthestPointAnchors(input: LandmarkInput, count: number, opts?): Uint32Array; // deterministic, degree-≥2-filtered
landmarkTables(input: LandmarkInput, anchors: Uint32Array): { to: Uint16Array; from: Uint16Array };
// LandmarkInput = { nodeCount, edges: [{from,to,seconds,flags}], quantumS }
// THROWS when any quantized distance > 65,000 (build must catch → fail() with the raise-quantum hint)
landmarkCoverage(to: Uint16Array, anchors: Uint32Array, nodeCount: number): number[]; // per-anchor reachable fraction
encodeGraph (Plan 08-02) REQUIRES landmarks: { quantumS: number; nodes: ArrayLike<number>; to: Uint16Array; from: Uint16Array } — a v1-shaped call now fails loudly.
Measured real-graph facts (Phase 6 build, 2026-10-24): 311,325 nodes / 677,068 directed edges / 14.7 MB base; ONE giant SCC covering ~305k of 311k nodes (coverage headroom: 0.98 ≫ the 0.90 gate); worst distance ≈ 5,400 s ≈ 54,000 quanta < 65,000 ceiling. Scaling rule → round(311,325 / 13,000) ≈ 24 landmarks. Byte budget: ~1.19 MB/landmark → ~42.8 MB total at L=24 — inside the enforced envelope AND the 30–80 MB advisory band (the below-band WARNING should disappear).
task 1: The preprocessing stage + --check landmark facts (code) routing/scripts/build-road-graph.mjs - routing/scripts/build-road-graph.mjs IN FULL (375 lines — you are inserting one stage and extending two print blocks; the fetch/assembly/collect-then-write laws are pinned) - routing/src/landmark.ts IN FULL (Plan 08-02's module — the exact LandmarkInput shape and the quantum-ceiling throw message you catch) - .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-RESEARCH.md (D-02 scaling rule; Pattern 2 — the script never quantizes, the module does; Pitfall 5 — the quantum knob doctrine; Open Question 1 — the parameterize-L recommendation this task implements) - .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-PATTERNS.md (build-road-graph section — named-constant law, fail() shape, gate-then-write block) - routing/test/fixtures/build-mini-graph.mjs (Plan 08-02 task 3 — the tiny-L precedent of the same stage shape) One stage, two print extensions, one override — nothing else moves.- Named constants join the constants block (rationale doc-comments, measured justification, ladder-history style):
/** D-02 scaling rule: node-count-proportional landmark count (NOT a fixed
* generous constant — per-landmark bytes scale with N, so over-provisioning
* explodes at Malaysia scale where ALT's edge itself narrows). */
const LANDMARK_COUNT_MIN = 16;
const LANDMARK_COUNT_MAX = 32;
const NODES_PER_LANDMARK = 13000; // 311,325 / 13,000 ≈ 24 at KL scale
/** Build-time coverage gate: every anchor's ĉ-Dijkstra must reach at least
* this fraction of nodes (the giant SCC covers ~0.98 — headroom is honest). */
const LANDMARK_MIN_COVERAGE_FRACTION = 0.9;
- CLI override parsed BEFORE the
--checkdispatch:--landmarks <n>— a positive integer, hard-capped at 128 (fail-loud beyond; the knob exists for measured retries within/at the top of the rule, not for unbounded table bombs). Usage doc-comment gains the flag. - THE STAGE — after the degree-0 compaction (line 331) and BEFORE the encode (line 336):
const { LANDMARK_QUANTUM_S, farthestPointAnchors, landmarkTables, landmarkCoverage } = await import("../src/landmark.ts");const landmarkCount = landmarksOverride ?? Math.min(LANDMARK_COUNT_MAX, Math.max(LANDMARK_COUNT_MIN, Math.round(cLat.length / NODES_PER_LANDMARK)));(print the rule's inputs and the chosen L)- Project the edges ONCE for the module:
const lmEdges = edges.map((e) => ({ from: e.from, to: e.to, seconds: e.seconds, flags: e.flags }));(the module's input shape; no mutation of the encode-side array). const anchors = farthestPointAnchors({ nodeCount: cLat.length, edges: lmEdges, quantumS: LANDMARK_QUANTUM_S }, landmarkCount);const { to, from } = landmarkTables({ nodeCount: cLat.length, edges: lmEdges, quantumS: LANDMARK_QUANTUM_S }, anchors);wrapped in try/catch →fail("landmark tables exceeded the u16 quantum ceiling (<message>) — raise LANDMARK_QUANTUM_S in routing/src/landmark.ts and rebuild (Pitfall 5)")on the module's throw.- Coverage gate:
const coverage = landmarkCoverage(to, anchors, cLat.length);→min < LANDMARK_MIN_COVERAGE_FRACTION⇒fail(...)naming L, the offending anchor index/id, its fraction, and the giant-SCC expectation. - Sentinel audit print: total 0xFFFF entries per family (cheap honesty; --check mirrors it).
- Encode passes the block:
encodeGraph({ lat: cLat, lng: cLng, edges, names, maxSpeedKmh: maxSeenKmh, landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from } })— the ONE authority; no local fallback encode, no try-to-encode-without-landmarks branch. - Stats print extension (the gate-then-write block): landmark lines — count, quantum, min/mean coverage, table bytes (
2·L·N·2 + 4·L), sentinel totals — BEFORE the size gates. The existing envelope gates are UNCHANGED (80 MB ceiling / 1 MB floor / 30 MB advisory). --checkextension: the decoded graph now carrieslandmarkCount/landmarkQuantumS/landmarkNodes— print them plus the per-anchor coverage (import landmarkCoverage; the u16 tables are on the decoded graph) and the sentinel totals. No network.- Header doc-comment: the A1 LADDER HISTORY block gains rung (c2) — "EXECUTED in this script's preprocessing stage (Phase 8): build-time landmark tables + ALT steering; the verdict lives in measure-route-cpu.mjs's history and docs/ROAD-GRAPH.md". Usage block documents
--landmarks. export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH" && node --check routing/scripts/build-road-graph.mjs && node routing/scripts/build-road-graph.mjs --check routing/data/kl-roadgraph.v1.bin 2>&1 | tail -2; test $? -eq 1 && echo "ARG1-HONEST-REFUSE-OK"; grep -c "farthestPointAnchors" routing/scripts/build-road-graph.mjs <acceptance_criteria>- grep gates:
farthestPointAnchors≥ 1;landmarkTables≥ 1;landmarkCoverage≥ 2 (stage + --check);LANDMARK_MIN_COVERAGE_FRACTION≥ 2 (const + gate);NODES_PER_LANDMARK≥ 1;--landmarks≥ 2 (parse + usage doc). - The encodeGraph call carries
landmarks: {(grep ≥ 1); the script contains NO local Dijkstra/priority-queue implementation (grep -ci "dijkstra\|priorityqueue" routing/scripts/build-road-graph.mjscounts only doc-comment mentions of the module's Dijkstras — zero code hits). --check routing/data/kl-roadgraph.v1.binexits 1 at this commit with the decode-null honest message — the on-disk binary is STILL ARG1 (the rebuild is task 2); the fail-honest path is proven, not assumed.- Collect-then-write preserved:
writeFileSyncappears exactly once, after the gates (grep -c = 1). node --check(syntax) passes. </acceptance_criteria> The build tool now produces ARG2 bytes with build-time tables through the one landmark module and the one encode authority, gated honestly on coverage and quantum ceiling.
- grep gates:
node routing/scripts/build-road-graph.mjs— expected stdout: the bbox line (identical bbox to the 2026-10-24 build — it is computed from the COMMITTED corpus ± margin), the fetch, the graph-stats block, the NEW landmark block (L = 24 by the rule at ~311k nodes; quantum 0.1; coverage min/mean; table bytes; sentinel totals), the envelope gates green, the write line. Expected size ≈ 43 MB (14.7 MB base + ~28 MB tables at L=24) — inside the envelope AND the advisory band; the old below-band WARNING should NOT appear.node routing/scripts/build-road-graph.mjs --check routing/data/kl-roadgraph.v1.bin— expected exit 0 printing nodeCount/edgeCount + landmarkCount/quantum/coverage/sentinels.- Record in the summary: L used, per-anchor coverage min/mean, table bytes, total MB, node/edge counts. These are the campaign's build facts (08-05 compares record-vs-current counts from them).
- HONEST FAILURE PATHS — do not paper over any of these:
- Overpass unreachable/timeout → the script exits 1, the previous binary untouched. STOP: record the failure; the campaign plans need the real binary. Do NOT fabricate, shrink the query, or retry beyond the script's own law.
- Coverage gate FAIL → the build refuses (exit 1). Do NOT lower LANDMARK_MIN_COVERAGE_FRACTION to force a pass — investigate (fragmented extract?) and report.
- Quantum ceiling throw → raise nothing silently; report (KL is measured-safe at 54,000/65,000 units — a throw means something is wrong, not tight).
- Do NOT run the CPU gates here — every verdict belongs to the campaign (08-05). This plan ends at a checked-green artifact.
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH" && node routing/scripts/build-road-graph.mjs --check routing/data/kl-roadgraph.v1.bin 2>&1 | tail -8 && echo "REBUILD-CHECK-GREEN"
<acceptance_criteria>
--checkexits 0 onrouting/data/kl-roadgraph.v1.binand printslandmarkCount≥ 16 with quantum 0.1 and per-anchor coverage ≥ 0.9.- The binary size is inside the enforced 1–80 MB envelope (expected ≈ 43 MB; the printed
binary sizeline is quoted in the summary). - The build's stdout (stats + landmark block + write line) is quoted in the summary — the measured facts exist outside the terminal.
git status: routing/src/** and routing/test/** UNCHANGED by this task (a build run touches only the gitignored artifact — any source drift here is a plan violation). </acceptance_criteria> An ARG2 KL binary exists locally, built through the staged pipeline with the D-02 rule's L, checked green, with every gate number printed and quoted.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Overpass extract → build | the fetch is remote data; a drifted/corrupt extract must not silently become a hosted binary |
| --landmarks override → table size | a fat-fingered count must not produce an envelope-busting or table-bomb binary |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-08-09 | Tampering | drifted/corrupt extract → subtly wrong binary | mitigate | the existing envelope gates + the NEW coverage/quantum gates run before any write; the D-03 optimality gate (08-05) then compares seconds against the frozen record — count drift prints a WARNING there; nothing hosts until the campaign passes |
| T-08-10 | DoS | build runtime / table size | mitigate | stage cost ≈ 2L Dijkstra passes (seconds-class, A4); --landmarks hard-capped at 128; the 80 MB envelope gate fires before any write regardless of L |
| T-08-11 | Repudiation | forcing a pass by lowering L or the coverage bar | mitigate | the coverage gate uses the same fraction for ANY L; the override cannot lower it; every build prints L + coverage — the summary quotes them, so a gamed build is visible in the record |
| T-08-12 | Information disclosure | build stdout | accept | prints counts/coverage/km-class stats of PUBLIC OSM data — no user coordinates exist in the build path |
| </threat_model> |
<success_criteria> build-road-graph.mjs gains the preprocessing stage with the D-02 scaling rule, the honest coverage/quantum gates, and the --landmarks measured knob — implementing nothing the landmark module doesn't already — and a real ARG2 KL binary sits on disk, checked green, with every gate number printed and quoted in the summary. </success_criteria>
After completion, create `.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-04-SUMMARY.md`