Compare commits

..
6 changed files with 446 additions and 2 deletions
+3 -2
View File
@@ -5,7 +5,7 @@ milestone_name: routing-engine-upgrades-and-extras
status: ready_to_plan
stopped_at: "F1 remediation applied (debug alt-potential-4x-slowdown RC-1): clean-columns-only ALT containment in routing/src/astar.ts — per-node sentinel skip removed; suite re-spec'd RED-first (A11) + new A13 mixed-column case; new committed oracle sweep measure-astar-oracle-sweep.mjs 0/200 suboptimal; CPU back in the pre-ALT band (p95 136.6 ms this host vs 1037.6 pre-fix — honest c3 FAIL vs 8 ms stands); D-03 gate green. Commits c071e20 / 2a5a0b5 / 97d6e35. NEXT: maintainer R2 upload + the SC-1 escalation decision (unchanged)"
last_updated: "2026-10-25T14:15:06.953Z"
last_activity: 2026-10-25
last_activity: 2026-10-26
progress:
total_phases: 4
completed_phases: 2
@@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-10-22)
Phase: 08.1
Plan: Not started
Status: Ready to plan
Last activity: 2026-10-25
Last activity: 2026-10-26 - Completed quick task 261026-d95: isolated ALT direction-split research (NO-GO; no production integration)
Progress: [██████████] 100%
@@ -82,6 +82,7 @@ None yet.
| # | Description | Date | Commit | Directory |
|---|-------------|------|--------|-----------|
| 261024-001 | Replace logo references with master assets | 2026-10-24 | TBD | [261024-001-logo-asset-replacement](./quick/261024-001-logo-asset-replacement/) |
| 261026-d95 | Isolated ALT direction-split research: proved and measured, NO-GO for integration | 2026-10-26 | 9a671bd | [261026-d95-alt-direction-split-isolated-research-si](./quick/261026-d95-alt-direction-split-isolated-research-si/) |
## Deferred Items
@@ -0,0 +1,57 @@
---
gsd_plan_version: 1.0
mode: quick
task: Prove and measure isolated asymmetric directional ALT bidirectional search
quick_id: "261026-d95"
slug: alt-direction-split-isolated-research-si
date: "2026-10-26"
branch: research/alt-direction-split
base_commit: ceb71c2
autonomous: true
---
# Quick task 261026-d95: Isolated direction-split ALT research
## Objective
Establish whether a **sound** asymmetric, direction-split ALT bidirectional search exists for this directed ARG2 graph with sentinel fringes, and whether it actually improves the fixed-seed KL workload. This is a research sidequest on `research/alt-direction-split` only: **never edit the shipped `routing/src/astar.ts`, `routing/src/index.ts`, baseline fixture, bot/web integrations, or the separate `/home/C-Fu/araharah` checkout; never merge into main or claim the Worker budget is met without a measured passing gate.** Reference `docs/ALT-DIRECTION-SPLIT.md`, `routing/src/{astar,graph,landmark}.ts`, and the existing routing scripts/tests. No ROADMAP update.
## Must-haves
- A written, checkable derivation **before experimental code** separates P-1 (each directional heuristic's edge consistency) from P-2 (a valid bidirectional meeting invariant and stopping bound), including directed edges, stale heap entries, signed potentials, unreachable pairs, and `0xffff` transitions. A counterexample or impossibility result is an honest outcome; do not implement a search with an unproved stop rule.
- Isolated experiments (only if both proofs succeed) compare seconds and reachability with Dijkstra on synthetic directed/sentinel cases and a fixed seeded real-graph sample. The shipped A1/625 test, D-03 frozen record, and original `routeAstar` remain the controls, never altered to make an experiment pass.
- Same-pair, warmed, host-relative expansion and CPU measurements against shipped haversine fallback are reported, including p95 and the 8 ms gate status, with missing binary/runtime and inconclusive results explicitly marked **NOT MEASURED** rather than PASS. Final verdict states proof status, counterexamples, optimality violations, expansion ratio, and budget verdict; experimental code stays on this branch.
## Tasks
<task type="auto">
<name>task 1: Derive P-1 and P-2, including sentinel boundary</name>
<files>docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md</files>
<action>First confirm branch/HEAD and clean status in this checkout; read the handoff and existing BH reductions/meet probes. Define table-indexing and quantized lower bounds without treating `0xffff` as numeric distance. For **every traversable directed edge** `u→v`, prove the forward `h_f(u) ≤ c(u,v)+h_f(v)` and backward (reverse-search) `h_b(v) ≤ c(u,v)+h_b(u)` independently; do NOT infer P-2 from these inequalities. Account for max-with-haversine, search-scoped landmark selection, and actual edges across finite↔sentinel cells; a per-node family skip is not automatically consistent. Explicitly examine whether graph-wide clean-column selection on this 0/24-clean binary makes ALT inert; propose a different rigorously proved frontier/domain mechanism only if required and possible. Derive the precise keys, bridge-meeting costs, v-independent offsets (or a conservative stopping lower bound), treatment of stale keys/reopenings, and termination when one frontier empties; algebraically show why the criterion implies no undiscovered cheaper path. Test proof claims on hand-calculated oneway ring/bridge/island counterexamples. If no correct nontrivial sentinel policy or meet rule is established, record a NO-GO counterexample/unresolved obligation and stop before coding the search.</action>
<verify><automated>test -s docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md &amp;&amp; grep -q 'P-1' docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md &amp;&amp; grep -q 'P-2' docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md &amp;&amp; grep -q '0xffff' docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md</automated></verify>
<done>Document contains independently checkable inequalities and stop-rule proof or an explicit NO-GO with counterexample, including finite/sentinel cross-edge and disconnected-component cases, before any search code.</done>
</task>
<task type="auto">
<name>task 2: Implement branch-only experimental runner and adversarial oracle checks, conditional on proof</name>
<files>routing/scripts/experiment-alt-direction-split.mjs, routing/test/alt-direction-split-experiment.test.mjs</files>
<action>Only after task 1 establishes P-1 **and** P-2 with an executable sentinel rule: add a separate maintainer-time experiment script exporting a testable `routeDirectionSplit` and counters; do not import it from production, change shipped ASTAR, or change existing A11's ban on `landmarkFrom` in `astar.ts`. Match ARG2 semantics (row `[l*n+v]`, floor-quantized edge costs, bit0 forbidden in both traversal directions) and validate/reconstruct paths with exact traversable edge seconds. Implement exactly the proven potential pair, reduced costs, meeting updates and stop criterion; assert reduced-cost non-negativity on test graphs, fail loud for violated proof assumptions rather than substituting an unproved shortcut. Write new tests using `encodeGraph`/`decodeGraph` + `landmarkTables`: exhaustive directed ring + flagged mirrors + incoming spur, one-way bridge in BOTH travel directions, isolated anchored/unanchored components, origin/goal and interior sentinel cells, mixed clean/dirty columns, equal-cost alternatives and unreachable nulls; check every pair against independent Dijkstra, including paths' legal edges and symmetric seconds delta. Use a targeted adversarial edge where a per-node sentinel mask would violate consistency. If task 1 is NO-GO, do not create this runner/tests; document the blocked verification in the experiment record instead.</action>
<verify><automated>if grep -q '^Verdict: NO-GO' docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md; then test ! -e routing/scripts/experiment-alt-direction-split.mjs; else test -s routing/scripts/experiment-alt-direction-split.mjs &amp;&amp; npm run routing:test &amp;&amp; npm run routing:typecheck; fi</automated></verify>
<done>Proof-qualified branch-only experiment passes adversarial oracle tests and preserves full existing suite; or NO-GO prevents unsupported code. If Node/npm unavailable, report tests NOT RUN (never green).</done>
</task>
<task type="auto">
<name>task 3: Measure fixed-seed oracle and paired CPU/expansions; record honest verdict</name>
<files>routing/scripts/experiment-alt-direction-split.mjs, docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md</files>
<action>For a proof-qualified candidate, run the experiment against the **same** compared 200 seeded cross-metro pairs as `measure-astar-oracle-sweep.mjs` (seed `0x08f1`, oversample 3x, 12 km floor, snap misses reported), with independent Dijkstra and symmetric ±1e-6 s verdict/class checks; additionally run A1 full suite and the existing D-03 frozen-record gate unchanged. Use the seeded `0x08a1` 100-pair extended workload for paired experimental-versus-shipped searches: same snapped endpoints, warm-up printed separately, count settled expansions per direction, report misses outside routed p95, median/mean/p95 CPU, expansion ratio and host/Node/version/graph SHA-256; alternate run order or repeated rounds to reduce JIT/thermal order bias. Shipped `routeAstar` with 0/24 clean columns is the **haversine-only control** for the present binary; instrument it only via an isolated copy/runner if needed and verify bit-identical costs before comparison. The original `measure-route-cpu.mjs --extended` budget gate is a separate shipped-control run and a FAIL exit is a measurement, not an instruction to weaken the gate. The gitignored ARG2 graph is available read-only at `/home/C-Fu/araharah/routing/data/kl-roadgraph.v1.bin`: if missing locally, link into this checkout's gitignored `routing/data/` only after verifying no existing path and binary ARG2; never write the target or any file in the other checkout. If unavailable/undecodable or Node/npm missing, record NOT MEASURED; never claim a speedup or PASS from synthetic data or 0 active landmarks. Report proof+oracle+expansion+p95 outcomes with a GO/NO-GO verdict and next-step recommendation confined to a later separately approved phase. Do not merge/integrate.</action>
<verify><automated>test -s docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md &amp;&amp; test -z "$(git diff --name-only ceb71c2 -- routing/src/astar.ts routing/src/index.ts routing/test/fixtures/alt-baseline.json .planning/ROADMAP.md)" &amp;&amp; test "$(git branch --show-current)" = research/alt-direction-split</automated></verify>
<done>Document prints the oracle and paired workload results with provenance and explicit gate statuses, or explains exactly why a proof or measurement blocked execution; no production/baseline/main-checkout changes.</done>
</task>
## Verification and guardrails
At execution, ensure Node/npm are actually available (this planning shell currently reports `node: command not found`; do **not** pass an absent-runtime gate). Use `npm run routing:test`, `npm run routing:typecheck`, `node routing/scripts/measure-optimality-gate.mjs`, and `node routing/scripts/measure-route-cpu.mjs --extended` where available; record expected budget-gate FAIL honestly. Existing `measure-astar-oracle-sweep.mjs` measures shipped search, **not** the experiment: inject the candidate into the exported `sweepOracle` core or reproduce its exact seeded pair-selection/oracle contract in the isolated runner, labeling each separately. A missing real graph must never be counted as a 0/200 success (the shipped sweep's SKIP exits 0). Verify scope at end with `git status --short`, `git branch --show-current`, and `git diff --name-only ceb71c2`; inspect any untracked experiment files separately. No production deployment, no main merge.
## Outcome
Completion means either a proved and measured branch-only search with 0 optimality violations and transparent steering/cost numbers, **or** a precise NO-GO identifying the failed proof/benchmark. The 8 ms Worker gate remains FAIL unless directly measured to pass on a representative free-plan host; a local CPU win alone cannot retire the budget concern.
@@ -0,0 +1,52 @@
---
phase: quick
plan: 261026-d95
subsystem: routing
tags: [ALT, bidirectional, directional, oracle, benchmark]
requires: [ARG2 landmark tables, shipped routeAstar]
provides: [proved pair-domain ALT, isolated research runner, NO-GO verdict]
affects: []
tech-stack:
added: []
patterns: [pair-restricted landmark family selection, independent A* frontier stop]
key-files:
created: [docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md, routing/scripts/experiment-alt-direction-split.mjs, routing/test/alt-direction-split-experiment.test.mjs]
modified: []
decisions: [Do not integrate candidate because paired p95 regresses despite reduced expansions]
metrics:
completed: 2026-10-26
---
# Quick Plan 261026-d95: Direction-split ALT isolated research summary
An independently consistent asymmetric ALT bound can be used on a query-restricted sentinel-free path domain with a conservative single-frontier A* stopping criterion, but this isolated implementation is **NO-GO**: p95 516.778 ms versus original shipped 118.923 ms on paired host measurements.
## Tasks and commits
1. `57fd5ed` — Proved P-1 and P-2 separately, including sentinel boundary and disconnected-path handling before writing experiment code.
2. `92fc213` — Added isolated experimental search and adversarial exhaustive Dijkstra-oracle tests (no production imports).
3. `9a671bd` — Added seeded real-graph oracle, paired original/clone measurement, expansion instrumentation, and recorded exact results/NO-GO in the experiment document.
## Validation and verdict
- New and existing routing suite: **85 passed, 0 failed**, including unchanged shipped A1/625. Typecheck passed.
- Fixed-seed 200-pair real graph: 182 routes, 18 unreachable, 120 rejected snap probes, **zero** suboptimal, oracle-mismatch or class-break cases.
- Extended paired workload: 53 routed/100 drawn, 41 snap misses, 6 unreachable. Two alternate-order rounds. Experiment/isolated shipped clone expansions **0.709×**; experiment/original shipped p95 **4.35× slower**. Original shipped p95 118.923 ms; experimental p95 516.778 ms.
- D-03 unchanged frozen gate passed. Shipped extended CPU gate **FAILED** at p95 127.933 ms > 8 ms. Representative free-plan Worker CPU: **NOT MEASURED**.
- Full provenance and proof details: `docs/ALT-DIRECTION-SPLIT-EXPERIMENT.md`. Neither this branch nor the read-only binary symlink was deployed or merged.
## Deviations from Plan
**[Rule 3 - Blocking]** Local `node_modules` was initially absent (L2 bundle marker failed and `tsc` was unavailable); ran `npm ci --ignore-scripts --no-audit --no-fund`, then tests and typecheck passed. No lockfile changes.
**[Rule 1 - Bug]** The initial diagnostic assertion reversed the backward edge inequality in code; corrected to compare the reverse search's `h_b(v)-h_b(u)` on `v→u`. Initial run failed loud on a real edge; rerun passed the 200-pair oracle and new tests.
**[Rule 2 - Measurement completeness]** The shipped search does not export expansion counters. Created and automatically removed a source-identical temporary clone with settled counters injected and measured the unmodified original separately; compared every route's seconds across the two controls. No production file modified.
## Known Stubs
None in the search. Shipped-original settled expansions are deliberately **NOT MEASURED** directly; the source-identical instrumented clone supplies an explicitly labeled proxy rather than fabricated numbers. Worker-host CPU remains NOT MEASURED.
## Self-Check: PASSED
Verified all three experiment files exist, commits `57fd5ed`, `92fc213`, `9a671bd` exist, branch is `research/alt-direction-split`, and protected production/baseline/roadmap files differ neither from `ceb71c2` nor this task's commits.
+47
View File
@@ -0,0 +1,47 @@
# Direction-split ALT: proof gate (research branch only)
**Verdict: NO-GO** for deployment and the 8 ms Worker budget. P-1 and P-2 proved for **pair-restricted domain and independent A* stopping** below; the measured branch-only implementation is 4.35× slower at paired p95 than the original control despite fewer expansions. This is not a proof for the shipped Ikeda sum-of-tops rule with asymmetric families.
## Graph and tables
Let `c(u,v)` be the stored finite nonnegative edge seconds, for directed CSR edge `u→v` **only when bit0 is clear**. Neither search traverses bit0 edges, even in reverse. Let `q>0` be `landmarkQuantumS`, `ĉ(u,v)=floor(c(u,v)/q)` and `D` be shortest path in integer quanta. Row-major `to[ℓ*n+v]=D(v,ℓ)`, `from[ℓ*n+v]=D(ℓ,v)`; `0xffff` means unreachable, **never** the number 65535. All finite stored values are at most 65000. Distances in seconds obey `qD` triangle inequalities against real `c` since `qĉ≤c`.
For this **fixed query** `o→g`, select to-columns `T={ℓ | to[ℓ*n+g] finite}` and from-columns `F={ℓ | from[ℓ*n+o] finite}`. If `to[o]` is sentinel for any ℓ in T, or `from[g]` is sentinel for any ℓ in F, return unreachable: otherwise an `o→g` path would respectively concatenate with `g→ℓ` or `ℓ→o`. Define the search domain `S` as nodes where `to[ℓ*n+v]` is finite for **every** ℓ in T and `from[ℓ*n+v]` finite for **every** ℓ in F. Exclude nodes outside S in **both** traversals. Every vertex of every actual `o→g` path belongs to S: its suffix reaches g then ℓ in T; its prefix is reached from o, which ℓ in F reaches. Thus domain restriction cannot remove any solution. It can remove dead-end branches and can change landmark shortest distances, but the original full-graph labels still satisfy the required triangle inequalities for every remaining edge. When T or F is empty the respective condition is vacuous and haversine remains available. Both endpoints are in S after the early reachability checks.
**P-1, independently:** on S use
`h_f(v)=max(haversine(v,g)/vmax, 0, max_{ℓ∈T} q*(to[ℓ*n+v]-to[ℓ*n+g]))`;
`h_b(v)=max(haversine(v,o)/vmax, 0, max_{ℓ∈F} q*(from[ℓ*n+v]-from[ℓ*n+o]))`.
For each traversable `u→v` inside S, `to[u]≤ĉ(u,v)+to[v]` implies `h_f(u)≤c(u,v)+h_f(v)` for each to-family. Independently `from[v]≤from[u]+ĉ(u,v)` implies `h_b(v)≤c(u,v)+h_b(u)` for each from-family on the reverse search edge `v→u`. Spherical haversine obeys both inequalities if real edge seconds lower-bound its endpoint chord at `vmax`; use the same `maxSpeedKmh` scaling as shipped A*, and require this graph assumption on test fixtures. The max of individually consistent families is consistent. Both heuristics vanish at their targets; by path telescoping they are admissible on S. Family values can be signed; max with zero/haversine keeps h nonnegative. Active sets T and F are query-scoped and unchanged across each edge **within S**. There is no numeric sentinel subtraction or per-node family skip.
Clean-column selection on the actual 0/24-clean binary gives 0 active landmarks globally. This domain construction instead selects endpoint-reachable columns for each query and excludes their holes *before search*: giant-component queries can activate 24 columns despite sentinel island cells; island queries may activate none. It cannot be replaced by evaluating a family only at the finite end of a cross-edge: if `to[u]=100`, `to[v]=0xffff`, and `u→v` costs 1, returning 100 at u and fallback 0 at v violates forward consistency. Likewise `from[u]=0xffff`, `from[v]=100` on `u→v` violates reverse consistency if the family is skipped at u. These edges are excluded when the corresponding column is selected. If they lie on an actual path to g or from o respectively, the endpoint selection/concatenation proves they cannot occur; if the column is not selected it contributes nowhere.
## P-2: independent frontier lower bound, NOT the BH sum rule
Maintain actual seconds distances `df` from o, `db` to g on the reversed induced graph, with keys `Kf(v)=df(v)+h_f(v)` and `Kb(v)=db(v)+h_b(v)`. These keys are not opposite potentials; **do not** add their minima or apply a v-independent offset. There is no v-independent cancellation of `h_f(v)+h_b(v)` for unrelated directed families: for example, a oneway chain `o→a→g` may have `h_f(a)=1,h_b(a)=1`, while a parallel dead-end has either bound 0 or a very large bound. Instead use the conservative stop `min_valid(Kf)≥U` **or** `min_valid(Kb)≥U`, where U is the best *real* discovered complete route (node meet `df(v)+db(v)` or traversable bridge `df(u)+c(u,v)+db(v)`). Strict floating-point tolerance must not weaken the inequality; all emitted seconds come from a traversable reconstructed path, not from subtracting potentials.
For each side, consistency gives reduced edge `c+h(next)-h(current)≥0` on that side, so settling min key fixes its actual distance; duplicate heap entries whose stored g differs from current g or whose vertex is closed are stale and must be discarded *before reading the frontier minimum*. Better g to a closed vertex is impossible under this premise; assert/fail loud if it occurs (or reopen if consistency is abandoned, but then this proof and stop do not apply). For a hypothetical route P of cost `<U`, consider the first vertex on P not yet forward-settled. Its predecessor is settled (or it is o); relaxation inserted a tentative g no larger than P's prefix, and admissibility bounds `Kf≤cost(P)<U`. Its live entry must still be in the forward heap. Therefore a forward minimum ≥U excludes P; the symmetric argument applies to the backward minimum. A bridge meet can only lower U via a genuine chain. If a frontier empties after stale removal, it has exhausted its induced reachable set: if no complete path was found, return null; if U finite the same first-unsettled argument shows U optimal. No assumption of simultaneous frontier growth or of a sum-of-minima bound is needed. Signed raw family differences are harmless since h is nonnegative and keys use real g; each direction has its own independent reduction and no shared potential.
Hand checks: oneway ring `0→1→2→0` with a forbidden bit0 mirror `1→0`, forward to[0] labels (0,2,1) for unit edges and backward from[0] (0,1,2); all allowed edges respect their *respective* oriented inequalities while mirror never relaxes. Bridge `A→B` costing 1 with no return, anchor in A: from[B] finite and to[B] sentinel; anchor in B: to[A] finite and from[A] sentinel. For a query A→B, applicable columns require origin/goal finite and S retains the bridge; B→A fails the endpoint reachability check and returns null. Disconnected anchored island vs unanchored component has sentinel endpoint(s); a route wholly in the unanchored component retains an empty active set and can still succeed, while a cross-component route is null. In all three examples, cross-boundary edges are excluded only if they cannot be on an o→g path.
## Measured experiment and verdict
Branch-only `routing/scripts/experiment-alt-direction-split.mjs` implements these independent A* keys, single-vertex meets, stale-key pruning and a query-domain mask. New suite `routing/test/alt-direction-split-experiment.test.mjs` tests **every pair** across one-way rings with blocked mirrors/spurs and equal costs, both directions of one-way bridges, isolated anchored/unanchored islands, endpoints and interior `0xffff` cells, mixed columns and a finite→sentinel cross-edge per-node skip adversary. Each checks independent O(V²) Dijkstra seconds, null classes and legal reconstructed edges. On real graph, `sweepOracle` uses the *exact* 200 compared snapped cross-metro pairs (seed `0x08f1`, 600 draws, 12 km minimum, 120 rejected snap misses) and independent heap-Dijkstra; **182 routed, 18 unreachable; suboptimal 0, oracle-mismatch 0, class-break 0** at symmetric ±1e-6 s. No synthetic or real pair disproved this conservative stop; the counterexample to naive per-node masking is above, not a measured result of this runner.
Measured 2026-10-26 on `HAFu`, Node v24.18.0, Linux, binary SHA-256 `366e1be2ca270e5e67cb0debc98887b6dcb66f29849747d59f4e0e58739ec5b8`, ARG2 311,361 nodes / 677,126 edges. This checkout's `routing/data/kl-roadgraph.v1.bin` is an **existing read-only symlink** to the other checkout; no binary modifications. Reproduction: `node routing/scripts/experiment-alt-direction-split.mjs`. The experimental instrumented comparison used an automatically removed temporary *source-identical* shipped clone with only two settled counters injected, and also timed original unmodified `routeAstar` on the same snapped endpoints. Clone/original seconds agreed to ±1e-6 for each routed sample. A dedicated warm-up (not included) took experimental 0.935 ms, original 20.005 ms, clone 27.471 ms.
The seed `0x08a1` 100-pair, ≥12 km extended draw produced **41 snap misses, 6 unreachable and 53 routed**; each routed pair was measured twice, reversed run order across rounds (106 routed measurements each). Misses are **outside** routed p95; each pair was compared at identical snapped endpoints. Times are Node `hrtime` wall elapsed for search call on this host, *not Cloudflare Worker CPU instrumentation*, so they cannot establish a Worker CPU PASS.
| Search | n | Median ms | Mean ms | p95 ms | Total settled expansions (106 calls) |
|---|---:|---:|---:|---:|---:|
| Direction-split / domain-mask experimental | 106 | 150.845 | 197.490 | **516.778** | 9,958,938 (forward/backward split NOT MEASURED) |
| Source-identical isolated shipped clone + settled counters | 106 | 73.588 | 72.331 | 129.327 | 14,037,662 (6,764,880 fwd / 7,272,782 bwd) |
| Original shipped haversine-only `routeAstar`, unmodified | 106 | 73.619 | 71.864 | **118.923** | NOT MEASURED directly (clone is expansion proxy) |
Experimental/clone expansion ratio **0.709×**; experimental/original shipped paired p95 **4.35× slower** despite fewer settles. The experiment selected an average of 22.64 to- and 18.87 from-columns per routed call (2400/106 and 2000/106 respectively); shipped has 0/24 clean columns and is the actual haversine-only control. No claim of runtime speedup. Search-scoped domain scanning, per-vertex `Math.max`, and heaps/allocations in this research runner may account for some overhead, but optimization is NOT demonstrated and no performance claim is transferable to an optimized Worker implementation.
Control gates unchanged: `npm run routing:test` **85 passed, 0 failed**, including shipped A1/625; `npm run routing:typecheck` **PASS**. `node routing/scripts/measure-optimality-gate.mjs` D-03 frozen record **PASS** (53 routed equal, 6 unreachable, 41 unsnappable; 0 class breaks). `node routing/scripts/measure-route-cpu.mjs --extended` shipped-control **FAIL**: 53 routed/100, p95 **127.933 ms**, >8 ms; 47 misses printed separately. Early test invocation lacked installed dependencies and failed L2/tsc; `npm ci --ignore-scripts --no-audit --no-fund` installed local dependencies, and both gates then passed; no source changes to those gates.
**Proof status:** P-1 PASS for induced pair domain; P-2 PASS for the conservative independent-frontier stopping bound, **NOT** a proof of asymmetric Ikeda sum stopping. **Oracle status:** 0 violations in synthetic exhaustive and real seeded runs. **Budget status:** local original control FAIL on 8 ms test; candidate local p95 FAIL; representative free-plan Worker 8 ms **NOT MEASURED**. **Recommendation:** NO-GO on integration; retain original A*, baseline and bot/web unchanged. A later, separately approved investigation could evaluate typed-array/precomputed component labels and a separately proved stronger meet bound, then rerun identical oracle and actual Worker-host CPU gates. No merge or Worker-budget retirement is justified.
@@ -0,0 +1,209 @@
#!/usr/bin/env node
/* Branch-only directional ALT experiment. Neither production nor integrations import this. */
import { readFileSync, existsSync, writeFileSync, unlinkSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { hostname } from 'node:os';
import { fileURLToPath } from 'node:url';
import { decodeGraph } from '../src/graph.ts';
import { haversineM } from '../src/geo.ts';
import { snapToGraph } from '../src/snap.ts';
import { routeAstar } from '../src/astar.ts';
import { generateCrossMetroPairs } from './measure-optimality-baseline.mjs';
import { sweepOracle } from './measure-astar-oracle-sweep.mjs';
// Source-identical isolated instrumented clone: only two settled counters are injected.
// The production file is never opened for writing; remove the ephemeral copy in finally.
async function instrumentedShipped() {
const source = new URL('../src/astar.ts', import.meta.url);
const clone = new URL('../src/astar-alt-measure.tmp.ts', import.meta.url);
let code = readFileSync(source, 'utf8');
const forward = 'if (expandForward) fClosed[u] = 1;', backward = 'else bClosed[u] = 1;';
if (!code.includes(forward) || !code.includes(backward)) throw Error('production settled markers changed; instrumentation not valid');
code = code.replace(forward, 'if (expandForward) { fClosed[u] = 1; globalThis.__altControlSettled.forward++; }')
.replace(backward, 'else { bClosed[u] = 1; globalThis.__altControlSettled.backward++; }');
if (existsSync(clone)) throw Error('temporary instrumentation clone already exists');
try {
writeFileSync(clone, code, { flag: 'wx' });
return await import(clone.href);
} finally { if (existsSync(clone)) unlinkSync(clone); }
}
const INF = Infinity, SENTINEL = 0xffff;
class Heap {
a = [];
push(k, g, v) {
const a = this.a; let i = a.length; a.push({ k, g, v }); const x = a[i];
while (i) { const p = (i - 1) >> 1; if (a[p].k <= k) break; a[i] = a[p]; i = p; } a[i] = x;
}
pop() {
const a = this.a, top = a[0], x = a.pop();
if (a.length) { let i = 0; for (;;) { let j = i * 2 + 1; if (j >= a.length) break; if (j + 1 < a.length && a[j + 1].k < a[j].k) j++; if (a[j].k >= x.k) break; a[i] = a[j]; i = j; } a[i] = x; }
return top;
}
peek(dist, closed) { while (this.a.length && (closed[this.a[0].v] || dist[this.a[0].v] !== this.a[0].g)) this.pop(); return this.a[0]?.k ?? INF; }
}
const reverseCache = new WeakMap();
function reverse(graph) {
if (reverseCache.has(graph)) return reverseCache.get(graph);
const rows = new Uint32Array(graph.nodeCount + 1);
for (let e = 0; e < graph.edgeCount; e++) rows[graph.edgeTarget[e] + 1]++;
for (let v = 0; v < graph.nodeCount; v++) rows[v + 1] += rows[v];
const source = new Uint32Array(graph.edgeCount), ids = new Uint32Array(graph.edgeCount), cursor = rows.slice(0, -1);
for (let u = 0; u < graph.nodeCount; u++) for (let e = graph.edgeStart[u]; e < graph.edgeStart[u + 1]; e++) {
const pos = cursor[graph.edgeTarget[e]]++; source[pos] = u; ids[pos] = e;
}
const result = { rows, source, ids }; reverseCache.set(graph, result); return result;
}
/** Returns null on an unreachable pair; throws on a violated proof assumption. */
export function routeDirectionSplit(graph, o, g) {
const n = graph.nodeCount;
if (!Number.isInteger(o) || !Number.isInteger(g) || o < 0 || g < 0 || o >= n || g >= n) return null;
if (o === g) return { nodes: [o], seconds: 0, expansionsForward: 0, expansionsBackward: 0, activeTo: 0, activeFrom: 0 };
const T = [], F = [], Q = graph.landmarkQuantumS;
for (let l = 0; l < graph.landmarkCount; l++) {
const base = l * n;
if (graph.landmarkTo[base + g] !== SENTINEL) {
if (graph.landmarkTo[base + o] === SENTINEL) return null;
T.push([base, graph.landmarkTo[base + g]]);
}
if (graph.landmarkFrom[base + o] !== SENTINEL) {
if (graph.landmarkFrom[base + g] === SENTINEL) return null;
F.push([base, graph.landmarkFrom[base + o]]);
}
}
const domain = new Int8Array(n);
function inside(v) {
if (domain[v]) return domain[v] === 1;
for (const [base] of T) if (graph.landmarkTo[base + v] === SENTINEL) { domain[v] = -1; return false; }
for (const [base] of F) if (graph.landmarkFrom[base + v] === SENTINEL) { domain[v] = -1; return false; }
domain[v] = 1; return true;
}
const scale = 3.6 / graph.maxSpeedKmh;
if (!(scale > 0 && Number.isFinite(scale))) throw Error('nonpositive graph maxSpeedKmh');
const hf = new Float64Array(n).fill(NaN), hb = new Float64Array(n).fill(NaN);
function heuristic(v, forward) {
const cache = forward ? hf : hb;
if (!Number.isNaN(cache[v])) return cache[v];
const target = forward ? g : o;
let h = haversineM({ lat: graph.lat[v], lng: graph.lng[v] }, { lat: graph.lat[target], lng: graph.lng[target] }) * scale;
for (const [base, constant] of (forward ? T : F)) {
const cell = (forward ? graph.landmarkTo : graph.landmarkFrom)[base + v];
if (cell === SENTINEL) throw Error('sentinel in included domain');
h = Math.max(h, (cell - constant) * Q);
}
cache[v] = h; return h;
}
const df = new Float64Array(n).fill(INF), db = new Float64Array(n).fill(INF);
const prev = new Int32Array(n).fill(-1), next = new Int32Array(n).fill(-1);
const cf = new Uint8Array(n), cb = new Uint8Array(n), a = new Heap(), b = new Heap(), rev = reverse(graph);
df[o] = 0; db[g] = 0; a.push(heuristic(o, true), 0, o); b.push(heuristic(g, false), 0, g);
let U = INF, meeting = -1, expansionsForward = 0, expansionsBackward = 0;
while (true) {
const fa = a.peek(df, cf), bb = b.peek(db, cb);
if (fa === INF || bb === INF || fa >= U || bb >= U) break;
const forward = a.a.length <= b.a.length;
const heap = forward ? a : b, distances = forward ? df : db, other = forward ? db : df;
const closed = forward ? cf : cb, chain = forward ? prev : next;
const u = heap.pop().v, du = distances[u]; closed[u] = 1;
if (forward) expansionsForward++; else expansionsBackward++;
if (du + other[u] < U) { U = du + other[u]; meeting = u; }
const start = forward ? graph.edgeStart[u] : rev.rows[u], end = forward ? graph.edgeStart[u + 1] : rev.rows[u + 1];
for (let r = start; r < end; r++) {
const e = forward ? r : rev.ids[r];
if (graph.edgeFlags[e] & 1) continue;
const v = forward ? graph.edgeTarget[e] : rev.source[r];
if (!inside(v)) continue;
const seconds = graph.edgeSeconds[e];
// Enforce both directed inequalities on each inspected edge. Tiny f64 rounding tolerance only in assertion.
const delta = heuristic(u, forward) - heuristic(v, forward);
if (delta > seconds + 1e-7) throw Error(`inconsistent ${forward ? 'forward' : 'backward'} potential ${u}->${v}: delta=${delta}, seconds=${seconds}, Q=${Q}`);
const tentative = du + seconds;
if (tentative < distances[v]) {
if (closed[v]) throw Error('improved closed label');
distances[v] = tentative; chain[v] = u; heap.push(tentative + heuristic(v, forward), tentative, v);
}
if (distances[v] + other[v] < U) { U = distances[v] + other[v]; meeting = v; }
}
}
if (meeting < 0) return null;
const left = []; for (let v = meeting; v !== -1; v = prev[v]) left.push(v); left.reverse();
const right = []; for (let v = next[meeting]; v !== -1; v = next[v]) right.push(v);
const nodes = left.concat(right);
let seconds = 0;
for (let i = 1; i < nodes.length; i++) {
let e = graph.edgeStart[nodes[i - 1]], end = graph.edgeStart[nodes[i - 1] + 1];
while (e < end && (graph.edgeTarget[e] !== nodes[i] || (graph.edgeFlags[e] & 1))) e++;
if (e === end) throw Error('invalid reconstructed edge');
seconds += graph.edgeSeconds[e];
}
if (Math.abs(seconds - U) > 1e-6) throw Error(`reconstructed seconds mismatch: ${seconds} vs ${U}`);
return { nodes, seconds, expansionsForward, expansionsBackward, activeTo: T.length, activeFrom: F.length };
}
export function dijkstra(graph, o, g) {
const dist = new Float64Array(graph.nodeCount).fill(INF), closed = new Uint8Array(graph.nodeCount), heap = new Heap();
dist[o] = 0; heap.push(0, 0, o);
while (heap.peek(dist, closed) !== INF) {
const u = heap.pop().v; if (u === g) return dist[u]; closed[u] = 1;
for (let e = graph.edgeStart[u]; e < graph.edgeStart[u + 1]; e++) {
if (graph.edgeFlags[e] & 1) continue;
const v = graph.edgeTarget[e], nd = dist[u] + graph.edgeSeconds[e];
if (nd < dist[v]) { dist[v] = nd; heap.push(nd, nd, v); }
}
}
return INF;
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
const path = new URL('../data/kl-roadgraph.v1.bin', import.meta.url);
if (!existsSync(path)) { console.error('NOT MEASURED: missing ARG2 binary'); process.exitCode = 2; }
else {
const bytes = readFileSync(path), graph = decodeGraph(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
if (!graph) { console.error('NOT MEASURED: undecodable ARG2 binary'); process.exitCode = 2; }
else {
console.log(`host=${hostname()} node=${process.version} sha256=${createHash('sha256').update(bytes).digest('hex')} graph=${graph.nodeCount}/${graph.edgeCount}`);
const raw = generateCrossMetroPairs(graph.bbox, { count: 600, seed: 0x08f1, minPairKm: 12, haversineM });
const pairs = [], missing = { snap: 0 };
for (const pair of raw) { if (pairs.length === 200) break; if (!snapToGraph(graph, pair.origin) || !snapToGraph(graph, pair.destination)) missing.snap++; else pairs.push(pair); }
if (pairs.length < 200) throw Error(`only ${pairs.length}/200 snapped pairs`);
const verdict = sweepOracle(graph, pairs, { snap: snapToGraph, astar: routeDirectionSplit, oracle: dijkstra });
console.log(`oracle: compared=${pairs.length} snap-miss=${missing.snap} routed=${verdict.routed} unreachable=${verdict.unreachable} suboptimal=${verdict.suboptimal} oracle-mismatch=${verdict.oracleMismatch} class-break=${verdict.classBreaks} pass=${verdict.pass}`);
if (!verdict.pass) { console.error(verdict.rows.filter(r => !['pass', 'unreachable'].includes(r.verdict))); process.exitCode = 1; }
globalThis.__altControlSettled = { forward: 0, backward: 0 };
const control = (await instrumentedShipped()).routeAstar;
const extended = generateCrossMetroPairs(graph.bbox, { count: 100, seed: 0x08a1, minPairKm: 12, haversineM });
const warm = (fn) => { const a = process.hrtime.bigint(); fn(); return Number(process.hrtime.bigint() - a) / 1e6; };
const warmExp = warm(() => routeDirectionSplit(graph, 0, 1));
const warmShipped = warm(() => routeAstar(graph, 0, 1));
const warmClone = warm(() => control(graph, 0, 1));
console.log(`warm-up ms (unmeasured): experimental=${warmExp.toFixed(3)} original-shipped=${warmShipped.toFixed(3)} instrumented-clone=${warmClone.toFixed(3)}`);
globalThis.__altControlSettled = { forward: 0, backward: 0 };
const times = [[], [], []], counts = [0, 0], misses = { snap: 0, unreachable: 0 }, active = [0, 0];
for (let round = 0; round < 2; round++) for (let i = 0; i < extended.length; i++) {
const pair = extended[i], s = snapToGraph(graph, pair.origin), t = snapToGraph(graph, pair.destination);
if (!s || !t) { if (round === 0) misses.snap++; continue; }
const results = [];
for (const k of ((i + round) % 2 ? [2, 1, 0] : [0, 1, 2])) {
const st = process.hrtime.bigint(), result = k === 0 ? routeDirectionSplit(graph, s.node, t.node) : k === 1 ? control(graph, s.node, t.node) : routeAstar(graph, s.node, t.node);
const ms = Number(process.hrtime.bigint() - st) / 1e6; results[k] = { result, ms };
}
if (results[0].result === null || results[1].result === null || results[2].result === null) {
if (results[0].result !== results[1].result || results[0].result !== results[2].result) throw Error(`paired reachability mismatch ${pair.pairId}`);
if (round === 0) misses.unreachable++; continue;
}
if (Math.abs(results[0].result.seconds - results[1].result.seconds) > 1e-6 || Math.abs(results[1].result.seconds - results[2].result.seconds) > 1e-6) throw Error(`paired cost mismatch ${pair.pairId}`);
for (let k = 0; k < 3; k++) times[k].push(results[k].ms);
counts[0] += results[0].result.expansionsForward + results[0].result.expansionsBackward;
active[0] += results[0].result.activeTo; active[1] += results[0].result.activeFrom;
}
const stats = a => { a.sort((x, y) => x - y); return `n=${a.length} median=${a[Math.floor(a.length / 2)].toFixed(3)} mean=${(a.reduce((x,y) => x + y,0)/a.length).toFixed(3)} p95=${a[Math.ceil(a.length * .95)-1].toFixed(3)} ms`; };
console.log(`extended snap-miss=${misses.snap} unreachable=${misses.unreachable} (outside routed p95), 2 alternating rounds`);
console.log(`experimental: ${stats(times[0])} expansions=${counts[0]} active-to/from=${active.join('/')}`);
counts[1] = globalThis.__altControlSettled.forward + globalThis.__altControlSettled.backward;
console.log(`shipped isolated instrumented clone: ${stats(times[1])} expansions=${counts[1]} forward/backward=${globalThis.__altControlSettled.forward}/${globalThis.__altControlSettled.backward}; experimental/control expansion ratio=${(counts[0]/counts[1]).toFixed(3)}`);
console.log(`original shipped routeAstar (paired unmodified haversine-only control): ${stats(times[2])}`);
}
}
}
@@ -0,0 +1,78 @@
import { strict as assert } from 'node:assert';
import { test } from './harness.mjs';
import { encodeGraph, decodeGraph } from '../src/graph.ts';
import { landmarkTables } from '../src/landmark.ts';
import { routeDirectionSplit } from '../scripts/experiment-alt-direction-split.mjs';
function fixture(n, edges, anchors) {
const q = 1;
const tables = landmarkTables({ nodeCount: n, edges: edges.map(e => ({ ...e, flags: e.flags ?? 0 })), quantumS: q }, Uint32Array.from(anchors));
const lat = Array(n).fill(3.14), lng = Array(n).fill(101.68);
return decodeGraph(encodeGraph({ lat, lng, edges, names: [], maxSpeedKmh: 120, landmarks: { nodes: anchors, quantumS: q, ...tables } }));
}
// Independent O(V²) oracle, not the experiment's heap-based Dijkstra.
function oracle(g, o, target) {
const d = Array(g.nodeCount).fill(Infinity), seen = new Set(); d[o] = 0;
for (let k = 0; k < g.nodeCount; k++) {
let u = -1;
for (let v = 0; v < g.nodeCount; v++) if (!seen.has(v) && (u < 0 || d[v] < d[u])) u = v;
if (u < 0 || d[u] === Infinity) break;
if (u === target) return d[u];
seen.add(u);
for (let e = g.edgeStart[u]; e < g.edgeStart[u + 1]; e++) if (!(g.edgeFlags[e] & 1)) d[g.edgeTarget[e]] = Math.min(d[g.edgeTarget[e]], d[u] + g.edgeSeconds[e]);
}
return Infinity;
}
function checkAll(graph) {
assert.ok(graph);
for (let o = 0; o < graph.nodeCount; o++) for (let g = 0; g < graph.nodeCount; g++) {
const actual = routeDirectionSplit(graph, o, g), expected = oracle(graph, o, g);
assert.equal(actual === null, expected === Infinity, `${o}→${g} reachability`);
if (!actual) continue;
assert.ok(Math.abs(actual.seconds - expected) <= 1e-6, `${o}→${g} cost ${actual.seconds} != ${expected}`);
assert.equal(actual.nodes[0], o); assert.equal(actual.nodes.at(-1), g);
let sum = 0;
for (let i = 1; i < actual.nodes.length; i++) {
const a = actual.nodes[i - 1], b = actual.nodes[i];
const edgeIds = [];
for (let e = graph.edgeStart[a]; e < graph.edgeStart[a + 1]; e++) if (graph.edgeTarget[e] === b && !(graph.edgeFlags[e] & 1)) edgeIds.push(e);
assert.equal(edgeIds.length, 1, `illegal ${a}→${b}`);
sum += graph.edgeSeconds[edgeIds[0]];
}
assert.ok(Math.abs(sum - actual.seconds) <= 1e-6);
}
}
test('direction split: directed ring, blocked mirrors, incoming spur and equal alternatives', () => {
checkAll(fixture(6, [
...[0, 1, 2, 3].map(u => ({ from: u, to: (u + 1) % 4, seconds: 2 })),
{ from: 1, to: 0, seconds: 1, flags: 1 }, { from: 0, to: 3, seconds: 1, flags: 1 },
{ from: 4, to: 1, seconds: 3 }, { from: 4, to: 2, seconds: 5 },
{ from: 0, to: 5, seconds: 2 }, { from: 3, to: 5, seconds: 2 },
], [0, 2, 4]));
});
test('direction split: one-way bridge both travel directions, anchored and unanchored islands', () => {
checkAll(fixture(9, [
{ from: 0, to: 1, seconds: 1 }, { from: 1, to: 0, seconds: 1 },
{ from: 1, to: 2, seconds: 2 }, { from: 2, to: 3, seconds: 1 }, { from: 3, to: 2, seconds: 1 },
{ from: 3, to: 2, seconds: 1, flags: 1 },
{ from: 4, to: 5, seconds: 1 }, { from: 5, to: 4, seconds: 1 },
{ from: 6, to: 7, seconds: 1 }, { from: 7, to: 6, seconds: 1 },
], [0, 3, 4]));
});
test('direction split: per-node skip adversary on finite→sentinel boundary and mixed clean/dirty columns', () => {
// At edge 1→2, to(anchor 0) at 1 is finite only via 1→0, while 2 cannot reach 0.
// At edge 1→2, from(anchor 2) at 1 is sentinel and at 2 finite.
const graph = fixture(5, [
{ from: 1, to: 0, seconds: 100 }, { from: 1, to: 2, seconds: 1 },
{ from: 2, to: 3, seconds: 1 }, { from: 3, to: 2, seconds: 1 },
{ from: 3, to: 4, seconds: 1 }, { from: 4, to: 3, seconds: 1 },
], [0, 2, 3]);
assert.equal(graph.landmarkTo[2], 0xffff);
assert.equal(graph.landmarkFrom[2 * 5 + 1], 0xffff);
checkAll(graph);
});