305 lines
29 KiB
Markdown
305 lines
29 KiB
Markdown
---
|
||
phase: 05-routing-route-display
|
||
plan: 02
|
||
type: execute
|
||
wave: 2
|
||
depends_on: ["01"]
|
||
files_modified:
|
||
- package.json
|
||
- routing/package.json
|
||
- routing/test/fixtures/golden-pairs.json
|
||
- routing/scripts/fetch-route-fixtures.mjs
|
||
- routing/scripts/route-corpus.mjs
|
||
- routing/src/data/route-fixtures.json
|
||
- routing/test/corpus.test.mjs
|
||
- routing/test/loader-env.test.mjs
|
||
autonomous: true
|
||
requirements: [RTNG-01, RTNG-02]
|
||
must_haves:
|
||
decisions_implemented:
|
||
- "D-02: fixture routes for the 10–20 golden KL pairs, geometries precomputed at BUILD time by a repo script hitting the public OSRM demo server once, committed as compact encoded polylines, each pair pinning distance/duration consistency gates"
|
||
- "D-12: fixture geometries are public-data routes between public places — the corpus gate mechanically proves every pinned origin equals a gazetteer landmark coordinate; the script header documents the maintainer-machine boundary"
|
||
- "D-11: the fixture payload stays inside the bundle budget — a fixture-level gzipped-size gate (≤ 100 KB) gives early warning under the 200 KB BA9 ceiling"
|
||
- "RTNG-02: the alternatives floor — at least 4 pairs carry ≥ 2 routes so the chips/redraw surface has real data"
|
||
truths:
|
||
- "15 golden pairs are committed in routing/src/data/route-fixtures.json, each with 1–3 best-first routes stored as verbatim polyline6 strings"
|
||
- "Every committed pair passes the OFFLINE tolerance gate: geometry↔summary haversine consistency (0.85–1.10 × distanceM), duration sanity (1.0–30 m/s average speed), and exact codec round-trip — no test ever touches a network"
|
||
- "Every pair's pinned origin coordinate equals SOME gazetteer entry's lat/lng within 1e-4 degrees — no user coordinate can have entered a fixture (mechanical D-12 proof)"
|
||
- "Every pair's destinationId exists in the shared gazetteer; exactly zero pairs target the pending-probe destination 'national-museum' (the 05-05 capture's pending-state carrier)"
|
||
- "The fetch script is a repo tool on the overpass-topup.mjs pattern: descriptive User-Agent, inter-request delay, per-pair hard-fail diagnostics, never imported at runtime, never run in CI or at web-build time"
|
||
- "The corpus gate is both a CLI (npm run routing:corpus) and an in-suite Nyquist gate (npm run routing:test) — one measurement core, two reporters, no drift"
|
||
artifacts:
|
||
- path: "routing/test/fixtures/golden-pairs.json"
|
||
provides: "the hand-authored pinned corpus: 15 pairs (origin landmark + destinationId + coordinates), the KL bbox, the size budget, the pending-probe id"
|
||
contains: "\"pairs\""
|
||
- path: "routing/scripts/fetch-route-fixtures.mjs"
|
||
provides: "the build-time OSRM-demo fetch tool (maintainer machine; D-12 boundary documented in header)"
|
||
contains: "router.project-osrm.org"
|
||
- path: "routing/scripts/route-corpus.mjs"
|
||
provides: "measurePairs() — the corpus measurement core the CLI prints and the suite asserts"
|
||
contains: "export function measurePairs"
|
||
- path: "routing/src/data/route-fixtures.json"
|
||
provides: "the COMMITTED fixture dataset — 15 pairs populated from the OSRM demo (this plan replaces 05-01's empty seed)"
|
||
contains: "\"pairId\""
|
||
- path: "routing/test/corpus.test.mjs"
|
||
provides: "the RT1–RT10 offline tolerance gate suite (in-process, deterministic)"
|
||
contains: "RT1"
|
||
key_links:
|
||
- from: "routing/scripts/fetch-route-fixtures.mjs"
|
||
to: "routing/test/fixtures/golden-pairs.json"
|
||
via: "the script reads the pinned corpus and fetches each pair from the OSRM demo"
|
||
pattern: "golden-pairs.json"
|
||
- from: "routing/scripts/fetch-route-fixtures.mjs"
|
||
to: "routing/src/data/route-fixtures.json"
|
||
via: "collect-then-write: the committed dataset is regenerated atomically per run"
|
||
pattern: "route-fixtures.json"
|
||
- from: "routing/scripts/route-corpus.mjs"
|
||
to: "routing/src/data/route-fixtures.json"
|
||
via: "the measurement core validates the committed dataset (never the script output directly)"
|
||
pattern: "route-fixtures.json"
|
||
- from: "routing/test/corpus.test.mjs"
|
||
to: "routing/scripts/route-corpus.mjs"
|
||
via: "the suite imports measurePairs and asserts the SAME numbers the CLI prints (gazetteer-corpus.mjs precedent)"
|
||
pattern: "measurePairs"
|
||
---
|
||
|
||
<objective>
|
||
Populate the committed fixture dataset with 10–20 real KL origin/destination pairs precomputed from the public OSRM demo server (build-time, public coordinates only), and pin them behind the offline corpus tolerance gate — the phase's primary acceptance instrument (the amended ROADMAP SC3: "the golden-route corpus routes cleanly through the fixture RoutingSource backend with bundle-size and type gates holding").
|
||
|
||
Purpose: D-02 makes fixture geometry the product's actual route data for v1's golden pairs; D-12 makes "public data between public places" a mechanically provable property (every pinned origin must equal a gazetteer landmark coordinate); RTNG-02 needs real alternatives (≥ 4 pairs with ≥ 2 routes) for the chips/redraw surface. This plan delivers the hand-authored pair list, the maintainer fetch script (overpass-topup.mjs pattern), the populated committed dataset, and the RT1–RT10 offline gate — both as a CLI and as an in-suite Nyquist gate.
|
||
|
||
Output: `routing/test/fixtures/golden-pairs.json` (15 pinned pairs), `routing/scripts/fetch-route-fixtures.mjs` (repo tool), a committed `routing/src/data/route-fixtures.json` with 15 real pairs, `routing/scripts/route-corpus.mjs` + `routing/test/corpus.test.mjs` (the gate), and the `routing:fixtures` root delegate.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
|
||
@$HOME/.config/opencode/get-shit-done/templates/summary.md
|
||
</execution_context>
|
||
|
||
<context>
|
||
@AGENTS.md
|
||
@.planning/phases/05-routing-route-display/05-CONTEXT.md
|
||
@.planning/phases/05-routing-route-display/05-RESEARCH.md
|
||
@.planning/phases/05-routing-route-display/05-PATTERNS.md
|
||
@.planning/phases/05-routing-route-display/05-01-PLAN.md
|
||
@search/scripts/overpass-topup.mjs
|
||
@search/scripts/gazetteer-corpus.mjs
|
||
@search/test/corpus.test.mjs
|
||
@search/src/data/kl-gazetteer.json
|
||
@routing/src/types.ts
|
||
@routing/src/index.ts
|
||
@routing/src/data/route-fixtures.json
|
||
|
||
<pinned_corpus>
|
||
The 15 golden pairs — hand-pinned NOW (the executor does not invent pairs; the executor verifies each pair routes via the script and records the outcome). All origins AND destinations are gazetteer entries from `search/src/data/kl-gazetteer.json` (public landmark coordinates; the D-12 proof keys off exactly this). Coordinates below are the gazetteer's own values — the corpus file repeats them verbatim so the file is self-contained, and gate RT2b re-proves they match the gazetteer.
|
||
|
||
| # | pairId | origin (id) | origin lat,lng | destinationId |
|
||
|---|--------|-------------|-----------------|---------------|
|
||
| 1 | kl-tower-to-kl-sentral | kl-tower | 3.1528, 101.7038 | kl-sentral |
|
||
| 2 | kl-sentral-to-suria-klcc | kl-sentral | 3.1343, 101.6869 | suria-klcc |
|
||
| 3 | suria-klcc-to-bangsar | suria-klcc | 3.1578, 101.7117 | bangsar |
|
||
| 4 | mid-valley-to-bukit-bintang | mid-valley | 3.118, 101.6779 | bukit-bintang |
|
||
| 5 | bukit-bintang-to-mid-valley | bukit-bintang | 3.1466, 101.7106 | mid-valley |
|
||
| 6 | pwtc-to-sunway-velocity | pwtc | 3.1768, 101.698 | sunway-velocity |
|
||
| 7 | masjid-jamek-to-lake-gardens | masjid-jamek | 3.1484, 101.6953 | lake-gardens |
|
||
| 8 | titiwangsa-to-petaling-street | titiwangsa | 3.1714, 101.7016 | petaling-street |
|
||
| 9 | sunway-pyramid-to-pavilion-kl | sunway-pyramid | 3.0726, 101.6069 | pavilion-kl |
|
||
| 10 | central-market-to-merdeka-118 | central-market | 3.1454, 101.6953 | merdeka-118 |
|
||
| 11 | little-india-brickfields-to-aquaria-klcc | little-india-brickfields | 3.1306, 101.6869 | aquaria-klcc |
|
||
| 12 | national-museum-to-pudu-raya | national-museum | 3.1378, 101.687 | pudu-raya |
|
||
| 13 | mid-valley-to-ioi-city-mall | mid-valley | 3.118, 101.6779 | ioi-city-mall |
|
||
| 14 | kl-sentral-to-sunway-pyramid | kl-sentral | 3.1343, 101.6869 | sunway-pyramid |
|
||
| 15 | berjaya-times-square-to-titiwangsa | berjaya-times-square | 3.1421, 101.7101 | titiwangsa |
|
||
|
||
Notes:
|
||
- The pair ids follow `<origin-id>-to-<dest-id>` (slug charset; the `?fixpair=` knob value in plan 05-05).
|
||
- `national-museum` appears ONLY as an ORIGIN (pair 12) — it is NEVER a destinationId (gate RT4 pins this; plan 05-05's pending capture uses `dest=national-museum` as the supported-but-out-of-corpus carrier).
|
||
- Distances span ~1 km (pair 10) to ~20 km (pair 13) — short/mid/long spread so the UI-SPEC's card/chips/camera behaviors are exercised across zoom ranges.
|
||
- Pairs 4/5 and 2/3 give direction-reversed coverage (one-way streets and alternatives differ by direction — the demo script walks these).
|
||
</pinned_corpus>
|
||
|
||
<osrm_contract>
|
||
The OSRM demo request shape (VERIFIED LIVE 2026-10-24 — see 05-RESEARCH.md Finding 2):
|
||
|
||
```
|
||
GET https://router.project-osrm.org/route/v1/driving/{originLng},{originLat};{destLng},{destLat}?alternatives=2&overview=full&geometries=polyline6
|
||
```
|
||
|
||
- `alternatives=2` → up to 2 alternates (≤ 3 total; matches RTNG-02's "up to 3").
|
||
- `overview=full` → complete geometry; `geometries=polyline6` → the string stored VERBATIM.
|
||
- Response: `code: "Ok"`, `routes[]` best-first with `distance` (m), `duration` (s), `geometry` (string).
|
||
- Per-pair HARD FAIL when `code !== "Ok"` or `routes.length === 0` — name the pair in the diagnostic, exit 1, write NOTHING (collect-then-write).
|
||
- Do NOT request `steps` (turn instructions are Phase 6; fixture steps stay empty — D-10).
|
||
- Do NOT retry on failure beyond ONE immediate retry per pair (transient 5xx); a second failure is a hard fail for that pair.
|
||
</osrm_contract>
|
||
|
||
<environment_facts>
|
||
Verified in this session — do not re-derive:
|
||
|
||
- The OSRM demo server answered a KL route request with `code: Ok`, 2 routes, polyline6 geometry (~1200 chars for ~6.9 km) on 2026-10-24 — the endpoint, parameter set, and response shape are current, not assumed.
|
||
- 15 pairs × 1 request each ≈ 15 requests + up to 1 retry each — light, fair use of a free demo server; the 1200 ms inter-request delay keeps it polite (~20 s total runtime).
|
||
- `search/scripts/overpass-topup.mjs` is the repo-tool precedent: mandatory descriptive User-Agent, inter-request delay, per-item soft diagnostics, writes committed JSON, imported by NOTHING at runtime.
|
||
- `search/scripts/gazetteer-corpus.mjs` exports `measureCorpus()` precisely so `search/test/corpus.test.mjs` can assert the same numbers in-process — "one measurement, two reporters, no drift" is the established corpus-gate shape.
|
||
- The gazetteer's 30 entries and exact coordinates are in `search/src/data/kl-gazetteer.json` — the corpus file's origin coordinates above are copied from there.
|
||
- The `search` workspace imports (`import … from "../src/index.ts"`) work in `.mjs` scripts under Node 24 type-stripping (`gazetteer-corpus.mjs` does exactly this).
|
||
</environment_facts>
|
||
|
||
<threat_model>
|
||
## Trust Boundaries
|
||
|
||
| Boundary | Description |
|
||
|----------|-------------|
|
||
| OSRM demo server → fetch script → committed dataset | A third-party free service supplies the geometry. Drift (data updates) is benign for a committed snapshot; POISONING (wrong/malformed geometry) must be caught offline by the tolerance gate before it can ship. |
|
||
| Pinned corpus file → fetch script | The hand-authored pairs file decides which coordinates leave the machine. A user coordinate or a private address entering this file would be shipped to a third party — the D-12 red line. |
|
||
| Committed dataset → bundle → users | Fixture bytes ride the web bundle (public data, but size is an availability budget — D-11). |
|
||
|
||
## STRIDE Threat Register
|
||
|
||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||
|-----------|----------|-----------|-------------|-----------------|
|
||
| T-05-05 | Tampering / Information Disclosure | (a) OSRM-supplied geometry inconsistent with its own summary silently shipping; (b) a non-public coordinate entering the pinned corpus and being sent to the demo server | mitigate | (a) RT5 recomputes each route's haversine polyline length OFFLINE and demands 0.85–1.10 × distanceM, RT6 bounds average speed to 1.0–30 m/s, RT3 re-decodes and re-encodes every geometry (exact round-trip) — a poisoned or drifting dataset fails `npm run routing:test`, not just the script. (b) RT2b mechanically proves every pinned origin equals SOME gazetteer entry coordinate within 1e-4 deg (≈11 m) — the corpus can only contain public landmark points, so the maintainer machine sends nothing private (D-12's "no user coordinate ever enters a fixture" made checkable). |
|
||
| T-05-12 | Denial of Service (availability budget) | Fixture payload growth pushing the web bundle over the D-11 budget | mitigate | RT9 gzips the committed dataset and fails above 100 KB (expected ≈ 20 KB at 15 pairs × ~1.2–2.5 KB) — early warning under BA9's 200 KB all-in ceiling (plan 05-05 owns the final gate; the two tiers share this threat ID); the gate prints the measured size so growth is visible, never discovered. |
|
||
| T-05-06 | Repudiation | Derivative OSM geometry shipped without licence/attribution | mitigate | The script writes the ODbL header fields (`_license`/`_attribution`/`_notice`/`_source`) into every regenerated file (they are part of the file template, not an optional add-on); RT8 asserts them post-write — the notice cannot be lost by a regeneration. |
|
||
</threat_model>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>task 1: pinned golden-pairs corpus + fetch script + populated committed dataset</name>
|
||
<files>package.json, routing/package.json, routing/test/fixtures/golden-pairs.json, routing/scripts/fetch-route-fixtures.mjs, routing/src/data/route-fixtures.json</files>
|
||
<read_first>
|
||
- `search/scripts/overpass-topup.mjs` (the repo-tool pattern: UA, delay, diagnostics, committed-JSON write)
|
||
- `search/src/data/kl-gazetteer.json` (the origin/destination coordinates and ids — the corpus file must agree with them)
|
||
- `routing/src/types.ts` (FixturePair/FixtureRoute — the written dataset must typecheck against these via the corpus runner's import)
|
||
- `routing/src/data/route-fixtures.json` (05-01's seed — this task REPLACES its `pairs: []` with real data, preserving the header fields)
|
||
- `.planning/phases/05-routing-route-display/05-RESEARCH.md` (Finding 2 — the verified request shape)
|
||
</read_first>
|
||
<action>
|
||
**`routing/test/fixtures/golden-pairs.json`** — hand-author the pinned corpus, exactly the `<pinned_corpus>` table:
|
||
|
||
```json
|
||
{
|
||
"_comment": "Pinned golden-pair corpus (D-02/D-12). Origins AND destinations are gazetteer landmarks (public data). RT2b proves every origin equals a gazetteer coordinate. national-museum is never a destinationId (RT4) — it is the 05-05 pending-probe carrier.",
|
||
"klBbox": { "minLat": 2.9, "maxLat": 3.4, "minLng": 101.4, "maxLng": 102.0 },
|
||
"fixtureGzipBudgetBytes": 102400,
|
||
"pendingProbeDestinationId": "national-museum",
|
||
"alternativesFloorPairs": 4,
|
||
"pairs": [
|
||
{ "pairId": "kl-tower-to-kl-sentral", "originId": "kl-tower", "origin": { "lat": 3.1528, "lng": 101.7038, "label": "Kuala Lumpur Tower" }, "destinationId": "kl-sentral" },
|
||
{ "pairId": "kl-sentral-to-suria-klcc", "originId": "kl-sentral", "origin": { "lat": 3.1343, "lng": 101.6869, "label": "KL Sentral" }, "destinationId": "suria-klcc" },
|
||
{ "pairId": "suria-klcc-to-bangsar", "originId": "suria-klcc", "origin": { "lat": 3.1578, "lng": 101.7117, "label": "Suria KLCC" }, "destinationId": "bangsar" },
|
||
{ "pairId": "mid-valley-to-bukit-bintang", "originId": "mid-valley", "origin": { "lat": 3.118, "lng": 101.6779, "label": "Mid Valley Megamall" }, "destinationId": "bukit-bintang" },
|
||
{ "pairId": "bukit-bintang-to-mid-valley", "originId": "bukit-bintang", "origin": { "lat": 3.1466, "lng": 101.7106, "label": "Bukit Bintang" }, "destinationId": "mid-valley" },
|
||
{ "pairId": "pwtc-to-sunway-velocity", "originId": "pwtc", "origin": { "lat": 3.1768, "lng": 101.698, "label": "PWTC" }, "destinationId": "sunway-velocity" },
|
||
{ "pairId": "masjid-jamek-to-lake-gardens", "originId": "masjid-jamek", "origin": { "lat": 3.1484, "lng": 101.6953, "label": "Masjid Jamek" }, "destinationId": "lake-gardens" },
|
||
{ "pairId": "titiwangsa-to-petaling-street", "originId": "titiwangsa", "origin": { "lat": 3.1714, "lng": 101.7016, "label": "Taman Tasik Titiwangsa" }, "destinationId": "petaling-street" },
|
||
{ "pairId": "sunway-pyramid-to-pavilion-kl", "originId": "sunway-pyramid", "origin": { "lat": 3.0726, "lng": 101.6069, "label": "Sunway Pyramid" }, "destinationId": "pavilion-kl" },
|
||
{ "pairId": "central-market-to-merdeka-118", "originId": "central-market", "origin": { "lat": 3.1454, "lng": 101.6953, "label": "Central Market" }, "destinationId": "merdeka-118" },
|
||
{ "pairId": "little-india-brickfields-to-aquaria-klcc", "originId": "little-india-brickfields", "origin": { "lat": 3.1306, "lng": 101.6869, "label": "Brickfields" }, "destinationId": "aquaria-klcc" },
|
||
{ "pairId": "national-museum-to-pudu-raya", "originId": "national-museum", "origin": { "lat": 3.1378, "lng": 101.687, "label": "National Museum" }, "destinationId": "pudu-raya" },
|
||
{ "pairId": "mid-valley-to-ioi-city-mall", "originId": "mid-valley", "origin": { "lat": 3.118, "lng": 101.6779, "label": "Mid Valley Megamall" }, "destinationId": "ioi-city-mall" },
|
||
{ "pairId": "kl-sentral-to-sunway-pyramid", "originId": "kl-sentral", "origin": { "lat": 3.1343, "lng": 101.6869, "label": "KL Sentral" }, "destinationId": "sunway-pyramid" },
|
||
{ "pairId": "berjaya-times-square-to-titiwangsa", "originId": "berjaya-times-square", "origin": { "lat": 3.1421, "lng": 101.7101, "label": "Berjaya Times Square" }, "destinationId": "titiwangsa" }
|
||
]
|
||
}
|
||
```
|
||
|
||
**`routing/scripts/fetch-route-fixtures.mjs`** — the maintainer build tool (overpass-topup.mjs pattern). Header comment MUST contain this D-12 boundary statement verbatim: `D-12 BOUNDARY: this script runs on the MAINTAINER'S machine against FIXED PUBLIC coordinates (gazetteer landmarks). No user coordinate can ever enter a fixture. The app itself gains NO runtime dependency on this server or any network.` Behavior:
|
||
- Read `routing/test/fixtures/golden-pairs.json` + the gazetteer (`../node_modules/@araharah/search/src/data/kl-gazetteer.json`? NO — resolve the workspace package by RELATIVE path `../../search/src/data/kl-gazetteer.json`, the `gazetteer-corpus.mjs` cross-workspace import precedent — verify the path exists at runtime and hard-fail with a diagnostic if not).
|
||
- For each pair: validate the origin coordinate matches the gazetteer's `originId` entry within 1e-4 deg AND the destinationId exists (refuse to even fetch a non-public or unknown coordinate — fail fast, name the pair).
|
||
- Fetch the `<osrm_contract>` URL with header `User-Agent: araharah-fixtures/1.0 (build-time fixture precompute; repo: araharah)`, `1200 ms` delay between pairs, ONE retry on network/5xx failure.
|
||
- Per-pair hard fail on `code !== "Ok"` or `routes.length === 0` (collect-then-write: on ANY pair failure, exit 1 with the failing pair named and the committed file UNTOUCHED).
|
||
- Map each OSRM route → `{ geometry: r.geometry, distanceM: r.distance, durationS: r.duration }` (order preserved, cap 3).
|
||
- Write `routing/src/data/route-fixtures.json` preserving 05-01's header fields (`_license`, `_attribution`, `_notice`, `_source`) with `version: 1`, `originToleranceM: 200`, and the populated `pairs` — 2-space JSON, trailing newline.
|
||
- Support `--only=<pairId>` to regenerate one pair (merging into the existing committed file — a single-pair repair must not churn the other 14).
|
||
|
||
**Run the script NOW** (this is the plan's build step): `node routing/scripts/fetch-route-fixtures.mjs` from the repo root. The committed `routing/src/data/route-fixtures.json` MUST end this task with 15 real pairs. If the OSRM demo server is unreachable at execution time: retry the fetch up to 3 times over ~10 minutes; if still unreachable, STOP this plan with an explicit CHECKPOINT note in the summary (fixtures are the phase's data product — shipping empty fixtures fails the corpus gate by design).
|
||
|
||
**Root `package.json`** — add `"routing:fixtures": "node routing/scripts/fetch-route-fixtures.mjs"` (delegate next to `routing:corpus`).
|
||
|
||
**`routing/package.json`** — add `"fixtures": "node scripts/fetch-route-fixtures.mjs"` and `"corpus": "node scripts/route-corpus.mjs"` scripts.
|
||
|
||
**Acceptance:** the committed dataset carries 15 pairs; `npm run routing:test` still exits 0 (05-01's suites pass against REAL data — F1's empty-seed case L3 now asserts the default source serves kl-tower→kl-sentraL… NOTE: loader-env L3 asserted the seed serves null; with real data it must now assert a KNOWN pair SERVES — update `L3` in `routing/test/loader-env.test.mjs` accordingly: the default source serves pair 1 for a request at its pinned origin, still returns null for an origin 300 m away).
|
||
</action>
|
||
<verify>
|
||
<automated>cd /home/C-Fu/araharah && node -e "const f=require('./routing/src/data/route-fixtures.json'); if(f.pairs.length<10||f.pairs.length>20) throw new Error('pair count '+f.pairs.length); for(const p of f.pairs){if(p.routes.length<1||p.routes.length>3) throw new Error(p.pairId+' route count');} console.log('pairs:', f.pairs.length)" ; grep -q '"routing:fixtures"' package.json && echo "delegate OK"</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `node -e "const f=require('./routing/src/data/route-fixtures.json'); if(f.pairs.length!==15) throw new Error(1); for(const p of f.pairs){ if(!/^[a-z0-9-]{1,64}$/.test(p.pairId)) throw new Error(2); if(p.routes.length<1||p.routes.length>3) throw new Error(3);} console.log('OK')"` exits 0
|
||
- `grep -q 'D-12 BOUNDARY: this script runs on the MAINTAINER' routing/scripts/fetch-route-fixtures.mjs` exits 0
|
||
- `grep -q 'router.project-osrm.org' routing/scripts/fetch-route-fixtures.mjs` exits 0
|
||
- `grep -q 'User-Agent' routing/scripts/fetch-route-fixtures.mjs` exits 0 AND `grep -q '1200' routing/scripts/fetch-route-fixtures.mjs` exits 0 (mandatory UA + inter-request delay)
|
||
- `grep -q '"routing:fixtures": "node routing/scripts/fetch-route-fixtures.mjs"' package.json` exits 0
|
||
- `test -f routing/test/fixtures/golden-pairs.json` exits 0 AND `node -e "const c=require('./routing/test/fixtures/golden-pairs.json'); if(c.pairs.length!==15) throw new Error(1); if(c.pendingProbeDestinationId!=='national-museum') throw new Error(2); console.log('OK')"` exits 0
|
||
- `node -e "const f=require('./routing/src/data/route-fixtures.json'); const bad=f.pairs.filter(p=>p.destinationId==='national-museum'); if(bad.length) throw new Error('probe id used as destination'); console.log('RT4 OK')"` exits 0
|
||
</acceptance_criteria>
|
||
<done>The pinned 15-pair corpus exists, the maintainer fetch tool exists with the D-12 boundary documented, and the committed fixture dataset carries 15 real OSRM-demo-precomputed pairs (1–3 routes each, verbatim polyline6).</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>task 2: the offline corpus tolerance gate (CLI + in-suite Nyquist gate)</name>
|
||
<files>routing/scripts/route-corpus.mjs, routing/test/corpus.test.mjs</files>
|
||
<read_first>
|
||
- `search/scripts/gazetteer-corpus.mjs` (the "one measurement, two reporters" shape: exported measure core + CLI report + fixed escalation ladder on failure)
|
||
- `search/test/corpus.test.mjs` (the in-process assertion suite that imports the measurement core — NEVER spawns the script)
|
||
- `routing/src/index.ts` (the public API the measurement core imports: decodePolyline6, haversineM, getPairs)
|
||
- `routing/test/fixtures/golden-pairs.json` (task 1's thresholds: bbox, gzip budget, alternatives floor, probe id)
|
||
</read_first>
|
||
<action>
|
||
**`routing/scripts/route-corpus.mjs`** — the corpus measurement core + CLI reporter. Header contract (gazetteer-corpus.mjs style): a LOCAL DEV TOOL over committed files; the WORKER/WEBVIEW never prints coordinates or pair data (D-15: this script prints pair ids and measurements only — never geometry strings). Exports:
|
||
|
||
```typescript
|
||
export function measurePairs(corpus, fixtures, gazetteer): MeasurementReport;
|
||
```
|
||
|
||
`MeasurementReport` carries per-check counts + violations arrays. The measurement core performs RT1–RT10 (ALL OFFLINE — zero network in the measurement path):
|
||
|
||
- **RT1 corpus size** — `10 <= pairs.length <= 20`.
|
||
- **RT2 schema + public origin** — every pair: pairId unique + slug charset; origin label non-empty; origin inside `klBbox`; destinationId exists in the gazetteer; AND **RT2b**: the gazetteer entry whose `id === originId` satisfies `|entry.lat − origin.lat| <= 1e-4` AND `|entry.lng − origin.lng| <= 1e-4` — i.e. the pinned origin EQUALS the named gazetteer landmark's committed coordinate (the mechanical no-user-coordinate proof, D-12).
|
||
- **RT3 route shape** — every pair has 1..3 routes; every route: finite `distanceM > 0`, finite `durationS > 0`, non-empty `geometry` string.
|
||
- **RT4 probe exclusion** — NO pair's `destinationId` equals `pendingProbeDestinationId` (`national-museum`).
|
||
- **RT5 geometry↔summary consistency** — decode each geometry; `haversineLengthM(points)` (sum of segment distances) within `[0.85, 1.10] × distanceM`; decoded points all finite and inside the KL bbox (with 0.05 deg slack for OSRM's road snapping).
|
||
- **RT6 duration sanity** — `distanceM / durationS` within `[1.0, 30]` m/s (≈ 3.6–108 km/h — KL congestion floor to DUKE highway ceiling).
|
||
- **RT7 codec round-trip** — `encodePolyline6(decodePolyline6(geometry)) === geometry` for every route (the 1e-6-grid law, verbatim storage proof).
|
||
- **RT8 licence header** — `_license === "ODbL-1.0"`, `_attribution` contains `OpenStreetMap`, `_notice` contains `ODbL`, `_source` contains `router.project-osrm.org` (T-05-06).
|
||
- **RT9 size budget** — `gzipSync(readFileSync(fixturesPath)).length <= corpus.fixtureGzipBudgetBytes` (102400); the report PRINTS the measured bytes (T-05-11 early warning).
|
||
- **RT10 alternatives floor** — `pairs.filter(p => p.routes.length >= 2).length >= corpus.alternativesFloorPairs` (4) — RTNG-02's real-data requirement.
|
||
|
||
CLI: prints a per-check table (`RT1..RT10` with counts + violation details: pairId + reason — never geometry text), exits 0 ONLY when every check passes; on failure prints the fixed escalation ladder (inspect `routing/scripts/fetch-route-fixtures.mjs --only=<pairId>` for the failing pair; re-run the fetch; re-run the gate).
|
||
|
||
**`routing/test/corpus.test.mjs`** — RED-first-then-GREEN (the fixtures already exist from task 1, so the suite goes straight to green; a future regression re-reddens it). Import `measurePairs` from the script + the three JSON inputs via `readFileSync`/`import … with { type: "json" }`. One `test()` per check: `RT1`…`RT10`, each asserting `violations.length === 0` with a named message. Mirror the gazetteer corpus suite's in-process shape exactly (never spawns the CLI).
|
||
|
||
**Acceptance:** `npm run routing:corpus` exits 0 printing RT1–RT10 green with the measured gz size; `npm run routing:test` exits 0 including `PASS RT1`…`PASS RT10`.
|
||
</action>
|
||
<verify>
|
||
<automated>cd /home/C-Fu/araharah && npm run routing:corpus 2>&1 | tail -15 ; npm run routing:test 2>&1 | tail -15</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `npm run routing:corpus` exits 0 and its output contains `RT1` through `RT10` and a gz-size line
|
||
- `npm run routing:test` exits 0 and the output contains `PASS RT1` through `PASS RT10`
|
||
- `grep -q 'export function measurePairs' routing/scripts/route-corpus.mjs` exits 0 (the shared measurement core)
|
||
- `grep -q 'measurePairs' routing/test/corpus.test.mjs` exits 0 (in-process import — the two-reporters law)
|
||
- `node -e "const f=require('./routing/src/data/route-fixtures.json'); const multi=f.pairs.filter(p=>p.routes.length>=2).length; if(multi<4) throw new Error('alternatives floor: '+multi); console.log('RT10 OK:', multi)"` exits 0
|
||
- `node -e "const{gzipSync}=require('node:zlib');const{readFileSync}=require('node:fs');const s=gzipSync(readFileSync('./routing/src/data/route-fixtures.json')).length; if(s>102400) throw new Error('gz '+s); console.log('gz bytes:', s)"` exits 0
|
||
- `grep -c 'fetch(' routing/scripts/route-corpus.mjs | head -1` returns 0 (the measurement core is OFFLINE — T-05-05a)
|
||
</acceptance_criteria>
|
||
<done>The committed 15-pair dataset is pinned behind ten offline checks — corpus size, public-origin proof, schema, probe exclusion, geometry↔summary consistency, duration sanity, codec round-trip, licence header, gz size budget, and the alternatives floor — enforced identically by the CLI and the in-suite gate.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<verification>
|
||
1. `npm run routing:corpus` — RT1–RT10 green, measured gz size printed (expect ≈ 15–25 KB, budget 100 KB).
|
||
2. `npm run routing:test` — 05-01's C/P/F/L suites PLUS RT1–RT10, all green (the loader-env L3 update: default source now SERVES pair 1 at its pinned origin and returns null 300 m away).
|
||
3. `npm run routing:typecheck` — exit 0.
|
||
4. The committed dataset: 15 pairs, ODbL header intact, `national-museum` never a destination.
|
||
5. Network isolation: `grep -rn "fetch(" routing/scripts/route-corpus.mjs routing/test/corpus.test.mjs` → no matches (the gate never touches the network; only the maintainer fetch script does).
|
||
</verification>
|
||
|
||
<success_criteria>
|
||
- 15 golden KL pairs committed with real OSRM-demo geometry, pinned origins proven public (RT2b), 1–3 best-first routes each.
|
||
- The offline tolerance gate holds in both reporters (CLI + suite) with zero network access.
|
||
- At least 4 pairs carry ≥ 2 routes (RTNG-02's alternatives floor).
|
||
- The fetch tool is a documented repo-tool with the D-12 boundary in its header.
|
||
</success_criteria>
|