diff --git a/.planning/phases/06-turn-by-turn-guidance-live-driving/06-04-PLAN.md b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-04-PLAN.md index 386ed83..91fc5b1 100644 --- a/.planning/phases/06-turn-by-turn-guidance-live-driving/06-04-PLAN.md +++ b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-04-PLAN.md @@ -30,7 +30,7 @@ user_setup: must_haves: truths: - "A maintainer can rebuild the KL road graph from Overpass with one committed script — $0, fork-friendly (D-01)" - - "The real graph binary lands inside D-01's 30–80 MB envelope with the stats gate printing its measurement" + - "The real graph binary passes the enforced size gate (> 1 MB, ≤ 80 MB) with the stats gate printing its measurement — a sub-30 MB result prints D-01's expected-band warning for review rather than failing" - "/api/route answers an arbitrary current position with a real A* route + named steps — engine A is the guidance engine (D-03)" - "/api/route without a valid Bearer claim is 401 — the routing oracle is closed (T-06-01)" - "A missing R2 graph degrades to honest null → the client's router-pending state (D-03), never an error" @@ -43,6 +43,7 @@ must_haves: exports: ["createHttpRoutingSource"] - path: "bot/src/routing/worker-router.ts" provides: "R2 graph load (isolate-memoized) + snap + A* + steps → RouteResult" + exports: ["handleRoute", "loadGraph"] - path: "bot/wrangler.jsonc" provides: "r2_buckets binding ROAD_GRAPH (still exactly ONE kv_namespaces — GE5)" - path: "docs/ROAD-GRAPH.md" @@ -128,6 +129,7 @@ names: u32 nameOffset[cnt+1] ++ u8 pool[] --> - `node routing/scripts/build-road-graph.mjs` fetches the Overpass extract for the KL bbox, encodes the pinned CSR binary, writes routing/data/kl-roadgraph.v1.bin, prints node/edge counts + binary size + bbox, exits 0 - binary size > 80 MB (D-01 envelope) → exit 1 with the stats printed (the stats gate) + - binary size < 30 MB → printed WARNING naming D-01's expected band and A2's 18–50 MB forecast ("below the expected 30–80 MB band — verify the drivable-class filter") — advisory only; the ENFORCED gate stays > 1 MB, ≤ 80 MB - ways with highway in the drivable class set only: motorway, trunk, primary, secondary, tertiary, residential, unclassified, living_street (footway/cycleway/service/path excluded — Pitfall 1) - oneway=yes → flags bit0 on the reverse direction entry; maxspeed tags → edge seconds; class defaults: motorway 100, trunk 80, primary 60, secondary 50, tertiary 40, residential 30, unclassified 30, living_street 20 km/h - way name tags → the name pool (unnamed ways → UNNAMED_NAME_ID) @@ -135,7 +137,7 @@ names: u32 nameOffset[cnt+1] ++ u8 pool[] --> - a second run with the committed file present and `--check` green needs no refetch (artifact, not input) -Write `routing/scripts/build-road-graph.mjs` in the fetch-route-fixtures.mjs shell: header naming the D-01 boundary ("A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src, web/src, or routing/src imports it"), named constants (`OVERPASS_ENDPOINT = "https://overpass-api.de/api/interpreter"`, `REQUEST_TIMEOUT_MS = 300_000`, `USER_AGENT`, `MAX_BINARY_BYTES = 80 * 1024 * 1024`, `DRIVABLE_CLASSES` + `CLASS_SPEED_KMH` tables, `BBOX_MARGIN_DEG = 0.05`), bbox COMPUTED at run start from `@araharah/search/src/gazetteer.ts` getEntries() + `routing/test/fixtures/golden-pairs.json` pair endpoints ± margin (printed; Open Q 4's mechanical pinning), the Overpass query `way[highway~"^(motorway|trunk|primary|secondary|tertiary|residential|unclassified|living_street)$"](bbox); out geom;`, response → nodes deduplicated by rounded coord → edges with oneway/flags/speed/name → `encodeGraph` (import `../src/graph.ts`) → collect-then-write (committed file untouched on any failure, exit 1) → stats gate (size ≤ MAX_BINARY_BYTES else exit 1) → post-write summary + `next:` hint naming the upload command. `--check` mode decodes and re-prints stats. Also create `routing/data/.gitignore` containing `*.bin` (the binary is an ODbL-derived build artifact; committed artifacts are the script, docs, and the tiny test mini-graph). +Write `routing/scripts/build-road-graph.mjs` in the fetch-route-fixtures.mjs shell: header naming the D-01 boundary ("A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src, web/src, or routing/src imports it"), named constants (`OVERPASS_ENDPOINT = "https://overpass-api.de/api/interpreter"`, `REQUEST_TIMEOUT_MS = 300_000`, `USER_AGENT`, `MAX_BINARY_BYTES = 80 * 1024 * 1024`, `DRIVABLE_CLASSES` + `CLASS_SPEED_KMH` tables, `BBOX_MARGIN_DEG = 0.05`), bbox COMPUTED at run start from `@araharah/search/src/gazetteer.ts` getEntries() + `routing/test/fixtures/golden-pairs.json` pair endpoints ± margin (printed; Open Q 4's mechanical pinning), the Overpass query `way[highway~"^(motorway|trunk|primary|secondary|tertiary|residential|unclassified|living_street)$"](bbox); out geom;`, response → nodes deduplicated by rounded coord → edges with oneway/flags/speed/name → `encodeGraph` (import `../src/graph.ts`) → collect-then-write (committed file untouched on any failure, exit 1) → stats gate (size ≤ MAX_BINARY_BYTES else exit 1; size < 30 MB prints the expected-band warning per the behavior list) → post-write summary + `next:` hint naming the upload command. `--check` mode decodes and re-prints stats. Also create `routing/data/.gitignore` containing `*.bin` (the binary is an ODbL-derived build artifact; committed artifacts are the script, docs, and the tiny test mini-graph). Then RUN it (network fetch, minutes) and confirm the stats gate passes; record the printed counts in the plan summary. @@ -173,7 +175,7 @@ Then RUN it (network fetch, minutes) and confirm the stats gate passes; record t 1. `routing/src/http-source.ts`: `createHttpRoutingSource(opts: { endpoint: string; bearer: () => string | null; fetchImpl?: typeof fetch }): RoutingSource` — name "engine-a"; POST JSON {origin, destinationId}; Authorization only when bearer() returns non-null; response.json() → defensive shape check (copy fixture-source's isUsablePair style: every field type-checked, non-finite refused, try/catch → null); export from `routing/src/index.ts` alongside a header-note update (backend #2 live, ladder: fixture → engine-a → engine B documented post-v1). -2. `bot/src/routing/worker-router.ts`: module-scope `let graphP: Promise | null = null;` + `loadGraph(env)` — `env.ROAD_GRAPH.get(GRAPH_KEY)` with `GRAPH_KEY = "kl-roadgraph.v1.bin"`, `.then(o => o === null ? null : decodeGraph(await o.arrayBuffer()))`, catch → null (a corrupt binary degrades honest); memo never reset (per-isolate). `handleRoute(req, env): Promise`: Bearer claim (extract + getClaimByToken — reuse webhook.ts's exact helper posture), body-cap 4096 (`MAX_ROUTE_BODY_CHARS`), origin {lat,lng} numeric + inside graph.bbox (400 otherwise), `getEntryById(destinationId)` (import `@araharah/search/src/gazetteer.ts`) → null miss → 200 null; snapToGraph origin + destination → null miss → 200 null; routeAstar → null → 200 null; else build RouteResult `{ destinationId, origin, options: [{ geometry: { points: nodePath points, cumulativeDistanceM: deriveCumulativeDistanceM(points) }, summary: { distanceM, durationS }, steps: deriveSteps(...) }], backend: "engine-a" }` → 200 JSON. +2. `bot/src/routing/worker-router.ts`: module-scope `let graphP: Promise | null = null;` + EXPORTED `loadGraph(env)` — `env.ROAD_GRAPH.get(GRAPH_KEY)` with `GRAPH_KEY = "kl-roadgraph.v1.bin"`, `.then(o => o === null ? null : decodeGraph(await o.arrayBuffer()))`, catch → null (a corrupt binary degrades honest); memo never reset (per-isolate). Exporting loadGraph is deliberate: the cron arm (06-07 scheduled()) imports this SAME memo — one R2 GET per cold isolate regardless of which arm warms it; a second R2 loader anywhere in bot/src fails review. `handleRoute(req, env): Promise`: Bearer claim (extract + getClaimByToken — reuse webhook.ts's exact helper posture), body-cap 4096 (`MAX_ROUTE_BODY_CHARS`), origin {lat,lng} numeric + inside graph.bbox (400 otherwise), `getEntryById(destinationId)` (import `@araharah/search/src/gazetteer.ts`) → null miss → 200 null; snapToGraph origin + destination → null miss → 200 null; routeAstar → null → 200 null; else build RouteResult `{ destinationId, origin, options: [{ geometry: { points: nodePath points, cumulativeDistanceM: deriveCumulativeDistanceM(points) }, summary: { distanceM, durationS }, steps: deriveSteps(...) }], backend: "engine-a" }` → 200 JSON. 3. `bot/src/webhook.ts`: add the `/api/route` arm delegating to handleRoute (405 guard for non-POST). `bot/src/config.ts`: add `ROAD_GRAPH: R2Bucket; // D-01 graph binary — R2 binding, first in the repo (never a KV namespace — GE5)`. `bot/wrangler.jsonc`: add `"r2_buckets": [{ "binding": "ROAD_GRAPH", "bucket_name": "araharah-graphs" }]` with a law comment; kv_namespaces stays exactly one entry. 4. RE-PIN `bot/test/route-privacy.test.mjs` in the same change: RP1 → the /api/route arm must reference the Bearer-claim derivation (structural: the handler path calls the claim helper; a bare unauthenticated fetch-forward shape fails); no external routing hostnames (`grep` for non-Telegram/non-Cloudflare API hosts). RP2 → allow-list `@araharah/search` + `@araharah/routing` in bot/src imports; any other `@araharah/*` fails. Extend `bot/test/webhook.test.mjs` with the /api/route behavior block (R2 mini-graph fake + mini graph binary loaded from routing/test/fixtures/mini-graph.bin via fs). @@ -182,7 +184,7 @@ Then RUN it (network fetch, minutes) and confirm the stats gate passes; record t - routing/src/http-source.ts exports createHttpRoutingSource (name "engine-a"); index.ts re-exports it - - bot/src/routing/worker-router.ts: module-scope graphP memo (grep `graphP ??=` or equivalent), GRAPH_KEY = "kl-roadgraph.v1.bin", null on absent/corrupt graph + - bot/src/routing/worker-router.ts: module-scope graphP memo (grep `graphP ??=` or equivalent), exported loadGraph, GRAPH_KEY = "kl-roadgraph.v1.bin", null on absent/corrupt graph - webhook.test.mjs: 401 unauth, 413 cap, 400 malformed origin, 200-null on unknown dest / snap miss / absent graph, 200 RouteResult happy path via R2 fake, backend === "engine-a", options.length === 1 - route-privacy.test.mjs: RP1 (auth-bound routing) + RP2 (allow-list search+routing only) re-pinned and green; GE5 green (one kv_namespaces) - bot/src/config.ts carries the ROAD_GRAPH: R2Bucket line with a law comment @@ -247,7 +249,7 @@ Then RUN it (network fetch, minutes) and confirm the stats gate passes; record t - `npm run routing:test && npm test -w bot` green; both typechecks clean -- Real graph: `--check` green, size inside the 30–80 MB envelope, stats printed +- Real graph: `--check` green, size inside the enforced gate (1–80 MB; sub-30 MB prints the band warning), stats printed - CPU gate: p95 ≤ 8 ms printed (A1 retired) or escalation recorded - RP1/RP2 re-pinned green; GE5 (one KV namespace) still green diff --git a/.planning/phases/06-turn-by-turn-guidance-live-driving/06-05-PLAN.md b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-05-PLAN.md index 97ff0e9..3439292 100644 --- a/.planning/phases/06-turn-by-turn-guidance-live-driving/06-05-PLAN.md +++ b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-05-PLAN.md @@ -167,12 +167,11 @@ Output: glyphs + 5 builders + 2 guard modules + webapp contract members + string - task 2: Banner + list + guidance/confirm/trip-end card builders + shell.css - web/src/guidance/banner.ts, web/src/shell.css, web/test/guidance-surface.test.mjs + task 2: Banner + maneuver-list builders + gantry blink + web/src/guidance/banner.ts, web/test/guidance-surface.test.mjs - web/src/route/route-card.ts (THE builder law: actions interface first, createElement+textContent only, aria on interactive groups, instrument formats composed in code) - - web/src/ui/strip.ts + the strip section of web/src/shell.css (what [data-guidance="active"] compacts — layout only, strip.ts logic untouched) - - .planning/phases/06-turn-by-turn-guidance-live-driving/06-UI-SPEC.md sections: Shell Layout, Maneuver Banner, Maneuver List, Guidance Card, Abandon-Route Confirm Card, Trip-Ended Card, State Coexistence + - .planning/phases/06-turn-by-turn-guidance-live-driving/06-UI-SPEC.md sections: Maneuver Banner, Maneuver List, State Coexistence (the banner's four registers + calm-state slot swaps) - web/test/route-surface.test.mjs RS5/RS9 (the structural-gate style guidance-surface mirrors) @@ -180,6 +179,36 @@ Output: glyphs + 5 builders + 2 guard modules + webapp contract members + string - the banner root is a