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)
This commit is contained in:
@@ -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=<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 ✓
|
||||||
Reference in New Issue
Block a user