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

14 KiB
Raw Blame History

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
06-turn-by-turn-guidance-live-driving 07 bot-cron
cron-turn-cards
scheduled-handler
d11-cadence
d12-street-bearing
d13-honesty
kv-write-budget
committed-glyphs
sharp
tdd
continuation
phase plan provides
06-turn-by-turn-guidance-live-driving 03 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 plan provides
06-turn-by-turn-guidance-live-driving 04 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 plan provides
06-turn-by-turn-guidance-live-driving 05 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
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
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)
added patterns
sharp@0.35.5 (routing devDependency — maintainer glyph rasterization only, zero runtime deps)
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
created modified
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
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
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)
GUID-05
12min 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 ✓