Files
arah2/.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-04-PLAN.md
T

18 KiB
Raw 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 04 execute 4
08-03
routing/scripts/build-road-graph.mjs
true
SC3
SC4
truths artifacts key_links
The build computes landmark tables at build time through the ONE landmark module (farthestPointAnchors + landmarkTables + landmarkCoverage from ../src/landmark.ts) — no second Dijkstra implementation ever exists in the script (single-authority law)
L follows the D-02 scaling rule clamp(round(N/13000), 16, 32) — node-count-proportional, ~24 at KL's 311k nodes — with a maintainer --landmarks override as the measured campaign knob; anchors are road-network nodes (NOT POIs), deterministic
Build fails honestly (fail() → exit 1, previous binary untouched — collect-then-write) when any anchor's coverage < LANDMARK_MIN_COVERAGE_FRACTION or landmarkTables throws the quantum-ceiling error (raise the quantum, never clamp)
--check prints the landmark facts (count, quantum, coverage) decoded from the binary; the stats gate prints L/quantum/coverage/table-bytes BEFORE the write
path provides
routing/scripts/build-road-graph.mjs the preprocessing stage between edge compaction and encode; the landmarks block into the one encodeGraph; extended stats + --check prints; the --landmarks override
from to via pattern
routing/scripts/build-road-graph.mjs preprocessing stage routing/src/landmark.ts dynamic import of LANDMARK_QUANTUM_S + farthestPointAnchors + landmarkTables + landmarkCoverage — the only table implementation farthestPointAnchors
from to via pattern
routing/scripts/build-road-graph.mjs encodeGraph call routing/src/graph.ts (Plan 08-02's required landmarks block) landmarks: { quantumS, nodes: anchors, to, from } — the one authority, no v2-only encode landmarks:\s*{
from to via pattern
routing/scripts/build-road-graph.mjs --check graph.landmarkCount / landmarkQuantumS (decoded fields) landmark stats in the --check print block landmarkCount
The build-side half of ALT: a preprocessing stage in `build-road-graph.mjs` that quantizes nothing itself, implements nothing itself — it feeds the graph's edges to the one landmark module, gates the result honestly, and encodes ARG2 through the one authority — then rebuilds the REAL KL binary with it.

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

Structure 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 (edges final 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.
  1. 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;
  1. CLI override parsed BEFORE the --check dispatch: --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.
  2. 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).
  3. 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.
  4. 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).
  5. --check extension: the decoded graph now carries landmarkCount / 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.
  6. 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.mjs counts only doc-comment mentions of the module's Dijkstras — zero code hits).
    • --check routing/data/kl-roadgraph.v1.bin exits 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: writeFileSync appears 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.
task 2: The real rebuild (network, maintainer-time) — an ARG2 KL binary on disk - The staged script (task 1 of this plan) — the exact stdout shape you will quote - .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-RESEARCH.md (Environment Availability — Overpass public API, REQUEST_TIMEOUT_MS 300 s, one request; A4 build-cost ≈ 2L Dijkstra passes ≈ seconds-class) - .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-CONTEXT.md (D-02 — the count is measured-justified; D-01 — same output path/key) Run the REAL build and check the artifact. These are maintainer-time commands with a network fetch (minutes); quote the full landmark stats block in the summary (print-the-measurement law).
  1. 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.
  2. node routing/scripts/build-road-graph.mjs --check routing/data/kl-roadgraph.v1.bin — expected exit 0 printing nodeCount/edgeCount + landmarkCount/quantum/coverage/sentinels.
  3. 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).
  4. 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).
  5. 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>
    • --check exits 0 on routing/data/kl-roadgraph.v1.bin and prints landmarkCount ≥ 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 size line 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>
- `node routing/scripts/build-road-graph.mjs --check routing/data/kl-roadgraph.v1.bin` exit 0 with landmark facts. - `npm run routing:test` still green (the suite never imports the build script; this proves no accidental coupling). - The summary quotes the full build stats + landmark block.

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