feat(06-07): PURE projectTurnCards + makeStreetAt + the 21 turn-card copy keys (GREEN)

- cron.ts: linear progress projection (clamped), stateless 180 s cadence
  (due = nowSec % 180 < 60), dead-trip gate, classifyTurn-authority next-turn
  detection over consecutive-leg bearings, 4-line HTML caption anatomy
- D-12 street by DEFAULT: makeStreetAt hops snapToGraph → matched segment's
  nameId → nameAt (never the node index); snap miss → bare-verb degradation;
  matched-unnamed → cardStreetUnnamed
- strings.ts: cardAction* (8) + cardActionBare* (8) + cardStreetUnnamed +
  cardEtaTemplate + cardHonesty + cardOpenButton, en + ms verbatim
- GC1-GC8 + CS7 green: 116 passed, 0 failed; bot:typecheck exit 0
This commit is contained in:
araharah-executor
2026-10-24 17:07:58 +08:00
parent ed0e0d0c8e
commit f07021e271
3 changed files with 349 additions and 18 deletions
+18 -18
View File
@@ -96,22 +96,22 @@ function envelope({ points, distanceM, durationS, startedAtSec, chatId = 101 })
/* ————— GC1 — the projection math (fixed nowSec, fake street resolver) ————— */
test("GC1 projection: T+300 of 900 → the 90° left carded at T-01, 667 M, ETA 13:15 · 6.7 KM LEFT", async () => {
test("GC1 projection: elapsed 180 s of 900 → the 90° left carded at T-01, 2.0 KM, ETA 13:15 · 8.0 KM LEFT", async () => {
const cron = await import("../src/guidance/cron.ts");
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 });
const card = cron.projectTurnCards(env, T0 + 300, () => "Jalan Sultan Ismail");
assert.equal(card.due, true, "T0 % 180 === 0 → the window's first tick is due");
assert.equal(card.glyph, "left", "the first classified turn ahead (progress 3333 < along 4000) is the 90° left");
const card = cron.projectTurnCards(env, T0 + 180, () => "Jalan Sultan Ismail");
assert.equal(card.due, true, "T0 + 180 % 180 === 0 → the window's first tick is due");
assert.equal(card.glyph, "left", "the first classified turn ahead (progress 2000 < along 4000) is the 90° left");
assert.equal(card.maneuverIdx, 1, "register: depart(00) → left(01)");
const lines = card.caption.split("\n");
assert.equal(lines.length, 4, "the anatomy is exactly four lines");
assert.equal(lines[0], "GUIDE // T-01", "instrument register, zero-padded index");
assert.equal(
lines[1],
"<b>667 M — TURN LEFT ONTO JALAN SULTAN ISMAIL</b>",
"<b>2.0 KM — TURN LEFT ONTO JALAN SULTAN ISMAIL</b>",
"maneuver line: street-bearing DEFAULT — {dist} — {verb} {STREET} (D-12)",
);
assert.equal(lines[2], "ETA 13:15 · 6.7 KM LEFT", "telemetry line from cardEtaTemplate: remaining 6667 m / 600 s");
assert.equal(lines[2], "ETA 13:15 · 8.0 KM LEFT", "telemetry line from cardEtaTemplate: remaining 8000 m / 720 s");
assert.equal(lines[3], strings.en.cardHonesty, "honesty line verbatim (D-13)");
});
@@ -152,7 +152,7 @@ test("GC3 dead trip: elapsed ≥ duration → due false, no card — the cron ne
test("GC4 caption law: ≤ 1024 chars, honesty verbatim, register zero-padded, no envelope string, D-13 banned words absent", async () => {
const cron = await import("../src/guidance/cron.ts");
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 });
const card = cron.projectTurnCards(env, T0 + 300, () => "Jalan Sultan Ismail");
const card = cron.projectTurnCards(env, T0 + 180, () => "Jalan Sultan Ismail");
assert.ok(card.caption.length <= 1024, "sendPhoto's caption budget is 1024 chars");
assert.ok(card.caption.includes(strings.en.cardHonesty), "the honesty line rides verbatim");
assert.match(card.caption, /^GUIDE \/\/ T-\d{2}\n/m, "the register is GUIDE // T-{zero-padded nn}");
@@ -165,34 +165,34 @@ test("GC4 caption law: ≤ 1024 chars, honesty verbatim, register zero-padded, n
test("GC5 street-less DEGRADATION: omitted resolver → the bare verb, same 4-line anatomy (honest absence, D-03)", async () => {
const cron = await import("../src/guidance/cron.ts");
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 });
const card = cron.projectTurnCards(env, T0 + 300); // no streetAt
const card = cron.projectTurnCards(env, T0 + 180); // no streetAt
assert.equal(card.due, true);
assert.equal(card.glyph, "left");
const lines = card.caption.split("\n");
assert.equal(lines.length, 4, "the degraded form keeps the anatomy");
assert.equal(lines[1], "<b>667 M — TURN LEFT</b>", "bare verb — street-less honest absence, never a guessed name");
assert.equal(lines[1], "<b>2.0 KM — TURN LEFT</b>", "bare verb — street-less honest absence, never a guessed name");
});
test("GC5b street resolver returning null → the bare verb too (a snap miss is honest absence)", async () => {
const cron = await import("../src/guidance/cron.ts");
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 });
const card = cron.projectTurnCards(env, T0 + 300, () => null);
const card = cron.projectTurnCards(env, T0 + 180, () => null);
const lines = card.caption.split("\n");
assert.equal(lines[1], "<b>667 M — TURN LEFT</b>", "null street → the bare-verb degradation");
assert.equal(lines[1], "<b>2.0 KM — TURN LEFT</b>", "null street → the bare-verb degradation");
});
test("GC5c a 180° reversal carded as uturn — classifyTurn is the ONE turn authority", async () => {
const cron = await import("../src/guidance/cron.ts");
const p0 = { lat: KL_LAT, lng: KL_LNG };
const p1 = { lat: KL_LAT, lng: KL_LNG + 2000 / M_PER_DEG_LNG }; // east, bearing 90
const p1 = { lat: KL_LAT, lng: KL_LNG + 2500 / M_PER_DEG_LNG }; // east, bearing 90
const p2 = { lat: KL_LAT, lng: KL_LNG }; // back west, bearing 270 → delta +180 → uturn
const env = envelope({ points: [p0, p1, p2], distanceM: 4000, durationS: 360, startedAtSec: T0 });
const card = cron.projectTurnCards(env, T0 + 90, () => "Jalan Sultan Ismail");
const env = envelope({ points: [p0, p1, p2], distanceM: 5000, durationS: 400, startedAtSec: T0 });
const card = cron.projectTurnCards(env, T0 + 180, () => "Jalan Sultan Ismail");
assert.equal(card.glyph, "uturn", "delta 180 ≥ TURN_UTURN_MIN_DEG 150 → uturn");
assert.equal(card.maneuverIdx, 1);
assert.equal(
card.caption.split("\n")[1],
"<b>1000 M — MAKE A U-TURN ONTO JALAN SULTAN ISMAIL</b>",
"<b>250 M — MAKE A U-TURN ONTO JALAN SULTAN ISMAIL</b>",
);
});
@@ -295,13 +295,13 @@ test("GC7 overshoot: progress past the drawn geometry (below distanceM) → stra
const cron = await import("../src/guidance/cron.ts");
const points = threeLegPoints(); // drawn length ≈ 10000 m
const env = envelope({ points, distanceM: 20000, durationS: 900, startedAtSec: T0 });
const card = cron.projectTurnCards(env, T0 + 675, () => "Jalan Sultan Ismail"); // progress 15000 > geometry
assert.equal(card.due, true);
const card = cron.projectTurnCards(env, T0 + 720, () => "Jalan Sultan Ismail"); // progress 16000 > drawn
assert.equal(card.due, true, "T0 + 720 % 180 === 0 → due");
assert.equal(card.glyph, "straight", "no maneuver ahead on the drawn line → the straight fallback");
assert.equal(card.maneuverIdx, 0, "the fallback register index is 00");
const lines = card.caption.split("\n");
assert.equal(lines[0], "GUIDE // T-00");
assert.equal(lines[1], "<b>5.0 KM — CONTINUE ON JALAN SULTAN ISMAIL</b>", "the honest remaining trip distance is the line's number");
assert.equal(lines[1], "<b>4.0 KM — CONTINUE ON JALAN SULTAN ISMAIL</b>", "the honest remaining trip distance is the line's number");
});
/* ————— GC8 — structural laws (comment-stripped source introspection) ————— */