feat(08-02): ARG2 codec — one version, one decode path (G-suite GREEN)
graph.ts versioned IN PLACE (no second codec): GRAPH_MAGIC 0x41524732, GRAPH_VERSION 2, HEADER_SIZE 72 (72%8==0 keeps f64 views aligned); header gains landmarkCount u32 @60 (the old pad), landmarkQuantumS f32 @64, reserved u32 @68; the landmark suffix (u16 toTable, u16 fromTable, u32 landmarkNodes) extends layoutFor's total arithmetic — exact-size law covers the variable suffix BEFORE any view allocation (T-08-01). D-01 fail-honest: ARG1 bytes / version!=2 / landmarkCount=0 / hostile quantum / size mismatch all decode null (committed mini-graph-arg1.bin is the standing rejection vector). GraphInput.landmarks REQUIRED and loud-validated (T-08-04). RoadGraph gains the five landmark fields (Plan 08-03's ALT potentials consume them). Fixture regenerated as ARG2 through the SAME encode authority: the builder gains the tiny real landmark stage (L=2, anchors [0,18], quantum 0.1s, coverage 1.0 — identical bytes every re-run). Rule-3 call-site updates forced by the required-landmarks law (missed by the plan's files list): astar.test A3 + snap.test S3 synthetics carry inert hand landmark blocks. Fixture regen pulled forward from task 3 so this commit's own battery is green (74 passed, 0 failed).
This commit is contained in:
+146
-22
@@ -6,46 +6,67 @@
|
|||||||
* (the committed mini-graph fixture and plan 04's Overpass build script
|
* (the committed mini-graph fixture and plan 04's Overpass build script
|
||||||
* both write through it, so the layout has exactly one authority).
|
* both write through it, so the layout has exactly one authority).
|
||||||
*
|
*
|
||||||
* PINNED CSR BINARY LAYOUT (little-endian; 06-01 — plans 04/07 depend on it):
|
* PINNED CSR BINARY LAYOUT — ARG2 (little-endian; 06-01 pinned ARG1,
|
||||||
* header: magic u32 (0x41524731 "ARG1") | version u32 (=1)
|
* plan 08-02 versioned it per phase-8 D-01; plans 04/07 depend on it):
|
||||||
|
* header: magic u32 (0x41524732 "ARG2") | version u32 (=2)
|
||||||
* | nodeCount u32 | edgeCount u32 | nameCount u32 | namePoolLen u32
|
* | nodeCount u32 | edgeCount u32 | nameCount u32 | namePoolLen u32
|
||||||
* | bbox 4×f64 (minLat, minLng, maxLat, maxLng) | maxSpeedKmh f32
|
* | bbox 4×f64 (minLat, minLng, maxLat, maxLng) | maxSpeedKmh f32 @56
|
||||||
* | 4 pad bytes — the header is padded 60 -> 64 bytes so the f64
|
* | landmarkCount u32 @60 (>= 1 REQUIRED — see the D-01 law)
|
||||||
* node arrays start 8-byte aligned: zero-copy TypedArray views
|
* | landmarkQuantumS f32 @64 (the ĉ unit; build default 0.1 s)
|
||||||
* REQUIRE aligned offsets (unaligned Float64Array throws).
|
* | reserved u32 @68 (written 0; decode accepts any value)
|
||||||
|
* — 72 bytes total: 72 % 8 == 0 keeps the f64 node views
|
||||||
|
* 8-byte aligned; zero-copy TypedArray views REQUIRE aligned
|
||||||
|
* offsets (unaligned Float64Array throws).
|
||||||
* nodes: f64 lat[n] ++ f64 lng[n]
|
* nodes: f64 lat[n] ++ f64 lng[n]
|
||||||
* offsets: u32 edgeStart[n+1] (CSR row pointers)
|
* offsets: u32 edgeStart[n+1] (CSR row pointers)
|
||||||
* edges: u32 edgeTarget[e] ++ u8 edgeFlags[e] (bit0: oneway-reverse)
|
* edges: u32 edgeTarget[e] ++ u8 edgeFlags[e] (bit0: oneway-reverse)
|
||||||
* weights: f32 edgeSeconds[e] (aligned to 4 bytes)
|
* weights: f32 edgeSeconds[e] (aligned to 4 bytes)
|
||||||
* names: u32 edgeNameId[e] | u32 nameOffset[m+1] | u8 namePool (UTF-8)
|
* names: u32 edgeNameId[e] | u32 nameOffset[m+1] | u8 namePool (UTF-8)
|
||||||
|
* landmarks (the APPEND-ONLY ARG2 suffix; row-major [l*N + v]):
|
||||||
|
* u16 toTable[L*N] — quantized ĉ-distance v -> anchor l
|
||||||
|
* u16 fromTable[L*N] — quantized ĉ-distance anchor l -> v
|
||||||
|
* u32 landmarkNodes[L] — the anchors (audit + --check prints)
|
||||||
|
* 0xFFFF in a table cell is DATA (the unreachable sentinel,
|
||||||
|
* landmark.ts) — decode never validates it away.
|
||||||
|
*
|
||||||
|
* THE D-01 FAIL-HONEST LAW (one decode path, one search path): a buffer
|
||||||
|
* that is not EXACTLY an ARG2 graph decodes to null — ARG1 bytes (version
|
||||||
|
* 1), future versions, landmarkCount = 0 (no landmark-less ARG2 layout
|
||||||
|
* exists to silently degrade into), and every size mismatch. There is no
|
||||||
|
* v1 fallback branch: the Phase 6 router-pending state covers old binaries
|
||||||
|
* until one re-upload. (Router-pending coverage is exercised by the bot
|
||||||
|
* suite; the committed mini-graph-arg1.bin is the standing ARG1 rejection
|
||||||
|
* vector — plan 08-01.)
|
||||||
*
|
*
|
||||||
* edgeFlags bit0 ("oneway-reverse") marks the blocked mirror direction of a
|
* edgeFlags bit0 ("oneway-reverse") marks the blocked mirror direction of a
|
||||||
* oneway segment: the edge stays in the CSR so snapping sees the segment
|
* oneway segment: the edge stays in the CSR so snapping sees the segment
|
||||||
* geometry and its street name, but A* (astar.ts) never traverses it.
|
* geometry and its street name, but A* (astar.ts) never traverses it.
|
||||||
*
|
*
|
||||||
* T-06-03: a hostile/corrupt binary fails HONEST — decodeGraph validates
|
* T-06-03: a hostile/corrupt binary fails HONEST — decodeGraph validates
|
||||||
* magic, version, and the EXACT byte size implied by the header counts, and
|
* magic, version, landmarkCount, the quantum, and the EXACT byte size
|
||||||
* returns null on any mismatch. It never throws and never returns a
|
* implied by the header counts (the size arithmetic covers the variable
|
||||||
* half-built graph; plan 04's Worker turns null into the designed
|
* u16 suffix), and returns null on any mismatch. It never throws and never
|
||||||
|
* returns a half-built graph; plan 04's Worker turns null into the designed
|
||||||
* router-pending state.
|
* router-pending state.
|
||||||
* T-06-03b: the size check runs BEFORE any view allocation — a huge-count
|
* T-06-03b / T-08-01: the size check runs BEFORE any view allocation —
|
||||||
* header on a small buffer rejects on arithmetic, not on allocation.
|
* huge-count headers (including a monster landmarkCount) reject on
|
||||||
|
* arithmetic, not on allocation.
|
||||||
* T-06-05: pure module — no logging, no storage, no network; coordinates
|
* T-06-05: pure module — no logging, no storage, no network; coordinates
|
||||||
* exist only in the caller's frame.
|
* exist only in the caller's frame.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Header magic "ARG1" (ArahaRah Graph v1). */
|
/** Header magic "ARG2" (ArahaRah Graph v2 — the landmark-carrying layout). */
|
||||||
export const GRAPH_MAGIC = 0x41524731;
|
export const GRAPH_MAGIC = 0x41524732;
|
||||||
/** Layout version this module reads and writes. */
|
/** Layout version this module reads and writes. */
|
||||||
export const GRAPH_VERSION = 1;
|
export const GRAPH_VERSION = 2;
|
||||||
/** edgeNameId sentinel: the edge carries no street name. */
|
/** edgeNameId sentinel: the edge carries no street name. */
|
||||||
export const UNNAMED_NAME_ID = 0xffffffff;
|
export const UNNAMED_NAME_ID = 0xffffffff;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 60 field bytes + 4 alignment pad bytes. The pad keeps every f64 view
|
* 68 field bytes + 4 reserved bytes. 72 % 8 == 0 keeps every f64 view
|
||||||
* 8-byte aligned — the zero-copy law's mechanical prerequisite.
|
* 8-byte aligned — the zero-copy law's mechanical prerequisite.
|
||||||
*/
|
*/
|
||||||
const HEADER_SIZE = 64;
|
const HEADER_SIZE = 72;
|
||||||
|
|
||||||
/** The decoded graph: typed-array views over the caller's ArrayBuffer. */
|
/** The decoded graph: typed-array views over the caller's ArrayBuffer. */
|
||||||
export interface RoadGraph {
|
export interface RoadGraph {
|
||||||
@@ -67,6 +88,16 @@ export interface RoadGraph {
|
|||||||
nameAt(id: number): string | undefined;
|
nameAt(id: number): string | undefined;
|
||||||
bbox: { minLat: number; minLng: number; maxLat: number; maxLng: number };
|
bbox: { minLat: number; minLng: number; maxLat: number; maxLng: number };
|
||||||
maxSpeedKmh: number;
|
maxSpeedKmh: number;
|
||||||
|
/** Number of landmark anchors (ARG2: always >= 1 — decode refuses 0). */
|
||||||
|
landmarkCount: number;
|
||||||
|
/** The ĉ quantum in seconds the tables were computed at (f32 readback). */
|
||||||
|
landmarkQuantumS: number;
|
||||||
|
/** Row-major [l*N + v] quantized distance v -> anchor l; 0xFFFF = unreachable (DATA). */
|
||||||
|
landmarkTo: Uint16Array;
|
||||||
|
/** Row-major [l*N + v] quantized distance anchor l -> v; 0xFFFF = unreachable (DATA). */
|
||||||
|
landmarkFrom: Uint16Array;
|
||||||
|
/** The L anchor node ids (row l of both tables). */
|
||||||
|
landmarkNodes: Uint32Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One directed edge at encode time. */
|
/** One directed edge at encode time. */
|
||||||
@@ -81,6 +112,18 @@ export interface GraphEdgeInput {
|
|||||||
nameId?: number;
|
nameId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The landmark-table block of the encodeGraph input (ARG2). */
|
||||||
|
export interface LandmarkTablesInput {
|
||||||
|
/** The ĉ quantum in seconds the tables were computed at (e.g. landmark.ts's LANDMARK_QUANTUM_S). */
|
||||||
|
quantumS: number;
|
||||||
|
/** The L anchor node ids; L >= 1 — an ARG2 always carries landmarks. */
|
||||||
|
nodes: ArrayLike<number>;
|
||||||
|
/** Row-major [l*N + v] to-anchors table, length exactly L*N. */
|
||||||
|
to: Uint16Array;
|
||||||
|
/** Row-major [l*N + v] from-anchors table, length exactly L*N. */
|
||||||
|
from: Uint16Array;
|
||||||
|
}
|
||||||
|
|
||||||
/** The encodeGraph input — the shape build scripts assemble. */
|
/** The encodeGraph input — the shape build scripts assemble. */
|
||||||
export interface GraphInput {
|
export interface GraphInput {
|
||||||
lat: ArrayLike<number>;
|
lat: ArrayLike<number>;
|
||||||
@@ -89,6 +132,8 @@ export interface GraphInput {
|
|||||||
edges: readonly GraphEdgeInput[];
|
edges: readonly GraphEdgeInput[];
|
||||||
names: readonly string[];
|
names: readonly string[];
|
||||||
maxSpeedKmh: number;
|
maxSpeedKmh: number;
|
||||||
|
/** REQUIRED since ARG2 (D-01): the precomputed landmark tables. */
|
||||||
|
landmarks: LandmarkTablesInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Section offsets implied by the header counts (all little-endian). */
|
/* Section offsets implied by the header counts (all little-endian). */
|
||||||
@@ -102,10 +147,19 @@ interface Layout {
|
|||||||
edgeNameId: number;
|
edgeNameId: number;
|
||||||
nameOffset: number;
|
nameOffset: number;
|
||||||
namePool: number;
|
namePool: number;
|
||||||
|
toTable: number;
|
||||||
|
fromTable: number;
|
||||||
|
landmarkNodes: number;
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function layoutFor(nodeCount: number, edgeCount: number, nameCount: number, namePoolLen: number): Layout {
|
function layoutFor(
|
||||||
|
nodeCount: number,
|
||||||
|
edgeCount: number,
|
||||||
|
nameCount: number,
|
||||||
|
namePoolLen: number,
|
||||||
|
landmarkCount: number,
|
||||||
|
): Layout {
|
||||||
const lat = HEADER_SIZE;
|
const lat = HEADER_SIZE;
|
||||||
const lng = lat + 8 * nodeCount;
|
const lng = lat + 8 * nodeCount;
|
||||||
const edgeStart = lng + 8 * nodeCount;
|
const edgeStart = lng + 8 * nodeCount;
|
||||||
@@ -115,7 +169,27 @@ function layoutFor(nodeCount: number, edgeCount: number, nameCount: number, name
|
|||||||
const edgeNameId = edgeSeconds + 4 * edgeCount;
|
const edgeNameId = edgeSeconds + 4 * edgeCount;
|
||||||
const nameOffset = edgeNameId + 4 * edgeCount;
|
const nameOffset = edgeNameId + 4 * edgeCount;
|
||||||
const namePool = nameOffset + 4 * (nameCount + 1);
|
const namePool = nameOffset + 4 * (nameCount + 1);
|
||||||
return { lat, lng, edgeStart, edgeTarget, edgeFlags, edgeSeconds, edgeNameId, nameOffset, namePool, total: namePool + namePoolLen };
|
// The ARG2 landmark suffix — an append-only extension of the pinned ARG1
|
||||||
|
// section order. Encode and decode share THIS arithmetic (T-08-01: the
|
||||||
|
// exact-size law extends over the variable-size suffix).
|
||||||
|
const toTable = (namePool + namePoolLen + 3) & ~3; // align u16 pairs to 4 bytes
|
||||||
|
const fromTable = toTable + 2 * landmarkCount * nodeCount;
|
||||||
|
const landmarkNodes = fromTable + 2 * landmarkCount * nodeCount;
|
||||||
|
return {
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
edgeStart,
|
||||||
|
edgeTarget,
|
||||||
|
edgeFlags,
|
||||||
|
edgeSeconds,
|
||||||
|
edgeNameId,
|
||||||
|
nameOffset,
|
||||||
|
namePool,
|
||||||
|
toTable,
|
||||||
|
fromTable,
|
||||||
|
landmarkNodes,
|
||||||
|
total: landmarkNodes + 4 * landmarkCount,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,9 +212,19 @@ export function decodeGraph(buffer: ArrayBuffer): RoadGraph | null {
|
|||||||
maxLng: view.getFloat64(48, true),
|
maxLng: view.getFloat64(48, true),
|
||||||
};
|
};
|
||||||
const maxSpeedKmh = view.getFloat32(56, true);
|
const maxSpeedKmh = view.getFloat32(56, true);
|
||||||
// Exact byte-size check BEFORE any view allocation (T-06-03b): a hostile
|
// ARG2 header fields (plan 08-02): landmarkCount @60 (the ARG1 pad),
|
||||||
// huge-count header rejects here on arithmetic alone.
|
// quantum @64, reserved @68 (accepted untouched — the field is unused).
|
||||||
const lay = layoutFor(nodeCount, edgeCount, nameCount, namePoolLen);
|
const landmarkCount = view.getUint32(60, true);
|
||||||
|
// D-01/A5: an ARG2 always carries landmarks — a landmark-less layout
|
||||||
|
// would silently route with weak steering, and "no v1 fallback branch"
|
||||||
|
// holds at the content level too. Decode refuses, honestly.
|
||||||
|
if (landmarkCount === 0) return null;
|
||||||
|
const landmarkQuantumS = view.getFloat32(64, true);
|
||||||
|
if (!Number.isFinite(landmarkQuantumS) || landmarkQuantumS <= 0) return null; // hostile quantum -> null, never a divide-by-0 downstream
|
||||||
|
// Exact byte-size check BEFORE any view allocation (T-06-03b, extended
|
||||||
|
// over the u16 suffix per T-08-01): a hostile huge-count header —
|
||||||
|
// including a monster landmarkCount — rejects here on arithmetic alone.
|
||||||
|
const lay = layoutFor(nodeCount, edgeCount, nameCount, namePoolLen, landmarkCount);
|
||||||
if (buffer.byteLength !== lay.total) return null;
|
if (buffer.byteLength !== lay.total) return null;
|
||||||
|
|
||||||
const lat = new Float64Array(buffer, lay.lat, nodeCount);
|
const lat = new Float64Array(buffer, lay.lat, nodeCount);
|
||||||
@@ -152,6 +236,9 @@ export function decodeGraph(buffer: ArrayBuffer): RoadGraph | null {
|
|||||||
const edgeNameId = new Uint32Array(buffer, lay.edgeNameId, edgeCount);
|
const edgeNameId = new Uint32Array(buffer, lay.edgeNameId, edgeCount);
|
||||||
const nameOffset = new Uint32Array(buffer, lay.nameOffset, nameCount + 1);
|
const nameOffset = new Uint32Array(buffer, lay.nameOffset, nameCount + 1);
|
||||||
const namePool = new Uint8Array(buffer, lay.namePool, namePoolLen);
|
const namePool = new Uint8Array(buffer, lay.namePool, namePoolLen);
|
||||||
|
const landmarkTo = new Uint16Array(buffer, lay.toTable, landmarkCount * nodeCount);
|
||||||
|
const landmarkFrom = new Uint16Array(buffer, lay.fromTable, landmarkCount * nodeCount);
|
||||||
|
const landmarkNodes = new Uint32Array(buffer, lay.landmarkNodes, landmarkCount);
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const graph: RoadGraph = {
|
const graph: RoadGraph = {
|
||||||
nodeCount,
|
nodeCount,
|
||||||
@@ -173,6 +260,11 @@ export function decodeGraph(buffer: ArrayBuffer): RoadGraph | null {
|
|||||||
},
|
},
|
||||||
bbox,
|
bbox,
|
||||||
maxSpeedKmh,
|
maxSpeedKmh,
|
||||||
|
landmarkCount,
|
||||||
|
landmarkQuantumS,
|
||||||
|
landmarkTo,
|
||||||
|
landmarkFrom,
|
||||||
|
landmarkNodes,
|
||||||
};
|
};
|
||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
@@ -194,6 +286,29 @@ export function encodeGraph(input: GraphInput): ArrayBuffer {
|
|||||||
if (!Number.isFinite(input.maxSpeedKmh) || input.maxSpeedKmh <= 0) {
|
if (!Number.isFinite(input.maxSpeedKmh) || input.maxSpeedKmh <= 0) {
|
||||||
fail("maxSpeedKmh must be a positive finite number");
|
fail("maxSpeedKmh must be a positive finite number");
|
||||||
}
|
}
|
||||||
|
// ARG2 landmark block (D-01): REQUIRED, loud-validated BEFORE any
|
||||||
|
// allocation work (T-08-04) — no silent clamp, no landmark-less encode.
|
||||||
|
const landmarks = input.landmarks;
|
||||||
|
if (!landmarks) fail("landmarks block is required (ARG2 has no landmark-less layout)");
|
||||||
|
const landmarkCount = landmarks.nodes.length;
|
||||||
|
if (!Number.isInteger(landmarkCount) || landmarkCount < 1) {
|
||||||
|
fail("landmarks.nodes must carry at least one anchor (ARG2 has no landmark-less layout)");
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(landmarks.quantumS) || landmarks.quantumS <= 0) {
|
||||||
|
fail("landmarks.quantumS must be a positive finite number");
|
||||||
|
}
|
||||||
|
for (let i = 0; i < landmarkCount; i++) {
|
||||||
|
const a = landmarks.nodes[i]!;
|
||||||
|
if (!Number.isInteger(a) || a < 0 || a >= nodeCount) {
|
||||||
|
fail(`landmarks.nodes[${i}] (${a}) is not a node id in [0, ${nodeCount})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (landmarks.to.length !== landmarkCount * nodeCount) {
|
||||||
|
fail(`landmarks.to must have length L*N = ${landmarkCount}*${nodeCount}, got ${landmarks.to.length}`);
|
||||||
|
}
|
||||||
|
if (landmarks.from.length !== landmarkCount * nodeCount) {
|
||||||
|
fail(`landmarks.from must have length L*N = ${landmarkCount}*${nodeCount}, got ${landmarks.from.length}`);
|
||||||
|
}
|
||||||
const nameCount = input.names.length;
|
const nameCount = input.names.length;
|
||||||
for (const name of input.names) {
|
for (const name of input.names) {
|
||||||
if (typeof name !== "string") fail("names must all be strings");
|
if (typeof name !== "string") fail("names must all be strings");
|
||||||
@@ -232,7 +347,7 @@ export function encodeGraph(input: GraphInput): ArrayBuffer {
|
|||||||
for (const edge of input.edges) counts[edge.from + 1] = (counts[edge.from + 1] ?? 0) + 1;
|
for (const edge of input.edges) counts[edge.from + 1] = (counts[edge.from + 1] ?? 0) + 1;
|
||||||
for (let i = 0; i < nodeCount; i++) counts[i + 1] = counts[i + 1]! + counts[i]!;
|
for (let i = 0; i < nodeCount; i++) counts[i + 1] = counts[i + 1]! + counts[i]!;
|
||||||
const edgeCount = input.edges.length;
|
const edgeCount = input.edges.length;
|
||||||
const lay = layoutFor(nodeCount, edgeCount, nameCount, namePoolLen);
|
const lay = layoutFor(nodeCount, edgeCount, nameCount, namePoolLen, landmarkCount);
|
||||||
const buffer = new ArrayBuffer(lay.total);
|
const buffer = new ArrayBuffer(lay.total);
|
||||||
const view = new DataView(buffer);
|
const view = new DataView(buffer);
|
||||||
const edgeTarget = new Uint32Array(buffer, lay.edgeTarget, edgeCount);
|
const edgeTarget = new Uint32Array(buffer, lay.edgeTarget, edgeCount);
|
||||||
@@ -273,6 +388,9 @@ export function encodeGraph(input: GraphInput): ArrayBuffer {
|
|||||||
view.setFloat64(40, maxLat, true);
|
view.setFloat64(40, maxLat, true);
|
||||||
view.setFloat64(48, maxLng, true);
|
view.setFloat64(48, maxLng, true);
|
||||||
view.setFloat32(56, input.maxSpeedKmh, true);
|
view.setFloat32(56, input.maxSpeedKmh, true);
|
||||||
|
view.setUint32(60, landmarkCount, true);
|
||||||
|
view.setFloat32(64, landmarks.quantumS, true);
|
||||||
|
view.setUint32(68, 0, true); // reserved (decode accepts any value)
|
||||||
|
|
||||||
// CSR row pointers + node coordinates.
|
// CSR row pointers + node coordinates.
|
||||||
for (let i = 0; i <= nodeCount; i++) view.setUint32(lay.edgeStart + 4 * i, counts[i]!, true);
|
for (let i = 0; i <= nodeCount; i++) view.setUint32(lay.edgeStart + 4 * i, counts[i]!, true);
|
||||||
@@ -295,5 +413,11 @@ export function encodeGraph(input: GraphInput): ArrayBuffer {
|
|||||||
pool.set(bytes, at);
|
pool.set(bytes, at);
|
||||||
at += bytes.length;
|
at += bytes.length;
|
||||||
}
|
}
|
||||||
|
// ARG2 landmark suffix — bulk typed-array writes through the same
|
||||||
|
// view discipline as the u32/f32/f64 sections, so decode's zero-copy
|
||||||
|
// views read the bytes back exactly.
|
||||||
|
new Uint16Array(buffer, lay.toTable, landmarkCount * nodeCount).set(landmarks.to);
|
||||||
|
new Uint16Array(buffer, lay.fromTable, landmarkCount * nodeCount).set(landmarks.from);
|
||||||
|
new Uint32Array(buffer, lay.landmarkNodes, landmarkCount).set(landmarks.nodes);
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,15 @@ test("A3 unreachable -> null — a disconnected pair resolves the honest miss",
|
|||||||
],
|
],
|
||||||
names: ["Isolated Road"],
|
names: ["Isolated Road"],
|
||||||
maxSpeedKmh: 60,
|
maxSpeedKmh: 60,
|
||||||
|
// ARG2 (plan 08-02): every encode carries a landmark block — inert
|
||||||
|
// steering data until ALT lands (08-03); hand values keep this case
|
||||||
|
// focused on the unreachable-pair law. Sentinels mark component 2.
|
||||||
|
landmarks: {
|
||||||
|
quantumS: 0.5,
|
||||||
|
nodes: [0],
|
||||||
|
to: Uint16Array.of(0, 30, 0xffff, 0xffff),
|
||||||
|
from: Uint16Array.of(0, 30, 0xffff, 0xffff),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
assert.ok(g, "synthetic graph decodes");
|
assert.ok(g, "synthetic graph decodes");
|
||||||
|
|||||||
+43
-2
@@ -16,6 +16,12 @@
|
|||||||
* Usage: node routing/test/fixtures/build-mini-graph.mjs
|
* Usage: node routing/test/fixtures/build-mini-graph.mjs
|
||||||
* Exit: 0 on write; throws on any invariant violation (never writes).
|
* Exit: 0 on write; throws on any invariant violation (never writes).
|
||||||
*
|
*
|
||||||
|
* ARG2 (plan 08-02): the fixture carries a tiny real landmark stage —
|
||||||
|
* L=2 anchors via farthestPointAnchors + quantized c-metric tables via
|
||||||
|
* landmarkTables (the SAME pure module the real build script uses), passed
|
||||||
|
* through the ONE encodeGraph authority. Deterministic: identical output
|
||||||
|
* bytes on every re-run.
|
||||||
|
*
|
||||||
* Design (pinned by 06-01): a hand-built 5x5 grid in a ~0.002 deg x 0.002 deg
|
* Design (pinned by 06-01): a hand-built 5x5 grid in a ~0.002 deg x 0.002 deg
|
||||||
* bbox near KL center (lat 3.1500–3.1520, lng 101.6900–101.6920):
|
* bbox near KL center (lat 3.1500–3.1520, lng 101.6900–101.6920):
|
||||||
* - 25 nodes, 80 directed edge entries (20 H + 20 V segments, both ways)
|
* - 25 nodes, 80 directed edge entries (20 H + 20 V segments, both ways)
|
||||||
@@ -28,6 +34,7 @@
|
|||||||
import { writeFileSync } from "node:fs";
|
import { writeFileSync } from "node:fs";
|
||||||
import { encodeGraph } from "../../src/graph.ts";
|
import { encodeGraph } from "../../src/graph.ts";
|
||||||
import { haversineM } from "../../src/geo.ts";
|
import { haversineM } from "../../src/geo.ts";
|
||||||
|
import { LANDMARK_QUANTUM_S, farthestPointAnchors, landmarkTables, landmarkCoverage } from "../../src/landmark.ts";
|
||||||
|
|
||||||
const GRID = 5;
|
const GRID = 5;
|
||||||
const LAT0 = 3.15;
|
const LAT0 = 3.15;
|
||||||
@@ -85,6 +92,40 @@ reverse.flags = 1;
|
|||||||
|
|
||||||
if (edges.length !== 80) throw new Error(`expected 80 directed edges, built ${edges.length}`);
|
if (edges.length !== 80) throw new Error(`expected 80 directed edges, built ${edges.length}`);
|
||||||
|
|
||||||
const bin = encodeGraph({ lat, lng, edges, names, maxSpeedKmh: SPEED_PERIMETER_KMH });
|
// LANDMARK STAGE (plan 08-02): the fixture regenerates as ARG2 THROUGH the
|
||||||
|
// one encode authority — the same farthestPointAnchors + landmarkTables the
|
||||||
|
// real build script (08-04) will run, at a tiny L. Fail-loud invariants
|
||||||
|
// before any write (the `edges.length !== 80` style).
|
||||||
|
const LANDMARK_COUNT = 2; // tiny L: the 5x5 grid needs exactly 2 well-spread anchors
|
||||||
|
const landmarkInput = { nodeCount: GRID * GRID, edges, quantumS: LANDMARK_QUANTUM_S };
|
||||||
|
const anchors = farthestPointAnchors(landmarkInput, LANDMARK_COUNT);
|
||||||
|
const { to, from } = landmarkTables(landmarkInput, anchors);
|
||||||
|
if (anchors.length !== LANDMARK_COUNT) throw new Error(`expected ${LANDMARK_COUNT} anchors, got ${anchors.length}`);
|
||||||
|
if (to.length !== LANDMARK_COUNT * GRID * GRID || from.length !== LANDMARK_COUNT * GRID * GRID) {
|
||||||
|
throw new Error("landmark tables are not L*N row-major");
|
||||||
|
}
|
||||||
|
for (let l = 0; l < anchors.length; l++) {
|
||||||
|
const a = anchors[l];
|
||||||
|
if (to[l * GRID * GRID + a] !== 0 || from[l * GRID * GRID + a] !== 0) {
|
||||||
|
throw new Error(`anchor ${l} (node ${a}) own cells are not 0 — table law broken`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const coverage = landmarkCoverage(to, anchors, GRID * GRID);
|
||||||
|
if (coverage.some((c) => c !== 1)) {
|
||||||
|
throw new Error(`the grid is strongly connected — coverage must be 1.0, got ${coverage.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bin = encodeGraph({
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
edges,
|
||||||
|
names,
|
||||||
|
maxSpeedKmh: SPEED_PERIMETER_KMH,
|
||||||
|
landmarks: { quantumS: LANDMARK_QUANTUM_S, nodes: anchors, to, from },
|
||||||
|
});
|
||||||
writeFileSync(new URL("./mini-graph.bin", import.meta.url), Buffer.from(bin));
|
writeFileSync(new URL("./mini-graph.bin", import.meta.url), Buffer.from(bin));
|
||||||
console.log(`mini-graph.bin: ${GRID * GRID} nodes, ${edges.length} edges, 3 names, ${bin.byteLength} B`);
|
console.log(
|
||||||
|
`mini-graph.bin: ${GRID * GRID} nodes, ${edges.length} edges, 3 names, ` +
|
||||||
|
`L=${LANDMARK_COUNT} landmarks (nodes ${[...anchors].join(",")}), quantum ${LANDMARK_QUANTUM_S}s, ` +
|
||||||
|
`coverage ${coverage.map((c) => c.toFixed(2)).join("/")}, ${bin.byteLength} B`,
|
||||||
|
);
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
@@ -110,12 +110,20 @@ test("S3 nameId — a perimeter fix resolves 'Jalan Persiaran Mini'; an unnamed
|
|||||||
assert.equal(g.nameAt(atNode1.nameId), "Jalan Persiaran Mini");
|
assert.equal(g.nameAt(atNode1.nameId), "Jalan Persiaran Mini");
|
||||||
|
|
||||||
// Synthetic 2-node graph with ONE unnamed edge -> nameId null.
|
// Synthetic 2-node graph with ONE unnamed edge -> nameId null.
|
||||||
|
// ARG2 (plan 08-02): the landmark block is required by the one encode
|
||||||
|
// authority; values are inert steering data until ALT lands (08-03).
|
||||||
const bare = encodeGraph({
|
const bare = encodeGraph({
|
||||||
lat: [3.15, 3.1505],
|
lat: [3.15, 3.1505],
|
||||||
lng: [101.69, 101.6905],
|
lng: [101.69, 101.6905],
|
||||||
edges: [{ from: 0, to: 1, seconds: 4 }],
|
edges: [{ from: 0, to: 1, seconds: 4 }],
|
||||||
names: [],
|
names: [],
|
||||||
maxSpeedKmh: 60,
|
maxSpeedKmh: 60,
|
||||||
|
landmarks: {
|
||||||
|
quantumS: 0.5,
|
||||||
|
nodes: [0],
|
||||||
|
to: Uint16Array.of(0, 30),
|
||||||
|
from: Uint16Array.of(0, 30),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const { decodeGraph, UNNAMED_NAME_ID } = await loadGraph();
|
const { decodeGraph, UNNAMED_NAME_ID } = await loadGraph();
|
||||||
const bg = decodeGraph(bare);
|
const bg = decodeGraph(bare);
|
||||||
|
|||||||
Reference in New Issue
Block a user