14 KiB
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 |
|
true |
|
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.tsestablishes the "entire persisted claim surface — a 5th property means drift" field-freeze law (itsSessionClaimdoc) and the every-write-carries-TTL grep-gate pattern (bot/test/privacy.test.mjsPG1).bot/wrangler.jsonckv_namespacescarries EXACTLY ONE entry (chat_sessions, id59f58a54bc44483e89872b20a005a5eb);bot/src/config.tsEnvcarries exactlyBOT_TOKEN,WEBHOOK_SECRET,chat_sessions,MINI_APP_URL.bot/test/runner.mjsauto-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
geometryfield 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> |
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 assertObject.keys()-equivalent surface by reading the source text: theGuidanceSessionEnvelopeinterface block contains EXACTLY the four fieldsid,chat_id,route_snapshot,created_at(regex the interface body betweeninterface GuidanceSessionEnvelope {and the closing}; field lines match/^\s{2}\w+:/); same forGuidanceRouteSnapshot→ exactlydestinationId,destinationName,distanceM,durationS,geometry,startedAtSec. A 5th property reddenens this gate (the SessionClaim law).GE3 inline snapshot— the source containsroute_snapshot: GuidanceRouteSnapshot(typed inline, notstring/numberreference) AND bot/src contains ZERO occurrences ofsnapshotReforsnapshot_ref(D-07's one-key-one-TTL law).GE4 types-only— strip comments fromguidance-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— parsebot/wrangler.jsonc(strip//line comments, JSON.parse) and assertkv_namespaces.length === 1with bindingchat_sessions.GE6 no Env field—bot/src/config.tssource contains NOguidancetoken (the Env contract gains nothing this phase).GE7 no premature consumer— grepbot/srcforguidance-sessionsimports: 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.
<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>