diff --git a/.planning/phases/06-turn-by-turn-guidance-live-driving/06-03-SUMMARY.md b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-03-SUMMARY.md new file mode 100644 index 0000000..d598522 --- /dev/null +++ b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-03-SUMMARY.md @@ -0,0 +1,179 @@ +--- +phase: 06-turn-by-turn-guidance-live-driving +plan: 03 +subsystem: bot +tags: [kv, guidance-sessions, envelope-service, ttl, api-endpoints, bearer-claim, privacy, tdd, gate-repin] + +# Dependency graph +requires: + - phase: 05-routing-route-display + plan: 03 + provides: the FROZEN GuidanceSessionEnvelope/GuidanceRouteSnapshot shapes + GUIDANCE_SESSION_TTL_SECONDS + the guidance: key scheme (implemented, never redefined — GE1/GE2/GE3 unchanged) + - phase: 05-routing-route-display + plan: 01 + provides: the polyline6 codec (decodePolyline6) the Worker now validates snapshot geometry with + - phase: 02 + provides: the Bearer-claim precedent (handleLocation posture), the WR-01 pre/post-read body-cap pattern, getClaimByToken +provides: + - "mintGuidanceSession / getGuidanceSession / deleteGuidanceSession — the TTL-only KV service over the frozen envelope (chat_sessions binding, guidance: keys, 7200 s)" + - "POST /api/guidance — mint a guidance session; chat_id from the Bearer claim ONLY (body chat_id structurally ignored, T-06-02); hand-rolled snapshot validation incl. polyline6 ≥2-point decode" + - "GET /api/guidance?id= — claim-owned envelope read; foreign/absent indistinguishable 404 (T-06-02b)" + - "DELETE /api/guidance?id= — D-14's server half: the active trip-end KV sweep" + - "RP2 re-pin — @araharah/routing allowlist in bot/src: exactly the polyline6 codec" +affects: [06 web guidance flows (POST on minimize), plan-07 cron (defensive reads + turn-card push), D-14 trip-end flow] + +# Tech tracking +tech-stack: + added: ["@araharah/routing:* (bot workspace dep — first routing import into the Worker, allowlisted to the codec)"] + patterns: + - "Defensive-read KV service: exact-key-set + type + finite/range validation on every read (the location-pin law extended to a 4+6-field nested record)" + - "UUID key-guard before any KV touch (V5: a malformed id never becomes arbitrary-key probing)" + - "Resolve-then-own authorization for GET/DELETE (claim comparison AFTER read, BEFORE mutate)" + - "Gate re-pin in the same commit as the invalidating change (GE4/GE7 with task 1; RP2 with the routing import)" + +key-files: + created: [] + modified: + - bot/src/kv/guidance-sessions.ts + - bot/src/webhook.ts + - bot/package.json + - bot/test/guidance-envelope.test.mjs + - bot/test/webhook.test.mjs + - bot/test/privacy.test.mjs + - bot/test/route-privacy.test.mjs + - package-lock.json + +key-decisions: + - "GE7's re-pin is a SHAPE gate, not a presence gate: bot/src MAY import the service but only via NAMED imports of the service surface (mint/get/delete, TTL const, the two types) — vacuous-green until the first consumer lands in this plan's webhook task, teeth grow with the consumer tree" + - "RP2 re-pin scope kept minimal: exactly @araharah/routing/src/polyline.ts allowlisted; every other routing subpath still banned — plan 06-01 (engine A worker-router) widens it under its own responsibility per PATTERNS" + - "deleteGuidanceSession applies the same UUID guard as reads (silent no-op on malformed ids) — no key is ever constructed from unvalidated input, read or delete" + - "GET/DELETE 404 bodies are {error:'not_found'} JSON so foreign and absent stay byte-indistinguishable (no existence oracle, T-06-02b)" + - "validateSnapshot builds a NEW six-field object field-by-field — extra body properties (chat_id included) can never reach storage even if future validation relaxes" + +patterns-established: + - "Fourth Bearer-claim arm copies the handleLocation posture verbatim (regex charset gate → getClaimByToken → category-only 401); guidance adds method dispatch POST/GET/DELETE under one path" + +requirements-completed: [GUID-05] + +# Metrics +duration: 12min +completed: 2026-10-24 +--- + +# Phase 6 Plan 3: Guidance Envelope Service + /api/guidance Endpoints Summary + +**The Phase 5-frozen `GuidanceSessionEnvelope` went LIVE (D-11's precondition): a TTL-only mint/get/delete KV service beneath the byte-identical shape, plus Bearer-gated POST/GET/DELETE on `/api/guidance` — chat identity from the claim only, every write TTL'd at 7200 s on its call line, GE4–GE7 and RP2 re-pinned in the same commits that changed their premises.** + +## Performance + +- **Duration:** 12 min +- **Started:** 2026-10-24T04:54:57Z +- **Completed:** 2026-10-24T05:06:41Z +- **Tasks:** 2/2 (both TDD RED→GREEN) +- **Files modified:** 8 (6 bot workspace + root package-lock.json; zero files created — the service grew inside the frozen contract module) + +## Accomplishments + +- **The envelope service exists** (task 1): `mintGuidanceSession` (crypto.randomUUID id, claim-derived chat_id argument, ONE put at `guidance:` with same-line `expirationTtl: GUIDANCE_SESSION_TTL_SECONDS`), `getGuidanceSession` (UUID-shape guard BEFORE any KV touch; exact 4-envelope + 6-snapshot key-set check; type + finite/range validation — a corrupt record reads as null and can never reach the plan-07 cron), `deleteGuidanceSession` (active sweep — D-14's server half). The frozen interfaces are byte-identical (GE2 green). +- **The endpoints are live** (task 2): `POST /api/guidance` (WR-01 cap at `MAX_GUIDANCE_BODY_CHARS = 8192` with the pre/post-read double check; hand-rolled validation — slug `destinationId` per the gazetteer's own `ENTRY_ID_PATTERN`, bounded name, finite non-negative metrics, integer `startedAtSec`, geometry that `decodePolyline6` decodes to ≥ 2 points) → 201 `{id}`; `GET`/`DELETE` resolve-then-own (`envelope.chat_id === claim.chat_id` else an indistinguishable 404); other methods 405. +- **The claim-only identity law is proven, not asserted** (T-06-02): GW4's injection probe posts `chat_id: 999` in the body alongside the claim's `101` — the stored envelope carries 101. T-06-02b: GW7/GW10 prove a foreign token can neither read nor delete another chat's record, and that the record survives the foreign attempt. +- **Every gate re-pinned WITH its invalidating change**: GE4 (types-only ban → service-export assert) and GE7 (total consumer ban → named-imports-of-service-surface) in task 1's RED; RP2 (total routing ban → polyline6-codec allowlist) in task 2's GREEN. GE1/GE2/GE3 unchanged and green; GE5 (exactly ONE kv namespace) and GE6 (no Env guidance field) held — the service reuses the existing `chat_sessions` binding. +- **PG1 now covers the new write path** (`kv/guidance-sessions.ts` in storageModules) — and proved itself immediately: it FAILED my first GREEN draft because the `put` had `expirationTtl` on a continuation line; the same-line law forced the fix before commit. +- **Full suite: 90 passed, 0 failed** (74 pre-existing + 5 GE re-pin/behavioral net-new + 11 GW); `bot:typecheck` exit 0. + +## Task Commits + +1. **task 1: envelope service + GE4–GE7 re-pins + PG1** + - `0cfa0bd` test(06-03): re-pin GE4/GE7 to the live service + behavioral envelope suite + PG1 storage list (RED) + - `c87dc7d` feat(06-03): guidance envelope service — TTL-only mint/get/delete beneath the frozen shape (GREEN) +2. **task 2: /api/guidance POST + GET + DELETE** + - `212aa57` test(06-03): guidance endpoint suite GW1-GW11 (RED) + - `7875ed4` feat(06-03): /api/guidance POST/GET/DELETE — Bearer-claim gated, TTL-only service beneath (GREEN) + +**Plan metadata:** committed separately (docs, force-added — `.planning/` is gitignored). + +## TDD Gate Compliance + +- Task 1 RED `0cfa0bd`: GE4 + GS1–GS5 fail on the absent service (6 failures, right reasons); GE1–GE3 pass unchanged; GE5–GE7 green-by-construction (documented in the suite header). +- Task 1 GREEN `c87dc7d`: 79/79 (PG1 caught the split-line TTL in the draft — fixed pre-commit). +- Task 2 RED `212aa57`: 10/11 GW fail on the absent endpoint; GW8 green-by-construction (the catch-all already 404s absent ids — its ownership teeth live in GW7/GW10). +- Task 2 GREEN `7875ed4`: 90/90 + typecheck exit 0. +- Git sequence: test→feat→test→feat — both RED and GREEN gates present for both tasks. + +## Files Created/Modified + +- `bot/src/kv/guidance-sessions.ts` — header rewritten to the shape-frozen/service-owns-every-write law; `GuidanceSessionKv` seam (structural — no KvNamespace type token, no new binding), UUID pattern, exact-key constants, mint/get/delete + `isEnvelopeValid`. +- `bot/src/webhook.ts` — `/api/guidance` route arm + `handleGuidance` (method dispatch, Bearer claim, WR-01 double check, mint/resolve-own/delete) + `validateSnapshot` + category-only response helpers; header route table updated. +- `bot/package.json` + root `package-lock.json` — `@araharah/routing: "*"` workspace dep (linked via npm install). +- `bot/test/guidance-envelope.test.mjs` — header rewritten (frozen vs re-pinned gate table); GE4/GE7 re-pinned; GS1–GS5 behavioral suite on a Map-fake KV with call-counting (proves the UUID guard never touches KV). +- `bot/test/webhook.test.mjs` — GW1–GW11: 401 classes, 413 pre/post-read, 10 malformed classes with per-class category-log + zero-write asserts, happy-path 201 + TTL + injection probe + extra-field drop, own/foreign GET/DELETE, survival asserts, 405. Claims minted through the real `mintSession` path. +- `bot/test/privacy.test.mjs` — PG1 storageModules += `kv/guidance-sessions.ts`. +- `bot/test/route-privacy.test.mjs` — RP2 re-pinned to the codec allowlist; RP1 unchanged. + +## Decisions Made + +- **GE7 as a shape gate (not a presence gate)** — the plan asked GE7 to assert consumers "MAY import" the service. A permission can't fail, so the gate enforces the permission's SHAPE: every `guidance-sessions` import in bot/src must be a named import of the service surface (default/namespace/side-effect forms fail via a specifier-count cross-check). Vacuous-green until the webhook import landed in task 2; documented in the gate. +- **RP2's allowlist stays minimal** — PATTERNS assigns the RP2 re-pin to "the plan adding the routing import"; that's this plan for the codec. Engine A's `worker-router` (06-01) widens the allowlist under its own plan. RP1 (no `/api/route` proxy) untouched — the guidance path name can't collide. +- **Delete mirrors the read guard** — the plan's behavior list guards only `get`; delete gets the same UUID check so no KV key is ever built from unvalidated input on any path. +- **404 responses are JSON `{error:"not_found"}`** — matching the endpoint's JSON error vocabulary while keeping foreign/absent byte-identical. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking gate] RP2 re-pin in `bot/test/route-privacy.test.mjs`** +- **Found during:** task 2 (planning — the conflict was visible at read-first time) +- **Issue:** the plan adds `@araharah/routing` to bot and imports `decodePolyline6` in `webhook.ts`, but `route-privacy.test.mjs` RP2 (Phase 5, 05-05) banned EVERY `@araharah/routing` specifier in bot/src — the plan's `files_modified` omitted the gate file, and GREEN would have shipped with a red suite. +- **Fix:** re-pinned RP2 to an explicit allowlist (exactly `@araharah/routing/src/polyline.ts`), shipping in the same commit as the import, per the plan's own gate-re-pin law and PATTERNS' explicit assignment of this re-pin to "the plan adding the routing import". Header rewritten to state the narrowed premise. +- **Files modified:** bot/test/route-privacy.test.mjs +- **Commit:** 7875ed4 + +**2. [Rule 1 - Bug] PG1 caught a split-line `expirationTtl` in the task 1 GREEN draft** +- **Found during:** task 1 GREEN verification +- **Issue:** the mint `put` wrapped `expirationTtl` onto its own line — legal TS, illegal under the same-line TTL law PG1 enforces (79 passed, 1 failed). +- **Fix:** single-line put with same-line `expirationTtl: GUIDANCE_SESSION_TTL_SECONDS`. +- **Files modified:** bot/src/kv/guidance-sessions.ts +- **Commit:** c87dc7d (fixed before the commit landed) + +**3. [Rule 1 - Bug] GW3/GW4/GW5 counted the claim-mint's session writes as guidance writes** +- **Found during:** task 2 GREEN verification +- **Issue:** the tests mint claims through real `mintSession` (2 TTL writes) into the same fake KV, then asserted absolute `ttlLog` lengths — off by the claim-mint writes. +- **Fix:** snapshot `writesBefore` after claim minting; assert deltas. +- **Files modified:** bot/test/webhook.test.mjs +- **Commit:** 7875ed4 (fixed before the commit landed) + +## Known Stubs + +None — every exported function is fully wired: the service writes/reads/deletes real KV records; the endpoints dispatch real traffic through the service. The cron consumer (plan 07) and the web POST caller (06-xx) are future consumers of THIS plan's surface, by design — not stubs. + +## Threat Surface Scan + +No new surface beyond the plan's ``: the three register entries (T-06-02 claim-only mint + unguessable ids; T-06-02b resolve-then-own 404s; T-06-04 category-only logs + TTL'd writes + active delete) are all implemented with gate coverage (GW4/GW7/GW10/GW1/GW3/GW5, GS1/GS4). The DoS row's 8192 cap with the pre/post-read double check is pinned by GW3. One boundary note, already gate-covered: `webhook.ts` now imports exactly one routing-workspace module (the pure polyline6 codec) — RP2's allowlist is the standing control, RP1's no-route-proxy ban is unchanged. No threat flags. + +## Issues Encountered + +- Pre-existing modified `.planning/**` files in the working tree (orchestrator-owned: PROJECT/REQUIREMENTS/ROADMAP/STATE/config + 06-01/06-07 PLANs) were left untouched and excluded from every commit. STATE.md and ROADMAP.md were not written (orchestrator-owned per dispatch). +- Node lives under nvm in this environment — verification commands sourced `~/.nvm/nvm.sh` first (the 05-01/05-03 precedent). No project impact. + +## User Setup Required + +None — no new bindings, no new Env fields, no deploy in this plan (wave 3/4 or close-out owns deploy). The endpoints reuse the existing `chat_sessions` KV namespace; the `@araharah/routing` link is a workspace-internal dependency resolved by `npm install`. + +## Next Phase Readiness + +- **Plan 06 (web flows):** POST the envelope on minimize (D-11 "envelope write timing" discretion honored — mint is one endpoint call); DELETE on trip end (D-14's client half now has its server counterpart). +- **Plan 07 (cron):** reads are defensively validated (`getGuidanceSession` null-classes); `guidance:` keys + 7200 s TTL bound the scan surface; the cron NEVER needs to trust record shape. +- **GE gates:** GE1/GE2/GE3 remain the frozen-shape tripwire; GE5/GE6 hold until a plan legitimately adds a binding class or Env field; GE7 grows teeth with each new consumer import automatically. +- **RP2:** engine A's worker-router plan (06-01) owns the next allowlist widening. + +--- +*Phase: 06-turn-by-turn-guidance-live-driving* +*Completed: 2026-10-24* + +## Self-Check: PASSED + +- All 6 modified bot files + package-lock.json verified in the four plan commits (git log + diff-filter=D count 0 — no accidental deletions). +- `npm test -w bot`: 90 passed, 0 failed (GE1–GE7 + GS1–GS5 + GW1–GW11 + PG1/PG2 + RP1/RP2 + all 74 pre-existing). `npm run bot:typecheck`: exit 0. +- Acceptance greps verified: `expirationTtl: GUIDANCE_SESSION_TTL_SECONDS` in guidance-sessions.ts (×1 write + docs); `MAX_GUIDANCE_BODY_CHARS = 8192` in webhook.ts; all guidance `console.` lines are literal category strings (`\[guidance\] rejected: \w+`), zero `${…}` interpolation; exactly one `@araharah/routing` specifier in bot/src (the allowlisted codec). +- Injection probe (GW4): body `chat_id: 999` stored as claim's `101`. +- STATE.md and ROADMAP.md untouched (orchestrator-owned writes).