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",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify({
|
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();
|
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 chat card's cadence (D-11/D-13): one card window every 3 minutes;
|
||||||
* the window's first tick is the due tick. */
|
* 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. */
|
/** The Worker's cron scan tick — wrangler's crons entry ticks every 60 s. */
|
||||||
const SCAN_TICK_S = 60;
|
const SCAN_TICK_S = 60;
|
||||||
|
|||||||
+12
-12
@@ -7,7 +7,7 @@
|
|||||||
* copy anywhere in the command tree.
|
* copy anywhere in the command tree.
|
||||||
*
|
*
|
||||||
* Locales: English default ("en") + Bahasa Melayu ("ms"). Tone is clinical
|
* 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.
|
* what is stored and what is not — no emoji, no marketing flourishes.
|
||||||
*
|
*
|
||||||
* Claim-surface honesty (D-17 / PRIV-01): `privacyStored` names the exact
|
* Claim-surface honesty (D-17 / PRIV-01): `privacyStored` names the exact
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface BotStrings {
|
export interface BotStrings {
|
||||||
/** D-01: what araharah is, in one sentence. */
|
/** D-01: what arah² is, in one sentence. */
|
||||||
startIntro: string;
|
startIntro: string;
|
||||||
/** PRIV-01: exactly what is stored — must mirror the D-08 claim fields and the TTL. */
|
/** PRIV-01: exactly what is stored — must mirror the D-08 claim fields and the TTL. */
|
||||||
privacyStored: string;
|
privacyStored: string;
|
||||||
@@ -86,18 +86,18 @@ export type Locale = "en" | "ms";
|
|||||||
export const strings: Record<Locale, BotStrings> = {
|
export const strings: Record<Locale, BotStrings> = {
|
||||||
en: {
|
en: {
|
||||||
startIntro:
|
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:
|
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:
|
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:
|
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.",
|
awaitingPin: "Send me a pin in this chat and I'll use it as your starting point.",
|
||||||
shareLocationButton: "Share my location",
|
shareLocationButton: "Share my location",
|
||||||
pinReceived: "Got it — your starting point is set.",
|
pinReceived: "Got it — your starting point is set.",
|
||||||
searchUsage: "/cari <place> — search for a destination in the KL metro.",
|
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.",
|
searchQueryTooLong: "That query is too long. Send just the place name.",
|
||||||
searchOpenButton: "OPEN IN ARAHARAH",
|
searchOpenButton: "OPEN IN ARAHARAH",
|
||||||
osmAttribution: "© OpenStreetMap contributors",
|
osmAttribution: "© OpenStreetMap contributors",
|
||||||
@@ -124,18 +124,18 @@ export const strings: Record<Locale, BotStrings> = {
|
|||||||
},
|
},
|
||||||
ms: {
|
ms: {
|
||||||
startIntro:
|
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:
|
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:
|
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:
|
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.",
|
awaitingPin: "Hantar saya pin dalam chat ini dan saya akan gunakannya sebagai titik mula anda.",
|
||||||
shareLocationButton: "Kongsi lokasi saya",
|
shareLocationButton: "Kongsi lokasi saya",
|
||||||
pinReceived: "Baik — titik mula anda sudah ditetapkan.",
|
pinReceived: "Baik — titik mula anda sudah ditetapkan.",
|
||||||
searchUsage: "/cari <tempat> — cari destinasi dalam kawasan KL.",
|
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.",
|
searchQueryTooLong: "Pertanyaan itu terlalu panjang. Hantar nama tempat sahaja.",
|
||||||
searchOpenButton: "OPEN IN ARAHARAH",
|
searchOpenButton: "OPEN IN ARAHARAH",
|
||||||
osmAttribution: "© OpenStreetMap contributors",
|
osmAttribution: "© OpenStreetMap contributors",
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
* Pure-function block (task 1):
|
* Pure-function block (task 1):
|
||||||
* GC1 projection math — T+300 of a 900 s / 10 km snapshot → the 90° left
|
* 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
|
* 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
|
* 180 s window (nowSec % 180 < 60); non-due ticks carry no card
|
||||||
* GC3 dead trip — elapsed ≥ duration (progress clamped ≥ distanceM) →
|
* GC3 dead trip — elapsed ≥ duration (progress clamped ≥ distanceM) →
|
||||||
* due false, no card: the cron never cards a dead trip
|
* 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) ————— */
|
/* ————— 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 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 });
|
||||||
const card = cron.projectTurnCards(env, T0 + 180, () => "Jalan Sultan Ismail");
|
const card = cron.projectTurnCards(env, T0 + 240, () => "Jalan Sultan Ismail");
|
||||||
assert.equal(card.due, true, "T0 + 180 % 180 === 0 → the window's first tick is due");
|
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.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)");
|
assert.equal(card.maneuverIdx, 1, "register: depart(00) → left(01)");
|
||||||
const lines = card.caption.split("\n");
|
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>",
|
"<b>2.0 KM — TURN LEFT ONTO JALAN SULTAN ISMAIL</b>",
|
||||||
"maneuver line: street-bearing DEFAULT — {dist} — {verb} {STREET} (D-12)",
|
"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)");
|
assert.equal(lines[3], strings.en.cardHonesty, "honesty line verbatim (D-13)");
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ————— GC2 — the cadence table ————— */
|
/* ————— 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");
|
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 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) {
|
for (const [nowSec, expectedDue] of table) {
|
||||||
const card = cron.projectTurnCards(env, nowSec);
|
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) {
|
if (!expectedDue) {
|
||||||
assert.equal(card.caption, null, "a non-due tick produces no card");
|
assert.equal(card.caption, null, "a non-due tick produces no card");
|
||||||
assert.equal(card.glyph, null, "a non-due tick produces no glyph");
|
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 () => {
|
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 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]) {
|
for (const nowSec of [T0 + 900, T0 + 901, T0 + 5000]) {
|
||||||
const card = cron.projectTurnCards(env, nowSec);
|
const card = cron.projectTurnCards(env, nowSec);
|
||||||
assert.equal(card.due, false, `elapsed ${nowSec - T0} s ≥ duration 900 s → the trip would have ended`);
|
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 () => {
|
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 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 });
|
||||||
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.length <= 1024, "sendPhoto's caption budget is 1024 chars");
|
||||||
assert.ok(card.caption.includes(strings.en.cardHonesty), "the honesty line rides verbatim");
|
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}");
|
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 () => {
|
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 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 });
|
||||||
const card = cron.projectTurnCards(env, T0 + 180); // no streetAt
|
const card = cron.projectTurnCards(env, T0 + 240); // no streetAt
|
||||||
assert.equal(card.due, true);
|
assert.equal(card.due, true);
|
||||||
assert.equal(card.glyph, "left");
|
assert.equal(card.glyph, "left");
|
||||||
const lines = card.caption.split("\n");
|
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 () => {
|
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 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 });
|
||||||
const card = cron.projectTurnCards(env, T0 + 180, () => null);
|
const card = cron.projectTurnCards(env, T0 + 240, () => null);
|
||||||
const lines = card.caption.split("\n");
|
const lines = card.caption.split("\n");
|
||||||
assert.equal(lines[1], "<b>2.0 KM — 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");
|
||||||
});
|
});
|
||||||
@@ -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 p0 = { lat: KL_LAT, lng: KL_LNG };
|
||||||
const p1 = { lat: KL_LAT, lng: KL_LNG + 2500 / 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 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 env = envelope({ points: [p0, p1, p2], distanceM: 5000, durationS: 400, startedAtSec: T0 + 60 });
|
||||||
const card = cron.projectTurnCards(env, T0 + 180, () => "Jalan Sultan Ismail");
|
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.glyph, "uturn", "delta 180 ≥ TURN_UTURN_MIN_DEG 150 → uturn");
|
||||||
assert.equal(card.maneuverIdx, 1);
|
assert.equal(card.maneuverIdx, 1);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -434,7 +436,7 @@ async function sentPhotos(calls) {
|
|||||||
return out;
|
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 () => {
|
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");
|
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
|
// (docs/ROAD-GRAPH.md is the fork contract). The bucket name is public
|
||||||
// configuration, never a secret.
|
// configuration, never a secret.
|
||||||
"r2_buckets": [{ "binding": "ROAD_GRAPH", "bucket_name": "araharah-graphs" }],
|
"r2_buckets": [{ "binding": "ROAD_GRAPH", "bucket_name": "araharah-graphs" }],
|
||||||
// The repo's FIRST Cron Trigger (06-07, D-11): 60 s scan; card cadence
|
// The repo's FIRST Cron Trigger (06-07, D-11): */2 scan (LAUNCH-GATE decision
|
||||||
// decided in code (CARD_CADENCE_S 180) — D-11's 60–120 s window. The tick
|
// 2025-10-25 — the $0 law holds: 2-min ticks × 1 list-op = 720/day, inside the
|
||||||
// is STATELESS (zero KV writes — the free-plan budget law). Local test:
|
// free plan's 1,000/day KV-list ceiling; card cadence is 240 s in code
|
||||||
// curl "http://localhost:8787/cdn-cgi/local/scheduled" against `wrangler dev`.
|
// (CARD_CADENCE_S). The tick is STATELESS (zero KV writes — the budget law).
|
||||||
"triggers": { "crons": ["*/1 * * * *"] },
|
// 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" }]
|
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,3 +215,12 @@ The launch gate (07-06) signs this record after the redeploy re-run:
|
|||||||
|
|
||||||
- Maintainer: __________________
|
- Maintainer: __________________
|
||||||
- Date: __________________
|
- Date: __________________
|
||||||
|
|
||||||
|
## RESOLUTION (launch-gate decision, 2025-10-25)
|
||||||
|
|
||||||
|
**KV-list over-ceiling: RESOLVED via the $0-safe retune.** Cron retuned `*/1` → `*/2`
|
||||||
|
(2-minute ticks) + CARD_CADENCE_S 180 → 240 (4-minute card cadence):
|
||||||
|
**720 list-ops/day — under the 1,000/day free ceiling.** The $0 law holds; no paid
|
||||||
|
tier. Consequence: turn-cards arrive at most every ~4 min while minimized (D-13's
|
||||||
|
best-effort framing absorbs this). Tests retuned (GC1/GC2/GC4/GC5*/GC-C timeline);
|
||||||
|
126/126 green.
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
# Known limitations
|
# Known limitations
|
||||||
|
|
||||||
araharah is a turn-by-turn navigator for the Kuala Lumpur metro, delivered inside
|
arah² is a turn-by-turn navigator for the Kuala Lumpur metro, delivered inside
|
||||||
Telegram: you search a destination with the bot (`/cari KLCC`), open the Mini App,
|
Telegram: you search a destination with the bot (`/cari KLCC`), open the Mini App,
|
||||||
and follow live route guidance built on OpenStreetMap data. It works today for real
|
and follow live route guidance built on OpenStreetMap data. It works today for real
|
||||||
drivers, and it has real limits. This page lists what araharah does **not** do well —
|
drivers, and it has real limits. This page lists what arah² does **not** do well —
|
||||||
stated plainly, for two readers: a driver deciding whether araharah fits today's trip,
|
stated plainly, for two readers: a driver deciding whether arah² fits today's trip,
|
||||||
and a fork deciding what they are inheriting. Nothing here is a hidden defect; every
|
and a fork deciding what they are inheriting. Nothing here is a hidden defect; every
|
||||||
claim names its evidence, and none of it is fixed by asking harder.
|
claim names its evidence, and none of it is fixed by asking harder.
|
||||||
|
|
||||||
## Data coverage
|
## Data coverage
|
||||||
|
|
||||||
araharah routes over a snapshot of OpenStreetMap, and inherits OSM's gaps honestly:
|
arah² routes over a snapshot of OpenStreetMap, and inherits OSM's gaps honestly:
|
||||||
|
|
||||||
- **No traffic awareness.** There is no live traffic feed; the ETA is a
|
- **No traffic awareness.** There is no live traffic feed; the ETA is a
|
||||||
distance-and-speed estimate from the route model, not a prediction.
|
distance-and-speed estimate from the route model, not a prediction.
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
<title>araharah</title>
|
<title>arah²</title>
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export const strings: Record<Locale, ShellStrings> = {
|
|||||||
ctaAskViaChat: "ASK VIA CHAT",
|
ctaAskViaChat: "ASK VIA CHAT",
|
||||||
ctaLocationSettings: "LOCATION SETTINGS",
|
ctaLocationSettings: "LOCATION SETTINGS",
|
||||||
deniedHeadline: "Location is switched off",
|
deniedHeadline: "Location is switched off",
|
||||||
deniedBody: "araharah guides you from where you physically are. Without location there is no route to draw.",
|
deniedBody: "arah² guides you from where you physically are. Without location there is no route to draw.",
|
||||||
deniedHint: "Grant location to Telegram, or send a pin in this chat: attach → Location.",
|
deniedHint: "Grant location to Telegram, or send a pin in this chat: attach → Location.",
|
||||||
lostRegister: "SYS // POSITION",
|
lostRegister: "SYS // POSITION",
|
||||||
lostHeadline: "Signal lost",
|
lostHeadline: "Signal lost",
|
||||||
@@ -193,7 +193,7 @@ export const strings: Record<Locale, ShellStrings> = {
|
|||||||
ctaAskViaChat: "MINTA VIA CHAT",
|
ctaAskViaChat: "MINTA VIA CHAT",
|
||||||
ctaLocationSettings: "TETAPAN LOKASI",
|
ctaLocationSettings: "TETAPAN LOKASI",
|
||||||
deniedHeadline: "Lokasi dimatikan",
|
deniedHeadline: "Lokasi dimatikan",
|
||||||
deniedBody: "araharah memandu dari kedudukan sebenar anda. Tanpa lokasi, tiada laluan boleh dilukis.",
|
deniedBody: "arah² memandu dari kedudukan sebenar anda. Tanpa lokasi, tiada laluan boleh dilukis.",
|
||||||
deniedHint: "Benarkan lokasi untuk Telegram, atau hantar pin dalam chat ini: lampiran → Lokasi.",
|
deniedHint: "Benarkan lokasi untuk Telegram, atau hantar pin dalam chat ini: lampiran → Lokasi.",
|
||||||
lostRegister: "SYS // POSITION",
|
lostRegister: "SYS // POSITION",
|
||||||
lostHeadline: "Isyarat hilang",
|
lostHeadline: "Isyarat hilang",
|
||||||
|
|||||||
+1
-1
@@ -233,7 +233,7 @@ async function boot(): Promise<void> {
|
|||||||
if (titleEl !== null) {
|
if (titleEl !== null) {
|
||||||
titleEl.textContent = destination !== null
|
titleEl.textContent = destination !== null
|
||||||
? strings[locale].titleRoute.replace("{name}", destination.name)
|
? strings[locale].titleRoute.replace("{name}", destination.name)
|
||||||
: "araharah";
|
: "arah²";
|
||||||
}
|
}
|
||||||
|
|
||||||
const recenter = createRecenterControl(mainEl, locale, () => {
|
const recenter = createRecenterControl(mainEl, locale, () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user