fix(08-05): D-03 optimality gate green — fromTable families removed from the ALT potential
Task 1: the D-03 gate script (measure-optimality-gate.mjs) reads the frozen pre-ALT record, re-routes the same coordinate-pinned pairs on the current ARG2 binary, and gates on seconds-equality (±0.1%) + verdict-class preservation — never on node paths (equal-cost ties are documented behavior). Rule-1 deviation the gate forced on its first run: routeAstar returned SUBOPTIMAL real-graph routes (x038: 1234.6s vs the 1214.8s Dijkstra optimum; x045: 1475.5s vs 1460.6s) — the fixture sweep could not see it. Root cause: the fromTable bound d̂(ℓ,t) − d̂(ℓ,v) is one-directional Lipschitz (its u-side term needs the REVERSE edge; a oneway does not provide one — the anchor reached node 264611 126.8s faster than its own 1.49s upstream neighbour 264610), and the shared Ikeda average p = (h_f − h_b)/2 is consistent only when every family inside h_f/h_b is two-sided. The toTable bound d̂(v,ℓ) − d̂(t,ℓ) rides stored traversable edges in BOTH directions; it alone joins the haversine now. Stop rule, reductions, meet probes, reconstruction: untouched. landmarkTables/build verified EXACT (0 mismatches vs independent integer Dijkstras, both rows, all 24 anchors). - measure-optimality-gate.mjs: exported core + CLI guard (route-corpus pattern); epsilon mirror + argVersion asserts precede any measurement; per-pair rows, extract-drift WARNING (never a re-record), honest exit law - astar.ts: potential() consults landmarkTo only; doc-comments carry the one-directional-Lipschitz law and the x038/x045 evidence - astar.test.mjs: A11 re-pinned (landmarkFrom must NOT appear in the comment-stripped search — re-adding it trips the pin); A12 oneway-ring sweep added (directed reach asymmetry stays Dijkstra-equal on every pair) Battery: routing 80/80, bot 126/126, both typechecks green. D-03 gate: GATE PASS — 53/53 routed pairs equal within ±0.1% (max |Δ| 0.005%), verdict classes preserved (6 unreachable + 41 unsnappable, 0 re-snapped).
This commit is contained in:
@@ -368,6 +368,81 @@ test("A8 structural — no `new Map(` in astar.ts (typed arrays only in the hot
|
||||
assert.ok(!code.includes("new Map("), "no Map allocation — the hot loop is typed arrays");
|
||||
});
|
||||
|
||||
/* ————— A12: oneway ring under ALT (plan 08-05) ————— */
|
||||
|
||||
/**
|
||||
* A directed 8-node ring (10 s one-way hops; every reverse direction stored
|
||||
* with bit0 — never relaxed) + a bidirectional chord (0<->4, 15 s) + a
|
||||
* one-way-in spur (8->3: nothing reaches 8, so its fromTable cells are the
|
||||
* 0xFFFF sentinel while its toTable cells are finite). This is the fixture
|
||||
* shape the 08-05 consistency fix is ABOUT: directed reach asymmetry (the
|
||||
* anchor reaches a node far faster than its upstream neighbour) is exactly
|
||||
* what made the four-family potential inconsistent on the real graph — the
|
||||
* D-03 gate caught it as x038/x045 suboptimality. Under the amended
|
||||
* toTable-only potential EVERY pair must stay Dijkstra-equal.
|
||||
*/
|
||||
test("A12 oneway ring under ALT — directed reach asymmetry stays Dijkstra-equal on every pair", async () => {
|
||||
const { encodeGraph, decodeGraph } = await loadGraph();
|
||||
const { routeAstar } = await loadAstar();
|
||||
const { LANDMARK_QUANTUM_S, landmarkTables } = await import("../src/landmark.ts");
|
||||
const lat = [];
|
||||
const lng = [];
|
||||
// Ring + chord + spur laid out around (3.10, 101.70), integer / 1e6.
|
||||
for (let i = 0; i < 8; i++) {
|
||||
lat.push((3100000 + Math.round(400 * Math.sin((i / 8) * 2 * Math.PI))) / 1e6);
|
||||
lng.push((101700000 + Math.round(400 * Math.cos((i / 8) * 2 * Math.PI))) / 1e6);
|
||||
}
|
||||
lat.push(3095000 / 1e6); // node 8: the spur head, off-ring
|
||||
lng.push(101705000 / 1e6);
|
||||
const edges = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const next = (i + 1) % 8;
|
||||
edges.push({ from: i, to: next, seconds: 10, flags: 0, nameId: 0 }); // the one-way hop
|
||||
edges.push({ from: next, to: i, seconds: 10, flags: 1, nameId: 0 }); // bit0: stored, never relaxed
|
||||
}
|
||||
edges.push({ from: 0, to: 4, seconds: 15, flags: 0, nameId: 0 }); // bidirectional chord
|
||||
edges.push({ from: 4, to: 0, seconds: 15, flags: 0, nameId: 0 });
|
||||
edges.push({ from: 8, to: 3, seconds: 10, flags: 0, nameId: 0 }); // one-way-in spur
|
||||
const anchors = new Uint32Array([0, 4]); // hand-pinned, in the ring's SCC
|
||||
const { to, from } = landmarkTables(
|
||||
{ nodeCount: 9, edges, quantumS: LANDMARK_QUANTUM_S },
|
||||
anchors,
|
||||
);
|
||||
const g = decodeGraph(
|
||||
encodeGraph({
|
||||
lat,
|
||||
lng,
|
||||
edges,
|
||||
names: ["Ring Road"],
|
||||
maxSpeedKmh: 60,
|
||||
landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from },
|
||||
}),
|
||||
);
|
||||
assert.ok(g, "the oneway-ring synthetic decodes");
|
||||
assert.equal(g.nodeCount, 9, "9 nodes: ring + spur head");
|
||||
let pairs = 0;
|
||||
let reachable = 0;
|
||||
for (let o = 0; o < g.nodeCount; o++) {
|
||||
for (let t = 0; t < g.nodeCount; t++) {
|
||||
pairs++;
|
||||
const optimal = dijkstra(g, o, t);
|
||||
const result = routeAstar(g, o, t);
|
||||
if (optimal === Infinity) {
|
||||
assert.equal(result, null, `pair ${o}->${t}: unreachable must resolve null`);
|
||||
continue;
|
||||
}
|
||||
reachable++;
|
||||
assert.ok(result, `pair ${o}->${t}: reachable must route`);
|
||||
assert.ok(
|
||||
Math.abs(result.seconds - optimal) < 1e-6,
|
||||
`pair ${o}->${t}: A* cost ${result.seconds} vs Dijkstra ${optimal}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.equal(pairs, 81, "the full 9x9 sweep ran");
|
||||
assert.ok(reachable > 60, `the ring is densely reachable (${reachable} pairs)`);
|
||||
});
|
||||
|
||||
test("A9 ALT island — anchors pinned in component A only; EVERY pair still Dijkstra-equal", async () => {
|
||||
const { routeAstar } = await loadAstar();
|
||||
const g = await buildIslandFixture(Uint32Array.of(0, 8));
|
||||
@@ -442,9 +517,16 @@ test("A11 structural (ALT) — astar.ts consults the landmark tables in CODE, no
|
||||
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1");
|
||||
// The potential provably consults Plan 08-02's ARG2 table fields — a
|
||||
// comment-only mention would strip away and fail this pin.
|
||||
for (const token of ["landmarkTo", "landmarkFrom", "landmarkQuantumS"]) {
|
||||
for (const token of ["landmarkTo", "landmarkQuantumS"]) {
|
||||
assert.ok(code.includes(token), `comment-stripped astar.ts must reference ${token} in code`);
|
||||
}
|
||||
// 08-05 amendment pin: the fromTable families are EXCLUDED from the
|
||||
// potential — their bounds are one-directional Lipschitz (the u-side term
|
||||
// needs the reverse edge; a oneway does not have one), which broke the
|
||||
// shared Ikeda average's consistency and returned suboptimal real-graph
|
||||
// routes (the D-03 gate's x038/x045 catch). Re-adding landmarkFrom to the
|
||||
// search trips this pin and re-opens that bug.
|
||||
assert.ok(!code.includes("landmarkFrom"), "comment-stripped astar.ts must NOT consult landmarkFrom (08-05 consistency law)");
|
||||
// Raw u16 sentinel handling on the table families — one spelling of the
|
||||
// sentinel (0xffff in code, never the decimal 65535).
|
||||
assert.ok(code.includes("0xffff"), "the sentinel is compared raw (u16, before float conversion)");
|
||||
|
||||
Reference in New Issue
Block a user