/* * fixture-source.ts — RoutingSource backend #1: the fixture corpus (D-02). * ------------------------------------------------------------------------- * Serves build-time-precomputed routes for the golden KL pairs. The dataset * enters by STATIC IMPORT (LAW 2 — esbuild and Vite inline it at build time; * bare Node 24 reads it via type-stripping; a runtime retrieval would hard- * fail in a Worker, which has no fs). The import binding is all module scope * holds — the index (Maps, decoded geometry) is built lazily on first use and * memoized (LAW 1 — the gazetteer posture; Workers 1 s startup limit and the * webview's first paint both pay for eager work). * * THE MATCHING LAW (D-02/D-03): a pair may serve a request only when * (1) the request's destinationId equals the pair's destinationId, and * (2) the request origin sits within ORIGIN_TOLERANCE_M (200 m, haversine) * of the pair's pinned PUBLIC origin. * The 200 m pad absorbs GPS jitter around a known landmark — it never * fabricates a route for an unknown origin. Anything unmatched resolves to * null: the honest "router pending" state (D-03), never a nearest-match. * * DEFENSIVE VALIDATION (the bot/src/kv/location-pin.ts precedent): a * malformed pair — non-finite distance/duration, geometry that throws on * decode, empty or oversized routes array — is SKIPPED at index-build time. * A malformed entry renders as an honest null downstream, never a throw in * the webview's render path and never a NaN coordinate (T-05-02). Plan * 05-02's corpus gate makes malformed pairs unshippable in practice; this is * the runtime backstop. * * LAW 3 — NO LOGGING, NO STORAGE, NO NETWORK (D-12/D-15): the origin is the * user's live fix. It exists in the caller's frame, is matched in memory, * and dies with the frame. Nothing about a request is persisted, logged, or * transmitted. Fixture geometries are public-data routes between public * places — no user coordinate can ever enter the dataset. */ import fixturesData from "./data/route-fixtures.json" with { type: "json" }; import { deriveCumulativeDistanceM, haversineM } from "./geo.ts"; import { decodePolyline6 } from "./polyline.ts"; import type { FixturePair, RouteFixturesFile, RouteOption, RoutePoint, RouteRequest, RouteResult, RoutingSource, } from "./types.ts"; /** A pair may serve a request only when the origin sits within this many * meters (haversine) of the pair's pinned public origin (D-02/D-03). */ export const ORIGIN_TOLERANCE_M = 200; /** pairIds and destination ids are slugs — anything else is refused before * the index is consulted (the search/src/source.ts ENTRY_ID_PATTERN guard; * bounds per-request work against hostile shapes, T-05-03). */ const ID_PATTERN = /^[a-z0-9-]{1,64}$/; /** The lazily built, memoized fixture index (never at module scope — LAW 1). */ interface PairIndex { byId: Map; byDestination: Map; /** pairId -> decoded options, memoized so geometry decodes once per pair. */ optionCache: Map; } /** A pair is admitted to the index only when every field is sane. */ function isUsablePair(pair: unknown): pair is FixturePair { if (pair === null || typeof pair !== "object") return false; const p = pair as { pairId?: unknown; destinationId?: unknown; origin?: { lat?: unknown; lng?: unknown; label?: unknown }; routes?: unknown; }; if (typeof p.pairId !== "string" || !ID_PATTERN.test(p.pairId)) return false; if (typeof p.destinationId !== "string" || !ID_PATTERN.test(p.destinationId)) return false; const origin = p.origin; if (origin === null || typeof origin !== "object") return false; if (typeof origin.lat !== "number" || !Number.isFinite(origin.lat)) return false; if (typeof origin.lng !== "number" || !Number.isFinite(origin.lng)) return false; if (typeof origin.label !== "string") return false; if (!Array.isArray(p.routes) || p.routes.length === 0 || p.routes.length > 3) return false; for (const route of p.routes) { if (route === null || typeof route !== "object") return false; const r = route as { geometry?: unknown; distanceM?: unknown; durationS?: unknown }; if (typeof r.geometry !== "string" || r.geometry.length === 0) return false; if (typeof r.distanceM !== "number" || !Number.isFinite(r.distanceM)) return false; if (typeof r.durationS !== "number" || !Number.isFinite(r.durationS)) return false; try { decodePolyline6(r.geometry); } catch { return false; // a throwing geometry is a malformed pair (T-05-02) } } return true; } function buildPairIndex(file: RouteFixturesFile): PairIndex { const index: PairIndex = { byId: new Map(), byDestination: new Map(), optionCache: new Map(), }; for (const pair of file.pairs) { if (!isUsablePair(pair)) continue; // malformed -> skipped, honest null downstream index.byId.set(pair.pairId, pair); const bucket = index.byDestination.get(pair.destinationId); if (bucket === undefined) { index.byDestination.set(pair.destinationId, [pair]); } else { bucket.push(pair); } } return index; } /** Options decode their geometry once, lazily, memoized per pair. */ function buildOptions(pair: FixturePair, cache: Map): RouteOption[] { const hit = cache.get(pair.pairId); if (hit !== undefined) return hit; const options: RouteOption[] = pair.routes.map((route) => { const points = decodePolyline6(route.geometry); return { geometry: { points, cumulativeDistanceM: deriveCumulativeDistanceM(points) }, summary: { distanceM: route.distanceM, durationS: route.durationS }, // D-10: fixture-world steps are ALWAYS empty — a fabricated maneuver // list would be dishonest; real steps arrive with backends #2/#3. steps: [], }; }); cache.set(pair.pairId, options); return options; } /** The default index over the committed seed — one lazy build, memoized. */ let defaultIndex: PairIndex | null = null; function loadDefaultIndex(): PairIndex { if (defaultIndex === null) { defaultIndex = buildPairIndex(fixturesData as RouteFixturesFile); } return defaultIndex; } /** pairId -> pinned origin, or null when unknown (the 05-05 harness knob * ?fixpair= reads this; public data only). */ export function getPairOrigin(pairId: string): RoutePoint | null { if (!ID_PATTERN.test(pairId)) return null; const pair = loadDefaultIndex().byId.get(pairId); return pair === undefined ? null : { lat: pair.origin.lat, lng: pair.origin.lng }; } /** Introspection for the corpus runner (plan 05-02) — lazy, memoized. */ export function getPairs(): readonly FixturePair[] { return [...loadDefaultIndex().byId.values()]; } /** Backend #1 of the D-01 ladder. `data` is optional injection — tests pass * synthetic datasets; production constructs it with no argument and the * committed seed serves. Backend selection happens HERE, in code, at the * import site — never via env or config (T-05-04). */ export function createFixtureRoutingSource(data?: RouteFixturesFile): RoutingSource { let index: PairIndex | null = null; const load = (): PairIndex => { if (index === null) { index = data === undefined ? loadDefaultIndex() : buildPairIndex(data); } return index; }; return { name: "fixture", async route(req: RouteRequest): Promise { // Slug guard FIRST — a hostile shape never reaches the index (T-05-03). if (!ID_PATTERN.test(req.destinationId)) return null; const idx = load(); const candidates = idx.byDestination.get(req.destinationId); if (candidates === undefined) return null; // D-03: honest pending for (const pair of candidates) { if (haversineM(req.origin, pair.origin) <= ORIGIN_TOLERANCE_M) { return { destinationId: req.destinationId, origin: req.origin, options: buildOptions(pair, idx.optionCache), backend: "fixture", }; } } return null; // origin outside tolerance for every pinned pair — D-03 }, }; }