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
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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
View File
@@ -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",
+21 -19
View File
@@ -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
View File
@@ -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" }]
} }
+9
View File
@@ -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.
+4 -4
View File
@@ -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
View File
@@ -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>
+2 -2
View File
@@ -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
View File
@@ -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, () => {