feat(05-01): geo math + fixture RoutingSource backend + public API

- geo.ts: haversineM + deriveCumulativeDistanceM (D-07 snapped-progress field)
- fixture-source.ts: backend #1 — ORIGIN_TOLERANCE_M=200 matching law,
  honest-null pending semantics (D-03), defensive pair skip at index build,
  lazy memoized index (LAW 1), static JSON import (LAW 2), zero
  logging/storage/network (LAW 3, D-12)
- index.ts: named-export public surface, subpath-with-extension import law
- loader proven in three environments (bare Node 24, esbuild, Vite-family)
This commit is contained in:
araharah-executor
2026-10-24 05:03:07 +08:00
parent 0599ffe14f
commit 3e0c5e3eb4
3 changed files with 268 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
/*
* geo.ts — spherical geo math for the route model (D-07).
* --------------------------------------------------------
* haversineM: great-circle distance in meters on the mean-earth sphere
* (radius 6 371 000 m — at KL-metro scale the model error is far below the
* fixture corpus's 200 m origin tolerance). Finite for finite inputs.
*
* deriveCumulativeDistanceM: the snapped-progress support field (D-07 — the
* GUID-05/06 upstream obligation). Produces [0, d(p0→p1), d(p0→p1)+d(p1→p2),
* …] — the same length as the input, [0] === 0, non-decreasing. Phase 6's
* guidance snaps a live fix's progress along the line using this array
* instead of re-deriving it per fix; the fixture source derives it once at
* decode/load and memoizes (RESEARCH Finding 1).
*
* Pure: no I/O, no logging, no storage (the D-15 analog — nothing about a
* request outlives the caller's frame).
*/
import type { RoutePoint } from "./types.ts";
const EARTH_RADIUS_M = 6_371_000;
const DEGREES_TO_RADIANS = Math.PI / 180;
/** Great-circle distance between two WGS84 points, in meters. */
export function haversineM(a: RoutePoint, b: RoutePoint): number {
const lat1 = a.lat * DEGREES_TO_RADIANS;
const lat2 = b.lat * DEGREES_TO_RADIANS;
const dLat = (b.lat - a.lat) * DEGREES_TO_RADIANS;
const dLng = (b.lng - a.lng) * DEGREES_TO_RADIANS;
const sinLat = Math.sin(dLat / 2);
const sinLng = Math.sin(dLng / 2);
const h = sinLat * sinLat + Math.cos(lat1) * Math.cos(lat2) * sinLng * sinLng;
return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
}
/** Meters walked along the polyline: same length as points, [0] === 0,
* non-decreasing. Empty -> []; single point -> [0]. */
export function deriveCumulativeDistanceM(points: readonly RoutePoint[]): number[] {
if (points.length === 0) return [];
const cumulative: number[] = [0];
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const curr = points[i];
const prevMeters = cumulative[i - 1];
if (prev === undefined || curr === undefined || prevMeters === undefined) break;
cumulative.push(prevMeters + haversineM(prev, curr));
}
return cumulative;
}