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:
@@ -50,7 +50,7 @@ const res = await fetch(`https://api.telegram.org/bot${token}/setChatMenuButton`
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
menu_button: { type: "web_app", text: "araharah", web_app: { url: appUrl } },
|
||||
menu_button: { type: "web_app", text: "arah²", web_app: { url: appUrl } },
|
||||
}),
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
@@ -52,7 +52,7 @@ import { strings } from "../i18n/strings.ts";
|
||||
|
||||
/** The chat card's cadence (D-11/D-13): one card window every 3 minutes;
|
||||
* the window's first tick is the due tick. */
|
||||
export const CARD_CADENCE_S = 180;
|
||||
export const CARD_CADENCE_S = 240;
|
||||
|
||||
/** The Worker's cron scan tick — wrangler's crons entry ticks every 60 s. */
|
||||
const SCAN_TICK_S = 60;
|
||||
|
||||
+12
-12
@@ -7,7 +7,7 @@
|
||||
* copy anywhere in the command tree.
|
||||
*
|
||||
* Locales: English default ("en") + Bahasa Melayu ("ms"). Tone is clinical
|
||||
* + honest (D-01): one sentence for what araharah is, plain statements of
|
||||
* + honest (D-01): one sentence for what arah² is, plain statements of
|
||||
* what is stored and what is not — no emoji, no marketing flourishes.
|
||||
*
|
||||
* Claim-surface honesty (D-17 / PRIV-01): `privacyStored` names the exact
|
||||
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
export interface BotStrings {
|
||||
/** D-01: what araharah is, in one sentence. */
|
||||
/** D-01: what arah² is, in one sentence. */
|
||||
startIntro: string;
|
||||
/** PRIV-01: exactly what is stored — must mirror the D-08 claim fields and the TTL. */
|
||||
privacyStored: string;
|
||||
@@ -86,18 +86,18 @@ export type Locale = "en" | "ms";
|
||||
export const strings: Record<Locale, BotStrings> = {
|
||||
en: {
|
||||
startIntro:
|
||||
"araharah — navigation through Telegram for the KL metro. Everything it knows about you expires in about fifteen minutes; /privacy shows exactly what that means.",
|
||||
"arah² — navigation through Telegram for the KL metro. Everything it knows about you expires in about fifteen minutes; /privacy shows exactly what that means.",
|
||||
privacyStored:
|
||||
"What araharah keeps: your Telegram user id, this chat id, and your interface language — kept only for about 15 minutes so the bot can answer you, then gone.",
|
||||
"What arah² keeps: your Telegram user id, this chat id, and your interface language — kept only for about 15 minutes so the bot can answer you, then gone.",
|
||||
privacyNotStored:
|
||||
"What araharah never keeps: your location, your routes, your trip history, any profile about you.",
|
||||
"What arah² never keeps: your location, your routes, your trip history, any profile about you.",
|
||||
helpMenu:
|
||||
"Commands:\n/help — How araharah works and what it keeps\n/privacy — What araharah stores and never stores\n/cari — Search for a destination in the KL metro\nKnown limits: http://gitmirah:3000/c-fu/arah2/src/branch/main/docs/KNOWN-LIMITATIONS.md",
|
||||
"Commands:\n/help — How arah² works and what it keeps\n/privacy — What arah² stores and never stores\n/cari — Search for a destination in the KL metro\nKnown limits: http://gitmirah:3000/c-fu/arah2/src/branch/main/docs/KNOWN-LIMITATIONS.md",
|
||||
awaitingPin: "Send me a pin in this chat and I'll use it as your starting point.",
|
||||
shareLocationButton: "Share my location",
|
||||
pinReceived: "Got it — your starting point is set.",
|
||||
searchUsage: "/cari <place> — search for a destination in the KL metro.",
|
||||
searchNoMatch: "Not in the v1 KL area. araharah only knows Kuala Lumpur and the Klang Valley for now.",
|
||||
searchNoMatch: "Not in the v1 KL area. arah² only knows Kuala Lumpur and the Klang Valley for now.",
|
||||
searchQueryTooLong: "That query is too long. Send just the place name.",
|
||||
searchOpenButton: "OPEN IN ARAHARAH",
|
||||
osmAttribution: "© OpenStreetMap contributors",
|
||||
@@ -124,18 +124,18 @@ export const strings: Record<Locale, BotStrings> = {
|
||||
},
|
||||
ms: {
|
||||
startIntro:
|
||||
"araharah — navigasi melalui Telegram untuk kawasan KL. Semua yang diketahui tentang anda luput dalam lebih kurang lima belas minit; /privacy menunjukkan dengan tepat maksudnya.",
|
||||
"arah² — navigasi melalui Telegram untuk kawasan KL. Semua yang diketahui tentang anda luput dalam lebih kurang lima belas minit; /privacy menunjukkan dengan tepat maksudnya.",
|
||||
privacyStored:
|
||||
"Yang araharah simpan: id pengguna Telegram anda, id chat ini, dan bahasa antara muka anda — disimpan hanya sekitar 15 minit supaya bot boleh menjawab anda, kemudian hilang.",
|
||||
"Yang arah² simpan: id pengguna Telegram anda, id chat ini, dan bahasa antara muka anda — disimpan hanya sekitar 15 minit supaya bot boleh menjawab anda, kemudian hilang.",
|
||||
privacyNotStored:
|
||||
"Yang araharah tidak pernah simpan: lokasi anda, laluan anda, sejarah perjalanan anda, apa-apa profil tentang anda.",
|
||||
"Yang arah² tidak pernah simpan: lokasi anda, laluan anda, sejarah perjalanan anda, apa-apa profil tentang anda.",
|
||||
helpMenu:
|
||||
"Arahan:\n/help — Cara araharah berfungsi dan apa yang ia simpan\n/privacy — Apa yang araharah simpan dan tidak pernah simpan\n/cari — Cari destinasi di kawasan KL\nHad panduan: http://gitmirah:3000/c-fu/arah2/src/branch/main/docs/KNOWN-LIMITATIONS.md",
|
||||
"Arahan:\n/help — Cara arah² berfungsi dan apa yang ia simpan\n/privacy — Apa yang arah² simpan dan tidak pernah simpan\n/cari — Cari destinasi di kawasan KL\nHad panduan: http://gitmirah:3000/c-fu/arah2/src/branch/main/docs/KNOWN-LIMITATIONS.md",
|
||||
awaitingPin: "Hantar saya pin dalam chat ini dan saya akan gunakannya sebagai titik mula anda.",
|
||||
shareLocationButton: "Kongsi lokasi saya",
|
||||
pinReceived: "Baik — titik mula anda sudah ditetapkan.",
|
||||
searchUsage: "/cari <tempat> — cari destinasi dalam kawasan KL.",
|
||||
searchNoMatch: "Bukan dalam kawasan KL v1. araharah hanya tahu Kuala Lumpur dan Lembah Klang buat masa ini.",
|
||||
searchNoMatch: "Bukan dalam kawasan KL v1. arah² hanya tahu Kuala Lumpur dan Lembah Klang buat masa ini.",
|
||||
searchQueryTooLong: "Pertanyaan itu terlalu panjang. Hantar nama tempat sahaja.",
|
||||
searchOpenButton: "OPEN IN ARAHARAH",
|
||||
osmAttribution: "© OpenStreetMap contributors",
|
||||
|
||||
@@ -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");
|
||||
|
||||
+8
-5
@@ -28,10 +28,13 @@
|
||||
// (docs/ROAD-GRAPH.md is the fork contract). The bucket name is public
|
||||
// configuration, never a secret.
|
||||
"r2_buckets": [{ "binding": "ROAD_GRAPH", "bucket_name": "araharah-graphs" }],
|
||||
// The repo's FIRST Cron Trigger (06-07, D-11): 60 s scan; card cadence
|
||||
// decided in code (CARD_CADENCE_S 180) — D-11's 60–120 s window. The tick
|
||||
// is STATELESS (zero KV writes — the free-plan budget law). Local test:
|
||||
// curl "http://localhost:8787/cdn-cgi/local/scheduled" against `wrangler dev`.
|
||||
"triggers": { "crons": ["*/1 * * * *"] },
|
||||
// The repo's FIRST Cron Trigger (06-07, D-11): */2 scan (LAUNCH-GATE decision
|
||||
// 2025-10-25 — the $0 law holds: 2-min ticks × 1 list-op = 720/day, inside the
|
||||
// free plan's 1,000/day KV-list ceiling; card cadence is 240 s in code
|
||||
// (CARD_CADENCE_S). The tick is STATELESS (zero KV writes — the budget law).
|
||||
// Local test: curl "http://localhost:8787/cdn-cgi/local/scheduled" under
|
||||
// `wrangler dev`. 90 s granularity is NOT cron-expressible — */2 is the
|
||||
// coarsest cadence that stays under the ceiling while honoring D-11's spirit.
|
||||
"triggers": { "crons": ["*/2 * * * *"] },
|
||||
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user