fix(08): WR-04 — quantized weight validated at the i32 boundary before the Int32Array store
- new named constant LANDMARK_I32_WEIGHT_CEILING (0x7fffffff) in landmark.ts: a seconds/quantumS quotient at or past 2^31 would wrap NEGATIVE on the Int32Array store, silently bypassing the LANDMARK_MAX_QUANTA throw in dijkstraPass (a negative contribution never exceeds it) and — at a -1 wrap — becoming the 0xFFFF sentinel; prepare() now fails loud at the table-build entry, naming the edge, the ceiling and the fix (raise landmarkQuantumS) - 'nothing is clamped, ever' now holds at the storage boundary; validation is per-edge at build entry — the search hot path stays allocation-free and validation-free - L5b regression vector: 3e10 s edge (3e11 quanta) throws in both landmarkTables and farthestPointAnchors; just-under-ceiling (2147483646) still fails via the documented 65000 law; ordinary weights untouched
This commit is contained in:
+24
-1
@@ -45,6 +45,16 @@ export const LANDMARK_MAX_QUANTA = 65000;
|
|||||||
* all-ones u16 pattern 0xffff is every table cell's out-of-domain value;
|
* all-ones u16 pattern 0xffff is every table cell's out-of-domain value;
|
||||||
* consumers check against this constant, never a per-callsite literal. */
|
* consumers check against this constant, never a per-callsite literal. */
|
||||||
export const LANDMARK_SENTINEL = 0xffff;
|
export const LANDMARK_SENTINEL = 0xffff;
|
||||||
|
/** The i32 weight-domain ceiling: qWeight is an Int32Array, so a quantized
|
||||||
|
* edge at or past 2^31 would WRAP NEGATIVE on store — silently bypassing
|
||||||
|
* the LANDMARK_MAX_QUANTA throw in dijkstraPass (a negative contribution
|
||||||
|
* never exceeds it), poisoning the Dijkstras, and at finish() a wrapped
|
||||||
|
* −1 label would BECOME the 0xFFFF sentinel. Any raw seconds/quantumS
|
||||||
|
* quotient at or past this ceiling throws at prepare time, BEFORE the
|
||||||
|
* store — nothing is clamped, ever. (Exactly 2^31−1 already cannot
|
||||||
|
* survive dijkstraPass's ceiling either, so the strict `>=` loses
|
||||||
|
* nothing and fails one boundary earlier, at the table-build entry.) */
|
||||||
|
const LANDMARK_I32_WEIGHT_CEILING = 0x7fffffff;
|
||||||
|
|
||||||
/** The landmark compute input — the shape build scripts assemble. */
|
/** The landmark compute input — the shape build scripts assemble. */
|
||||||
export interface LandmarkInput {
|
export interface LandmarkInput {
|
||||||
@@ -111,7 +121,20 @@ function prepare(input: LandmarkInput): Prepared {
|
|||||||
}
|
}
|
||||||
// THE ĉ RULE (Pattern 2): floor keeps the surrogate at or below the
|
// THE ĉ RULE (Pattern 2): floor keeps the surrogate at or below the
|
||||||
// live edge — the exactness hinge for admissibility downstream.
|
// live edge — the exactness hinge for admissibility downstream.
|
||||||
qWeight[e] = Math.floor(edge.seconds / input.quantumS);
|
const q = Math.floor(edge.seconds / input.quantumS);
|
||||||
|
// RANGE LAW at the storage boundary (08-review WR-04): the quotient is
|
||||||
|
// validated BEFORE the Int32Array store — at or past the i32 ceiling
|
||||||
|
// the store wraps NEGATIVE, silently bypassing the dijkstraPass ceiling
|
||||||
|
// (a negative contribution never exceeds LANDMARK_MAX_QUANTA) and
|
||||||
|
// corrupting the tables. This is a per-edge check at the table-build
|
||||||
|
// entry: the search hot path stays allocation-free and validation-free.
|
||||||
|
if (q >= LANDMARK_I32_WEIGHT_CEILING) {
|
||||||
|
fail(
|
||||||
|
context,
|
||||||
|
`edge ${edge.from}->${edge.to} quantizes to ${q} quanta, at or past the i32 weight ceiling ${LANDMARK_I32_WEIGHT_CEILING} (raise landmarkQuantumS)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
qWeight[e] = q;
|
||||||
flags[e] = edge.flags & 1;
|
flags[e] = edge.flags & 1;
|
||||||
target[e] = edge.to;
|
target[e] = edge.to;
|
||||||
source[e] = edge.from;
|
source[e] = edge.from;
|
||||||
|
|||||||
@@ -197,6 +197,58 @@ test("L5 overflow throws — any label > 65000 quanta fails loud, naming the fix
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("L5b i32 wrap throws — an edge quantizing to >= 2^31 fails at prepare, before the Int32Array store", async () => {
|
||||||
|
const { landmarkTables, farthestPointAnchors } = await loadLandmark();
|
||||||
|
// seconds 3e10 at the 0.1 s quantum = 3e11 quanta — an absurd but FINITE
|
||||||
|
// positive weight. Stored unchecked it would wrap NEGATIVE in the
|
||||||
|
// Int32Array (-64771072 for 3e10), silently bypassing the LANDMARK_MAX_QUANTA
|
||||||
|
// throw (a negative contribution never exceeds it) and poisoning the
|
||||||
|
// tables. The wrap must be caught at the table-build entry instead.
|
||||||
|
const input = {
|
||||||
|
nodeCount: 3,
|
||||||
|
edges: [
|
||||||
|
{ from: 0, to: 1, seconds: 3e10, flags: 0 }, // floor(3e10/0.1) = 3e11 quanta >= 2^31
|
||||||
|
{ from: 1, to: 0, seconds: 10, flags: 0 },
|
||||||
|
{ from: 1, to: 2, seconds: 10, flags: 0 },
|
||||||
|
{ from: 2, to: 1, seconds: 10, flags: 0 },
|
||||||
|
],
|
||||||
|
quantumS: 0.1,
|
||||||
|
};
|
||||||
|
assert.throws(
|
||||||
|
() => landmarkTables(input, Uint32Array.of(0)),
|
||||||
|
/i32 weight ceiling 2147483647 \(raise landmarkQuantumS\)/,
|
||||||
|
"landmarkTables throws at the storage boundary, naming the edge, the ceiling and the fix",
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => farthestPointAnchors(input, 1),
|
||||||
|
/i32 weight ceiling 2147483647/,
|
||||||
|
"the anchor-selection entry shares the same prepare-time law",
|
||||||
|
);
|
||||||
|
// The ceiling itself stays inside the legal domain: 2147483646 quanta on
|
||||||
|
// one edge does NOT throw here (the dijkstraPass 65000 ceiling is the law
|
||||||
|
// that fires later — prepare only rejects the i32 WRAP class).
|
||||||
|
const justUnder = {
|
||||||
|
nodeCount: 2,
|
||||||
|
edges: [
|
||||||
|
{ from: 0, to: 1, seconds: 214748364.6, flags: 0 }, // floor(/0.1) = 2147483646 < 2^31
|
||||||
|
{ from: 1, to: 0, seconds: 10, flags: 0 },
|
||||||
|
],
|
||||||
|
quantumS: 0.1,
|
||||||
|
};
|
||||||
|
assert.throws(
|
||||||
|
() => landmarkTables(justUnder, Uint32Array.of(0)),
|
||||||
|
/exceeds 65000/,
|
||||||
|
"just-under-ceiling still fails — via the documented 65000 law, never silently",
|
||||||
|
);
|
||||||
|
assert.doesNotThrow(
|
||||||
|
() => landmarkTables(
|
||||||
|
{ nodeCount: 2, edges: [{ from: 0, to: 1, seconds: 25.5, flags: 0 }, { from: 1, to: 0, seconds: 10, flags: 0 }], quantumS: 0.1 },
|
||||||
|
Uint32Array.of(0),
|
||||||
|
),
|
||||||
|
"ordinary weights are untouched by the new check",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("L6 5x5 grid shape — anchors [0,18]; farthest 297 quanta; coverage 1.0", async () => {
|
test("L6 5x5 grid shape — anchors [0,18]; farthest 297 quanta; coverage 1.0", async () => {
|
||||||
const { farthestPointAnchors, landmarkTables, landmarkCoverage } = await loadLandmark();
|
const { farthestPointAnchors, landmarkTables, landmarkCoverage } = await loadLandmark();
|
||||||
const input = await gridInput();
|
const input = await gridInput();
|
||||||
|
|||||||
Reference in New Issue
Block a user