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

14 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
05-routing-route-display 03 execute 1
bot/src/kv/guidance-sessions.ts
bot/test/guidance-envelope.test.mjs
true
decisions_implemented truths artifacts key_links
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
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
path provides contains
bot/src/kv/guidance-sessions.ts the frozen Phase 6 contract: GuidanceSessionEnvelope + GuidanceRouteSnapshot types, GUIDANCE_SESSION_TTL_SECONDS = 7200, the guidance:<id> key-scheme doc header export interface GuidanceSessionEnvelope
path provides contains
bot/test/guidance-envelope.test.mjs the GE1–GE7 types-only gates (field freeze, inline snapshot, TTL constant, no-runtime-behavior, no new namespace, no Env field, no premature consumer) GE1
from to via pattern
bot/src/kv/guidance-sessions.ts bot/src/kv/sessions.ts the SessionClaim 'entire persisted claim surface — a 5th property means drift' law, applied to the envelope's field freeze SessionClaim
from to via pattern
bot/test/guidance-envelope.test.mjs bot/src/kv/guidance-sessions.ts source-text + structural gates pin the types-only boundary until Phase 6's service replaces it 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).

<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_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 The frozen contract (verbatim — Phase 6 consumes these exact names):
/** ~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):

/*
 * 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
 */

<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>
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 <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> 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`.

<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>