docs(07): plan phase 07 — hardening + private launch (6 plans, 3 waves)

This commit is contained in:
araharah-executor
2026-10-25 02:31:50 +08:00
parent 0d437db10c
commit 27b340fa6b
7 changed files with 3106 additions and 0 deletions
@@ -0,0 +1,478 @@
---
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>