chore(planning): phase 5/6/7 planning artifacts + milestone-completion state sync

This commit is contained in:
araharah-executor
2026-10-25 12:51:27 +08:00
parent bb587ab537
commit db27aa8440
34 changed files with 3949 additions and 214 deletions
@@ -26,7 +26,7 @@ requirements: [GUID-05]
must_haves:
truths:
- "A Worker Cron Trigger scans guidance:* envelopes every 60 s, projects progress along the snapshot, and pushes a glyph+text turn-card on its 3-minute cadence (D-11/D-12)"
- "Card maneuver lines carry the STREET by DEFAULT — reconstructed by snapping the carded turn's vertex to the memoized R2 graph (snapToGraph + nameAt, per D-12's ETA/distance/street text); street-less bare-verb cards ONLY when the graph is absent in the isolate or the snap misses (D-03-style honest degradation)"
- "Card maneuver lines carry the STREET by DEFAULT — reconstructed by snapping the carded turn's vertex to the memoized R2 graph (snapToGraph → the matched segment's nameId → nameAt(nameId), per D-12's ETA/distance/street text); street-less bare-verb cards ONLY when the graph is absent in the isolate or the snap misses (D-03-style honest degradation)"
- "Cards push ONLY to envelope.chat_id read from KV — no client-supplied chat id exists anywhere in the cron path (T-06-02)"
- "The cron performs ZERO KV writes — the stateless cadence holds the 1,000-writes/day free budget (Pitfall 3)"
- "Card copy never promises live tracking — 'best-effort projection' is the strongest claim (D-13)"
@@ -45,8 +45,8 @@ must_haves:
key_links:
- from: "bot/src/guidance/cron.ts"
to: "routing/src (classifyTurn, decodePolyline6, deriveCumulativeDistanceM, snapToGraph, nameAt)"
via: "projection + street reconstruction over shared primitives — one turn/street authority with engine A"
pattern: "classifyTurn|snapToGraph"
via: "projection + street reconstruction over shared primitives (snapToGraph → SnapResult.nameId → nameAt(nameId)) — one turn/street authority with engine A"
pattern: "classifyTurn|snapToGraph|nameId"
- from: "bot/src/webhook.ts scheduled()"
to: "bot/src/routing/worker-router.ts"
via: "loadGraph(env) — the exported isolate-memoized R2 graph feeding makeStreetAt (no second R2 loader)"
@@ -86,8 +86,10 @@ Output: cron.ts + scheduled() + crons config + bot copy keys + cron suite with K
<!-- From 06-01 (routing workspace — bot already depends on it): classifyTurn(prevDeg, nextDeg),
decodePolyline6(s), deriveCumulativeDistanceM(points), snapToGraph(graph, point) →
{ node, distanceM } | null (grid-indexed, per-graph memoized — cheap enough for a
one-vertex snap per carded tick), nameAt(id) on RoadGraph, haversineM.
{ node, distanceM, nameId } | null (grid-indexed, per-graph memoized — cheap enough for a
one-vertex snap per carded tick; nameId = the MATCHED SEGMENT's edge-name id, null when
unnamed — 06-01's additive field), nameAt(nameId) on RoadGraph consumes a NAME-POOL id
(edges carry names; nodes don't — never feed it the node index), haversineM.
Subpath-with-extension imports only. -->
<!-- From 06-04 (bot/src/routing/worker-router.ts — EXPORTED there): loadGraph(env):
@@ -101,7 +103,7 @@ Output: cron.ts + scheduled() + crons config + bot copy keys + cron suite with K
The FROZEN envelope carries geometry only (GE2 field freeze holds — no steps field,
no envelope extension: path (b) is user-gated and NOT implemented), so the cron
reconstructs the street itself: snap the carded turn's vertex to the memoized R2
graph (snapToGraph) → nameAt — the SAME one-authority argument the app-side banner
graph (snapToGraph) → the matched segment's nameId → nameAt(nameId) — the SAME one-authority argument the app-side banner
relies on. DEGRADATION (D-03's designed-degradation pattern): graph absent/corrupt
in the cron isolate, or a snap miss → street-less composition with the bare verbs —
honest absence, never a guessed name. CPU tradeoff (cron-side snap per due tick vs
@@ -173,13 +175,13 @@ Output: cron.ts + scheduled() + crons config + bot copy keys + cron suite with K
- caption: exactly 4 lines per the anatomy, HTML parse mode, ≤ 1024 chars, contains the honesty line verbatim, register zero-padded
- street-bearing DEFAULT (D-12): with a streetAt resolver returning a name, the maneuver line is "{dist} — {cardAction[action]} {STREET}" (the verb carries ONTO/KE; street uppercased); nameAt's unnamed id → cardStreetUnnamed ("UNNAMED ROAD"/"JALAN TANPA NAMA")
- street-less DEGRADATION: streetAt omitted or null for the carded vertex → "{dist} — {cardActionBare[action]}" — bare verbs, honest absence, same 4-line anatomy
- makeStreetAt(graph) is PURE (graph in → resolver out; snapToGraph + nameAt imported from @araharah/routing, zero I/O): against decodeGraph of the committed mini-graph fixture, a vertex on the fixture's named way resolves that way's name; a vertex off-graph → null
- makeStreetAt(graph) is PURE (graph in → resolver out; snapToGraph + nameAt imported from @araharah/routing, zero I/O): against decodeGraph of the committed mini-graph fixture, a vertex on the fixture's named way resolves that way's name via the snap's matched-segment nameId (the node index is never fed to nameAt); a vertex off-graph → null
- copy law: caption contains none of "live", "real-time", "guaranteed" (case-insensitive) — D-13's strongest-claim gate
</behavior>
<action>
1. Bot copy: append the 23 keys from the context box to `BotStrings` + both locale records (verbatim values); extend the copy gate in `bot/test/commands.test.mjs` to assert them in en + ms.
2. RED: `bot/test/guidance-cron.test.mjs` — build a synthetic envelope (geometry via encodePolyline6 of a known 3-leg path with 90° and 45° turns), assert every behavior above with fixed nowSec values. Street cases: (a) fake resolver `streetAt = () => "JALAN SULTAN ISMAIL"` → the ONTO line composes with that street; (b) `streetAt` omitted / returns null → the bare-verb line; (c) mini-graph integration — construct the synthetic envelope geometry ALONG the mini-graph fixture's named way so the carded turn's vertex snaps deterministically onto it, run the REAL `makeStreetAt(decodeGraph(miniGraphBuffer))`, and assert the caption contains the fixture way's name.
3. GREEN: `bot/src/guidance/cron.ts` — header states the pure law ("projectTurnCards and makeStreetAt are PURE: injected nowSec/graph, no KV, no R2, no Telegram, no clock reads — Node-testable"); `projectTurnCards(envelope: GuidanceSessionEnvelope, nowSec: number, streetAt?: (vertexIdx: number) => string | null): { due: boolean; glyph: GlyphKindName | null; caption: string | null; maneuverIdx: number | null }` — decode geometry → cumulative → progressM (clamped to distanceM) → walk bearings (bearing changes between segments; skip degenerate sub-m points) → classifyTurn → first turn ≥ progressM (none → "straight" glyph, index 00) → compose caption via strings + the exact anatomy: street string present → `{cardAction[action]} {street}`; null/absent resolver → `{cardActionBare[action]}`. Also export `makeStreetAt(graph: RoadGraph): (vertexIdx: number) => string | null` — snap the geometry vertex at vertexIdx (snapToGraph) → `graph.nameAt(node)`; snap miss → null; unnamed id → the cardStreetUnnamed constant. GlyphKindName is cron.ts's local union mirroring the 8 PNG names. HTML-escape surface stays ZERO: all caption parts are code-composed from numbers + string-table constants — assert no envelope-derived string (e.g. destinationName) enters the caption; street text arrives only via makeStreetAt's graph lookup. CATEGORY LAW: the module has no console at all (pure), the wiring logs (task 2).
3. GREEN: `bot/src/guidance/cron.ts` — header states the pure law ("projectTurnCards and makeStreetAt are PURE: injected nowSec/graph, no KV, no R2, no Telegram, no clock reads — Node-testable"); `projectTurnCards(envelope: GuidanceSessionEnvelope, nowSec: number, streetAt?: (vertexIdx: number) => string | null): { due: boolean; glyph: GlyphKindName | null; caption: string | null; maneuverIdx: number | null }` — decode geometry → cumulative → progressM (clamped to distanceM) → walk bearings (bearing changes between segments; skip degenerate sub-m points) → classifyTurn → first turn ≥ progressM (none → "straight" glyph, index 00) → compose caption via strings + the exact anatomy: street string present → `{cardAction[action]} {street}`; null/absent resolver → `{cardActionBare[action]}`. Also export `makeStreetAt(graph: RoadGraph): (vertexIdx: number) => string | null` — snap the geometry vertex at vertexIdx (snapToGraph) → the matched segment's `nameId` from SnapResult → `graph.nameAt(nameId)`; snap miss → null; nameId null/UNNAMED_NAME_ID → the cardStreetUnnamed constant. (Edges carry names; nodes don't — 06-01's SnapResult was extended additively with `nameId` for exactly this hop; never pass the node index to nameAt.) GlyphKindName is cron.ts's local union mirroring the 8 PNG names. HTML-escape surface stays ZERO: all caption parts are code-composed from numbers + string-table constants — assert no envelope-derived string (e.g. destinationName) enters the caption; street text arrives only via makeStreetAt's graph lookup. CATEGORY LAW: the module has no console at all (pure), the wiring logs (task 2).
</action>
<verify>
<automated>npm test -w bot</automated>
@@ -187,6 +189,7 @@ Output: cron.ts + scheduled() + crons config + bot copy keys + cron suite with K
<acceptance_criteria>
- guidance-cron.test.mjs green: projection math, next-turn kind + zero-padded index, cadence table, dead-trip no-card, 4-line caption ≤ 1024, honesty-line present, street-bearing line asserted against the mini-graph fixture (real makeStreetAt), street-less bare-verb case asserted in the graph-absent test, unnamed-id → cardStreetUnnamed, D-13 banned-words scan
- bot/src/guidance/cron.ts imports classifyTurn + snapToGraph from @araharah/routing (grep) — no local turn classifier, no local snapper
- street hop is id-based: grep `nameId` matches bot/src/guidance/cron.ts AND grep `nameAt\(node` → 0 matches (nameAt consumes the snap's matched-segment nameId, never the node index)
- bot/src/i18n/strings.ts carries cardAction* (8, UI-SPEC verbatim) + cardActionBare* (8) + cardStreetUnnamed + cardEtaTemplate + cardHonesty + cardOpenButton in en + ms; copy gate green
- grep `console\.` → 0 matches in bot/src/guidance/cron.ts (pure module)
</acceptance_criteria>