Files
arah2/.planning/phases/05-routing-route-display/05-03-PLAN.md
T

212 lines
14 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: 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:<id> 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"
---
<objective>
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:<id>` 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).
</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
@bot/src/kv/sessions.ts
@bot/src/config.ts
@bot/wrangler.jsonc
@bot/test/privacy.test.mjs
@routing/src/types.ts
<interfaces>
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:<id> → 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:<id> → JSON GuidanceSessionEnvelope
*/
```
</interfaces>
<environment_facts>
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.
</environment_facts>
<threat_model>
## 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. |
</threat_model>
<tasks>
<task type="auto" tdd="true">
<name>task 1: the frozen envelope types + the types-only gate suite</name>
<files>bot/src/kv/guidance-sessions.ts, bot/test/guidance-envelope.test.mjs</files>
<read_first>
- `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)
</read_first>
<action>
**`bot/src/kv/guidance-sessions.ts`** — implement the `<interfaces>` 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).
</action>
<verify>
<automated>cd /home/C-Fu/araharah && npm test -w bot 2>&1 | tail -12 ; npm run bot:typecheck 2>&1 | tail -4</automated>
</verify>
<acceptance_criteria>
- `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:<id>' 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)
</acceptance_criteria>
<done>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.</done>
</task>
</tasks>
<verification>
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`.
</verification>
<success_criteria>
- 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.
</success_criteria>