--- phase: 05-routing-route-display plan: 03 type: execute wave: 1 depends_on: [] files_modified: - bot/src/kv/guidance-sessions.ts - bot/test/guidance-envelope.test.mjs autonomous: true requirements: [] must_haves: decisions_implemented: - "D-06: the guidance_sessions envelope is a TYPES-ONLY frozen contract — code types + docs, NO KV namespace, NO service, NO runtime writes; Phase 6 builds the service on the frozen shape" - "D-07: the route snapshot lives INLINE in the envelope record — one key, one TTL (~2h); nothing outlives 2h, nothing reconstructable after expiry" truths: - "GuidanceSessionEnvelope has EXACTLY four fields: id, chat_id, route_snapshot, created_at — a 5th property means drift (the SessionClaim 'entire persisted claim surface' law)" - "route_snapshot is an INLINE interface (GuidanceRouteSnapshot), never a reference/id — grep-gated: no snapshotRef/snapshot_ref token exists in bot/src" - "GUIDANCE_SESSION_TTL_SECONDS === 7200 — the ~2h TTL exists as a documented constant awaiting its Phase 6 consumer, pinned by test" - "guidance-sessions.ts contains zero runtime behavior: no KvNamespace reference, no .put/.get/.delete, no exported function — types + one constant + the key-scheme doc only" - "bot/wrangler.jsonc still carries EXACTLY ONE kv namespace (chat_sessions) and Env carries NO guidance field — the grep gates prove the types-only boundary mechanically" artifacts: - path: "bot/src/kv/guidance-sessions.ts" provides: "the frozen Phase 6 contract: GuidanceSessionEnvelope + GuidanceRouteSnapshot types, GUIDANCE_SESSION_TTL_SECONDS = 7200, the guidance: key-scheme doc header" contains: "export interface GuidanceSessionEnvelope" - path: "bot/test/guidance-envelope.test.mjs" provides: "the GE1–GE7 types-only gates (field freeze, inline snapshot, TTL constant, no-runtime-behavior, no new namespace, no Env field, no premature consumer)" contains: "GE1" key_links: - from: "bot/src/kv/guidance-sessions.ts" to: "bot/src/kv/sessions.ts" via: "the SessionClaim 'entire persisted claim surface — a 5th property means drift' law, applied to the envelope's field freeze" pattern: "SessionClaim" - from: "bot/test/guidance-envelope.test.mjs" to: "bot/src/kv/guidance-sessions.ts" via: "source-text + structural gates pin the types-only boundary until Phase 6's service replaces it" pattern: "GE1" --- Freeze the Phase 6 guidance-session contract NOW as a types-only module: the `guidance_sessions` envelope (id, chat ref, inline route snapshot, created_at) with its ~2h TTL constant and `guidance:` key-scheme documentation — no KV namespace, no service, no runtime writes. Purpose: ROADMAP §Phase 5 notes bind the GUID-05/06 upstream design obligation here ("the ephemeral guidance_sessions KV envelope (id, chat ref, route snapshot ref, ~2h TTL) is designed here so the backgrounded turn-card service in Phase 6 has its session contract already defined"). The pivot (D-06) sharpens "route snapshot ref" into an INLINE snapshot (D-07 — one key, one TTL, nothing reconstructable after expiry) and pins the whole thing as types + docs. Phase 6 implements the service against this frozen shape without renegotiation. Output: `bot/src/kv/guidance-sessions.ts` (types + TTL constant + key-scheme header) and `bot/test/guidance-envelope.test.mjs` (GE1–GE7 gates proving the types-only boundary mechanically). @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md @AGENTS.md @.planning/phases/05-routing-route-display/05-CONTEXT.md @bot/src/kv/sessions.ts @bot/src/config.ts @bot/wrangler.jsonc @bot/test/privacy.test.mjs @routing/src/types.ts The frozen contract (verbatim — Phase 6 consumes these exact names): ```typescript /** ~2h (D-06) — the envelope's every future write carries THIS value as its * expirationTtl (Phase 6's privacy grep gate extends the TTL-law to it). */ export const GUIDANCE_SESSION_TTL_SECONDS = 7200; /** * The route snapshot — INLINE in the envelope (D-07): one KV key, one TTL. * Nothing outlives 2h; nothing is reconstructable after expiry. Scoped to * what a chat turn-card needs (GUID-05): the selected route's headline data * + its geometry for the map snapshot render + the trip-start anchor for * progress math on resume (GUID-06). */ export interface GuidanceRouteSnapshot { /** The gazetteer id of the trip's destination. */ destinationId: string; /** The destination's display name (verbatim gazetteer name). */ destinationName: string; /** Selected route summary (meters / seconds). */ distanceM: number; durationS: number; /** The SELECTED route's geometry, polyline6-encoded — the routing * workspace's storage format (05-01), never a re-serialized array. */ geometry: string; /** Trip start, seconds-since-epoch — the resume-progress anchor (GUID-06). */ startedAtSec: number; } /** * The guidance_sessions envelope — Phase 6's frozen contract (D-06). * EXACTLY these four fields: a 5th property means drift (the SessionClaim * law, bot/src/kv/sessions.ts). * * Key scheme: guidance: → JSON GuidanceSessionEnvelope * Every write MUST carry expirationTtl: GUIDANCE_SESSION_TTL_SECONDS. */ export interface GuidanceSessionEnvelope { /** Opaque session id — crypto.randomUUID() at Phase 6 mint (T-02-05 precedent). */ id: string; /** The Telegram chat the turn-cards push to (GUID-05). */ chat_id: number; /** The INLINE route snapshot (D-07) — never a reference or a second key. */ route_snapshot: GuidanceRouteSnapshot; /** Envelope creation, seconds-since-epoch. */ created_at: number; } ``` Module header contract (sessions.ts style — states the types-only law): ```typescript /* * guidance-sessions.ts — the FROZEN Phase 6 contract, types-only (D-06/D-07). * ------------------------------------------------------------------------ * THIS MODULE SHIPS NO BEHAVIOR. No KVNamespace binding, no Env field, no * wrangler namespace, no runtime function — the envelope is designed here * (ROADMAP §Phase 5 GUID-05/06 upstream obligation) so Phase 6 builds its * service on a frozen shape. Phase 6's service owns every write; every * write carries expirationTtl: GUIDANCE_SESSION_TTL_SECONDS (~2h) and the * snapshot is INLINE (one key, one TTL) — nothing outlives 2h, nothing is * reconstructable after expiry (D-07, the location-pin TTL precedent). * * Key scheme: guidance: → JSON GuidanceSessionEnvelope */ ``` Verified in this session — do not re-derive: - `bot/src/kv/sessions.ts` establishes the "entire persisted claim surface — a 5th property means drift" field-freeze law (its `SessionClaim` doc) and the every-write-carries-TTL grep-gate pattern (`bot/test/privacy.test.mjs` PG1). - `bot/wrangler.jsonc` `kv_namespaces` carries EXACTLY ONE entry (`chat_sessions`, id `59f58a54bc44483e89872b20a005a5eb`); `bot/src/config.ts` `Env` carries exactly `BOT_TOKEN`, `WEBHOOK_SECRET`, `chat_sessions`, `MINI_APP_URL`. - `bot/test/runner.mjs` auto-discovers `*.test.mjs` — a new suite file is picked up with zero registration. - The routing workspace's geometry storage format is polyline6 (05-01) — the snapshot's `geometry` field references that format by name in its doc comment. ## Trust Boundaries | Boundary | Description | |----------|-------------| | This module → the Worker runtime | A types-only module that accidentally grew behavior (a helper, a KV call) would create an unreviewed write path outside Phase 6's privacy gates — the threat is PREMATURE PERSISTENCE. | | The frozen shape → Phase 6's service | Contract drift between what Phase 5 froze and what Phase 6 builds silently widens the persisted surface (a 5th field, a snapshot reference instead of an inline record). | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-05-08 | Tampering / Information Disclosure | Premature persistence — a half-built guidance write path landing before Phase 6's TTL/privacy gates exist | mitigate | GE4 pins the module to types + one constant (comment-stripped scan: zero `KvNamespace`, `.put(`, `.get(`, `.delete(`, `export function`, `export async function`); GE5 pins wrangler.jsonc to exactly one kv namespace; GE6 pins `Env` to its four existing fields; GE7 pins zero non-test imports of the module. The contract CANNOT acquire a runtime shadow this phase. | | T-05-08b | Repudiation (retention) | The envelope silently outliving its ~2h promise (D-07) | mitigate | GE1 pins `GUIDANCE_SESSION_TTL_SECONDS === 7200`; GE3 pins the INLINE snapshot (no `snapshotRef`/`snapshot_ref`/`snapshot_ref:` token anywhere in bot/src) — one key, one TTL, nothing reconstructable after expiry; the Phase 6 service extends the existing privacy TTL grep gate to `guidance:` writes. | | T-05-08c | Tampering | Contract drift before Phase 6 (field added/renamed, geometry format changed) | mitigate | GE2 pins the envelope field set EXACTLY (`["id","chat_id","route_snapshot","created_at"]`) and the snapshot field set EXACTLY (`["destinationId","destinationName","distanceM","durationS","geometry","startedAtSec"]`) — any drift reddenens the suite. | task 1: the frozen envelope types + the types-only gate suite bot/src/kv/guidance-sessions.ts, bot/test/guidance-envelope.test.mjs - `bot/src/kv/sessions.ts` (the field-freeze law + TTL-constant style + module header contract) - `bot/src/config.ts` (the Env contract GE6 pins — the file being modified is NOT this one; read it to assert its shape) - `bot/wrangler.jsonc` (the kv_namespaces array GE5 pins) - `bot/test/privacy.test.mjs` (the comment-stripping grep-gate style GE4 copies) - `routing/src/types.ts` (the polyline6 storage format the snapshot's geometry field cites) **`bot/src/kv/guidance-sessions.ts`** — implement the `` block EXACTLY: the module header (types-only law, key scheme, D-06/D-07 citations), `GUIDANCE_SESSION_TTL_SECONDS = 7200`, `GuidanceRouteSnapshot` (6 fields), `GuidanceSessionEnvelope` (4 fields). NOTHING ELSE — no imports, no functions, no class, no default export. **`bot/test/guidance-envelope.test.mjs`** — RED-first, lazy-import style. Cases: - `GE1 TTL constant` — `(await import("../src/kv/guidance-sessions.ts")).GUIDANCE_SESSION_TTL_SECONDS === 7200` (D-06's ~2h). - `GE2 field freeze` — compile-time shape check via a typed probe: import the module under Node type-stripping and assert `Object.keys()`-equivalent surface by reading the source text: the `GuidanceSessionEnvelope` interface block contains EXACTLY the four fields `id`, `chat_id`, `route_snapshot`, `created_at` (regex the interface body between `interface GuidanceSessionEnvelope {` and the closing `}`; field lines match `/^\s{2}\w+:/`); same for `GuidanceRouteSnapshot` → exactly `destinationId`, `destinationName`, `distanceM`, `durationS`, `geometry`, `startedAtSec`. A 5th property reddenens this gate (the SessionClaim law). - `GE3 inline snapshot` — the source contains `route_snapshot: GuidanceRouteSnapshot` (typed inline, not `string`/`number` reference) AND bot/src contains ZERO occurrences of `snapshotRef` or `snapshot_ref` (D-07's one-key-one-TTL law). - `GE4 types-only` — strip comments from `guidance-sessions.ts` (the privacy.test.mjs pattern) and assert ZERO of: `KvNamespace`, `.put(`, `.get(`, `.delete(`, `export function`, `export async function`, `import ` (T-05-08: the module ships NO behavior). - `GE5 no new namespace` — parse `bot/wrangler.jsonc` (strip `//` line comments, JSON.parse) and assert `kv_namespaces.length === 1` with binding `chat_sessions`. - `GE6 no Env field` — `bot/src/config.ts` source contains NO `guidance` token (the Env contract gains nothing this phase). - `GE7 no premature consumer` — grep `bot/src` for `guidance-sessions` imports: ZERO matches (only the test imports it — Phase 6 builds the consumer). **Acceptance:** `npm test -w bot` exits 0 with `PASS GE1`…`PASS GE7` (plus all existing bot suites still green); `npm run bot:typecheck` exits 0 (the new module must typecheck — it lives in the bot workspace's tsconfig include). cd /home/C-Fu/araharah && npm test -w bot 2>&1 | tail -12 ; npm run bot:typecheck 2>&1 | tail -4 - `npm test -w bot` exits 0 and the output contains `PASS GE1` through `PASS GE7` - `npm run bot:typecheck` exits 0 - `grep -q 'export interface GuidanceSessionEnvelope' bot/src/kv/guidance-sessions.ts` exits 0 AND `grep -q 'export interface GuidanceRouteSnapshot' bot/src/kv/guidance-sessions.ts` exits 0 - `grep -q 'GUIDANCE_SESSION_TTL_SECONDS = 7200' bot/src/kv/guidance-sessions.ts` exits 0 - `grep -q 'guidance:' bot/src/kv/guidance-sessions.ts` exits 0 (the key scheme is documented) - `grep -rc 'KvNamespace' bot/src/kv/guidance-sessions.ts | head -1` returns 0 (no runtime binding anywhere in the file, comments included — the word never appears) - `grep -q 'route_snapshot: GuidanceRouteSnapshot' bot/src/kv/guidance-sessions.ts` exits 0 (inline, D-07) The guidance_sessions envelope exists as a frozen types-only contract with its ~2h TTL constant, inline route snapshot, key-scheme documentation, and seven mechanical gates proving nothing runtime-shaped leaked in. 1. `npm test -w bot` — all existing suites + GE1–GE7 green. 2. `npm run bot:typecheck` — exit 0 (the new module is inside the bot tsconfig's include). 3. `bot/wrangler.jsonc` unchanged (still exactly one kv namespace); `bot/src/config.ts` unchanged. 4. `grep -rn "guidance" bot/src --include="*.ts" -l` → only `bot/src/kv/guidance-sessions.ts`. - The Phase 6 contract is frozen in code: envelope + inline snapshot + TTL constant + key scheme. - Seven gates prove the types-only boundary (no behavior, no namespace, no Env field, no consumer). - Zero existing bot files modified.