Files

483 lines
51 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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>
## 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)
</user_constraints>
## 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/<optimality-gate>.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)