19 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-alt-landmark-preprocessing-routing-backend-v2 | 03 | execute | 3 |
|
|
true |
|
|
Purpose: this is the entire runtime change of Phase 8 — steering sharpened on the cross-metro class, optimality carried by the already-proven skeleton (A1), and the Worker/upload seam proven untouched.
Output: ALT-live routing/src/astar.ts, extended astar.test.mjs (A9/A10/A11 + A1 re-pin), new routing/test/rollout.test.mjs, full battery green.
<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/STATE.md @.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-CONTEXT.md @.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-RESEARCH.md @.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-PATTERNS.md @.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-VALIDATION.mdrouteAstar(graph, originNode, goalNode) @ line 219. The heuristic scaffolding (lines 225–230):
const maxSpeedMps = graph.maxSpeedKmh / 3.6;
const heuristicScale = maxSpeedMps > 0 ? 1 / maxSpeedMps : 1;
const goalLat = graph.lat[goalNode]!; /* … origin coords likewise … */
The lazy potential (lines 232–251) — the ONLY code that changes:
const hfCache = new Float64Array(n).fill(Number.NaN);
const hbCache = new Float64Array(n).fill(Number.NaN);
const pf = new Float64Array(n);
function potential(v: number): number {
let hf = hfCache[v]!;
if (Number.isNaN(hf)) { hf = metersTo(graph, v, goalLat, goalLng) * heuristicScale; hfCache[v] = hf; }
/* … hb likewise … */ const value = (hf - hb) / 2; pf[v] = value; return value;
}
Everything else is PINNED untouched: per-search init (263–268), the BH loop with reductions du + c + pfu − pv / du + c − pfu + pvv and meet probes (274–340), the stop rule (277), the real-seconds recompute (352–364), reverseFor's WeakMap memo (146–192). A8 (astar.test.mjs 277–283) greps comment-stripped source for new Map( — keep the hot loop typed-arrays-only.
RoadGraph gains (Plan 08-02): landmarkCount: number; landmarkQuantumS: number; landmarkTo: Uint16Array; landmarkFrom: Uint16Array; landmarkNodes: Uint32Array — u16 rows row-major [ℓ*N + v], sentinel 0xFFFF = v cannot reach / be reached from ℓ (DATA, never validated away). decodeGraph guarantees landmarkCount ≥ 1.
landmark.ts exports (Plan 08-02 task 1) for the test fixtures: farthestPointAnchors(input, count), landmarkTables(input, anchors) → { to, from } with LandmarkInput = { nodeCount, edges: [{from,to,seconds,flags}], quantumS }.
From 08-RESEARCH: Pattern 1 (the four bound families + the max-rule recommendation), Pattern 2 (why the ĉ-metric bounds are EXACTLY admissible + consistent — no epsilon), Pattern 3 (sentinel containment layers), Anti-Patterns ("Losing sentinels into Float64Array", "per-node skip rule that varies the landmark set mid-max" for VALUE-based masking).
task 1: ALT potentials in routeAstar + the containment A-cases (RED→GREEN) routing/src/astar.ts, routing/test/astar.test.mjs - routing/src/astar.ts IN FULL (470 lines — you are editing potential() and its doc-comments; every other function is pinned) - .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-RESEARCH.md (Pattern 1 verbatim families + max-rule; Pattern 2 exactness; Pattern 3 sentinel containment; the potential() sketch in Code Examples) - routing/test/astar.test.mjs IN FULL (283 lines — the case-index doc-comment, A1's sweep, A3's synthetic-graph mechanic you extend, A8's pin) - routing/src/landmark.ts exports (Plan 08-02 task 1 — the fixture-table builder) - .planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-PATTERNS.md (astar.ts section — the pinned laws) - A11 structural (the RED case — fails before the swap): comment-stripped astar.ts code (A8's exact stripping shape) references `landmarkTo`, `landmarkFrom`, and `landmarkQuantumS` — the potential provably consults the tables; `new Map(` still absent (A8's law, unchanged) - A9 island + per-node fallback: a two-component synthetic (e.g. a 3×3 grid + a separate 4-node path) whose landmark anchors are HAND-PINNED inside component A only (landmarkTables takes the anchors array — pin them, don't derive) so every component-B node is 0xFFFF in both families; every pair inside B routes Dijkstra-equal (haversine-fallback steering), every cross-component pair resolves null, every pair inside A routes Dijkstra-equal with real ALT bounds live - A10 search-scoped constant skip: origin AND goal inside the anchorless component — every ℓ's goal/origin-side constant is sentinel, so all families drop for the whole search and the run is pure-haversine; assert the reachable pairs still route Dijkstra-equal (skip law: constants dropped at SEARCH scope, results still optimal) - A1 (existing, re-pinned by execution): the 625-pair sweep on the ARG2 mini fixture stays green with ALT live — no case edits needed, the sweep IS the seal; seconds-only assertions everywhere (never node-path equality — D-03's tie law) Write the A9/A10/A11 cases FIRST (they run against the current haversine implementation: A11 fails = RED; A9/A10 pass as baseline-equivalence and become the containment seal after the swap). Build each fixture via `encodeGraph({ …, landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from } })` with tables from `landmarkTables({ nodeCount, edges, quantumS }, anchors)` — keep a tiny local `buildFixture(edges, anchors)` helper; update the case-index doc-comment (A1–A8 → A1–A11).Then swap the heuristic bodies in routeAstar (potential() + its init only):
- Per-search constants (after the existing heuristic consts, before the caches):
const L = graph.landmarkCount;const Q = graph.landmarkQuantumS;and fourFloat64Array(L)tables in QUANTA units —goalToU[ℓ] = graph.landmarkTo[ℓ*n + goalNode],goalFromU[ℓ] = graph.landmarkFrom[ℓ*n + goalNode],origToU[ℓ] / origFromU[ℓ]likewise for the origin — each entry stored as-is, with 0xFFFF written as NaN (the search-scoped sentinel skip; the constant tables are the ONLY place skip decisions live). L ≥ 1 is a decode guarantee (D-01) — no L===0 branch, no dead fallback path (one search path law; a comment cites the guarantee). - Inside potential()'s cache-miss branch: per direction,
bestTo/bestFromstart at −Infinity; loop ℓ = 0..L−1 over RAW u16 reads:const tv = graph.landmarkTo[ℓ*n + v]—if (tv === 0xffff) continue(per-node sentinel skip, exactly the research sketch); ifgoalToU[ℓ]is not NaN →bestTo = max(bestTo, tv − goalToU[ℓ])(family A: d̂(v,ℓ) − d̂(t,ℓ));const fv = graph.landmarkFrom[ℓ*n + v]—if (fv === 0xffff) continue; ifgoalFromU[ℓ]is not NaN →bestFrom = max(bestFrom, goalFromU[ℓ] − fv)(family B: d̂(ℓ,t) − d̂(ℓ,v)). Thenconst altF = Math.max(bestTo, bestFrom)andhf = altF > -Infinity ? Math.max(havF, altF * Q) : havFwherehavFis the EXISTING haversine expression (the max-rule — research Pattern 1 recommendation; sentinel-flooded nodes degrade to exactly today's behavior). Mirror for hb toward the origin. Theconst value = (hf − hb) / 2; pf[v] = value; return value;tail is BYTE-IDENTICAL to today. - Bounds may be negative (v closer to ℓ than t is) — legal: p_f already goes negative today when h_b > h_f; the BH meet-sum law is algebraic in p_f's sign. The max-rule with the non-negative haversine bound keeps hf/hb ≥ 0 anyway.
- NO Map/Object allocations (A8); no new imports (graph types + geo are already imported); sentinel comparisons happen on the raw u16 BEFORE any float conversion (anti-pattern: 65535 × quantum as a bogus finite bound).
- Doc-comments: the module header and routeAstar's BH block gain the ALT paragraph — the survey formulation (πf−πr)/2 is what p_f already implements (cite it), the ĉ-metric exactness in one sentence (floor-quantized edge weights → exact 1-Lipschitz bounds, no epsilon), the sentinel laws (search-scoped constant skip, per-node fallback, 0xFFFF is data), and that A1 re-run under ALT is the standing optimality proof. Keep the Ikeda/balanced-alternation history intact.
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH" && npm run routing:test 2>&1 | tail -3 && npm run routing:typecheck && echo "ALT-POTENTIALS-GREEN"
<acceptance_criteria>
npm run routing:testgreen INCLUDING A9, A10, A11 and the re-pinned A1 sweep (625 pairs) with ALT live; the case-index doc-comment lists A1–A11.- A8's pin still green (no
new Map(in comment-stripped astar.ts). - grep law:
grep -c "landmarkTo" routing/src/astar.ts≥ 1 AND the A11 case (not just the grep) proves it is CODE not comments (comment-stripped source). - grep law:
grep -c "0xffff" routing/src/astar.ts≥ 2 (raw sentinel comparisons on both table families) andgrep -c "65535" routing/src/astar.ts= 0 (one spelling of the sentinel). - Seconds-only assertions: no new case compares node paths (
grep -c "nodes.length ===" routing/test/astar.test.mjsunchanged from its current A2 usage — A9/A10 assert seconds + nullness). - typecheck clean; no new imports in astar.ts. </acceptance_criteria> ALT steering is live inside the byte-identical BH skeleton; A1's exhaustive equivalence re-proves optimality under ALT; the island and skip laws have dedicated containment cases.
- RO1 upload-format-agnostic: upload-road-graph.mjs source contains
KEY = "kl-roadgraph.v1.bin"andBUCKET = "araharah-graphs"(the storage contract); comment-stripped code contains NONE oflandmark,encodeGraph,decodeGraph,0x41524731,0x41524732,GRAPH_VERSION— the migration is byte-shipping of the same key (D-01 lock); the script must never grow format awareness (a fork's upload path stays layout-agnostic forever). - RO2 one decode path at the Worker: worker-router.ts comment-stripped code contains
GRAPH_KEY = "kl-roadgraph.v1.bin"(storage name unchanged), exactly onedecodeGraphreference inside the loadGraph flow, and NO version-conditional branch (noGRAPH_VERSION, no=== 2against the graph bytes) — the router never branches on format; an undecodable binary flows to the existing null → router-pending path (D-01's one-path law at the consumer).
Then run the FULL battery at this commit — npm run routing:test, npm test -w bot, npm run routing:typecheck, npm run bot:typecheck. This is the first commit where routeAstar consults REAL tables on the bot suite's ARG2 corridor graphs (built in 08-02 task 3): the RT trunks and guidance-cron cases must stay green. Any failure here is an ALT integration bug — fix the integration, never the assertion (seconds are A1-law stable; the corridors' expected costs must not move).
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH" && npm run routing:test 2>&1 | tail -2 && npm test -w bot 2>&1 | tail -2 && npm run routing:typecheck && npm run bot:typecheck && echo "BATTERY-ALT-GREEN"
<acceptance_criteria>
- routing/test/rollout.test.mjs committed with RO1 + RO2 green (npm run routing:test reports 0 failed).
- Full battery green: routing suite, bot suite, both typechecks at this commit.
- grep law: grep -c "0x41524732\|GRAPH_VERSION" over comment-stripped worker-router.ts = 0 (enforced by RO2 itself).
- git status shows worker-router.ts and upload-road-graph.mjs UNMODIFIED by this plan (the pins protect files this phase must not touch — git diff --stat on both empty).
</acceptance_criteria>
The D-01 locks are mechanically pinned suite-side, and the whole battery is green with ALT potentials live on every fixture surface.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| graph binary (tables) → potential evaluation | the u16 tables are hostile-input-derived data at the Worker boundary; the potential is the consumer |
| measurement verdict → astar.ts | the campaign must never be able to "tune" the search into the gate (Pitfall 3) |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-08-05 | DoS | potential() under sentinel-flooded tables | mitigate | search-scoped constant skip drops unusable ℓ wholesale; per-node sentinel skips are O(L) raw reads; worst case degrades to exactly today's haversine behavior (measured-safe), never to incorrect output (A1 seal) |
| T-08-06 | Tampering | potentials weakened to pass the CPU gate | mitigate | admissibility is STRUCTURAL (ĉ ≤ c edge-wise; no tunable scale on ALT bounds — heuristicScale touches only the haversine term); A1's 625-pair equivalence + A9/A10 re-prove optimality on every run; the campaign ladder (08-05) is rebuild-L-only |
| T-08-07 | Information disclosure | astar.ts internals | accept | pure-module law preserved (no logging/storage/network, T-06-05); no new imports |
| T-08-08 | Tampering | rollout drift (upload grows format logic / router grows a version branch) | mitigate | RO1/RO2 structural pins fail the suite on any such edit — the locks are enforced, not customary |
| </threat_model> |
<success_criteria> routeAstar consults the ARG2 landmark tables through the survey-exact (πf−πr)/2 construction — max-of-bounds with sentinel-safe raw-u16 handling and a haversine max-fallback — while every other line of the search stays byte-identical; A1's 625-pair Dijkstra equivalence is the standing proof with ALT live; the island and search-scoped-skip laws have dedicated cases; the upload/router seams are pinned format-agnostic; the full battery is green. </success_criteria>
After completion, create `.planning/phases/08-alt-landmark-preprocessing-routing-backend-v2/08-03-SUMMARY.md`