Commit Graph
257 Commits
Author SHA1 Message Date
araharah-executor 13e5073261 feat(04-03): the alternates callback handler + the bot factory wiring (D-02)
- search-alternate.ts: scoped alt: callback matcher that answers FIRST
  (30-second rule), validates the id against the slug charset BEFORE the
  lookup (T-04-23), resolves via source.byId() — honest null, never a
  fabricated card; catch-all callback_query:data answers everything else
  so Telegram never spins for 60 s (T-04-27)
- sendVenueCard: the ONE card builder shared by /cari and the swap path —
  first card and swapped card structurally identical; a tap sends a NEW
  sendVenue (no in-place edits — they destroy the venue payload, T-04-28)
- alternatesFor(entry, source): async — awaits search() on the entry's own
  name, excludes the entry, caps at 4 (D-02)
- bot.ts: ONE createGazetteerSource() shared by both registrations (single
  memoized index); origin = env.MINI_APP_URL ?? ''; header names the second
  documented D-02 exception
- cari.ts now calls the shared card builder
- wrangler dry-run inlines the gazetteer (suria-klcc marker in the bundle) —
  completes the L3 bundle-marker half deferred from plan 04-01
2026-10-24 01:15:21 +08:00
araharah-executor a73a38830a feat(04-03): the /cari command + venue card + OPEN deep link (D-01/D-02/D-10/D-11)
- cari.ts: /cari + /search twin, 128-char pre-search cap (T-04-21), honest
  no-match refusal (D-13), single sendVenue card with inline OPEN + alternates
- deep-link carrier corrected per RESEARCH Pitfall 1: web_app button URL
  carries ?dest=<plain slug id> — WebAppInfo has only a url field; no crypto
- origin from configured MINI_APP_URL only (never user input); blank origin
  omits the OPEN button instead of guessing (T-04-30)
- strings.ts: searchUsage/searchNoMatch/searchQueryTooLong/searchOpenButton/
  osmAttribution in en+ms; helpMenu gains the /cari line in both locales
- category-only logging law (D-15): four literal rejection categories, query
  never logged; success path logs nothing
- config: MINI_APP_URL in Env + wrangler vars (public, not secret) +
  .dev.vars.example + docs/CONFIGURATION.md row and sections
- bot deps gain @araharah/search (workspace link)
2026-10-24 01:13:13 +08:00
araharah-executor 3aec9542a3 docs(04-02): complete golden-corpus + bake-off-close plan
- SUMMARY: 3/3 tasks, 5 commits, corpus gate green at 100% top-3 / 7-7 refusals,
  lever None; deviations documented (fixture-source implication, bbox pin,
  pavilion-kl coordinate fix)
2026-10-24 01:05:25 +08:00
araharah-executor 99bc9c9a09 docs(04-02): close the D-14 bake-off — threshold met on the first run, lever None
- docs/SEARCH-QUALITY.md: the bake-off close record — corpus v1 (75 cases) x
  gazetteer v1 (30 entries), measured 2026-10-23T17:02Z via npm run search:corpus
  - top-1 66/68 (97.1%), top-3 68/68 (100.0%) >= 90% threshold
  - negatives refused 7/7, refusal margin 0.643 < 0.72 admission line
  - per-category table (landmark/abbreviation/glued/typo/alias all 100%)
  - escalation lever: None — met on first run; no abbreviations.ts row, no
    alias, no corpus edit; the fixed ladder was never climbed
  - coordinate verification: 10 seed entries spot-checked against Nominatim
    (identifying UA, 1 req/s): 8/10 within 0.01°; bukit-bintang outlier is an
    area-vs-point discrepancy (no change); pavilion-kl was a real ~2 km error
  - both top-1 misses documented as expected disjoint-tier behaviour
- kl-gazetteer.json: pavilion-kl coordinate corrected (3.1478,101.695) ->
  (3.149,101.7132) — Rule 1 data bug found by the prescribed Nominatim
  verification; post-fix delta 0.0002°; corpus is coordinate-blind, all suites green
2026-10-24 01:03:39 +08:00
araharah-executor c6a5f2b008 feat(04-02): Overpass top-up proposer (D-06) + JSON-to-SQLite converter (D-05)
overpass-topup.mjs — maintainer-time repo tool, six hard laws:
- never imported by bot/src or web/src (TP1 gate); Worker reads only the committed JSON
- exactly ONE request per run: both live paths (Overpass POST, Nominatim GET)
  share a single requestJson helper — one fetch( call site (TP7)
- identifying User-Agent araharah-gazetteer-topup/1.0 mandatory (missing UA = HTTP 406)
- busy/saturated/unusable response -> 'Overpass instance busy — retry later', exit 0 (TP4)
- output is a PROPOSAL only; source never names the curated file, so it cannot write it (TP6)
- --source=overpass|nominatim|fixture (--fixture implies fixture), --endpoint/OVERPASS_ENDPOINT
  override, bbox-bounded single-family query pinned verbatim (3.05,101.55,3.25,101.80) (TP3)
- --fixture selects the offline path by itself (a fixture flag silently hitting the
  live network would be a trap — fixed during GREEN)

overpass-response.json — committed real-shape Overpass response (6 KL elements:
nodes with lat/lon, ways/relation with center) so the test is deterministic.

json-to-sqlite.mjs — backend #2's dataset format reachable today (D-05):
- reads the same committed kl-gazetteer.json, emits kl-gazetteer.sqlite via
  node:sqlite (zero added deps) with the SAME id set (CV1), plus the ODbL
  notice embedded in a metadata table + NOTICE sidecar (ODbL s4.4, T-04-20)
- deterministic: rows in dataset order, no timestamps
2026-10-24 01:01:00 +08:00
araharah-executor ec98c8d454 test(04-02): RED — D-06 topup law suite (TP1-TP7) + D-05 converter suite (CV1-CV3)
- TP1 no 'overpass' under bot/src|web/src (never a runtime dependency)
- TP2 identifying User-Agent pinned (missing UA = HTTP 406, live-verified)
- TP3 bbox-bounded query tuple present
- TP4 busy fixture -> exit 0 + 'Overpass instance busy' soft-exit line
- TP5 committed fixture -> PROPOSAL output with name= lines + NEVER-writes law
- TP6 source never names the curated dataset file
- TP7 exactly one fetch( call site (shared helper for both live paths)
- CV1 converter preserves the gazetteer id set exactly (read back via node:sqlite)
- CV2 converter reads the committed kl-gazetteer.json
- CV3 no sqlite/json-to-sqlite under bot/src|web/src

8 cases verified failing (scripts absent) before GREEN.
2026-10-24 00:56:08 +08:00
araharah-executor b2689f71b1 feat(04-02): pinned golden corpus + D-14 corpus runner + Nyquist gate
- golden-corpus.json: 75 hand-written pinned cases (D-13) — landmark 17 /
  abbreviation 15 / glued 7 / typo 14 / alias 15 / negative 7, incl. two
  near-miss negatives (Penang 0.625, Kampung Cina ~0.58) below the 0.72
  admission line; threshold.top3_hit_rate pinned at 0.9
- gazetteer-corpus.mjs: the D-14 runner — imports the SAME src/index.ts the
  bot uses, measures top-1/top-3 + per-category rates + avg ms/query, prints
  the refusal margin and the fixed escalation ladder on failure, exports
  measureCorpus() for the in-process suite, exits 0 only on threshold + zero leaks
- corpus.test.mjs goes green: CO1-CO8 (the Nyquist gate inside npm run search:test)
- delegates: search:corpus (root) + corpus (search workspace)

Measured: top-3 68/68 (100.0%) >= 90%, negatives refused 7/7, margin 0.643 < 0.72 —
threshold met on the first run (D-14 lever: None).
2026-10-24 00:54:47 +08:00
araharah-executor dea7971358 test(04-02): RED — golden-corpus D-14 gate suite (CO1-CO8)
- CO1 top-3 >= corpus threshold (D-14), measured in-process via the runner's measureCorpus
- CO2 zero negative leaks (honest refusal, D-13)
- CO3 tautology floor: >= 15 non-negative queries not string-equal to any alias/name
- CO4/CO5 every expectId exists in the gazetteer + start_param-safe charset
- CO6 size >= 60 + per-category floors (landmark 12 / abbreviation 15 / glued 6 / typo 12 / alias 12 / negative 6)
- CO7 per-category rates printed so suite and runner output agree
- CO8 refusal margin pinned: every negative's best two-level fuzzy sim < 0.72, one near-miss in [0.55, 0.72)

All 8 cases verified failing (ENOENT corpus fixture) before GREEN.
2026-10-24 00:52:33 +08:00
araharah-executor 2e724cbee1 docs(04): commit plan-revision residue + 04-01 SUMMARY 2026-10-24 00:39:32 +08:00
araharah-executor 8b6ad2047f docs(04-01): complete shared-search-module plan
- 3 tasks executed RED→GREEN (6 commits): workspace scaffold + JUPEM
  normalizer, OSA distance + disjoint-tier scorer, curated 30-entry KL
  gazetteer + lazy three-environment loader + GPL/ODbL licensing
- 39/39 suite green; typecheck clean; esbuild inlines the dataset;
  wrangler dry-run exit 0; bot + web suites unaffected
- SRCH-01/SRCH-02 foundation proven: KLCC→suria-klcc, negatives refused
2026-10-24 00:38:58 +08:00
araharah-executor 8579da9d71 feat(04-01): curated KL gazetteer + lazy three-environment loader + GPL/ODbL licensing
- kl-gazetteer.json: 30 hand-curated KL-metro landmarks (10 required + 20
  more) with locals' aliases as native data (D-08) — canonical OSM names
  as name, shorthand ('KLCC', 'DUKE', 'Sg Wang') as aliases — plus the
  ODbL-1.0 attribution header traveling inside the JSON (D-06/D-07)
- gazetteer.ts: the three laws — lazy memoized index never at module
  scope (Workers startup, T-04-04), static JSON import surviving
  wrangler/Vite/bare-Node (D-07), zero logging/storage (D-15); aliases
  normalized once at index-build (D-09 symmetry)
- source.ts: the D-03 SearchSource — async search() + synchronous byId()
  (slug-validated, honest null); tiered admission (fuzzy only at sim
  >= 0.72) so 'Kuching'/'Penang' refuse as [] — never a fabricated pin
- index.ts: the named-export public API (no default, subpath+extension
  imports per the @araharah/theme precedent)
- LICENSE: the verbatim GPL-3.0 text (D-04); LICENSES.md: the dual-licence
  note (code GPL-3.0, data ODbL-1.0 + OSM attribution); README: corrected
  licence section + search/ in the layout, four-package table, commands

G1-G11 + L1-L3 green (39/39); typecheck clean; esbuild bundle carries the
suria-klcc marker; wrangler dry-run exits 0; bot + web suites unaffected.
2026-10-24 00:36:03 +08:00
araharah-executor f1b31ca7ed test(04-01): RED — gazetteer schema/license gates (G1-G11) + three-environment loader proof (L1-L3)
- G1-G6: dataset invariants — schema, slug-id uniqueness, canonical-name
  law ('KLCC' is an alias, never a name), DUKE-as-alias, KL bbox, the
  ODbL header traveling inside the JSON
- G7-G9: the licensing gate — verbatim GPL-3.0 LICENSE, LICENSES.md,
  the README no longer claiming 'all rights reserved'
- G10-G11: no logging/storage anywhere in the module (D-15); the loader
  builds its index inside a function, never at module top level
- L1-L3: the phase's top mechanical risk — bare-Node loading with the
  SRCH-01/negative probes, the static JSON import law, esbuild INLINING
  the dataset + the wrangler dry-run build guard

13 cases fail while the dataset, loader, LICENSE files, and README fix
are absent — the RED gate.
2026-10-24 00:32:57 +08:00
araharah-executor 6368bad3d5 feat(04-01): OSA Damerau distance + disjoint-tier scorer (the residual net)
- distance.ts: hand-rolled 3-band OSA, adjacent transposition = 1 edit,
  zero imports (D-07); the row rotation is pinned by the D1-D9 vectors
- score.ts: TIER bands strictly disjoint descending — EXACT 1.00 /
  ALIAS_EXACT 0.90 / PREFIX 0.75 / TOKEN_SET 0.65 / FUZZY as the 0.60
  CEILING (a fuzzy score is fuzzySim * 0.60, never a 0.00 multiplier);
  FUZZY_ADMIT_SIMILARITY = 0.72 exported for source.ts + the corpus
  runner; token-level residual lets 'Sentrl' reach 'KL Sentral' while
  'Jln Tun Razak' cannot reach 'Jalan Ampang'
- scoreCandidate returns the RAW matched alias (corpus provenance);
  an exact ALIAS hit rides ALIAS_EXACT so a name-exact outranks it
- score.test.mjs S7 corrected during GREEN: admission belongs to
  source.ts — scoreCandidate asserts the sub-floor similarity instead

D1-D9 + S1-S7 green; typecheck clean.
2026-10-24 00:32:44 +08:00
araharah-executor 25ee1e9c61 test(04-01): RED — OSA distance vectors (D1-D9) + disjoint-tier scorer suite (S1-S7)
- 9 hand-computed Damerau-Levenshtein vectors (transpositions, empties,
  kitten/sitting sanity, the corpus's klcc/kclc + sunway/sunwya typos)
- 7 scorer laws: tier literals + disjointness, min-length guard before
  exact, exact-outranks-fuzzy, mid-word PREFIX vs token-boundary
  TOKEN_SET, fuzzy residual 0.75*0.60 arithmetic, the 0.72 admission
  boundary (Penang/Padang 0.667 refuses), matchedAlias raw provenance

All 16 new cases fail while src/distance.ts + src/score.ts are absent.
2026-10-24 00:25:19 +08:00
araharah-executor a9f9ab380c feat(04-01): search contracts + JUPEM abbreviation table as DATA + symmetric normalizer
- types.ts: GazetteerEntry / SearchQuery / SearchHit / SearchSource (D-03/D-07/D-08)
  — byId() is separate and synchronous; empty search() array = honest refusal
- rules/abbreviations.ts: the vendored JUPEM table (49 rows incl. Presint→Prsn
  vowel-drop and MS 2039 A3 Kpg forms) as a ReadonlyArray of pairs, inverted once
  by a single generic builder — extends by file edit, never a code change (D-09)
- normalize.ts: NFKD fold → punctuation collapse (keeps '/') → whole-token
  expansion (canonical = long form) → longest-first glued split (remainder >= 3)

T1-T9 green; typecheck clean.
2026-10-24 00:23:37 +08:00
araharah-executor ebcd542643 test(04-01): RED — search workspace scaffold + normalizer vector suite (T1-T9)
- add the search/ npm workspace (root workspaces + search:test/search:typecheck delegates)
- zero-dependency runner/harness copied from the bot/test precedent
- 9 named normalizer laws: rule vectors, symmetry, whole-token-only,
  slash preservation, glued split, vowel-drop, idempotence, folding,
  table-is-data (>= 47 rows, one generic builder)

All 9 cases fail while src/normalize.ts is absent — the RED gate.
2026-10-24 00:21:59 +08:00
araharah-executor c9458add47 docs(04): plan revision 2 — 04-03 contract wording fixes (inline-card sequencing, async alternatesFor) 2026-10-24 00:14:02 +08:00
araharah-executor 4a83ec6871 docs(04): plan revision 1 — fix wave-1 bundle proof + acceptance-criteria corrections 2026-10-24 00:02:27 +08:00
araharah-executor c0134c7e99 docs(04): pattern map — analogs + conventions for every phase-4 surface 2026-10-23 23:40:46 +08:00
araharah-executor a575ecd139 docs(04): create phase plan — 4 plans in 3 waves (search module, corpus/bake-off, /cari surface, webview hand-off + device gate) 2026-10-23 23:40:41 +08:00
araharah-executor aa34e7b50d docs(04): research verification pass — track .planning/, Overpass UA correction, deep-link MTProto confirmation 2026-10-23 23:40:41 +08:00
araharah-executor 7994c84a40 docs(04): research phase domain — destination search + KL gazetteer
Phase 4 research: in-Worker SearchSource over a curated KL gazetteer.

Two critical platform findings:
- Telegram start_param is NEVER delivered to inline web_app buttons
  (direct links + attachment menu only). The destination id must ride
  as a URL query param on the button's URL; D-11's substance preserved.
- beforeinstallprompt never fires in a WebView. ONBD-03 must use
  Telegram's native WebApp.addToHomeScreen(); iOS status is always
  "unknown" by platform design.

Benchmarked the CPU budget: naive full-corpus Damerau is 52ms at
D-07's max scale vs the 10ms Workers Free limit. Exact-hash-first +
token-prefiltered residual is 0.0003ms.

Also: Phase 2 CS3/CS4/CS5 gates need declared amendments; ODbL
attribution obligation for the OSM-derived gazetteer.
2026-10-23 22:50:13 +08:00
araharah-executor 275879a18b ambek .planning 2026-10-23 22:37:55 +08:00
araharah-executor e512104d5e assets: add arah² brand assets — logo + PSD source; display logo in README header 2026-10-23 21:10:00 +08:00
araharah-executor 91ad0c9d93 docs: generate + verify project documentation — canonical docs, per-package READMEs, verified fix pass
- New: docs/ARCHITECTURE.md, docs/GETTING-STARTED.md, docs/DEVELOPMENT.md,
  docs/TESTING.md, docs/CONFIGURATION.md (colored mermaid diagrams, quoted labels)
- Regenerated README.md (arah² / arah2bot naming, canonical clone URL)
- New per-package READMEs: theme/, bot/, web/
- Verified 13 docs against the live codebase (2 fix iterations + 2 residual
  corrections); corrected 56 stale claims across AGENTS/DESIGN/IDEAS/GSD_FIX
2026-10-23 20:56:40 +08:00
araharah-executor af3edefbc6 add sume 2026-10-23 19:18:43 +08:00
araharah-executor 1890b5b1ca docs(03): verification exit addendum — probe removal + GEO-03 platform allowlist 2026-10-23 18:53:28 +08:00
araharah-executor 8a5c8b415f fix(03-04): GEO-03 probe platform allowlist — official telegram-web-app.js writes one sessionStorage bookkeeping key; app surfaces stay empty 2026-10-23 18:52:59 +08:00
araharah-executor 2589e2126f docs(03-04): checkpoint resolution — device matrix passed 2026-10-23 18:45:15 +08:00
araharah-executor 9d6fdc64be docs(03): device matrix PASSED + phase verification 2026-10-23 18:45:15 +08:00
araharah-executor fc99745672 chore(03-04): remove the device-matrix diagnostic probe — exit obligation, matrix passed 2026-10-23 18:41:41 +08:00
araharah-executor 28045d9b9e docs(03-04): log round 7 — probe readings; version-gate the settings CTA 2026-10-23 01:44:14 +08:00
araharah-executor 19a51e5882 fix(03-04): LOCATION SETTINGS CTA version-gated 8.0+ — hides the no-op off-Telegram (Chrome SDK stub reports 6.0) 2026-10-23 01:43:34 +08:00
araharah-executor e64cf7003b docs(03-04): log debug round 6 — signature-field HMAC erratum (the live blocker) 2026-10-23 01:34:53 +08:00
araharah-executor 22fda6d12a fix(03-04): HMAC data-check-string excludes only hash — signature field participates (real clients rejected as tampered; erratum M6) 2026-10-23 01:34:19 +08:00
araharah-executor c062ebaee1 docs(03-04): log debug round 5 — diagnostic probe armed 2026-10-23 01:25:35 +08:00
araharah-executor f4453996ac fix(03-04): on-screen diagnostic probe — WA version/platform, initData char-count, session + location categories 2026-10-23 01:24:52 +08:00
araharah-executor de07c99e8a docs(03-04): log debug round 4 — settings CTA on the wrong instance 2026-10-23 01:19:41 +08:00
araharah-executor 38ce59b1ca fix(03-04): LOCATION SETTINGS CTA on the denied card (was only on awaiting-pin instance — patch-order bug) 2026-10-23 01:19:10 +08:00
araharah-executor caeb9f9729 docs(03-04): log debug round 3 — telegram-web-app.js integration erratum 2026-10-23 01:15:10 +08:00
araharah-executor 3793741d2f fix(03-04): load telegram-web-app.js in head — production WebApp was never defined (no initData, no LocationManager, no BackButton) 2026-10-23 01:14:27 +08:00
araharah-executor ad4d18b180 docs(03-04): UI-SPEC amend — third denied CTA (LOCATION SETTINGS) as debug-round-2 amendment 2026-10-23 01:07:50 +08:00
araharah-executor 89268074a6 docs(03-04): log debug round 2 — LocationManager openSettings deep-link round 2026-10-23 01:07:27 +08:00
araharah-executor 9fa4acb319 fix(03-04): Telegram LocationManager deep-link — LOCATION SETTINGS CTA on denied cards + locationManagerUpdated chain re-run 2026-10-23 01:06:52 +08:00
araharah-executor fff865f6cd docs(03-04): log mid-gate debug round 1 (menu button + dead CTAs) 2026-10-23 01:01:20 +08:00
araharah-executor b1d8768866 fix(03-04): web_app menu button on deployed origin + wire CTA actions on both denied-shaped cards 2026-10-23 01:00:27 +08:00
araharah-executor fc183216c3 docs(03-04): complete one-deployable architecture plan — SUMMARY (checkpoint armed) 2026-10-23 00:46:44 +08:00
araharah-executor 5c155e393d feat(03-04): Mock-Telegram runner page + Playwright capture harness — 7/7 captures + GEO-03 probe
- runner page: ?mock/?scheme/?pin/?locale profile block wholly inside if (__ARAHARAH_HARNESS__) — installer + /api/session + /api/location + /api/location-fallback fetch stubs dead-code-eliminated from production; main.ts import stays outside the guard
- harness-only ?sessiondelay/?fixdelay boot-stage photograph knobs (localhost resolves the chain in ms — staged BOOT-01/BOOT-02 cards need a hold)
- capture.mjs extends theme/scripts/screenshot.mjs: MIN_BYTES, WR-05 docroot guard, READY-signal race, dist-harness docroot, DENIED_LEG_TIMEOUT_MS=90000 for LINK 3's 60s pre-denial poll, timeout = hard failure with a signal diagnostic
- 7 captures: boot-1/boot-2 stages, live light+dark (theme-slot attribute asserted), Desktop denied card w/ ASK VIA CHAT, chat-pin relay LIVE, DSGN-02 side-by-side vs navigate_map_overview
- GEO-03 runtime probe: localStorage/sessionStorage/cookies/IndexedDB asserted empty after boot-to-live
- mock fixes (Rule 1): LocationManager delivery now uses the Telegram wire shape {latitude,longitude,horizontal_accuracy,course} (authoring shape mapped to undefined → NaN LngLat pageerror); languageCode→initData so localeFromInitData parses the real wire format

CAPTURE OK — 7/7 non-blank captures; GEO-03 probe PASSED
2026-10-23 00:37:58 +08:00
araharah-executor 8b39ea70c3 feat(03-04): Worker [assets] binding + one-command deploy + D-14 build-artifact gate
- wrangler.jsonc assets block serves ../web/dist; run_worker_first keeps /api/* + /telegram/* on the Worker (D-13)
- root npm run deploy = build theme → web → bot wrangler deploy + set-webhook (D-15); dev = build + wrangler dev --port 8787 --local (D-12)
- vite.config.ts dual-build: __ARAHARAH_HARNESS__ define keyed on --mode harness, separate dist/dist-harness outDirs
- main.ts guard unchanged; ambient constant declared once in web/src/vite-env.d.ts
- build-artifact.test.mjs BA1..BA6: zero CDN refs, self-hosted fonts, MapLibre worker, inlined style, no mock installer in ANY emitted file (HTML included)
2026-10-23 00:15:26 +08:00
araharah-executor d573acc583 feat(03-03): status strip, state cards, recenter control, boot choreography — map-first, D-09 staged states
- strip.ts: six-state lamp+word left cluster, ±M / FIX Ns AGO telemetry (U+00B1 glyph), elapsed-seconds for acquiring
- cards.ts: boot/denied/lost builders from theme classes, machine-blink gantry (500ms JS duty cycle), GEO/SYS registers from strings.ts, D-06 CTA inversion (chat-fallback=btn-primary, retry=btn-secondary)
- recenter.ts: 40x40 plate w/ >=44 hit slop, orthogonal inline-SVG crosshair, three data-attribute notch states, no inline handlers
- main.ts: boot order map->session->chain (bootMap before acquireFix), null-map guards on every map consumer, ARAHARAH-LIVE emitted outside the guard, data-map=absent fallback, localeFromInitData, ASK VIA CHAT w/ D-03 revalidation, 1s tick re-running the chain at 45s staleness
- map.ts: attribution control moved bottom-left (UI-SPEC flush chip, compact)
- shell.css: attrib chip #map-scoped override (wins cascade over maplibre css), strip left cluster, recenter z-index
- vite.config.ts: define __ARAHARAH_HARNESS__ keyed on --mode harness (bare ambient global would ReferenceError in plain dev; Rule 3)
- session.test.mjs: SESS1-4 memory-token gates
2026-10-23 00:08:34 +08:00