Files
arah2/.planning/phases/06-turn-by-turn-guidance-live-driving/06-07-SUMMARY.md
T
araharah-executor 001c8ac8b0 docs(06-07): complete the cron turn-cards plan — SUMMARY
- continuation session: adopted the dead executor's in-flight task-2 GREEN,
  finished + committed it atomically (ad61229), then executed task 3 (9c50fb0)
- 125/0 bot tests, 65/0 routing tests, typechecks clean, 8 committed PNGs
- STATE.md and ROADMAP.md intentionally untouched (orchestrator-owned)
2026-10-24 19:55:47 +08:00

155 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: 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=<id> 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=<id> (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:<uuid>` 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 ✓