Files
arah2/.planning/phases/06-turn-by-turn-guidance-live-driving/06-03-SUMMARY.md
T
araharah-executor 371b2a0ad7 docs(06-03): complete guidance envelope service + /api/guidance plan
- 2/2 tasks TDD RED->GREEN: envelope service (0cfa0bd/c87dc7d) +
  /api/guidance endpoints (212aa57/7875ed4)
- GE4-GE7 + RP2 re-pinned with their invalidating changes; GE1-GE3 frozen
- 90/90 bot tests green; typecheck clean
2026-10-24 13:08:05 +08:00

180 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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:<id> 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:<id> 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=<uuid> — claim-owned envelope read; foreign/absent indistinguishable 404 (T-06-02b)"
- "DELETE /api/guidance?id=<uuid> — 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:<id>` 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 `<threat_model>`: 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:<id>` 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).