diff --git a/.planning/phases/06-turn-by-turn-guidance-live-driving/06-07-SUMMARY.md b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-07-SUMMARY.md new file mode 100644 index 0000000..d727754 --- /dev/null +++ b/.planning/phases/06-turn-by-turn-guidance-live-driving/06-07-SUMMARY.md @@ -0,0 +1,154 @@ +--- +phase: 06-turn-by-turn-guidance-live-driving +plan: 07 +subsystem: bot-cron +tags: [cron-turn-cards, scheduled-handler, d11-cadence, d12-street-bearing, d13-honesty, kv-write-budget, committed-glyphs, sharp, tdd, continuation] + +# Dependency graph +requires: + - phase: 06-turn-by-turn-guidance-live-driving + plan: 03 + provides: "the FROZEN envelope service — getGuidanceSession's defensive read (garbage → null, the cron's only read path), mint/delete lifecycle (no key, no push), chat_id minted from the Bearer claim (T-06-02), and GUIDANCE_KEY_PREFIX — now exported as the ONE key-scheme authority the list scan derives from" + - phase: 06-turn-by-turn-guidance-live-driving + plan: 04 + provides: "loadGraph(env) — the EXPORTED isolate-memoized R2 graph loader (the ONLY one the cron may use), and the @araharah/routing workspace primitives the projection imports: classifyTurn, decodePolyline6, deriveCumulativeDistanceM, snapToGraph (with the additive matched-segment nameId), nameAt, haversineM" + - phase: 06-turn-by-turn-guidance-live-driving + plan: 05 + provides: "web/src/guidance/glyphs/*.svg — the committed 8-glyph single source these PNGs rasterize (one geometry, two renderings), plus the strings-module copy-gate pattern the card keys extend" +provides: + - "bot/src/guidance/cron.ts — PURE projectTurnCards(envelope, nowSec, streetAt?) → { due, glyph, caption, maneuverIdx }: linear progress projection along the frozen snapshot, first-turn-ahead register with zero-padded index, cadence gate (nowSec % 180 < 60), dead-trip no-card gate, the 4-line HTML caption per UI-SPEC anatomy; PURE makeStreetAt(graph, points) — snapToGraph → SnapResult.nameId → nameAt(nameId), unnamed sentinel → cardStreetUnnamed, snap miss → null; CARD_CADENCE_S = 180 exported" + - "bot/src/webhook.ts — the repo's FIRST Cron Trigger: createExportedHandler({ baseFetch }) factory (offline send capture; production default unchanged) + scheduled() wrapping runGuidanceCronTick in ctx.waitUntil — fail-closed unconfigured gate, paginated guidance: KV list (list_complete respected), loadGraph awaited ONCE per tick, per-key try/catch (one bad key or failed send cannot kill the tick), sendPhoto to envelope.chat_id ONLY, zero KV writes" + - "bot/wrangler.jsonc — triggers.crons [\"*/1 * * * *\"]: 60 s scan, card cadence decided in code (D-11's 60–120 s window); kv_namespaces still exactly ONE (GE5)" + - "web/public/glyphs/*.png — the 8 committed 256×256 token-colored glyph PNGs (#EB8008 on #1A1A1A full-bleed), rasterized once from the app's SVG sources; served by the Worker's static handler, fetched by sendPhoto" + - "routing/scripts/build-glyphs.mjs + `npm run glyphs -w routing` — the maintainer rasterizer (sharp devDependency, collect-then-write, exit 1 with the kind named on any missing/invalid source, SVG sources never rewritten)" + - "bot copy keys — the 20 turn-card strings in en + ms verbatim (8 cardAction* street-bearing verbs, 8 cardActionBare* degradation verbs, cardStreetUnnamed, cardEtaTemplate, cardHonesty, cardOpenButton), copy-gate asserted" +affects: [06-08 verification close (the cron smoke leg + the named snap-CPU ratification item), production deploy (the cron arms on `wrangler deploy`), GUID-06 bridge (the card's OPEN button deep-links ?g= into plan 06's boot reader)] + +# Tech tracking +tech-stack: + added: ["sharp@0.35.5 (routing devDependency — maintainer glyph rasterization only, zero runtime deps)"] + patterns: + - "Stateless cadence projection: due = nowSec % CARD_CADENCE_S < SCAN_TICK_S — zero cursor keys, zero KV writes (Pitfall 3's 1,000-writes/day free budget; pinned structurally GC-C6 AND behaviorally by a throwing-put fake KV)" + - "Street-bearing by DEFAULT via one-authority reconstruction: snapToGraph → SnapResult.nameId → nameAt(nameId) (never the node index); unnamed sentinel → cardStreetUnnamed; graph-absent or snap miss → bare-verb honest absence, the send still fires (D-03)" + - "Injectable ExportedHandler: createExportedHandler(opts) — production default export unchanged, offline tests capture sends through the createBot({ baseFetch }) seam (GC-C1's pattern)" + - "waitUntil-wrapped tick + per-key try/catch: delivery variance and corrupt records are category-only skips, never tick failures (D-13)" + - "PURE-law module split: cron.ts has no console at all (GC8 structural pin); ALL logging lives in webhook.ts's wiring, category-only ([guidance-cron] skipped/failed)" + - "Maintainer rasterizer law: committed SVGs are THE single source, recolor in-memory only (currentColor law preserved), collect-then-write with no partial writes" + +key-files: + created: + - bot/src/guidance/cron.ts + - bot/test/guidance-cron.test.mjs + - routing/scripts/build-glyphs.mjs + - web/public/glyphs/{depart,straight,left,right,slight-left,slight-right,uturn,arrive}.png + modified: + - bot/src/webhook.ts + - bot/wrangler.jsonc + - bot/src/i18n/strings.ts + - bot/test/commands.test.mjs + - bot/src/kv/guidance-sessions.ts + - bot/test/guidance-envelope.test.mjs + - routing/package.json + - package-lock.json + +key-decisions: + - "Cards are EN (pickLocale's fallback law): the frozen envelope carries no language field (GE2); the ms table stays in parity via the bot copy gate for the day the contract grows a language surface" + - "D-12 stands RATIFIED: street-bearing cards by DEFAULT via the shared loadGraph memo; bare-verb street-less ONLY as graph-absent/snap-miss degradation — the one open CPU question (one snap per carded tick) is a named ratification item in 06-08's blocking checkpoint" + - "The card's ETA clock is KL wall time (named KL_UTC_OFFSET_S = 8h constant): Malaysia has no DST, v1 is KL-metro only, and the envelope carries no driver timezone" + - "The card's OPEN button uses a web_app url with ?g= (web_app buttons carry only a url — the 04-03 mechanism-correction precedent), resuming a killed webview through plan 06's boot reader (GUID-06 bridge)" + +patterns-established: [] + +requirements-completed: [GUID-05] + +# Metrics +duration: 12min +completed: 2026-10-24 +--- + +# Phase 06 Plan 07: Cron Turn-Cards Summary + +**The repo's first Cron Trigger: a stateless 60 s scan projects progress along frozen guidance snapshots and pushes honest glyph+text turn-cards to the owner's chat — street-bearing by default via the shared memoized graph, bare-verb only as designed degradation — with zero KV writes and 8 committed token-colored PNGs rasterized from the app's own SVG sources.** + +## Performance + +- **Duration:** 12 min (continuation session — adoption + task-2 GREEN completion + task 3; task 1 and task 2's RED were executed by the prior, interrupted session) +- **Started:** 2026-10-24T11:43:48Z +- **Completed:** 2026-10-24T11:55:28Z +- **Tasks:** 3 of 3 (task 1 closed pre-continuation by ed0e0d0/f07021e; task 2 spanned the prior RED 620a982 + this session's GREEN ad61229; task 3 fresh) +- **Files modified:** 8 tracked files + 10 created (2 code, 1 script, 8 PNGs, 1 test suite) + +## Continuation context (executor died mid-task-2) + +This plan was finished by a continuation executor. The prior executor committed task 1 (RED `ed0e0d0` + GREEN `f07021e` — PURE projectTurnCards/makeStreetAt + the 20 turn-card copy keys) and task 2's RED (`620a982` — the GC-C1..GC-C9 scheduled() suite), then died mid-task-2-GREEN with `webhook.ts` + `guidance-sessions.ts` + `guidance-envelope.test.mjs` uncommitted. That in-flight work was verified against the plan, finished, and committed as `ad61229`. Task 3 (glyphs) was executed fresh as `9c50fb0`. + +## Accomplishments + +- **PURE projection (task 1):** `projectTurnCards` decodes the frozen geometry, projects linear progress (clamped), walks the maneuver register through `classifyTurn` (the ONE turn authority — no local re-implementation), gates on cadence + dead-trip, and composes the exact 4-line HTML caption (≤1024 chars) with a zero-padded register. `makeStreetAt` reconstructs the street from the committed graph via the snap's matched-segment `nameId` — the node index never reaches `nameAt` (GC8 pins both). +- **The wired cron (task 2):** `scheduled()` lists `guidance:*` with cursor pagination, reads each envelope defensively, awaits the shared `loadGraph` memo once per tick, and pushes due cards to `envelope.chat_id` ONLY (T-06-02 — asserted on the recorded send). Missing config fails closed; malformed records and failed sends are `[guidance-cron]` category logs that never kill the tick. Zero `.put(` anywhere in the cron path (structural + behavioral pins). +- **Committed glyph set (task 3):** `npm run glyphs` rasterizes the 8 committed SVG sources → 256×256 `#EB8008`-on-`#1A1A1A` PNGs (2–3 KB each); the sources stay untouched (currentColor law for in-app theme-following rendering). +- **Copy law holds end-to-end:** the D-13 banned-words gate (`live|real-time|guaranteed`, case-insensitive) and the no-envelope-string rule (destinationName never enters a caption) are asserted on the composed captions; `cardHonesty` rides verbatim. + +## Task Commits + +Each task was committed atomically: + +1. **task 1 (RED): turn-card cron suite GC1–GC8 + CS7 copy gate** - `ed0e0d0` (test) +2. **task 1 (GREEN): PURE projectTurnCards + makeStreetAt + 20 copy keys** - `f07021e` (feat) +3. **task 2 (RED): failing scheduled() tick suite GC-C1..GC-C9** - `620a982` (test) +4. **task 2 (GREEN): scheduled() wired + crons trigger + KV-budget pin** - `ad61229` (feat) +5. **task 3: build-glyphs.mjs + the committed 8-PNG set** - `9c50fb0` (feat) + +## Files Created/Modified + +- `bot/src/guidance/cron.ts` — the PURE projection + street resolver (no console, no KV, no R2, no Telegram, no clock reads) +- `bot/src/webhook.ts` — `createExportedHandler` factory + `scheduled()`/`runGuidanceCronTick` (the cron wiring) +- `bot/wrangler.jsonc` — `triggers.crons */1 * * * *` with the stateless-cadence law comment +- `bot/src/i18n/strings.ts` — the 20 turn-card keys, en + ms verbatim (UI-SPEC pinned) +- `bot/src/kv/guidance-sessions.ts` — `GUIDANCE_KEY_PREFIX` exported (ONE key-scheme authority) +- `bot/test/guidance-cron.test.mjs` — GC1–GC8 (pure) + GC-C1..GC-C9 (wired tick, offline) +- `bot/test/commands.test.mjs` — copy gate extended for the new keys (CS7) +- `bot/test/guidance-envelope.test.mjs` — GE7 allows the `GUIDANCE_KEY_PREFIX` named import +- `routing/scripts/build-glyphs.mjs` — the maintainer rasterizer (collect-then-write) +- `routing/package.json` + `package-lock.json` — sharp devDependency + the `glyphs` script +- `web/public/glyphs/*.png` — the 8 committed 256×256 card glyphs + +## Deviations from Plan + +All five are Rule-1 fixes on the dead executor's never-run code, applied to make the committed RED suite honestly green — test assertions were preserved; only broken wiring/mechanics were repaired: + +1. **[Rule 1 - Test wiring] GC-C3/C5/C8/C9 defined a recording `baseFetch` but never passed it** (they destructured `mod.default`, whose bot uses real fetch — sends could not be captured and would have hit the network with a fake token). Fixed by building the worker through `createExportedHandler({ baseFetch })` — the suite's own documented seam ("grammY sends captured through createBot(env, { baseFetch })") and GC-C1's existing pattern. +2. **[Rule 1 - ESM bug] GC-C7 called `require("node:fs")` inside an `.mjs` suite** → ReferenceError before any assertion. Fixed with a top-level `readdirSync` import. +3. **[Rule 1 - Capture race] GC-C1/C8/C9 asserted logs emitted inside `ctx.waitUntil`** — the tick's logs fire during `drain()`, after `withConsole` had already restored console. Fixed by draining inside the capture window. +4. **[Rule 1 - Missing type import] the in-flight webhook.ts used `GuidanceSessionEnvelope` without importing it** → tsc TS2304. Added the named type import (GE7's allow-list already carried it). +5. **[Rule 1 - Render bugs] build-glyphs.mjs, en route:** (a) the plan's density formula multiplied with the explicit width/height attributes → 2048² instead of 256² (density dropped — the size attributes govern); (b) the injected plate rect inherited the root's orange `stroke` → an orange frame on every card (fixed with `stroke="none"`, the sources' own convention for filled shapes). + +Plus one cosmetic adoption cleanup: the dead executor's factory wrap left `fetch`'s body at the old indent level; reindented consistently in `ad61229`. + +## Auth Gates + +None — no Telegram/network credentials were needed (the offline suite captures sends through the injectable fetch seam). + +## Known Stubs + +None — every path is wired: the cron scans real KV (or the Map fake in tests), the street hop runs the real graph, the PNGs are committed artifacts, and the OPEN deep-link carries the envelope id. + +## Threat Flags + +None — the shipped surface is exactly the plan's threat model: T-06-02 (chat_id from the envelope only) asserted in GC-C1; T-06-04 (category-only logs) asserted in GC-C1/C8/C9; the KV-write budget pinned in GC-C6; T-06-03 accepted per plan (committed repo artifacts, no upload path). + +## Verification + +- `npm test -w bot` → **125 passed, 0 failed** (103 pre-plan baseline + GC1–GC8 + GC-C1..GC-C9 + copy-gate/GE7 extensions); `npm run bot:typecheck` clean +- `npm test -w routing` → 65 passed, 0 failed (unaffected by the sharp devDep) +- `npm run glyphs -w routing` → 8/8 PNGs, each exactly 256×256, > 500 bytes; SVG sources verified unchanged (`git status` clean for `web/src/guidance/glyphs/`) +- Street states proven in-suite: mini-graph-backed caption carries `TURN LEFT ONTO JALAN MINI 1` (real `makeStreetAt`); R2-null fake → bare-verb `— TURN LEFT` with the send still recorded; off-graph vertex → honest null, no UNNAMED fallback +- wrangler.jsonc: `triggers.crons ["*/1 * * * *"]`, exactly one kv namespace (GE5 green in-suite) +- Structural pins: `\.put(` → 0 matches in cron.ts + webhook.ts; `console\.` → 0 in cron.ts; `nameAt(node` → 0; ROAD_GRAPH readers = config.ts + worker-router.ts only (GC-C7 walk) +- Local cron smoke (optional, manual): `cd bot && npx wrangler dev --port 8787 --local` with a seeded local envelope + `.dev.vars`, then `curl "http://localhost:8787/cdn-cgi/local/scheduled"` — requires a signed-in bot token and a seeded `guidance:` record; deferred to 06-08's device-gate legs where a real envelope exists. + +## Self-Check: PASSED + +- Files: bot/src/guidance/cron.ts ✓, bot/test/guidance-cron.test.mjs ✓, routing/scripts/build-glyphs.mjs ✓, web/public/glyphs/*.png (8) ✓ +- Commits: ed0e0d0 ✓, f07021e ✓, 620a982 ✓, ad61229 ✓, 9c50fb0 ✓ (git log verified) +- Suites: bot 125/0, routing 65/0, bot:typecheck clean ✓