chore(planning): phase 5/6/7 planning artifacts + milestone-completion state sync
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user