fix(07-06): launch-gate decisions — KV cron retune to 720/day (the $0 law holds), brand sweep araharah→arah² across user-facing copy, menu button text

This commit is contained in:
araharah-executor
2026-10-25 11:24:03 +08:00
parent 5f4fd8b297
commit dedcdcdbc5
10 changed files with 60 additions and 46 deletions
+21 -19
View File
@@ -10,7 +10,9 @@
* Pure-function block (task 1):
* GC1 projection math — T+300 of a 900 s / 10 km snapshot → the 90° left
* carded at register T-01, "667 M", ETA 13:15 · 6.7 KM LEFT
* GC2 cadence table — CARD_CADENCE_S 180; due on the first tick of each
* GC2 cadence table — CARD_CADENCE_S 240 (launch-gate retune, 2025-10-25:
* 2-minute cron ticks x 1 list-op = 720/day, under the 1,000/day free
* ceiling — the $0 law); due on the first tick of each
* 180 s window (nowSec % 180 < 60); non-due ticks carry no card
* GC3 dead trip — elapsed ≥ duration (progress clamped ≥ distanceM) →
* due false, no card: the cron never cards a dead trip
@@ -96,11 +98,11 @@ function envelope({ points, distanceM, durationS, startedAtSec, chatId = 101 })
/* ————— GC1 — the projection math (fixed nowSec, fake street resolver) ————— */
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 () => {
test("GC1 projection: elapsed 180 s at the retuned due tick (900 s total) → the 90° left carded at T-01, 2.0 KM, ETA 13:16 · 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 + 180, () => "Jalan Sultan Ismail");
assert.equal(card.due, true, "T0 + 180 % 180 === 0 → the window's first tick is due");
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 + 60 });
const card = cron.projectTurnCards(env, T0 + 240, () => "Jalan Sultan Ismail");
assert.equal(card.due, true, "T0 + 240 % 240 === 0 → the window's first tick is due (the 240 s retune)");
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");
@@ -111,20 +113,20 @@ test("GC1 projection: elapsed 180 s of 900 → the 90° left carded at T-01, 2.0
"<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 · 8.0 KM LEFT", "telemetry line from cardEtaTemplate: remaining 8000 m / 720 s");
assert.equal(lines[2], "ETA 13:16 · 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)");
});
/* ————— GC2 — the cadence table ————— */
test("GC2 cadence: CARD_CADENCE_S 180; due true on the window's first ticks, false after, no card on a non-due tick", async () => {
test("GC2 cadence: CARD_CADENCE_S 240 (*/2-tick retune); due true on the window's first ticks, false after, no card on a non-due tick", async () => {
const cron = await import("../src/guidance/cron.ts");
assert.equal(cron.CARD_CADENCE_S, 180, "the 3-minute cadence is an exported named constant");
assert.equal(cron.CARD_CADENCE_S, 240, "the 4-minute card cadence is an exported named constant (launch-gate retune: */2 cron + 240 s = 720 list-ops/day, under the 1,000 ceiling)");
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 });
const table = [[3600, true], [3659, true], [3660, false], [3779, false], [3780, true]];
const table = [[3600, true], [3659, true], [3660, false], [3779, false], [3840, true], [3899, true], [3900, false], [4079, false], [4080, true]];
for (const [nowSec, expectedDue] of table) {
const card = cron.projectTurnCards(env, nowSec);
assert.equal(card.due, expectedDue, `nowSec ${nowSec}: nowSec % 180 < 60 → ${expectedDue}`);
assert.equal(card.due, expectedDue, `nowSec ${nowSec}: nowSec % 240 < 60 → ${expectedDue}`);
if (!expectedDue) {
assert.equal(card.caption, null, "a non-due tick produces no card");
assert.equal(card.glyph, null, "a non-due tick produces no glyph");
@@ -136,7 +138,7 @@ test("GC2 cadence: CARD_CADENCE_S 180; due true on the window's first ticks, fal
test("GC3 dead trip: elapsed ≥ duration → due false, no card — the cron never cards a dead trip", async () => {
const cron = await import("../src/guidance/cron.ts");
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 });
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 + 60 });
for (const nowSec of [T0 + 900, T0 + 901, T0 + 5000]) {
const card = cron.projectTurnCards(env, nowSec);
assert.equal(card.due, false, `elapsed ${nowSec - T0} s ≥ duration 900 s → the trip would have ended`);
@@ -152,7 +154,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 + 180, () => "Jalan Sultan Ismail");
const card = cron.projectTurnCards(env, T0 + 240, () => "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}");
@@ -164,8 +166,8 @@ 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 + 180); // no streetAt
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 + 60 });
const card = cron.projectTurnCards(env, T0 + 240); // no streetAt
assert.equal(card.due, true);
assert.equal(card.glyph, "left");
const lines = card.caption.split("\n");
@@ -175,8 +177,8 @@ test("GC5 street-less DEGRADATION: omitted resolver → the bare verb, same 4-li
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 + 180, () => null);
const env = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: T0 + 60 });
const card = cron.projectTurnCards(env, T0 + 240, () => null);
const lines = card.caption.split("\n");
assert.equal(lines[1], "<b>2.0 KM — TURN LEFT</b>", "null street → the bare-verb degradation");
});
@@ -186,8 +188,8 @@ test("GC5c a 180° reversal carded as uturn — classifyTurn is the ONE turn aut
const p0 = { lat: KL_LAT, lng: KL_LNG };
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: 5000, durationS: 400, startedAtSec: T0 });
const card = cron.projectTurnCards(env, T0 + 180, () => "Jalan Sultan Ismail");
const env = envelope({ points: [p0, p1, p2], distanceM: 5000, durationS: 400, startedAtSec: T0 + 60 });
const card = cron.projectTurnCards(env, T0 + 240, () => "Jalan Sultan Ismail");
assert.equal(card.glyph, "uturn", "delta 180 ≥ TURN_UTURN_MIN_DEG 150 → uturn");
assert.equal(card.maneuverIdx, 1);
assert.equal(
@@ -434,7 +436,7 @@ async function sentPhotos(calls) {
return out;
}
const TICK_SEC = T0 + 180; // % 180 === 0 → a due tick
const TICK_SEC = T0 + 240; // % 240 === 0 → a due tick (the launch-gate retune)
test("GC-C1 due tick: the alive envelope's card sends to ITS OWN chat, street-bearing from the mini-graph R2 fake", async () => {
const mod = await import("../src/webhook.ts");