Files

479 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 07-hardening-private-launch
plan: 05
type: execute
wave: 1
depends_on: []
files_modified:
- bot/src/security/initdata.ts
- bot/test/initdata.test.mjs
- bot/wrangler.jsonc
- bot/src/i18n/strings.ts
- docs/KNOWN-LIMITATIONS.md
- .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md
autonomous: true
requirements:
- PX-04
must_have_notes:
- "This plan pays the deferred debts (D-07) and ships the known-limitations document (D-08) — SC4's second half, delivered under the PX-04 phase."
must_haves:
truths:
- "The exact-900-second initData freshness boundary is pinned by a test — a `>=` regression cannot ship."
- "The dead `ParsedInitData` export and the unreachable branch are gone; the read helpers carry their intent comment."
- "The stale wrangler.jsonc comment is replaced with a present-tense record of the live namespace id."
- "A driver can reach the known-limitations document from /help in BOTH locales with one pointer line."
- "The limitations document tells the truth about OSM data gaps, the cross-metro CPU degradation, the /cari menu defect, the beta quality, and the roadmap pointers."
artifacts:
- path: "bot/test/initdata.test.mjs"
provides: "The T4b exact-900s boundary vector (IN-03)"
contains: "age 900 s"
- path: "bot/src/security/initdata.ts"
provides: "The IN-01 cleanup — the dead export deleted, the intent comments added"
contains: "tampered-vs-malformed"
- path: "bot/wrangler.jsonc"
provides: "The IN-02 present-tense namespace comment"
contains: "chat_sessions namespace created via"
- path: "bot/src/i18n/strings.ts"
provides: "The /help limitations pointer line (en + ms)"
contains: "Known limits:"
- path: "docs/KNOWN-LIMITATIONS.md"
provides: "The known-limitations document for drivers AND forks (D-08)"
contains: "Known limitations"
key_links:
- from: "bot/src/i18n/strings.ts"
to: "docs/KNOWN-LIMITATIONS.md"
via: "the helpMenu pointer line names the doc's public URL — a SINGLE constant, never a scattered URL"
pattern: "Known limits:"
- from: "bot/src/i18n/strings.ts"
to: "bot/scripts/register-commands.mjs"
via: "the pointer line does NOT start with '/', so the menu parse (CS5) and the setMyCommands derivation are unaffected"
pattern: "Had panduan:"
- from: "bot/src/security/initdata.ts"
to: "bot/test/initdata.test.mjs"
via: "the exact-900s vector pins the strictly-greater comparator's INCLUSIVE edge"
pattern: "900"
---
<objective>
Pay every debt D-07 parked, and ship the known-limitations document the drivers
(and forks) need — with a single pointer line from /help.
Purpose: D-07 — the three Phase-2 review INFOs (IN-01 dead code, IN-02 stale
comment, IN-03 the unpinned 900 s security boundary), the /cari command-menu
re-arm, and the Phase-3 A1/A3 assumption notes. D-08 — the roadmap's SC4
requires a known-limitations document BEFORE the first real drivers; an honest
one is also the product's integrity claim. The privacy audit in plan 07-03
links to it, and the launch gate in plan 07-06 checks it.
Output: the IN-01/IN-02/IN-03 fixes, the /cari re-arm record, the A1/A3
closure, `docs/KNOWN-LIMITATIONS.md` (new), and the /help pointer line in
`bot/src/i18n/strings.ts` (en + ms).
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/07-hardening-private-launch/07-CONTEXT.md
@.planning/phases/07-hardening-private-launch/07-RESEARCH.md
@.planning/phases/07-hardening-private-launch/07-PATTERNS.md
@.planning/phases/02-bot-skeleton-initdata-security/02-REVIEW.md
<interfaces>
The IN-03 boundary vector — copy the T4 harness shape verbatim from
`bot/test/initdata.test.mjs` lines 172–189:
```javascript
test("T4 freshness boundary: age 899 s → ok:true", async () => {
const validateInitData = await loadValidator();
const now = Math.floor(Date.now() / 1000);
const raw = signedUser(now - 899);
const res = await validateInitData(raw, TEST_TOKEN);
assert.equal(res.ok, true, `expected ok:true at 899 s, got ${JSON.stringify(res)}`);
});
```
The comparator under test — `bot/src/security/initdata.ts` lines 193–196:
```typescript
// — stale: age strictly beyond the D-13 window (899 ok / 900 ok / 901 stale) —
if (nowSeconds - authDate > maxAgeSeconds) {
return { ok: false, reason: "stale" as const };
}
```
The IN-01 targets — `bot/src/security/initdata.ts` lines 59–66 (the dead
export) and lines 87–104 / 154–165 / 198–214 (the parse blocks whose catch
branches keep raw/undefined):
```typescript
/** Shape of a fully parsed initData payload (hash/signature excluded from
* rawFields — they are auth metadata, not payload fields). */
export interface ParsedInitData {
user: WebAppInitUser;
chat?: WebAppInitChat;
authDate: number;
rawFields: Map<string, string>;
}
```
The IN-02 target — `bot/wrangler.jsonc` line 2 (the stale comment) versus the
live id on the `kv_namespaces` line:
```jsonc
// KV namespace id is filled in plan 03 task 1 after `npx wrangler kv namespace create chat_sessions`
...
"kv_namespaces": [{ "binding": "chat_sessions", "id": "59f58a54bc44483e89872b20a005a5eb" }]
```
The CS5 gate — `bot/test/commands.test.mjs` lines 134–155 (the menu parse and
the exact-command assertion). The limitations pointer line MUST NOT start with
`/` or it is parsed as a command and breaks CS5:
```javascript
function menuEntries(helpMenu) {
return helpMenu
.split("\n")
.filter((line) => line.startsWith("/"))
.map((line) => {
const em = line.indexOf(" — ");
assert.ok(em !== -1, `helpMenu line lacks its " — " description marker: ${line}`);
return { command: line.slice(1, em), description: line.slice(em + 3).trim() };
});
}
```
The `helpMenu` strings as they stand — `bot/src/i18n/strings.ts` lines 94–95
(en) and 132–133 (ms):
```typescript
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",
```
```typescript
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",
```
The `/help` handler — `bot/src/bot/commands/help.ts` lines 18–20 (the reply is
the `helpMenu` string verbatim, so the pointer line rides it automatically):
```typescript
export function helpReplyText(locale: Locale): string {
return strings[locale].helpMenu;
}
```
The re-arm tool — `bot/scripts/register-commands.mjs` (it arms every scope it
may shadow: default, default+ms, all_private_chats, all_private_chats+ms, and
clears the group scopes; it derives its descriptions from `helpMenu`).
</interfaces>
<tasks>
<task type="auto">
<name>task 1: pay IN-01 and IN-02 (dead code + the stale wrangler comment) and pin IN-03 (the exact-900s boundary)</name>
<files>bot/src/security/initdata.ts, bot/test/initdata.test.mjs, bot/wrangler.jsonc</files>
<read_first>
- bot/src/security/initdata.ts (the dead export, the parse blocks, the comparator)
- bot/test/initdata.test.mjs (the T4 harness shape + `signedUser`/`loadValidator` helpers)
- bot/wrangler.jsonc (line 2's stale comment versus the live namespace id)
- .planning/phases/02-bot-skeleton-initdata-security/02-REVIEW.md §Info (IN-01, IN-02, IN-03 verbatim)
</read_first>
<action>
**IN-01** — in `bot/src/security/initdata.ts`:
1. DELETE the dead `ParsedInitData` interface (lines 59–66) entirely, INCLUDING
its doc comment. It is never constructed and never returned (`validateInitData`
returns `InitDataResult`). Before deleting, run
`grep -rn "ParsedInitData" bot/ web/ routing/ search/` and confirm the ONLY
hits are the declaration itself — if another file imports it, do NOT delete
it; instead re-point it with an intent comment naming why it stays, and
record that in the SUMMARY.
2. Add ONE intent comment over the `parseFields` catch block (lines 96–100)
reading, in the file's voice:
`// Intent: an undecodable percent-escape keeps the RAW value so the HMAC compare fails ("tampered") rather than silently dropping the field — malformed-vs-tampered precedence (D-14).`
3. Add ONE intent comment over the `user` JSON parse catch (lines 161–164)
reading:
`// Intent: a malformed user payload is the "malformed" class, not a tampered one — the field never reaches the HMAC path.`
4. Add ONE intent comment over the `chat` JSON parse catch (lines 210–213)
reading:
`// Intent: a malformed chat payload degrades to "no chat context" (a private chat) — it never fails an otherwise-valid private-chat payload.`
**IN-02** — in `bot/wrangler.jsonc`, REPLACE line 2 (the stale
"filled in plan 03 task 1" comment) with this present-tense record, in the
file's own comment voice:
```jsonc
// chat_sessions namespace created via `npx wrangler kv namespace create chat_sessions` — the id below is live.
```
**IN-03** — in `bot/test/initdata.test.mjs`, add EXACTLY ONE new case
immediately after the existing `T4 freshness boundary: age 901 s → reason stale`
case, copying that case's harness shape verbatim (`loadValidator()`,
`signedUser(now - 900)`, `TEST_TOKEN`):
```javascript
test("T4 freshness boundary: age 900 s → ok:true (the boundary is INCLUSIVE)", async () => {
const validateInitData = await loadValidator();
const now = Math.floor(Date.now() / 1000);
const raw = signedUser(now - 900);
const res = await validateInitData(raw, TEST_TOKEN);
assert.equal(res.ok, true, `expected ok:true at exactly 900 s, got ${JSON.stringify(res)}`);
});
```
The case name MUST contain the literal `age 900 s` — it pins the comparator
direction (`>` vs `>=`) that the implementation comment already claims. Do
NOT change the comparator in `initdata.ts` — the implementation is correct
today; the test is what makes the claim a pin.
Finally, run `npm test -w bot` and `npm run bot:typecheck` — both must pass.
</action>
<acceptance_criteria>
- `grep -c "ParsedInitData" bot/src/security/initdata.ts` returns 0 (the dead export is gone) and `grep -rn "ParsedInitData" bot/src web/src` returns nothing
- `grep -c "Intent:" bot/src/security/initdata.ts` returns ≥ 3 (the three read-helper intent comments)
- `grep -c "age 900 s" bot/test/initdata.test.mjs` returns ≥ 1 (the IN-03 vector)
- `grep -c "plan 03 task 1" bot/wrangler.jsonc` returns 0 (the stale comment is gone) and `grep -c "the id below is live" bot/wrangler.jsonc` returns 1
- `npm test -w bot` exits 0 with `0 failed`
</acceptance_criteria>
<verify>
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm test -w bot 2>&1 | tail -12 && npm run bot:typecheck 2>&1 | tail -5</automated>
</verify>
<done>
IN-01's dead export is gone and the three read helpers carry their intent
comments; IN-02's comment states the post-fact truth; IN-03's exact-900s
vector is green.
</done>
</task>
<task type="auto">
<name>task 2: the known-limitations document (D-08) + the /help pointer line (en + ms)</name>
<files>docs/KNOWN-LIMITATIONS.md, bot/src/i18n/strings.ts</files>
<read_first>
- docs/CONFIGURATION.md (the clinical-prose style: a leading "surfaces, by kind" table, `File | Applies to` scoping, backticked identifiers, bold for requirements, every statement names its evidence)
- bot/src/i18n/strings.ts (the D-05 law in the header + the two helpMenu strings)
- bot/test/commands.test.mjs (the CS5 menu parse — the pointer line must not start with "/")
- .planning/phases/07-hardening-private-launch/07-CONTEXT.md (D-08 verbatim)
- routing/scripts/measure-route-cpu.mjs (the LADDER HISTORY, lines 30–40 — the c3 evidence)
</read_first>
<action>
Create `docs/KNOWN-LIMITATIONS.md` (NEW — the whole file is new), in the
`docs/CONFIGURATION.md` clinical-prose style (no marketing, no emoji, one
honest sentence per claim, every claim naming its evidence). It is written for
TWO readers — a DRIVER and a FORK — and must contain these sections in this
order:
1. `# Known limitations` + a one-paragraph lead: what araharah is today (a
KL-metro turn-by-turn navigator inside Telegram, on OpenStreetMap data),
and the plain statement that this page lists what it does NOT do well.
2. `## Data coverage` — the OSM gaps, honestly:
- **No traffic awareness.** There is no live traffic feed; the ETA is a
distance-and-speed estimate from the route model, not a prediction.
- **No lane guidance.** There is no lane-level arrow guidance; the
maneuver glyph names the turn, not the lane.
- **Speed limits appear only where OSM maps them** — many KL roads have
no `maxspeed` tag, so no limit is shown.
- **Turn restrictions and one-ways** are only as good as the OSM snapshot
the road graph was built from; a recent change may not be reflected.
- **The road graph is a build-time artifact** (`routing/data/kl-roadgraph.v1.bin`),
rebuilt by a maintainer — forks rebuild their own (`docs/ROAD-GRAPH.md`).
3. `## Cross-metro routes may not resolve` — the c3 class, in one paragraph:
short product routes (2–8 km) measure 2–8 ms of Worker CPU (inside the
free plan's 10 ms), while cross-metro routes (20–30 km) measured p95
38–93 ms and therefore degrade HONESTLY to the router-pending card
("the router is not ready for this trip") rather than freezing or drawing
a wrong line. Evidence: `routing/scripts/measure-route-cpu.mjs` (LADDER
HISTORY) and the Phase 6 plan summary. The committed upgrade lever is the
ALT landmark preprocessing — a NEXT-MILESTIME item, NOT available in this
release. State the free-tier reason: Workers Paid was rejected (the $0
mandate).
4. `## The /cari command may be missing from the Telegram menu` — the
Telegram `setMyCommands` non-default-scope defect. State the symptom: the
menu shows /help and /privacy but not /cari (Telegram resolves a chat's
menu from the narrowest scope that has ANY commands set, and a stale
all_private_chats set shadows the default scope). The WORKAROUND: type
`/cari` in the chat — the command works. The bot re-arms every scope
(`bot/scripts/register-commands.mjs`) but the Telegram client may cache the
old menu until the app restarts.
5. `## Beta quality` — one honest paragraph: this is a private beta with a
handful of drivers in Kuala Lumpur; the device matrix was walked on real
Android, iOS, Telegram Desktop and a plain browser; battery drain was
MEASURED on one Android phone (record the method and the caveats pointer:
the launch gate file holds the run); fullscreen and orientation behaviour
varies by Telegram client version, and the app degrades to a usable
portrait/landscape layout when the platform refuses a fullscreen request.
Name the KNOWN limits honestly: no voice guidance, no live trip sharing,
no toll toggle, no traffic ETA — those are post-validation milestones, not
defects.
6. `## What is being worked on next` — the roadmap pointers D-08 names, each
with its honest status: the WASM-router track (a client-side router as the
static-hosted endgame), the ALT landmark preprocessing (the cross-metro
lever), and the "engine B" (a fully client-side webview router). State
that none of them are in this release.
7. `## Privacy in one paragraph` — coordinates live only in memory and in
TTL-expiring KV (15-minute sessions, 5-minute location pins, 2-hour
guidance envelopes); there is NO telemetry of any kind in this release —
no analytics, no crash reporting, no usage tracking; the shipped bundle
contacts only the map tile host, the Telegram API, and the app's own
Worker. Point at `docs/PRIVACY-AUDIT.md` for the full record.
8. `## How to run the audit yourself` — for the FORK reader: the exact
command `npm run privacy-audit` and the exact command `npm run budget-audit`,
with one line on what each proves and what exit code 2 means (environment
drift, never a pass).
THEN — in `bot/src/i18n/strings.ts`, add ONE pointer line to the END of BOTH
`helpMenu` strings. It MUST NOT start with `/` (CS5). Use EXACTLY these
strings:
- en — append `\nKnown limits: https://github.com/araharah/araharah/blob/main/docs/KNOWN-LIMITATIONS.md`
- ms — append `\nHad panduan: https://github.com/araharah/araharah/blob/main/docs/KNOWN-LIMITATIONS.md`
The URL MUST be identical in both locales and MUST appear in EXACTLY ONE place
per locale (the strings module is the single copy source — the D-05 law). If
the repository's public URL is not `github.com/araharah/araharah`, use the
repo's actual remote URL (run `git remote -v` and use the HTTPS remote) —
but the SAME URL in both locales, and record the choice in the SUMMARY.
After the edit, run `npm test -w bot` — CS5 must still pass (the pointer line
is filtered out of the menu parse because it does not start with `/`).
</action>
<acceptance_criteria>
- `docs/KNOWN-LIMITATIONS.md` exists and contains the section headings `## Data coverage`, `## Cross-metro routes may not resolve`, `## The /cari command may be missing from the Telegram menu`, `## Beta quality`, `## What is being worked on next`, `## Privacy in one paragraph`, `## How to run the audit yourself`
- `grep -c "KNOWN-LIMITATIONS.md" bot/src/i18n/strings.ts` returns exactly 2 (one per locale — the single-copy law)
- `grep -c "Known limits:" bot/src/i18n/strings.ts` returns 1 and `grep -c "Had panduan:" bot/src/i18n/strings.ts` returns 1
- `grep -c "helpMenu" bot/src/i18n/strings.ts` returns ≥ 2 (unchanged shape)
- `npm test -w bot` exits 0 with `0 failed` (CS5 still green — the pointer line is not parsed as a command)
</acceptance_criteria>
<verify>
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && npm test -w bot 2>&1 | tail -12 && grep -c "KNOWN-LIMITATIONS.md" bot/src/i18n/strings.ts</automated>
</verify>
<done>
The limitations document exists with all seven sections and the honest c3
and /cari content, and /help carries one pointer line per locale that the
CS5 menu gate still ignores.
</done>
</task>
<task type="auto">
<name>task 3: the /cari re-arm attempt + the A1/A3 assumption closure, recorded honestly</name>
<files>.planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md</files>
<read_first>
- .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md (the A1 stale-fix leg at line 44 and the A3 local-dev note at line 99, plus the FINAL VERDICT table at line 258)
- bot/scripts/register-commands.mjs (the re-arm tool — its scope table)
- .planning/phases/07-hardening-private-launch/07-CONTEXT.md (D-07 items 2 and 3)
</read_first>
<action>
Two records, both in
`.planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md`
(append a new dated section at the END of that file — do not rewrite its
history):
**A — the /cari re-arm attempt.** Attempt to re-arm the command menu by
running the existing tool:
```bash
npm run bot:commands
```
This requires `BOT_TOKEN` in `bot/.dev.vars` (gitignored) or the environment.
Record the outcome HONESTLY in a new `## 2026-10-25 — Phase 7 debt closure (/cari re-arm + A1/A3)` section:
- If the script ran and every scope reported `ok:true`, record the six scope
labels it printed and the literal line `all command scopes armed`.
- If it could not run (no token, no network, a Telegram API error), record the
EXACT error line and mark the re-arm **NOT RUN** — a fabricated "re-armed"
claim is forbidden (D-05's honesty law).
- In BOTH cases, state the residual risk: Telegram clients cache the command
menu; the defect is a Telegram `setMyCommands` scope-resolution behaviour,
not a bot bug — and `docs/KNOWN-LIMITATIONS.md` now carries the user-facing
statement plus the workaround (typing `/cari`).
**B — the A1/A3 assumption closure.** In the same section, close or re-point
the two Phase-3 assumptions:
- **A1** (the Android stale-fix hypothesis — that
`navigator.geolocation.getCurrentPosition()` refreshes the stale native
cache so the next `LocationManager.getLocation()` returns fresh coordinates).
The Phase-3 device matrix recorded the A1 leg as passed with the note
"A1 notes stand in 03-RESEARCH.md" — the walk completed WITHOUT needing the
toggle. Record the honest closure: **A1 is NOT RETIRED as a mechanism
hypothesis** — it was simply not needed on the tested device and route;
the app's chain degrades gracefully to polling when the stale-fix path does
not fire. The risk it covers is therefore MITIGATED-BY-DESIGN rather than
closed. State that in one sentence and point at the degradation path
(`web/src/location/chain.ts`).
- **A3** (that `localhost:8787` is a secure context so `navigator.geolocation`
works under `wrangler dev` while a LAN-IP dev URL would not). Record the
closure: **A3 IS RETIRED as a decision constraint** — the project deploys to
a remote origin (`workers.dev`), not to a LAN-IP dev server, so the
assumption no longer constrains any open decision. State it in one sentence.
Do NOT edit the Phase-3 FINAL VERDICT table (it is the historical record); the
new dated section is the closure.
</action>
<acceptance_criteria>
- `.planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md` contains the literal `## 2026-10-25 — Phase 7 debt closure (/cari re-arm + A1/A3)`
- the new section contains the literal `A1` and the literal `A3` and the literal `/cari`
- the new section contains either `all command scopes armed` (the run happened) OR an explicit `NOT RUN` with the exact error line (it did not) — never a fabricated success
- `grep -c "FINAL VERDICT" .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md` returns 1 (the historical table is untouched)
</acceptance_criteria>
<verify>
<automated>export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"; cd /home/C-Fu/araharah && grep -c "Phase 7 debt closure" .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md && grep -c "A3 IS RETIRED" .planning/phases/03-mini-app-shell-geolocation/03-DEVICE-MATRIX.md</automated>
</verify>
<done>
The /cari re-arm outcome and the A1/A3 closure are recorded in the
Phase-3 device matrix, honestly — with the re-arm either armed or
explicitly NOT RUN.
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| The Telegram client → the bot's command menu | The client resolves the menu from the narrowest scope with any commands; a stale scope silently hides a command. |
| The initData comparator → the session gate | The 900 s freshness edge is the authentication boundary; a `>=` regression widens the acceptance window by one second. |
| The limitations document → the driver | A doc that overstates capability is a false promise; the doc must state only what the measurements support. |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-07-08 | Spoofing (Authentication Bypass) | `bot/src/security/initdata.ts` — the 900 s freshness comparator | mitigate | The exact-900s inclusive boundary is pinned by a named test vector (`T4 freshness boundary: age 900 s → ok:true`); a `>=` regression turns that case red. The comparator itself is unchanged — the test is what makes the existing claim a pin. |
| T-07-23 | Spoofing | `bot/wrangler.jsonc` — the stale comment telling a future maintainer to re-create the namespace | mitigate | IN-02 replaces the plan-scoped comment with a present-tense record naming the live id's provenance; a maintainer following the comment can no longer create a replacement namespace. |
| T-07-24 | Elevation of Privilege | the `/cari` menu gap — a driver cannot find the search command | mitigate | The re-arm tool arms every shadowing scope (default, default+ms, all_private_chats, all_private_chats+ms) and clears the group scopes; the residual Telegram-side caching behaviour is disclosed to drivers with the type-the-command workaround in `docs/KNOWN-LIMITATIONS.md`. The command itself was always functional — the gap is discoverability, not authorization. |
| T-07-25 | Information Disclosure | `docs/KNOWN-LIMITATIONS.md` — a doc that names internal identifiers | accept | The doc names the road-graph binary path and the CPU measurement script — both are PUBLIC repository artifacts under GPL-3.0 and are documented for forks by design. No secret, no token, no coordinate appears. |
| T-07-26 | Repudiation | the debt closure record | mitigate | The re-arm outcome is recorded either as the printed scope results or as an explicit `NOT RUN` with the verbatim error — the record never asserts an outcome that did not happen (the phase's no-fake protocol). |
</threat_model>
<verification>
Run after all tasks:
```bash
export PATH="$HOME/.nvm/versions/node/v24.18.0/bin:$PATH"
cd /home/C-Fu/araharah
npm test -w bot
npm run bot:typecheck
```
Both must exit 0. `bot/test/commands.test.mjs`'s CS5 case must pass — the
limitations pointer line must stay invisible to the command-menu parse.
</verification>
<success_criteria>
- The dead `ParsedInitData` export is gone, the three read helpers carry their intent comments, and the stale wrangler comment states the post-fact truth.
- The exact-900s inclusive boundary is pinned by a green test vector.
- `docs/KNOWN-LIMITATIONS.md` exists with all seven sections, covering OSM data gaps, the c3 cross-metro degradation with its measurement evidence, the /cari menu defect with its workaround, the beta quality statement, the roadmap pointers, the privacy posture, and the fork-facing audit commands.
- `/help` carries exactly one limitations pointer line per locale, in the strings module only, and CS5 still passes.
- The /cari re-arm attempt and the A1/A3 closure are recorded in the Phase-3 device matrix honestly.
</success_criteria>
<output>
After completion, create `.planning/phases/07-hardening-private-launch/07-05-SUMMARY.md`
</output>