# Phase 8: ALT Landmark Preprocessing (Routing Backend v2) - Research **Researched:** 2026-10-25 **Domain:** ALT (A\*, Landmarks, Triangle inequality) preprocessing over a hand-rolled typed-array CSR graph; binary layout versioning (ARG1 → ARG2); Cloudflare Workers free-plan CPU budget **Confidence:** HIGH (core algorithm + integration verified against code + literature; build-runtime figures derived from measured numbers and flagged) ## User Constraints (from CONTEXT.md — LOCKED, do not re-litigate) ### D-01 — ARG2-only, fail-honest Graph layout version bumps to a new version carrying landmark tables. Old ARG1 binaries decode to **null** → the Phase 6 designed router-pending state covers them. Exactly **one decode path and one search path** — no v1 fallback branch, no silent quality regression. Upload script stays unchanged (roadmap lock); **re-uploading the rebuilt binary is the whole migration** (same R2 key, new bytes). ### D-02 — Landmark count is a scaling rule, not a fixed constant Selection *approach* is locked: build-time Dijkstra passes over well-spread anchor nodes (farthest-point / hill-climb family over the road graph); **anchors are road-network nodes, NOT POIs**. The count is measurement-justified and node-count-proportional (expected lean ~16–32 at KL's 311k nodes). A future Malaysia-wide build re-derives the count from the same rule; nothing over-provisioned for a graph that doesn't exist. ### D-03 — Equal seconds, ties allowed (the optimality gate) Corpus re-run asserts: total route **seconds** match the pre-ALT baseline exactly or within a named tiny epsilon (~±0.1%) and every pair keeps the same verdict class (reachable/unreachable). ALT changes steering, never the cost model. Bidirectional search + different potentials may legitimately change tie-breaking (a different equal-cost path) — tie-path differences are documented behavior, NOT failures. Bit-identical node paths is explicitly NOT the gate. ### D-04 — Extended seeded maintainer-time sample for the ALT verdict The existing 25-route seeded gate (`measure-route-cpu.mjs`, seed `0x06d4`) stays as the CI-size snapshot; the ALT verdict additionally runs a maintainer-time extended seeded sample (~100 cross-metro worst-case pairs). Same print-the-measurement law. Maintainer-time measurement is free. ### D-05 — Retire the c3 class with evidence `docs/KNOWN-LIMITATIONS.md`'s cross-metro CPU degradation section is replaced by a short retirement record: what changed (ALT preprocessing), the measured numbers that retired it (p95 under the ceiling, from D-04's run), and the date. The ALT row in the limitations' roadmap-pointer table flips from "Next-milestone item, not built" to built (Phase 8). Because of D-01 there is NO "fallback for landmark-less graphs" to document — an old graph is simply router-pending, which is already a designed, documented degradation. ### OpenCode's Discretion (research must recommend inside these) - Landmark-selection algorithm specifics (farthest-point vs hill-climb vs hybrid), anchor count formula constants, quantization of stored distances within the scaling rule - Preprocessing build-time cost budget (each landmark = one full graph pass; build script runtime stays sanity-bounded) - The exact epsilon value and its name (named constant, replay-tested, per phase-6 constant law) - ARG2 layout byte design (header extensions aligned to the typed-array law; corrupt-binary fail-honest validation carried forward) - How the committed mini-graph fixture and test suite version-bump (new-ARG2 fixtures; old-v1 fixture kept only as a decode-rejection case) - KNOWN-LIMITATIONS retirement wording within the honest-engineering tone law ### Deferred Ideas (OUT OF SCOPE — do not touch) - Contraction Hierarchies (Phase 6 deferral stands; the noted future tier for Malaysia-wide scale) - Engine B client-side router consuming the ARG2 asset (Phase 11) - Turn restrictions in the graph (Phase 6 deferral stands) - Any traffic/time-dependent weighting (v2 per REQUIREMENTS) ## Summary This phase adds ALT preprocessing to the existing bidirectional A\* (`routeAstar` in `routing/src/astar.ts`) without a new search skeleton, a new binary layout (ARG2) that appends quantized landmark distance tables to the pinned CSR layout, and a measurement campaign (build → check → upload → gate → corpus → docs) that retires the c3 degradation class. The decisive verified finding: **the repo's BH potential structure is already the canonical bidirectional-ALT combination scheme**. The route-planning survey (Bast, Delling, Goldberg, Müller-Hannemann, Pajor, Sanders, Wagner 2015) states that two arbitrary *feasible* potentials πf, πr are combined into consistent potentials by using (πf−πr)/2 forward and (πr−πf)/2 backward — which is *literally* the code's existing `p_f(v) = (h_f(v) − h_b(v))/2, p_b = −p_f` (Ikeda) form with its proven v-independent meet-sum constant. Consequently **ALT slots in by replacing the two haversine/max-speed heuristics h_f, h_b with ALT triangle-inequality bounds — the only function that changes in `routeAstar` is `potential()`**. The heap logic, balanced alternation, meet probes, stop rule, and real-seconds reconstruction all stay byte-for-byte identical, and A1's exhaustive 625-pair Dijkstra-equivalence sweep remains the standing optimality proof. The second decisive finding is the **quantization trick that makes u16 tables exactly correct with no epsilon fudge**: instead of quantizing stored landmark *distances* and patching admissibility with a subtractive constant, the build quantizes the *edge weights* of a surrogate metric (ĉ(u,v) = floor(seconds/q) in fixed 0.1 s units) and runs the landmark Dijkstras over that integer metric. Distances within the ĉ metric are exactly 1-Lipschitz in ĉ, and ĉ ≤ c edge-wise, so ALT bounds computed inside the metric are (a) exactly admissible lower bounds on real seconds (`d̂(v,ℓ) − d̂(t,ℓ) ≤ d̂(v,t) ≤ d(v,t)` — the directed triangle via t) and (b) exactly 1-Lipschitz w.r.t. the real f32 edges (since |d̂(u,ℓ)−d̂(v,ℓ)| ≤ ĉ(u,v) ≤ c(u,v)). Both properties A* needs — admissibility *and* the feasibility/consistency that the balanced-potential construction and the BH stop rule require — hold by construction with integer arithmetic, no rounding correction, no epsilon. The U16 range check (KL's worst distance ≈ 5,400 s ≈ 54,000 units of 0.1 s < 65,535) closes the design. Budget arithmetic (from the measured 311,325 nodes / 677,068 edges / 14.7 MB base): dual-family u16 tables cost L × 311,325 × 2 B × 2 ≈ 1.19 MB per landmark — L=16 → ~33.7 MB total, L=24 → ~43.2 MB, L=32 → ~52.7 MB. All fit the enforced 1–80 MB envelope with the advisory 30–80 MB band *satisfied* (the current below-band WARNING disappears at L≥16). Build-time preprocessing is ~2L Dijkstra passes over 677k edges ≈ 2 × 24 × ~40 ms ≈ 2 s of measured-order work (the 38 ms unidirectional A* p95 over the same graph bounds one full pass) — comfortably inside any sane build budget. Expected query behavior: the 20–30 km cross-metro class that currently measures p95 38–93 ms is exactly the regime where ALT's sharpened potentials cut settled nodes by roughly an order of magnitude; the pass verdict (p95 ≤ 8 ms) is *expected but not guaranteed* — the gate decides, and the D-02 scaling rule (raise L toward 32) is the pre-planned remediation knob before anything else. **Primary recommendation:** Implement ALT as (1) an ARG2 layout appending two u16 landmark-table families (to-landmark + from-landmark) produced by integer ĉ-metric Dijkstras; (2) a `potential()`-only change in `astar.ts` that max-combines the four triangle-bound families per landmark with sentinel-skip and a haversine fallback; (3) a committed coordinate-identified baseline record *measured before the code change*, then the D-03 seconds-equality gate + D-04 extended 100-pair verdict + the existing 25-route p95 gate; (4) D-05's docs retirement. Do not touch `upload-road-graph.mjs`, `worker-router.ts`'s API contract, the snap module, or the reverse-CSR/WeakMap patterns. ## Architectural Responsibility Map | Capability | Primary Tier | Secondary Tier | Rationale | |------------|-------------|----------------|-----------| | Landmark selection + table Dijkstras | Build scripts (`routing/scripts/`, Node, maintainer-time) | — | Preprocessing is build-time by design (D-02, context); no runtime preprocessing is even conceivable in the Worker (V8 isolate CPU budget) | | ARG2 binary encode/decode | `routing/src/graph.ts` (typed arrays, pure) | — | Single authority law: encode is inverse of decode, one module | | ALT potentials at query time | `routing/src/astar.ts` `routeAstar` | — | The search is the only consumer; probe lookups are typed-array reads inside the existing lazy-potential mechanism | | Binary hosting + rollout | Cloudflare R2 (same bucket/key) | `bot/src/routing/worker-router.ts` | R2 `ROAD_GRAPH` binding already deployed; Worker reads bytes → decodeGraph; zero API change | | Rollout degradation (old binary present) | Worker `loadGraph` → decode null → 200-null | Mini App router-pending UI | D-01 fail-honest posture already shipped and tested (RT1–RT11) | | Measurement + optimality gates | `routing/scripts/measure-route-cpu.mjs` + new baseline/gate tooling | corpus scripts | Maintainer-time repo tools, never runtime imports | | Docs retirement record | `docs/KNOWN-LIMITATIONS.md`, `docs/ROAD-GRAPH.md` | `docs/CONFIGURATION.md` (verify touch) | D-05 + fork contract surface | ## Standard Stack ### Core | Component | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | Node.js | v24.x (nvm; 24.17/24.18 installed) [VERIFIED: shell] | Build scripts + hand-rolled test runners | Repo standard | | `routing/src/graph.ts` codec | current (ARG1 v1) | The layout single authority this phase versions to ARG2 | Encode/decode laws + zero-copy typed-array alignment laws already | | `routing/src/astar.ts` routeAstar | current (BH/Ikeda bidirectional) | The search ALT plugs into via `potential()` | Exhaustively proven optimal (A1, 625-pair sweep); meet-sum law | | Zero new dependencies | — | Landmarks are arithmetic over existing typed arrays; Dijkstra runs inside build scripts | Repo zero-dep law (Phase 6 added zero runtime deps) | ### Supporting (existing, no changes) `routing/scripts/build-road-graph.mjs` (gains a preprocessing stage), `routing/scripts/measure-route-cpu.mjs` (gains the extended-sample flag + ladder-history rung), `routing/test/fixtures/build-mini-graph.mjs` (regenerates fixture as ARG2), `routing/test/harness.mjs` (registry unchanged). ### Alternatives Considered (from Phase 6's A1 ratification — all waived, listed for completeness only) | Instead of ALT (c2) | Option | Why rejected / out of scope | |------------|-----------|----------| | — | (c1) Workers Paid $5/mo | Breaks the $0 mandate (A1 ratification) | | — | (c3) Accept degradation | retired BY this phase (D-05) | | — | Contraction Hierarchies | Deferred future tier (Phase 6 D-02; Malaysia-scale job per D-02 rationale) | **Installation:** none — zero dependencies. (Node via nvm present; verify `node --version` resumes on PATH inside the executing session's shell, v24.x installed.) ## Architecture Patterns ### System Architecture Diagram ``` ┌──────────────────────── MAINTAINER TIME ───────────────────────────┐ Overpass extract ──▶│ build-road-graph.mjs │ (same query) │ fetch → dedup → CSR edges ──▶ NEW preprocessing stage: │ │ 1. quantize edges to integer ĉ (0.1 s units, floor) │ │ 2. select L anchors (farthest-point over ĉ, seeded/ID-tiebreak) │ │ 3. per landmark: reversed-graph ĉ-Dijkstra → TO table │ │ 4. per landmark: forward-graph ĉ-Dijkstra → FROM table │ │ 5. SCC-coverage assertion per anchor (fail honest otherwise) │ │ 6. encodeGraph (ARG2) — header v2 + u16 table sections │ │ output: routing/data/kl-roadgraph.v1.bin (ARG2 bytes, ~43 MB) │──┐ │ --check prints node/edge/landmark stats │ │ └────────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────── RUNTIME (Cloudflare Workers isolate) ─────────┐ upload (UNCHANGED bot /api/route ────▶│ worker-router.loadGraph: 1 R2 GET (same key) → decodeGraph │◀── script, same key) │ ARG2 header? → typed-array views (zero-copy), tables included │ │ ARG1/corrupt/absent? → null → 200 null (router-pending, D-01) │ │ routeAstar(graph, o, g): │ │ per-search landmark constants (L lookups) │ │ lazy potential(v) = (h_f(v) − h_b(v))/2 │ │ h_f(v) = max over valid ℓ of ALT toward-goal bounds │ │ (fall back to haversine bound at sentinel nodes) │ │ h_b(v) = same toward ORIGIN │ │ …everything else in routeAstar UNTOUCHED… │ └────────────────────────────────────────────────────────────────────┘ output: route → deriveSteps → RouteResult (unchanged) ``` Primary use case trace: maintainer builds ARG2 → uploads same key → Worker cold isolate R2 GET → decodeGraph validates v2 + exact size → route request → snap → routeAstar with ALT potentials → real-seconds recompute → deriveSteps. Rollout path: stale ARG1 in R2 → decode null → router-pending (designed, tested) until one upload. ### Component-file mapping (implementation side) | Piece | Lives in | |---|---| | ARG2 codec (layoutFor, header, views, exact-size law) | `routing/src/graph.ts` (extended in place — remains THE single authority) | | ALT potential + table probes | `routing/src/astar.ts` (`routeAstar.potential()` only) | | Preprocessing stage (quantize, select, Dijkstras, coverage assert) | `routing/scripts/build-road-graph.mjs` (new stage between edge assembly and encode) | | Extended sample + verdict | `routing/scripts/measure-route-cpu.mjs` (parameter/flag — "a parameter, not a rewrite" per 08-CONTEXT) | | D-03 optimality gate + baseline record | new maintainer-time script beside `route-corpus.mjs` (naming discretion), fixtures/record under `routing/test/fixtures/` or `routing/data/` as fits the repo's artifact policy | | Fixture version bump | `routing/test/fixtures/build-mini-graph.mjs` + committed `mini-graph.bin` (ARG2) + old bytes kept as the v1 rejection vector | | D-05 records | `docs/KNOWN-LIMITATIONS.md`, `docs/ROAD-GRAPH.md`, (check) `docs/CONFIGURATION.md` | ### ARG2 Binary Layout (recommended — within D-01/D-01-adjacent discretion) Extension discipline: **append, never mutate**. All ARG1 sections keep their absolute offsets and sizes; the landmark section is a suffix; the only header region touched is the 4 reserved pad bytes plus 8 fresh bytes past 64. ``` ARG2 layout (little-endian): header (72 bytes, was 64): magic u32 @0 = 0x41524732 ("ARG2") [VERIFIED pattern: graph.ts] version u32 @4 = 2 nodeCount u32 @8 | edgeCount u32 @12 | nameCount u32 @16 | namePoolLen u32 @20 bbox 4×f64 @24..47 | maxSpeedKmh f32 @52..55 (unchanged) landmarkCount u32 @56 (was pad; ≥ 1 REQUIRED — see law below) landmarkQuantumS f32 @60 (recommended default 0.1 — the ĉ unit, seconds) reserved u32 @64..67 (= 0) reserved u32 @68..71 (= 0) …all ARG1 node/edge/name sections byte-identical to ARG1, HEADER_SIZE = 72 (72 % 8 == 0 keeps the f64 lat/lng views 8-byte aligned — the alignment law holds)… landmark tables (suffix; TOTAL must extend the ARG1-implied size by exactly 2 × landmarkCount × nodeCount × 2 bytes so the exact-size law extends): toTable u16[landmarkCount × nodeCount] rounded 4-byte-aligned start row ℓ = quantized ĉ-distance v→ℓ for every node v sentinel 0xFFFF = v cannot reach ℓ fromTable u16[landmarkCount × nodeCount] (immediately after) row ℓ = quantized ĉ-distance ℓ→v landmarkNodes u32[landmarkCount] rows' anchor node ids (audit + --check prints) total = 72 + ARG1 sections + 2·L·N·2 + 4·L (+ alignment pad to 4) ``` **Laws carried forward into decodeGraph:** - magic/version mismatch → null (D-01: version 1 → null; version ≥ 3 → null). The existing `GRAPH_VERSION` constant becomes 2; G8's pin updates. - Exact byte-size check BEFORE any view allocation (T-06-03b) — now including the landmark suffix; a hostile huge-landmarkCount header rejects on arithmetic. - `landmarkCount === 0` → null: D-01's no-silent-degradation law extended to the *content* level. A landmark-less ARG2 would silently route with weak steering; the build always ships ≥ 1 anchor, so decode refusing 0 keeps "no fallback branch" true at every layer. [ASSUMED — this is my recommended interpretation of D-01's spirit; flag in plan review.] - Sentinel sanity: decode asserts every u16 table byte read is structurally possible (nothing to validate beyond exact-size + type law; 0xFFFF is data). **Byte-budget table (measured base 14,749,343 B = 14.7 MB):** | L (anchors) | u16 dual-family table bytes | Total binary | Envelope? | Advisory band? | |---|---|---|---|---| | 16 | 18.7 MB | ~33.4 MB | ✓ (≤80) | ✓ (≥30 — CURRENT below-band WARNING disappears) | | 24 | 28.1 MB | ~42.8 MB | ✓ | ✓ | | 32 | 37.4 MB | ~52.1 MB | ✓ | ✓ | | 32 with f32 tables | — | ~90.7 MB | ✗ over 80 MB | — | f32 tables die at L=32; u16 is what makes the L=16–32 scaling rule byte-safe. Engine-B/Phase 11's "phone payload ~30–50 MB class" stays satisfied for L ≤ ~28. **u16/quantum range law:** with quantum 0.1 s, tables hold distances ≤ 65,535 → max expressible ≈ 6,553 s. KL's worst-route seconds ≈ 5,400 s measured-class → ~54,000 units, comfortable. Build asserts no stored value and no intermediate Dijkstra label exceeds 65,000 units (fail honest, printed). For forks at larger scale: the header's `landmarkQuantumS` is the knob (raise to 0.2 s × 65,535 ≈ 3.6 h), documented in ROAD-GRAPH.md. [VERIFIED numbers from phase-6 build stats; quantum ceiling arithmetic VERIFIED trivially.] ### Pattern 1: the ALT potential inside the existing BH (THE core integration) **What:** `routeAstar`'s potentials are currently `h_f(v) = metersTo(v, goal)/maxSpeedMps`, `h_b(v) = metersTo(v, origin)/maxSpeedMps`, combined as `p(v) = (h_f(v) − h_b(v))/2` (astar.ts lines 232–251), with the doc-comment proof that the meet keys satisfy `d_f(v) + d_b(v) = real(o→g through v) − p_f(o) + p_f(g)`. The survey verifies this class of construction is exactly how bidirectional A* takes arbitrary feasible potentials: "[one] can combine two arbitrary feasible functions πf and πr into consistent potentials by using (πf−πr)/2 for the forward search and (πr−πf)/2 for the backward search". So ALT shipping means: replace the bodies of h_f/h_b with ALT triangle bounds; the combination, reductions, meet-probes, stop rule, and reconstruction are untouched. **The four bound families per landmark ℓ** (Bast et al. survey, §2.2 Goal-Directed Techniques — verbatim inequality set): - toward-goal family A: `dist(v,t) ≥ dist(v,ℓ) − dist(t,ℓ)` — uses **toTable** for both terms: per-node `toTable[ℓ][v]`, per-search constant `toTable[ℓ][t]`. - toward-goal family B: `dist(v,t) ≥ dist(ℓ,t) − dist(ℓ,v)` — uses **fromTable** both: `fromTable[ℓ][t]` const, `fromTable[ℓ][v]` per-node. - toward-origin family A: `dist(v,o) ≥ dist(v,ℓ) − dist(o,ℓ)` — toTable for both (per-node + const). - toward-origin family B: `dist(v,o) ≥ dist(ℓ,o) − dist(ℓ,v)` — fromTable for both. Rich lower bound = `h_f(v) = QUANTUM × max over ℓ of max(famA, famB)` in units, converted to seconds; `h_b(v)` likewise toward origin. Both h_f and h_b are then (i) admissible lower bounds on real seconds and (ii) exactly consistent (1-Lipschitz) — see Pattern 2 for why (i) and (ii) hold exactly, and Pattern 3 for the sentinel-island caveat plus its containment. **Runtime shape (allocation-free, A8-law compatible):** ```ts // search start: L-length constant tables (Float64Array sec) + skip-sentinel masks const gTo = new Float64Array(L), gFrom = new Float64Array(L); // per-search goal consts, NaN = sentinel const oTo = new Float64Array(L), oFrom = new Float64Array(L); // origin consts // per-node lazy evaluation, max-of-bounds loop over L; on "no valid ℓ" fall back to // the (already-consistent) haversine bound computed as today. ``` This is the same lazy/cache shape the module already has (`hfCache`/`hbCache` Float64Arrays, NaN-init). Cost per node touch: L ≈ 16–32 iterations of 2 typed-array reads + arithmetic — comparable to today's two haversine trig chains; the win is that vastly FEWER nodes are touched. No `Map`, no per-node objects (A8 pin stays green). **Why this works with `p_b = −p_f`:** the survey-scheme potentials πf = h_f, πr = h_b are each feasible (lower bounds toward their own target); (πf−πr)/2 and (πr−πf)/2 are consistent in exactly the code's sign convention (`candidate = du + c + pfu − pv` forward; `du + c − pfu + pvv` backward telescopes to `d_b(v) = d(v,g) − p_f(v) + p_f(g)`). The v-independent meet-sum follows; the stop `topF + topB ≥ mu` retains its optimality proof verbatim. [VERIFIED: survey formulation; code sign-analysis verified line-by-line against astar.ts during research.] **ALT beats the haversine bound exactly where it's weak** — survey: "geographical distance divided by the maximum travel speed… corresponding bounds are poor, and the performance gain is small or non-existent." That is the measured Phase 6 failure mode (38 ms p95 despite BH). **Recommendation (discretion):** also `max` the ALT bound with the existing haversine bound per direction — max of two consistent admissible heuristics stays consistent and admissible, and it guarantees a sane bound at sentinel/edge nodes. [ASSUMED max-rule; standard result.] ### Pattern 2: the ĉ-metric surrogate — quantization WITHOUT an admissibility epsilon The classic u16 question ("does rounding break admissibility?") is resolved by never quantizing *stored distances* at all — quantize the *metric*, once: 1. Build computes integer weights ĉ(u,v) = `Math.floor(seconds(u,v) / q)` with q = 0.1 s (0 ≤ ĉ·q ≤ c — floor keeps the surrogate BELOW live edge cost; ≤ q loss per edge, ≈ 0.1 s on 1–5 s edges ⇒ < ~2% weight error localized to short residential edges; NIL on arterials). 2. Landmark Dijkstras run over ĉ **in exact integer arithmetic** (distances in q-units; JS numbers hold these exactly ≤ 65,535). No float round-up rule, no per-entry fudge. 3. The two triangle families are evaluated **within the ĉ metric**: `h_ℓ(v) = d̂(v,ℓ) − d̂(t,ℓ) ≤ d̂(v,t) ≤ d(v,t)` — the first ≤ is the *directed triangle* (v→t ≥ (v→ℓ) − (t→ℓ), i.e. d(v,ℓ) ≤ d(v,t) + d(t,ℓ)), the second is ĉ ≤ c. **EXACT lower bound with no constant subtraction.** Every intermediate step is an exact statement about one integer metric; "difference of rounded distances" — the usual quantization leak — never occurs because a true metric is defined by the quantized weights. 4. Consistency (the property the balanced-potential construction + stop rule need): `|d̂(u,ℓ) − d̂(v,ℓ)| ≤ ĉ(u,v) ≤ c(u,v)` — Dijkstra distances are Lipschitz-1 in their own graph's edges, and ĉ ≤ c makes the bound hold against REAL f32 edgeSeconds. So the potentials are feasible on every real edge, both directions, exactly. 5. `max` over landmarks: `|max_ℓ f_ℓ(u) − max_ℓ f_ℓ(v)| ≤ max_ℓ |f_ℓ(u) − f_ℓ(v)| ≤ c(u,v)` — the max-combination stays exactly consistent (max of a fixed set of 1-Lipschitz functions is 1-Lipschitz). Storage impact: distances same small integer domain as edge counts → u16 suffices; total bytes = 2 families × L × N × 2 B (see budget table). Build arithmetic (u16 stores + Int32/Float64 accumulation) is standard practice last verified in CH-era implementations that integerize weights for exactly this reason [ASSUMED as common practice; the *exactness proof* is mine and self-contained]. **Build must assert the metric doesn't overflow:** no final table value and no intermediate Dijkstra label > 65,000 units. If it ever fails (future fork at nation scale): raise q via the header knob before re-encoding (documented). Fail-honest exit otherwise. ### Pattern 3: sentinel / small-island handling (the classic ALT edge case, contained) Sentinel (0xFFFF) appears only when v cannot reach / be reached from landmark ℓ — i.e. v lies outside ℓ's SCC fragment. Known graph facts: the real KL build is ONE giant SCC covering 305k of 311k nodes (Phase 6 build fact); landmarks are chosen inside it (build asserts each anchor's Dijkstra reached ≥ N·coverageFact nodes — abuse the same fragmentation machinery the build already has). Containment layers (recommendation): 1. **Build-time:** anchors must live in the giant SCC; per-anchor coverage assertion; fail-honest otherwise (named constant `LANDMARK_MIN_COVERAGE_FRACTION`, e.g. 0.90, measured justification). 2. **Search-scoped constant skip:** for a given (o, g) search, drop landmark ℓ entirely from the max if its goal- or origin-side constant is sentinel (g or o can't reach ℓ) — a per-SEARCH decision, never per-node. 3. **Per-node fallback:** if a node's table entries are sentinel for all remaining ℓ (a fringe island node), its h_f/h_b fall back to the haversine bound (existing code, admissible + consistent everywhere). 4. **Correctness containment even on the boundary:** every node u that lies on ANY o→g path necessarily reaches g; if g reaches some valid ℓ then u reaches ℓ too (u → g → ℓ), so **u's table entries are non-sentinel for every useful ℓ** — the fully-connected consistency argument applies on precisely the productive search region. Dead-end island nodes only ever see locally-misordered keys, cannot create a premature valid mu, and their real-seconds are recomputed exact. ∎ [ASSUMED: my proof sketch — planner should schedule the A-case that empirically seals it: the A1 sweep MUST include an islanded synthetic fixture where ALT is ACTIVE and Dijkstra-equivalence holds on every pair.] 5. **Empirical seal:** A1's 625-pair exhaustive Dijkstra-equivalence re-run on the ARG2 fixture with ALT potentials live, plus a new dedicated island case. ### Anti-Patterns to Avoid - **Quantizing stored distances then patching with an epsilon bound:** breaks exact consistency (max-of-bounds + per-node masking interacts badly); the ĉ-metric removes the whole class. - **A per-node skip rule that varies the landmark set mid-max:** kills the max-of-Lipschitz consistency argument. Skip at the SEARCH scope (constants) and fall back per NODE, never mask individual ℓ per node while others stay active. - **Reusing `pool`/DBG-style offsets:** the landmark suffix must extend `layoutFor`'s `total` arithmetic so decode's exact-size law stays arithmetic-only (T-06-03b). - **Rebuilding the node order silently:** the baseline record must identify pairs by COORDINATES (snap at run time), never graph node indices — a re-fetch of Overpass (or any dedup-order drift) shifts indices and would fake a tie-difference. (The existing 25-route gate samples quadrant node indices — fine for CI-size CPU snapshotting, NOT for cross-version optimality equality.) - **Losing sentinels into Float64Array:** 0xFFFF must be treated as sentinel BEFORE conversion (65535 × quantum is a large-but-finite bogus distance that would quietly dominate the max). - **Renaming the R2 key or local artifact path:** upload script and key are locked; the "v1" suffix is a storage NAME, the ARG2 header is the format truth. Document loudly; never fix the name in this phase. - **Second `encodeGraph`:** GraphInput gains landmark input; all call sites (graph.test synthetics, fixture builder, bot webhook corridor graphs) update in the same change. No v2-only encode function. ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Shortest-path preprocessing to cut A\* worst case | custom steering hacks, speed-class masks, radius clamps | ALT with well-spread anchors (the literature's measured lever; beaten only by CH at scale, which is deferred) | Preserves exact optimality (proven formulation), $0, one-function integration | | Lower-bound math on quantized values | admissibility epsilon patches, round-up tables + mixed corrections | ĉ-metric integer Dijkstra (Pattern 2) | Exact admissibility + consistency by construction | | Bidirectional potential | new symmetric/dual search variant, meet-constant search | the existing πf/πr → (πf−πr)/2 scheme already in astar.ts | Verified canonical; the stop rule/meet-probe machinery carries over untouched | | Landmark selection quality metric | hand-tuned POI picks ("anchors are NOT POIs" — D-02 context) | farthest-point over the graph metric, seeded/ID-deterministic | Determinism, reproducibility, spread; measured verdict decides count | **Key insight:** the repo's remaining risk is measurement honesty, not algorithm novelty — every needed primitive (exact potentials, integer tables, fail-honest decode, seeded gates) already has a tested home in the codebase. ## Common Pitfalls ### Pitfall 1: baseline cannot be re-measured after ARG2 lands **What goes wrong:** D-03 compares post-ALT seconds to pre-ALT seconds. Once `graph.ts` versions to 2, `decodeGraph` rejects the v1 binary — the "old code + old binary" baseline is no longer reproducible from the working tree. **Why it happens:** D-01's ARG2-only law (correctly) removed the v1 path. **How to avoid:** measure and COMMIT the baseline record FIRST (a Wave-0 task): current pre-phase code + the existing v1 build, pairs identified by coordinates, seconds + verdict class + pair fingerprints written to a committed JSON. All comparison happens against the record. **Warning signs:** a plan that generates the baseline AFTER the codec change. **JSON float caveat:** record seconds as JSON numbers only if produced by the same f32→f64 accumulation the runtime uses; routeAstar returns exact f64 accumulations of f32 edgeSeconds, JSON round-trips IEEE doubles losslessly — safe [VERIFIED: JS number semantics; the same equality law the A1 sweep uses]. ### Pitfall 2: node-index instability across rebuilds **What goes wrong:** the 25-route gate pricks quadrant arrays by node INDEX; a re-build of the binary (even byte-different only in name bytes) reorders nodes, and the same seed selects different pairs — the pre/post "same pair" claim collapses. **How to avoid:** the extended sample + baseline record pin pairs by lat/lng (derived from the graph's order-independent bbox + seeded PRNG, snapped at run time); document that index-keyed sampling is a CI-size convenience only. **Warning signs:** any "verify same route as baseline" logic that prints node ids instead of endpoint coordinates. ### Pitfall 3: L mis-tuned → gate still fails → temptation to touch the search **What goes wrong:** ALT with too few anchors leaves the worst pairs slow; the wrong next move (heuristic scale-down, speed tweaks) breaks the A1 law. **How to avoid:** pre-commit the remediation ladder in the plan: (a) raise L within the 16–32 rule and rebuild (lines up with D-02's scaling law); (b) only then report faithfully (print-the-measurement law gates even a FAIL verdict; a measured-fail c2 is an escalation, never a hack). **Warning signs:** any edit to `routeAstar`'s stop rule or reductions during the campaign. ### Pitfall 4: header-extension breaks f64 alignment or exact-size law **What goes wrong:** moving HEADER_SIZE from 64 to 72 must keep node f64s 8-byte aligned (72 % 8 = 0 ✓) and decode's total-size arithmetic must cover the variable-size suffix — a miscounted suffix manifests as "corrupt binary" on the WORKER without a single test failing (if tests skip the big fixture). **How to avoid:** the G-suite gains count-driven table arithmetic cases; `--check` prints landmarkCount + coverage; the round-trip verify command in the fork contract re-runs after upload. **Warning signs:** `RangeError` on any typed-array construction (alignment) — instant signal. ### Pitfall 5: the u16 quantum ceiling surprises a fork **What goes wrong:** at 0.1 s units, distances ≥ 6,553.5 s are inexpressible; a larger-city fork overflows silently into wrong sentinel-collisions. **How to avoid:** build-time assertion (≤ 65,000 units) + ROAD-GRAPH.md documents the `landmarkQuantumS` knob and its formula. **Warning signs:** table bytes saturated at exactly 0xFFFF in `--check` stats. ### Pitfall 6: docs divorce (v1/ARG2 naming) **What goes wrong:** keeping the R2 key `kl-roadgraph.v2.bin`… — NOT ALLOWED anyway (upload locked); but if ROAD-GRAPH.md / CONFIGURATION.md silently still say "v1 layout", the next fork's rebuild breaks. **How to avoid:** D-05's docs tasks include ROAD-GRAPH.md's layout table (ARG2 version field, table sections, quantum knob) and the CPU-gate decision table gaining the (c2) rung. **Warning signs:** any mention of "ARG1" as still-decodeable. ## Runtime State Inventory > Refactor/version-bump phase — included. No rename of user-facing strings. | Category | Items Found | Action Required | |----------|-------------|------------------| | Stored data | R2 object `araharah-graphs`/`kl-roadgraph.v1.bin` currently ARG1 bytes | Re-upload once post-build (whole migration per D-01). No data migration — the object is a build artifact; decode-null rollout covers the window | | Live service config | `bot/wrangler.jsonc` ROAD_GRAPH binding (bucket name), `GRAPH_KEY` constant in `worker-router.ts` ("kl-roadgraph.v1.bin") | NONE — both unchanged (D-01 lock). The name-vs-format rule documented | | OS-registered state | None — no cron/scheduler registrations for the graph | None (verified: cron arm reads the same loadGraph, no separate key) | | Secrets/env vars | `bot/.dev.vars` four keys (BOT_TOKEN/WEBHOOK_SECRET/WEBHOOK_URL/MINI_APP_URL) — unchanged | None | | Build artifacts | `routing/data/kl-roadgraph.v1.bin` (gitignored, 14.7 MB, ARG1 on disk) — must be REBUILT by the phase; committed mini-graph fixture `mini-graph.bin` currently ARG1 → regenerated as ARG2; old bytes preserved as the v1 rejection vector | Rebuild + regenerate fixture + keep rejection fixture (fixture build script gains the landmark stage) | **Nothing found in category:** OS-registered state — verified (repo has no launchd/systemd/pm2 surfaces; Workers cron is declared in wrangler.jsonc and untouched). ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | Geographic max-speed A\* bound (2026-10-24 unidirectional) | ALT landmark bounds | This phase | Survey-verified: geo/max-speed is "poor… small or nonexistent gain"; ALT is the proven sharpening | | Unidirectional → BH 2026-10-24 | BH + ALT (planes exactly on top) | This phase | Steering restored on both sides; no new search skeleton | | ARG1 format (2026-10-24, 14.7 MB) | ARG2 append-only extension, u16 suffix | This phase | Exact-size law extends mechanically; v1 decode retired | **Deprecated/outdated:** none of the repo's patterns become dead; the reverse-CSR WeakMap, snap grid, corpus harness, RT/H gate suites all carry unchanged. ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | hand-rolled registry (`routing/test/harness.mjs`) + `node:test`-style runner, **zero deps** [VERIFIED: harness.mjs] | | Config file | none — `routing/test/runner.mjs` + npm workspace scripts (`npm run routing:test`, `npm test -w bot`) | | Quick run command | `npm run routing:test` (seconds) | | Full suite command | `npm run routing:test && npm test -w bot && npm run routing:typecheck && npm run bot:typecheck` | | Big-graph gates | `node routing/scripts/build-road-graph.mjs` (preprocessing stage) · `… --check` · `node routing/scripts/measure-route-cpu.mjs` (p95 ≤ 8) · extended verdict flag · D-03 gate script | ### Phase Requirements → Test Map No new REQ ids (SC3/SC4 carry-over). The phase requirements below mirror the ROADMAP §Phase 8 success criteria + the locked decisions: | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | SC-1 P95 within free-plan budget | p95 ≤ 8 ms on the real ARG2 build | measurement gate | `node routing/scripts/measure-route-cpu.mjs` | ✅ exists (extends: history + campaign) | | SC-1′ extended verdict (D-04) | ~100 cross-metro seeded pairs, p95 ≤ 8 | maintainer-time (script flag) | `node routing/scripts/measure-route-cpu.mjs --extended` (flag name discretion) | ❌ Wave 0 | | SC-2 optimality unchanged (D-03) | corpus re-run: seconds equal (± named epsilon ~±0.1%) + same verdict class per pair vs committed baseline | offline gate script + in-process mirror | new `node routing/scripts/.mjs` (name discret.) | ❌ Wave 0 (baseline record ✅ prerequisite task) | | A1 ALT-equivalence (outstanding optimality proof) | 625-pair sweep on ARG2 fixture WITH ALT potentials live | unit (suite) | `npm run routing:test` (A1 case) | ✅ exists — must stay green; new island case ❌ | | G-suite ARG2 codec | ARG2 roundtrip incl. tables; ARG1 fixture → decode null; corrupt/version/landmarkCount=0/oversized-count → null; zero-copy views; sentinel integrity | unit (suite) | `npm run routing:test` (G cases) | partially — new cases ❌, G3/G8 re-pin ❌ | | Rollout null path (RT trunks) | corridor/worker behavior on ARG2 corridor graph; decode-null → 200 null unchanged | bot suite | `npm test -w bot` (RT1–RT11 + H1–H8) | ✅ — corridor graphs flow through updated encodeGraph signature | | Build pipeline | preprocessing stage prints L, per-anchor coverage, quantum check, byte budget; exit 1 honest | script gate | `node routing/scripts/build-road-graph.mjs` (+ `--check`) | ✅ extends (stage ❌ new) | | Upload unchanged | `upload-road-graph.mjs` byte-for-byte unchanged | structural grep pin (RP-style) | recorded in bot/routing suite (add 1-line pin case) | ❌ small new case | | SC-3 docs retirement (D-05) | KNOWN-LIMITATIONS c3 section → retirement record w/ measured numbers + date; ALT roadmap row flipped; ROAD-GRAPH.md CPU-gate table gains (c2) rung | doc acceptance (human-verifiable, supported by printed gates) | review leg of phase gate | edit ❌ new | ### Sampling Rate - **Per task commit:** `npm run routing:test` (fast, includes A1 sweep + new G/A cases). - **Per wave merge:** full suite (routing + bot + typechecks) + build `--check` on a committed-adjacent stub if built. - **Phase gate:** the REAL build → `--check` → 25-route gate (p95 ≤ 8) → extended verdict → D-03 optimality gate green → upload once → `bot` deploy heartbeat (existing RT smoke path) → D-05 docs record carries the printed numbers. ### Wave 0 Gaps (new files/cases before implementation tasks) - [ ] **Baseline record + recorder tool** (D-03 prerequisite — MUST be Wave 0 / first task; Pitfall 1) - [ ] Extended-sample flag implementation (D-04) — deterministic coordinate-pair generator, `MIN_PAIR_KM`-style named constant, p95 print + exit-gate - [ ] `fixtures/mini-graph-arg1.bin` — bytes preserved from the current committed ARG1 fixture (decode-rejection vector) - [ ] `fixtures/build-mini-graph.mjs` gains a landmark stage (tiny L; ARG2 output) - [ ] New G cases (ARG2 codec + rejection matrix) and an island A case (Pattern 3 seal) - [ ] Upload-unchanged structural pin - Test framework install: none needed (zero-dep harness present) *Constraint to re-verify in planning:* `budget-audit` (Phase 7) re-runs `measure-route-cpu.mjs` — keep its CLI contract (silent default mode = the 25-route gate, exit codes) so the audit keeps passing, or account for the flag addition there. ## Security Domain > security_enforcement enabled by default; included. ### Applicable ASVS Categories | ASVS Category | Applies | Standard Control | |---------------|---------|-----------------| | V2 Authentication | no (untouched) — Bearer-claim gate on /api/route unchanged | existing `getClaimByToken` posture | | V3 Session Management | no | KV TTL classes untouched | | V4 Access Control | no (maintainer scripts stay repo-market internal) | scripts never imported by runtime (existing law) | | V5 Input Validation | **yes — the binary is hostile input** | decodeGraph exact-size + version + landmarkCount laws (T-06-03/03b extended to the suffix); hostile header on tiny buffer rejects arithmetically BEFORE view allocation | | V6 Cryptography | no | HMAC/uniqueness paths untouched | | V7/V11 … | no | unchanged posture | ### Known Threat Patterns for this stack | Pattern | STRIDE | Standard Mitigation | |---------|--------|---------------------| | Hostile/corrupt binary with monster `landmarkCount` (allocation bomb) | Tampering/DoS | exact-size check before allocation (T-06-03b law extends — verified pattern in graph.ts today) | | Sentinel-flood graphs (all u16 = 0xFFFF) degrading search CPU into 1102 | DoS | search-scoped constant skip drops such ℓ wholesale; the CPU gate prints per-route ms; category-only logs unchanged | | Rollout confusion: Service receives ARG1 | Tampering/integrity | decode null → 200 null (router-pending); `--check` + roundtrip-verify groom | | Coordinate/payload logging leakage | Info disclosure | category-only log law carries unchanged; gate prints per-route ms + ids-of-nothing (RT1–RT3 pins remain) | **Threat surface unchanged beyond the binary content** — same one `/api/route` boundary, same Bearer claim, no new endpoints (RP1 law holds; nothing new fetches). ## Assumptions Log | # | Claim | Section | Risk if Wrong | |---|-------|---------|---------------| | A1 | The (πf−πr)/2 + (πr−πf)/2 survey formulation is exactly what astar.ts's p_f/p_b already implement (signs verified against `candidate` equations) | Pattern 1 | Would force a `potential`-pair redesign — but verified line-by-line; low | | A2 | ALT bound families computed in the ĉ metric are exactly admissible AND exactly consistent vs real f32 edges (directed-triangle + Lipschitz + ĉ≤c arguments) | Pattern 2 | Admissibility epsilon would be needed (a named-constant variant exists as fallback: subtract one quantum `K`); medium — planner should treat the epsilon-variant as a plan-level contingency note only; A1 sweep is the empirical seal | | A3 | Sentinel/island handling: nodes on any o→g path have non-sentinel entries for validated anchors; boundary edges can't corrupt mu | Pattern 3 | Contained by A1 + island case + real-seconds recompute; would degrade to haversine-only behavior (perf regression, not correctness) | | A4 | Build preprocessing cost ≈ 2L Dijkstra passes ≈ 2 s measured-order at L=24 (derived from measured 38 ms/full-graph pass) | Budget | Larger build times acceptable anyway (maintainer-time); low | | A5 | `landmarkCount === 0 → decode null` is the correct reading of D-01's no-silent-degradation spirit | ARG2 layout | If the maintainer wants a landmark-less v2 capability, decode should accept 0 and fall back to haversine silently (contradicts D-01 wording "no v1 fallback branch"); flagged for plan review | | A6 | p95 ≤ 8 achievable with L in 16–32 for the 20–30 km class (order-of-magnitude settled-node reduction expectation vs measured 38–93 ms) | Summary/gate | Verdict may land FAIL at first L — the D-02 scaling rule is the measured knob; escalate honestly if still failing at L=32 (the same print-the-measurement law) | | A7 | u16 sentinel 0xFFFF never collides with real data (build asserts ≤ 65,000) | u16 design | Overflow → silent table corruption; the assertion makes it loud | | A8 | f64 JSON round trip of route seconds in the baseline record is lossless | Pitfall 1 | Equality gate needs a fudge — trivially fixed by storing unit strings; low | ## Open Questions 1. **Final L count and its exact rule constant.** - What we know: expected 16–32; D-02 makes it a measured product of the gate. - What's unclear: should the plan hard-pin a formula (e.g. `clamp(round(N/13000), 16, 32)`) or build once per candidate and pick the smallest passing L? - Recommendation: parameterize L in the build script (CLI arg + named default); land default 24; the campaign prints p95 at 24 and (only if FAIL) at 32; the verdict documents the chosen count. 2. **`landmarkQuantumS` value.** - What we know: 0.1 s fits KL (54,000 max units) and gives sub-decisecond resolution; forks can raise it. - Recommendation: 0.1, header field, doc'd knob. 3. **Does `docs/CONFIGURATION.md` / `budget-audit` need a touch?** - Verified likely: `budget-audit` re-runs measure-route-cpu (per KNOWN-LIMITATIONS "run the audit yourself"); flag addition must stay default-compatible. Plan task: read `bot/scripts` budget-audit runner + keep contract. ## Environment Availability | Dependency | Required By | Available | Version | Fallback | |------------|------------|-----------|---------|----------| | Node.js | build/test/measurement | ✓ | v24.15/24.17/24.18 (nvm) [VERIFIED] | — | | npx/wrangler | upload (unchanged), R2 roundtrip verify | ✓ (repo standard; R2 binding live) | — | — | | Overpass public API | build re-fetch (preprocessing rides the SAME extract) | ✓ (used 2026-10-24) | — | retry logic already in build script | | git | commits | ✓ | — | — | | No new deps | — | — | — | — | **Missing dependencies with no fallback:** none. **Missing dependencies with fallback:** none. (PATH caveat: bare `node` resolves only with nvm sourced; executing sessions run through npm scripts which inherit the shell's node.) ## Code Examples ### (Reference-grade) the reduced-cost search with ALT potentials — the EXISTING loop, unchanged ```ts // astar.ts (current, stays) — only `potential()` internals change: const expandForward = fHeap.size <= bHeap.size; const u = (expandForward ? fHeap : bHeap).pop()!; // … forward relaxation: const v = graph.edgeTarget[e]!; const pv = potential(v); const candidate = du + graph.edgeSeconds[e]! + pfu - pv; // … the stop rule / meet probes / real-seconds reconstruction unchanged. ``` ### Potential() rewrite sketch (typed arrays, allocation-free, sentinel-safe) ```ts // L = graph.landmarkCount; T_TO = graph.landmarkTo (u16 rows of N); // T_FROM = graph.landmarkFrom; Q = graph.landmarkQuantumS; per search init: for ℓ: goalTo[ℓ] = toTable[ℓ*N+goal] (NaN if 0xFFFF — search-scoped skip) goalFrom[ℓ] = fromTable[ℓ*N+goal] … likewise for origin per-node lazy potential(v): bestTo = -Infinity, bestFrom = -Infinity for ℓ in 0..L-1: const tv = toTable[ℓ*N+v]; if (tv === 0xFFFF) continue node-entry // per-node fallback candidate if (goalTo[ℓ] not NaN) bestTo = max(bestTo, tv - goalTo[ℓ]); if (goalFrom[ℓ] not NaN) bestFrom = max(bestFrom, goalFrom[ℓ] - fromTable[ℓ*N+v]); // h_f = bestTo > -inf ? max(bestTo, havF(v)) * Q : havF(v) ← fallback keeps isle behavior sane // h_b likewise with origin consts; return (h_f - h_b)/2 ← the SURVEY combination, exactly today's ``` *(Sketch only — final shape/naming is the executor's; laws to preserve: no `Map`, lazy caches, no logging.)* ### Build preprocessing stage skeleton ```js // inside build-road-graph.mjs, after `edges` assembly, before encodeGraph: const QUANTUM_S = 0.1; // named constant; header field value const quanta = Math.floor(seconds / QUANTUM_S); // integer ĉ, ≥ per-edge (0.1s unit) // 1) integer Dijkstra over REVERSED edges from each anchor → toTable row // 2) integer Dijkstra over forward edges from each anchor → fromTable row // 3) farthest-point selection over the ĉ metric (seeded, degree-≥2 filtered, ID tie-break) // 4) coverage assertion per anchor; quantum-overflow assert; encode through encodeGraph ``` ## Sources ### Primary (HIGH confidence) - Bast, Delling, Goldberg, Müller-Hannemann, Pajor, Sanders, Wagner — *Route Planning in Transportation Networks* (arXiv:1504.05140, fetched via ar5iv): bidirectional potential combination (πf−πr)/2 scheme; ALT triangle-inequality bounds verbatim + feasibility claim; geographic-bound weakness; landmark selection guidance ("well-spaced close to the boundary leads to the best results"; minor-speedup caveat). - Campbell — *Computing Shortest Paths using A\*, Landmarks, and Polygon Inequalities* (arXiv:1603.01607): ALT storage θ(|L|·|V|); heuristic = max over landmark inequalities; consistency is theorem-grade (their ALP variant NOT consistent — the class of pitfall Pattern 3 guards). - Goldberg & Harrelson — *Computing the Shortest Path: A\* Search Meets Graph Theory* (SODA 2005; Princeton/MSR TR-2004-24 PDFs surfaced via search): far/sector landmark selection ("For each sector, pick a vertex farthest away from the center…"). - Goldberg & Werneck — *Better Landmarks within Reach* (via wea PDF snippet): the "avoid" greedy strategy ("picks landmarks one by one, always in regions… not already covered"). - Repo code + docs read in full this session: `routing/src/graph.ts`, `routing/src/astar.ts`, `routing/scripts/{build-road-graph,measure-route-cpu,route-corpus,upload-road-graph}.mjs`, `routing/test/{harness.mjs,graph.test.mjs,astar.test.mjs}`, `bot/src/routing/worker-router.ts`, `docs/ROAD-GRAPH.md`, `docs/KNOWN-LIMITATIONS.md`, fixture builder, `.planning/{STATE,REQUIREMENTS}.md`, `.planning/phases/{06…,07…,08…}/…`. ### Secondary (MEDIUM confidence) - Build-runtime cost estimate A4 (derived, not measured). - u16 integer-weight practice precedent (common in the CH-era ecosystem, not fetched specifically). ### Tertiary (LOW confidence) - Expected p95 magnitude post-ALT (A6) — no published KL-metro analogue; the gate is the truth. ## Metadata **Confidence breakdown:** - Standard stack / integration shape: HIGH — verified against code + the canonical survey; the potential-integration finding is the load-bearing fact and it is pinned twice (survey + line-by-line code check). - ARG2 layout: HIGH — append-only discipline keeps every pinned law mechanical; byte math checkerboarded against measured counts. - Quantization exactness: HIGH on the math (self-contained proofs documented in Pattern 2); its *empirical* seal relies on the A1 sweep + new island case (planner should keep both mandatory). - Perf verdict: MEDIUM — order-of-magnitude argument from measured baselines; the gate decides. - Pitfalls: HIGH (all six are chase-able in this repo's own artifacts). **Research date:** 2026-10-25 **Valid until:** 2026-11-24 (binary layout insights stable; only Node minor versions drift)