Lumen/ARCHITECTURE-VALIDATION.md
Lumen Stage1 c0bfd413ff Stage 1: project foundation (strict TS, lint boundaries B-1..B-7, directory structure, CI, boundary tests)
- dedicated git repo at /home/avi/Projects/Lumen (main)
- TypeScript strict (target ES2022, bundler, exactOptionalPropertyTypes, noUncheckedIndexedAccess)
- ESLint 9 + typescript-eslint strictTypeChecked + eslint-plugin-boundaries for B-1..B-7, no-restricted-globals/syntax for B-1/B-7
- Prettier 3.5
- Structure per IMPLEMENTATION-CONTRACT.md §4 (src/platform/idb|cache|sw, storage, data, sync/{transport,verifier}, domain/{emergency,schedule,map,festival,readiness,clock,favorites}, ui/{components,views,router,render}, app, emergency-baseline, assets, public, content, pipeline, tests, scripts)
- CI: .github/workflows/ci.yml (typecheck + lint + format + test)
- Boundary tests: tests/unit/boundaries.test.ts (4 tests) + scripts/check-boundaries.ts
- No feature code, no PWA/IDB/sync/mesh/accounts per contract Stage 1
2026-08-30 23:25:35 -05:00

573 lines
63 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.

# Lumen — Architecture Validation Report
- **Phase:** Architecture Validation — consolidation of SPIKE-01 … SPIKE-08
- **Date:** 2026-08-30
- **Source documents:** `DISCOVERY.md`, `ARCHITECTURE-DESIGN.md`, `ARCHITECTURE-DECISIONS.md`, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md`, `SPIKE-01`…`SPIKE-08`, `experiments/README.md` + `exp1`/`exp2`/`exp3`
- **Scope guard:** No application code, no `package.json`, no dependencies, no PWA/DB/network implementation was created in this phase. All claims below reconcile spike evidence against design decisions without introducing implementation.
- **Previous state:** 7 spikes existed (`SPIKE-01`…`SPIKE-07`) when this report was requested; `SPIKE-08` was created immediately before this report (exp1 already existed per `experiments/README.md:11`). This report is the first document that reconciles all eight together and freezes the architecture.
## Relationship to prior phase
`DISCOVERY.md:9` and `ARCHITECTURE-DESIGN.md:9` noted narrowings/resolutions. This report does not re-litigate discovery; it checks whether the design decisions survive adversarial validation and records the exact changes the spikes require before implementation may start.
---
# 1. Validation summary
## 1.1 Spike inventory
| Spike | Decision(s) under test | Method | Evidence class | Result |
|---|---|---|---|---|
| SPIKE-01 IndexedDB + iOS (`SPIKE-01-INDEXEDDB-IOS.md:1`) | ADR-004, foundations of ADR-006 | Spec + WebKit/Chromium policy analysis; workload ≤40 MB/90 MB worst case (`SPIKE-01:22`) | CONFIRMED (spec), INFERRED (quota/BLOB), UNVERIFIED (device) | **ACCEPT WITH CHANGES** P1–P8 |
| SPIKE-02 Atomic A/B (`SPIKE-02-ATOMIC-DATASET-UPDATES.md:1`) | ADR-006 | State-machine simulation `exp2-ab-update-sim.mjs:1` with crash/fault injection at 9 stages | CONFIRMED (simulation) 16/16 PASS | **ACCEPT WITH CHANGES** F-1…F-4 |
| SPIKE-03 Map (`SPIKE-03-MAP-REPRESENTATION.md:1`) | ADR-008 | Candidate matrix + decode-memory analysis + `exp3-map-bench.html` (headless) | CONFIRMED (JS ~0ms), INFERRED (GPU/memory), UNVERIFIED (device fps) | **ACCEPT WITH CHANGES** F-1,F-3,F-4 |
| SPIKE-04 Package (`SPIKE-04-FESTIVAL-DATA-PACKAGE.md:1`) | ADR-005, parts of ADR-013/ADR-007 | Schema design + pipeline gate analysis | CONFIRMED (design) | **ACCEPT WITH CHANGES** F-1…F-4 |
| SPIKE-05 Offline Ready (`SPIKE-05-OFFLINE-READY.md:1`) | ARCH §12, invariants 8/10, ADR-006/ADR-005 | Predicate formalization | CONFIRMED (logic) | **ACCEPT (formalized)** C1–C8, time independence, FAILED state |
| SPIKE-06 Emergency baseline (`SPIKE-06-EMERGENCY-BASELINE.md:1`) | ADR-007 | Tier analysis + failure cases | CONFIRMED (design) | **ACCEPT** + F-1 hardening |
| SPIKE-07 Bootstrap (`SPIKE-07-BOOTSTRAP.md:1`) | ADR-002, ARCH §11/§12 | Journey + failure-point analysis | CONFIRMED (design) | **ACCEPT WITH CHANGES** L0–L2 |
| SPIKE-08 Time model (`SPIKE-08-TIME-MODEL.md:1`) | ADR-009, SPIKE-05 time independence | `exp1-time-model.mjs:1` 24/24 PASS (V8/ICU ECMA-402) | CONFIRMED (V8/ICU), INFERRED (JSC), UNVERIFIED (device) | **ACCEPT WITH CHANGE** F-3 |
Experiments re-executed for this report on this host: `exp1-time-model.mjs:129` 24/24 PASS; `exp2-ab-update-sim.mjs:241` 16/16 PASS. `exp3` desktop caveat stands (`experiments/README.md:36`).
## 1.2 What was validated vs what was not
- **Validated at design-logic level on this host:** UTC/IANA rendering, dayKey, DST, Now/Next, A/B invariant under 14 crash/fault points, map-candidate JS cost, readiness predicate, emergency tier resolution, bootstrap levels, package schema/no-expiration/emergency sub-versioning.
- **Inferred from vendor policy/spec but not observed here:** iOS storage quotas/eviction/persist heuristics, BLOB/IDB near-quota behaviour, JSC parity for Intl, CDN Date accuracy, SW lifecycle on iOS.
- **Unverified and queued for physical devices:** all of the above as experienced by Lumen on actual iPhones/Androids plus performance (fps, decode, cold start). This is expected and documented in every spike's §5.
---
# 2. Decision reconciliation
For each ADR: original decision → spike finding → final validated decision → changes incorporated → unresolved.
## ADR-001 Offline-first
- **Original (`ARCHITECTURE-DECISIONS.md:14`):** Accepted. Offline-first; local source of truth; no blocking network on critical paths.
- **Spikes touching it:** All. SPIKE-05 makes the readiness predicate time-independent; SPIKE-07 defines L0–L2 so the invariant holds even partially provisioned; SPIKE-02 proves updates preserve it under crashes.
- **Final:** **Unchanged — Accepted.** No spike contradicts it. Reinforced by SPIKE-05 F-1 (clock does not demote READY) and SPIKE-07 minimum-safe guarantees. No unresolved issues.
## ADR-002 PWA / installation
- **Original (`ARCHITECTURE-DECISIONS.md:48`):** Installed-first PWA, single origin, manual iOS coach, functional in tab with degraded persistence warning.
- **Spike findings:** SPIKE-01 §3.12/§5 confirms 7-day ITP tab eviction vs installed exemption is INFERRED/UNVERIFIED and load-bearing; SPIKE-07 introduces **L0–L2 preparation levels** (`SPIKE-07-BOOTSTRAP.md:31`), fixed download order `emergency→schedule→info→map→assets`, minimum-safe L1 (emergency+schedule), and explicit rule that **prep before install is allowed** (`SPIKE-07:58`). SPIKE-05 maps L0/L1/L2 to NOT_READY/PARTIAL/READY.
- **Final:** **Accepted with addendum.** Install-first stands; the only change is the formal L0–L2 model, ordered download, and tab-before-install allowance. No scope creep. **Unresolved:** `persist()` grant rates and tab-eviction timing remain UNVERIFIED (device matrix).
## ADR-003 Frontend technology
- **Original (`ARCHITECTURE-DECISIONS.md:82`):** Vanilla TypeScript, no framework; Preact as explicit reconsider trigger.
- **Spike findings:** No dedicated spike (TQ-1 not run as a benchmark beyond EXP-3 JS cost). SPIKE-03 EXP-3 confirms no candidate has disqualifying JS cost (`SPIKE-03-MAP-REPRESENTATION.md:28`), which is consistent with but not proof of vanilla perf. DISCOVERY §11 TQ-1/TQ-10 remain device-dependent.
- **Final:** **Accepted — unchanged.** The reconsider trigger is retained deliberately. No spike requires a framework. **Unresolved:** low-end Android render/JS parse cost remains UNVERIFIED until device matrix and real schedule-list measurement (≤100 ms target, `ARCHITECTURE-DESIGN.md:853`). Risk RK-7 unchanged; mitigation is the layering in `ARCHITECTURE-DESIGN.md:195` B-1…B-7 which keeps domain/data untouched by a future view-layer swap.
## ADR-004 Local storage
- **Original (`ARCHITECTURE-DECISIONS.md:126`):** Proposed. IndexedDB (thin wrapper) + Cache Storage for shell + localStorage flags; assets as Blobs in slot DB; OPFS/SQLite-WASM rejected.
- **Spike findings:** SPIKE-01 ACCEPT WITH CHANGES. Validates IDB is correct (spec CONFIRMED for transactions/atomicity/multiple DBs) but mandates defensive protocol **P1–P8** (`SPIKE-01-INDEXEDDB-IOS.md:140`): one short txn per file (bytes+progress together), activation single txn, wrapped IDB + QuotaExceededError handling, free-space pre-check 2× package, 6 MB single-record cap, persist requested but not relied upon, boot light verification as detection, no dataset state in SW. SPIKE-02 confirms this protocol plus F-1 (bytes+progress atomic) is what makes the A/B invariant hold.
- **Final:** **Accepted with conditions — status moves from Proposed to Accepted (YELLOW, device-dependent).** The layout is validated at design-logic level; production readiness is conditional on the device tests in `SPIKE-01:159` (fresh install + reboot with ~40 MB, quota-near-full writes, kill-mid-download resume, Blob read-back). No alternative store is better for this workload. Changes P1–P8 are normative and now reflected in `ARCHITECTURE-DESIGN.md:9`.
## ADR-005 Festival Data Package
- **Original (`ARCHITECTURE-DECISIONS.md:174`):** Accepted. JSON sections + assets + manifest + Ed25519 signature; content repo → pipeline → static CDN; immutable versioned URLs + mutable latest.json.
- **Spike findings:** SPIKE-04 ACCEPT WITH CHANGES + SPIKE-05/06/08 cross-checks. Adds: **F-1** `sections[*].required` flag gating readiness (`SPIKE-04:64`), **F-2** no-expiration rule for datasets (`SPIKE-04:50`), **F-3** emergency independent `emergencySchemaVersion`/`contentVersion` (`SPIKE-04:70`, `SPIKE-06:110`), **F-4** user-data schema separate from package schema (`SPIKE-04:210`). SPIKE-08 confirms UTC+IANA+dayKey works offline; SPIKE-05 confirms time-independent readiness needs F-2.
- **Final:** **Accepted with addendum.** Schema is now normative as in `SPIKE-04:79` fragments; no binary format needed at this scale (F-5). **Unresolved:** pipeline gates need real content to prove budgets; stable IDs (AQ-20) remain Proposed until source samples are inspected.
## ADR-006 Atomic updates and rollback
- **Original (`ARCHITECTURE-DECISIONS.md:220`):** Accepted. A/B dual-slot, staged hash-verified, single-txn activation, rollback, page-context downloads (C-21), monotonic versions.
- **Spike findings:** SPIKE-02 ACCEPT WITH CHANGES, 16/16 PASS (`SPIKE-02:50`). Mandates: **F-1** bytes+progress in same txn, **F-2** verification record in activation txn, **F-3** `readbackPending` flag carried across boots, **F-4** rollback depth = 1 (new staging wipes inactive slot — accepted trade-off). SPIKE-01 P1–P8 are preconditions. Ordering proof in `SPIKE-02:73` — single-DB atomicity is sufficient because staging completes and validates before the pointer moves.
- **Final:** **Accepted with addendum (F-1…F-4).** Invariants 5–7 are proven at state-machine level given IDB transaction atomicity (which itself is INFERRED/UNVERIFIED on iOS per `SPIKE-01:75`). The only remaining blocker is physical proof of IDB atomicity under jetsam kill on iOS.
## ADR-007 Emergency baseline
- **Original (`ARCHITECTURE-DECISIONS.md:266`):** Accepted. Three tiers: embedded floor (≤16 KB, same-source-generated), dataset section, reserved T3 notices; provenance labels.
- **Spike findings:** SPIKE-06 ACCEPT. Fixes tier-1 contents (`SPIKE-06:12` exact JSON shape, life-safety minimum only), resolution rules (`SPIKE-06:60` prefers highest compatible, defensive merge), forward-tolerant floor renderer and **zero-IDB requirement** for floor path (`SPIKE-06:75` F-1), eviction/compatibility/corruption cases. SPIKE-05 C8 asserts floor presence in readiness.
- **Final:** **Accepted — addendum records fixed tier contents, merge rules, and F-1.** No direction change. **Unresolved:** organizer-provided emergency content correctness still requires the sign-off gate (`DISCOVERY.md:346` OQ-2, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:62` O-02) — a product/ops gate, not an architecture defect.
## ADR-008 Map
- **Original (`ARCHITECTURE-DECISIONS.md:308`):** Proposed. Raster WebP ≤2 levels + DOM POI overlay + CSS transform + Facilities list; no GPS; no tiles.
- **Spike findings:** SPIKE-03 ACCEPT WITH CHANGES. **F-1** tightens caps: overview ≤1600 px longest edge, detail ≤3072 px, total decoded ≤~35 MB, at most one detail resident (`SPIKE-03:58`); replaces prior 4096 cap. **F-3** lazy object-URL lifecycle (`SPIKE-03:68`), **F-4** dark-mode treatment (`brightness(.72) saturate(.85)`, optional night asset hook `SPIKE-03:88`). EXP-3 desktop result is non-discriminating — decision rests on memory/a11y/workflow reasoning. Flip conditions documented (`SPIKE-03:98`).
- **Final:** **Accepted with addendum — YELLOW (device-dependent).** Representation stands; production declaration is conditional on device protocol in `SPIKE-03:116` (fps ≥30, decode latency, no URL leaks, screen-reader pass, 2048 texture-cap fallback). Budgets updated (`ARCHITECTURE-DESIGN.md:855`).
## ADR-009 Time model
- **Original (`ARCHITECTURE-DECISIONS.md:354`):** Accepted. UTC epoch + IANA zone + precomputed dayKey + ClockService (server offset + monotonic anchor + ±45d sanity window).
- **Spike findings:** SPIKE-08 ACCEPT WITH CHANGE, 24/24 PASS (`SPIKE-08:43`). Confirms T1 (zone rendering), T2 (dayKey), T3 (DST), T4 (unusual zones), T5 (skew arithmetic), T7 (Now/Next). **F-3 change** is required: sanity warning suppressed until `now ≥ festivalStart − WINDOW` or a skew has been captured (`SPIKE-08-TIME-MODEL.md:97`), otherwise early preparation (July for September festival) would spuriously warn every user. F-4 clarification: only `Intl.DateTimeFormat` with explicit `timeZone` is allowed.
- **Final:** **Accepted with change F-3 + clarification F-4.** Time remains the only user-visible heuristic; readiness is explicitly time-independent per `SPIKE-05:47`. **Unresolved:** JSC parity on iOS and real CDN `Date` header observation remain UNVERIFIED (device matrix).
## ADR-010 Synchronization
- **Original (`ARCHITECTURE-DECISIONS.md:396`):** Accepted. Pull-only on open / online hint / manual; monotonic versions; no background sync, no push, no uploads.
- **Spike findings:** No dedicated spike; covered indirectly by SPIKE-02 verification ordering (`SPIKE-02:29` cheapest/authoritative first) and SPIKE-05 time independence. Discovery C-19/B-06 (no background work) and PW-4 (no iOS Background Sync) confirm the pull model is the only viable one.
- **Final:** **Accepted — unchanged.** Latency = time-until-next-open-online remains the documented trade-off. No spike contradicts it. **Unresolved:** `online` as a hint is best-effort; update urgency still depends on organizers opening the app online.
## ADR-011 Networking abstraction
- **Original (`ARCHITECTURE-DECISIONS.md:431`):** Accepted. Minimal byte-oriented Transport interface `isAvailable()/fetchPointer()/fetchBytes()` under SyncService; V1 has only HttpTransport; Verifier decides truth.
- **Spike findings:** Consistent with SPIKE-02 F-5/F-6 and SPIKE-06/ADR-012's signed-payload rule. No spike required a richer interface; mesh semantics are correctly pushed into future transport impls per `DISCOVERY.md:596`.
- **Final:** **Accepted — unchanged.** Cost stays one interface + one impl. No evidence that a message/CRDT abstraction is needed in V1.
## ADR-012 Future mesh strategy
- **Original (`ARCHITECTURE-DECISIONS.md:472`):** Accepted (strategy only). Three obligations: Transport seam, signed-payload rule, package/announcement monotonicity; all mesh concerns deferred; out-of-browser companion assumed likely per `DISCOVERY.md:581`.
- **Spike findings:** SPIKE-04 F-2/F-3, SPIKE-02 ordering, SPIKE-06 T3 reserved shape are all compatible with this seam. No spike built mesh groundwork and none was needed. `ARCHITECTURE-DESIGN.md:895` risk 16 documents the impossibility-in-browser posture correctly.
- **Final:** **Accepted — unchanged.** V1 loses nothing if future mesh proves impossible via browser; HTTP transport is complete product functionality.
## ADR-013 Security model
- **Original (`ARCHITECTURE-DECISIONS.md:514`):** Accepted on model, Proposed on crypto library choice pending audit. Ed25519 over manifest SHA-256 + per-file SHA-256, WebCrypto SHA-256 + bundled pure-JS Ed25519 verifier (WebCrypto Ed25519 not on iOS 16.4), key set in shell, CSP `default-src 'self'` etc., data minimization.
- **Spike findings:** SPIKE-04 §2 fixes signature/manifest/hash/compatibility ordering (`SPIKE-02:34`), SPIKE-02 proves rejection path (bad sig → keep old, quarantine). Discovery TQ-9/SQ-1 predicted the WebCrypto gap; the pure-JS verifier is the correct resolution per `ARCHITECTURE-DESIGN.md:14`. No spike claims to have audited the verifier library — AQ-06 remains Proposed. Strict CSP impacts hashed-asset build output — design-consistent.
- **Final:** **Accepted as a model; crypto library choice remains Proposed (YELLOW) pending audit.** The scheme defeats TH-1…TH-6 with minimal machinery; key custody/rotation runbook remains an ops requirement (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:85` S-01, AQ-21).
## ADR-014 Deployment
- **Original (`ARCHITECTURE-DECISIONS.md:564`):** Accepted on topology (static CDN, single origin, immutable versioned URLs, two environments), Proposed on provider choice (AQ-14).
- **Spike findings:** SPIKE-04 pipeline gates (`SPIKE-04:189` 5 gates) and SPIKE-01 storage origin-keying make provider choice the only unresolved piece. No spike proposes a dynamic backend; A-17 small-team ops budget still favours static.
- **Final:** **Accepted on topology; provider remains Proposed (YELLOW).** Origin choice is hard to reverse (storage is origin-keyed → strands installed data on change), so provisional staging origin is fine for development but production origin must be chosen before any public staging link (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:116` AQ-18, explicitly not blocking implementation).
---
# 3. Critical invariants
Each invariant is checked against the reconciled architecture (after incorporating spike changes). All pass at design-logic level; device-dependent invariants are marked YELLOW pending physical proof.
| # | Invariant | Verdict | Evidence |
|---|---|---|---|
| 1 | Emergency baseline always available from app shell (`DISCOVERY.md:230` C2) | **PASS (GREEN)** | Tier-1 floor compiled into shell bytes; renderer has zero-IDB path and is forward-tolerant (`SPIKE-06:75` F-1); survives eviction, failed update, incompatible dataset, BASELINE_ONLY (`SPIKE-06:81`); C8 asserts floor in readiness (`SPIKE-05:43`). |
| 2 | Emergency never requires internet | **PASS (GREEN)** | Floor path and T2 rendering have no network calls (`SPIKE-06:91`); sync is enhancement-only (`ADR-001`). |
| 3 | Failed update cannot destroy active valid dataset (`DISCOVERY.md:230` C9) | **PASS (YELLOW)** | A/B inactive-slot staging + single-txn activation + automatic rollback on readback fail (`SPIKE-02:50` 14 scenarios + X1–X3 PASS). YELLOW only because IDB atomicity under iOS jetsam is INFERRED/UNVERIFIED (`SPIKE-01:75`). Logic is proven. |
| 4 | Partially downloaded dataset can never become active (`DISCOVERY.md:230` C8) | **PASS (GREEN at state-machine level, YELLOW end-to-end)** | Per-file atomic staging + full verification before pointer flip + quarantine (`SPIKE-02:29` ordering). Same device caveat as #3. |
| 5 | Integrity & authenticity verified before activation | **PASS (GREEN)** | Ordering: signature → compatibility/budgets → per-file SHA-256+size → schema → activate (`SPIKE-02:29`); verifier is sole decider (B-4). `SPIKE-02:52` bad-sig/bad-hash cases PASS (keep old). |
| 6 | Shell/dataset compatibility explicitly checked | **PASS (GREEN)** | Dual check: `schemaVersion ∈ shell.supportedRange` ∧ `appCompatibility` (`SPIKE-04:22`); boot check prefers compatible slot else limited mode (`ARCHITECTURE-DESIGN.md:688`); ordering rule C-23 prevents most skew (`ARCHITECTURE-DESIGN.md:690`). |
| 7 | OFFLINE READY = actual local availability, not connectivity (`DISCOVERY.md:230` C10) | **PASS (GREEN)** | Predicate C1–C8 is evidence-based and time-independent (`SPIKE-05:28` + `SPIKE-05:47`); states READY/PARTIAL/NOT_READY/RECOVERY/BASELINE_ONLY/FAILED are proven local evidence (`SPIKE-05:60`). |
| 8 | GPS optional (invariant 9) | **PASS (GREEN)** | V1 has no GPS dependency at all; POI `lat/lng` hook preserved only (`SPIKE-03:112`, `ARCHITECTURE-DESIGN.md:540`/`565`). |
| 9 | Schedule works offline (`DISCOVERY.md:132` R-S1…S4, R-O1…R-O5) | **PASS (GREEN)** | Schedule is signed section in active slot; all queries (Now/Next, My Schedule, filters/search) are local (`ARCHITECTURE-DESIGN.md:508`); time model works offline (`SPIKE-08:43`). |
| 10 | Map works offline (`DISCOVERY.md:172` R-M1, R-O1) | **PASS (YELLOW)** | Raster WebP base + POI overlay all local; Facilities list is first-class GPS-free access (`SPIKE-03:79`). YELLOW pending device performance/decoding (`SPIKE-03:116`). |
| 11 | Festival info works offline (`DISCOVERY.md:182` R-F1) | **PASS (GREEN)** | Info is signed required section (`SPIKE-04:30`); renderer maps structured nodes → safe DOM (C-22). |
| 12 | Favorites/My Schedule work offline | **PASS (GREEN)** | `lumen-user` store separate; never touched by updates/rollback/GC (B-6, `SPIKE-02:70` X3 favourites survive) |
| 13 | No V1 feature depends on mesh (`DISCOVERY.md:268` NR-1, invariant 11) | **PASS (GREEN)** | V1 builds no mesh; seam costs one interface (`ADR-012`). |
| 14 | Future mesh has defined extension boundary | **PASS (GREEN)** | Transport seam (ADR-011) + signed-payload rule + immutable versioned payloads (`ADR-012`); location per `ARCHITECTURE-DESIGN.md:704`. |
| 15 | No user accounts in V1 (`DISCOVERY.md:270` NR-4) | **PASS (GREEN)** | No auth, no server state; favourites device-local (`ADR-010`). |
No invariant requires new architecture. Two are YELLOW solely due to device-dependent platform behaviour already isolated and queued for physical testing.
---
# 4. Physical-device validation matrix
## 4.1 Device definitions
| ID | Device | OS/browser | Why it matters |
|---|---|---|---|
| D1 | iPhone low-bound | iOS 16.4 Safari (tab + installed PWA) | Baseline policy A-06; worst WebKit for storage/Persist/Intl |
| D2 | iPhone latest | iOS latest Safari installed PWA | Current WebKit reality; EU variability sanity check |
| D3 | Low-end Android | 2021 mid-range, Chrome ~110+ (and latest if different) | Performance budget target (`ARCHITECTURE-DESIGN.md:851`); 2048 texture-cap cohort |
| D4 | Modern Android | Current flagship Android, Chrome latest, installed PWA | High-end correctness + Chrome persistence auto-grant |
All tests are manual scripted scenarios per `ARCHITECTURE-DESIGN.md:838`. Expired context after 24h; bugs found on device that harness missed become harness cases (`ARCHITECTURE-DESIGN.md:843`).
## 4.2 Legend
Severity: **BLOCKING** = cannot ship/recurring data loss or safety impact; **HIGH** = major degradation; **MEDIUM** = degraded UX/workaround exists; **LOW** = polish. BLOCKING tests must pass before public festival use.
## 4.3 Matrix (19 groups — each has 4 device columns; shared steps where identical)
### T01 — First visit
- **Objective:** Shell loads over HTTPS, SW registers, status is NOT_READY, emergency floor renders.
- **Preconditions:** Site data cleared for origin; normal (non-private) browsing; HTTPS.
- **Steps:** 1) Open `https://<origin>/` 2) Wait for load 3) Check status chip 4) Open Emergency.
- **Expected:** Shell ≤1 MB loads quickly; SW installed; chip NOT_READY / "Get festival data"; emergency shows BASELINE with version and address/coordinates/procedures.
- **Pass:** All visible within target (<2 s emergency usable on D3 per `ARCHITECTURE-DESIGN.md:851`) with no console errors.
- **Severity:** BLOCKING. **Applies:** D1–D4.
### T02 — Installation
- **Objective:** Installed-first flow produces persistence-exempt, standalone launch.
- **Preconditions:** First visit completed.
- **Steps (D1/D2 iOS):** Share → Add to Home Screen → launch from home screen → check `display-mode: standalone` / `navigator.standalone`. (D3/D4 Android): trigger `beforeinstallprompt` where available → install → launch standalone. Attempt "install first (recommended)" and "get data now" paths per `SPIKE-07:58`.
- **Expected:** Standalone launch with no browser chrome; `viewport-fit=cover` safe-area respected; install-state detectable; coach dismissible but re-surfaced until installed or READY.
- **Pass:** Standalone detection true when launched from home screen; tab still functional when dismissed.
- **Severity:** BLOCKING (iOS). HIGH (Android — eviction risk lower). **Precondition note:** failure to install must not block use (`SPIKE-07:48` FP-1) — verify degraded-persistence warning appears in tab mode.
### T03 — Offline launch
- **Objective:** Cold start with no network serves shell from Cache Storage + data from IDB; no error walls.
- **Preconditions:** READY state achieved (see T09). Then enable airplane mode.
- **Steps:** Kill app/browser → airplane ON → launch PWA from home screen → navigate all 4 destinations + Status.
- **Expected:** Every screen renders from local stores; status READY (or PARTIAL if L1 only); no spinner that never resolves; light boot check ≤~150 ms (`SPIKE-05:83`).
- **Pass:** Emergency, Schedule, Map, Festival all usable offline; no network calls in devtools.
- **Severity:** BLOCKING. D1–D4 airplane mode.
### T04 — Airplane mode (festival simulation)
- **Objective:** Full festival day in airplane mode — schedule Now/Next, filters/search, favorites, facilities list all local.
- **Preconditions:** READY. Airplane ON for entire test.
- **Steps:** Browse schedule by day (dayKey groups), test Now/Next at several simulated times, toggle stage/type filters, use shared search, add favourites, open map + facilities list, follow emergency dial/text paths (do not actually dial).
- **Expected:** All of the above work with zero connectivity. Favorites persist after kill/restart (write-through).
- **Pass:** No feature silently disabled without indication (FA-1 requirement, `DISCOVERY.md:653`).
- **Severity:** BLOCKING. D1–D4.
### T05 — Browser restart
- **Objective:** Browser/process kill does not lose committed data.
- **Preconditions:** READY.
- **Steps:** Force-quit browser (swipe away) or `chrome://restart` equivalent → relaunch PWA.
- **Expected:** State intact; data verified via light check; favourites preserved.
- **Pass:** READY without re-prep.
- **Severity:** BLOCKING. D1–D4.
### T06 — Phone restart
- **Objective:** Device reboot preserves committed dataset and shell; SW re-registers; cold-start budget met.
- **Preconditions:** READY.
- **Steps:** Reboot device → launch PWA → measure cold start.
- **Expected:** Data intact; SW rehydrates; emergency usable ≤2 s and interactive ≤3 s on D3 (`ARCHITECTURE-DESIGN.md:851`); boot verification is the detection, not prevention (`SPIKE-01:154` P7).
- **Pass:** Meets budgets; no re-prep needed. UNVERIFIED on iOS until measured (`SPIKE-01:94`).
- **Severity:** BLOCKING. D1–D4.
### T07 — Storage persistence (`persist()` / exempt)
- **Objective:** Installed PWA gets favourable persistence signalling; 7-day tab vs installed behaviour is honest.
- **Preconditions:** Fresh profile.
- **Steps:** Fresh install → prep READY → query `navigator.storage.persist()` (grant) and `persisted()`. Separately: prepare same origin in Safari tab (D1) and leave unused 7+ days vs installed PWA — observe eviction honesty.
- **Expected:** D4 auto-granted; D1/D2 heuristic grant when installed (best-effort, never relied upon per `ARCHITECTURE-DESIGN.md:285`); tab data evictable per PW-1, installed exempt per `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:44` B-01 — but must be confirmed on our test devices (`SPIKE-01:162`).
- **Pass:** Grant outcome recorded; missing grant does not block use — design never relies on it (P6). Failure of the exemption claim would be BLOCKING on iOS.
- **Severity:** HIGH (grant itself) / BLOCKING if exemption fails. D1–D2 primary.
### T08 — Storage pressure
- **Objective:** QuotaExhaustedError path keeps active dataset; guidance shown; baseline still works.
- **Preconditions:** READY. Device storage deliberately near-full (or devtools fill + `storage.estimate()` low free space).
- **Steps:** Attempt new staging (publish new packageVersion) with free < 2× package; then attempt with free just barely enough; observe behaviour when writing blobs near quota.
- **Expected:** Pre-check refuses staging if free < 2× size (P4) with plain guidance; mid-write QuotaExceeded aborts staging, active untouched (P3) — `SPIKE-02:60` hash/schema/validation failures map to keep-old.
- **Pass:** No crash, no partial activation; Status shows guidance; emergency floor intact. UNVERIFIED on both platforms (`SPIKE-01:56`).
- **Severity:** BLOCKING. D1–D4.
### T09 — Dataset update (happy path)
- **Objective:** Modern monotonic update stages, verifies, atomically activates, shows READY with new version.
- **Preconditions:** READY vN. Staging origin has vN+1 published and signed; online.
- **Steps:** Open app → online hint fires → fetch `latest.json` → fetch manifest → signature/compatibility/budget checks → stage sections in priority order `emergency→schedule→info→map` (`SPIKE-07:39`) → per-file hash → full verification → atomic flip → READY confirmation.
- **Expected:** New version active after single txn; old slot retained for rollback; status shows version/generatedAt/fetchedAt; quarantine for rejected versions.
- **Pass:** EXP-2 S10 PASS analog on device; favourites survive (X3); bloated version rejected by budget gate.
- **Severity:** BLOCKING. D1–D4.
### T10 — Interrupted update
- **Objective:** Every interruption point preserves the old valid dataset; resume works.
- **Preconditions:** READY vN, staging a new vN+1.
- **Steps (repeat, kill at different points):** Kill app between files (staged 0/3/5), kill mid-file, kill before activation, kill during activation txn, kill immediately after flip but before readback — as modelled in `SPIKE-02:52` S01–S14. Also: connectivity lost mid-prep, phone reboot before activation.
- **Expected:** Before flip → old pointer stands; during flip → txn uncommitted, old stands; after flip → new stands and next-boot light verify confirms or rolls back (`SPIKE-02:64`); progress persisted per file for resume (`SPIKE-01:145` P1); staging older than 7 days discarded (`ARCHITECTURE-DESIGN.md:669`).
- **Pass:** No scenario exposes a partial dataset; boot always yields READY (old or new) or RECOVERY+baseline if both slots lost (X2). UNVERIFIED on iOS jetSAM kill (`SPIKE-01:75`).
- **Severity:** BLOCKING. D1–D4.
### T11 — Recovery (eviction / corrupt / missing)
- **Objective:** Every "data is gone/wrong" state enters RECOVERY (or BASELINE_ONLY) with honest messaging and a one-tap fix.
- **Preconditions:** READY.
- **Steps:** 1) Simulate eviction: Settings → clear site data (or devtools → delete databases/caches) → relaunch. 2) Simulate corrupt: manually corrupt active slot (test harness analog `SPIKE-02:225` X1/X2) → relaunch. 3) Private-mode launch or storage blocked. 4) Incompatible shell/dataset: launch old data with new-shell supportedRange (`ARCHITECTURE-DESIGN.md:688`).
- **Expected:** Missing/corrupt → RECOVERY with reason `missing`/`corrupt`/`incompatible`, emergency floor works; other slot's fallback served when intact (X1); both gone → RECOVERY+baseline+favourites logic preserved (`SPIKE-02:69`). BASELINE_ONLY when storage is entirely unavailable (`SPIKE-05:68`). FAILED(activation) distinct from RECOVERY for repeated txn failures (`SPIKE-05:62`).
- **Pass:** No blank screen; correct chip colour; "Restore festival data" or "Check my data" affordances present (`SPIKE-05:62`).
- **Severity:** BLOCKING (eviction/corrupt); HIGH (private-mode guidance). D1–D4.
### T12 — Map rendering
- **Objective:** Map base decodes within memory/performance budgets and renders legibly.
- **Preconditions:** READY with real map art.
- **Steps:** Launch Map with overview already staged; measure initial load; verify budgets: overview ≤1600 px / ~7.3 MB decoded, detail ≤3072 px / ~27 MB, total ≤~35 MB, at most one detail resident (`SPIKE-03:58` F-1, `ARCHITECTURE-DESIGN.md:855`).
- **Expected:** Overview renders immediately (~0.5 MB WebP); detail lazy-decodes on first zoom-in; no 48–64 MB single bitmap; object URLs revoked on level switch (F-3).
- **Pass:** Decode time acceptable; no OOM/jetsam; dimension caps enforced by pipeline.
- **Severity:** BLOCKING (OOM/fails to render) / HIGH (quality). D3 primary, D1–D2 sanity.
### T13 — Map interaction
- **Objective:** Pan/zoom via pointer-events + pinch meets fps and hit-target and a11y contract.
- **Preconditions:** READY; 200 POIs visible.
- **Steps:** 1) Pan/zoom sustained 10 s, observe frame drops 2) Overview↔detail↔overview cycling, watch `performance.memory` (Chrome) / Instruments (iOS) for leaks 3) Dark-mode filter `brightness(.72) saturate(.85)` (`SPIKE-03:88`) visual check 4) Older 2048-texture-cap path on D3 low-end vs 3072 image 5) Tap POI → detail sheet, test category chip filtering, search highlight.
- **Expected:** ≥30 fps sustained (`ARCHITECTURE-DESIGN.md:854`); no leaked object URLs; screen readers (TalkBack/VoiceOver) traverse POI buttons and Facilities list (`SPIKE-03:120` protocol); fallback overview-only mode engages if detail path janks.
- **Pass:** All six items in `SPIKE-03:116` protocol PASS.
- **Severity:** BLOCKING (fps/leak), HIGH (a11y), MEDIUM (filter). D3/B1 primary.
### T14 — GPS (V1 absence)
- **Objective:** App requests no location permission and shows no blue dot; map/facilities work GPS-free.
- **Preconditions:** Fresh profile; READY.
- **Steps:** Open app → verify no permission prompt → map → facilities list; optionally deny location if prompted by OS.
- **Expected:** Zero permission requests (`ARCHITECTURE-DESIGN.md:222`); last-known behaviour from earlier discovery R-M4 narrowed to no-GPS in V1 (`ARCHITECTURE-DESIGN.md:18`); optional `lat/lng` hook does not affect UX.
- **Pass:** No prompt; finding facilities never requires location; available without consent.
- **Severity:** MEDIUM (UX trust). D1–D4.
### T15 — Incorrect device clock
- **Objective:** ClockService corrects when online, warns honestly when not, never bricks READY.
- **Preconditions:** Two runs — (a) never-online device, (b) device later brought online so skew captures.
- **Steps (per SPIKE-08 §5):** 1) Render July event → expect 01:00 PM CDT on `America/Chicago` (T1a) 2) Set device clock +30 min → open online → capture skew from `Date` header → go airplane → check Now/Next uses corrected time 3) Mid-session bump clock +10 min → expect drift warning 4) Set clock to month before festival → no warning; then +60 days past end → warning shown (F-3 suppressed-until-near) 5) Airplane + clock wrong by 2h at festival-time → warning, schedule still readable.
- **Expected:** Corrected Now when skew exists; warning only near festival or when evidence of skew exists (`SPIKE-08:104` F-3); READY unaffected by clock (`SPIKE-05:47` time independence); never-online wrong clock is accepted residual (`SPIKE-08:124`).
- **Pass:** Behaviours match lines above; JSC parity with V8/ICU at DST edges (T3a–T3d).
- **Severity:** HIGH (festival-time correctness), MEDIUM (early-prep noise), LOW (never-online residual accepted).
### T16 — Service worker lifecycle
- **Objective:** Tiny SW (precache + navigation fallback only) survives iOS killing; page-context downloads are unaffected.
- **Preconditions:** READY; staging not required for basic check.
- **Steps:** Simulate SW idle kill (devtools terminate SW, or background app for 30 s on iOS) → stage a package in document context → kill SW again → resume staging → verify page progress persists.
- **Expected:** C-21 respected: dataset staging runs in page context, not SW (`ARCHITECTURE-DESIGN.md:83`); SW handles only install/activate/fetch (`ARCHITECTURE-DESIGN.md:243`); operations are idempotent/re-runnable; no dataset state in SW (`SPIKE-01:157` P8); activation is next-start, not `clients.claim` mid-session (`ARCHITECTURE-DESIGN.md:256`).
- **Pass:** Stage completes regardless of SW death; no corrupted slot.
- **Severity:** BLOCKING (if SW death could corrupt — must not). D1/D2 iOS primary.
### T17 — Application shell update
- **Objective:** New shell installs under new cache name; activates on next full start; compatibility with both slots checked; mid-session not disrupted.
- **Preconditions:** READY vX. New shell vY available (different `lumen-shell-v<build>`).
- **Steps:** Fetch new shell → install new SW → verify old SW still serves current session → full restart → observe `SKIP_WAITING` / next-start activation → boot compatibility check (`ARCHITECTURE-DESIGN.md:688` §18.5) → prefer compatible slot, else limited mode with update guidance.
- **Expected:** Old session uninterrupted; compatibility envelope publishing rule C-23 honoured (schemas `{1,2}` range, datasets only after shell window, freeze 7 days before festival — `ARCHITECTURE-DESIGN.md:690` §18.6).
- **Pass:** No blank screen; no incompatible dataset exposed.
- **Severity:** BLOCKING. D1–D4.
### T18 — Dataset compatibility
- **Objective:** Incompatible packageVersion or schemaVersion never activates.
- **Preconditions:** READY. Staging server publishes v incompatible with this shell.
- **Steps:** Attempt sync → observe manifest `appCompatibility`/`schemaVersion` check before download (`SPIKE-02:29` step 4) → attempt older monotonic version.
- **Expected:** Incompatible → reject + quarantine + diag; "please update app" when `minAppVersion > current` (`ARCHITECTURE-DESIGN.md:668`); lower version from network never downgrades (`ARCHITECTURE-DESIGN.md:349` monotonic rule, SPIKE-02 F-5).
- **Pass:** Active untouched; READY stays; no refetch loop.
- **Severity:** BLOCKING. D1–D4.
### T19 — Emergency baseline availability
- **Objective:** Every failure path still renders emergency floor with provenance and dial/text paths.
- **Preconditions:** Enumerate: never-prepared (NOT_READY), evicted (RECOVERY), corrupt (FAILED), incompatible, storage-blocked (BASELINE_ONLY), offline, update mid-flight.
- **Steps:** In each state, open Emergency → verify contents (emergency number + security/first-aid summaries + muster/exits + procedures), labels (`BASELINE v<...> ` vs `FESTIVAL DATA v<package>` per `SPIKE-06:62`), forward-tolerant rendering (unknown fields ignored), `tel:` primary + copyable text, explicit tap to dial, zero IDB access required for floor path.
- **Expected:** Floor always renders; highest compatible tier shown; 16 KB cap enforced by pipeline (`ARCHITECTURE-DESIGN.md:367`); single-source generation prevents drift (`ARCHITECTURE-DESIGN.md:345`).
- **Pass:** No state shows blank emergency; `tel:` works on standalone (D1/D2) where possible, else number copyable (`SPIKE-06:81`); one-tap away from every screen via persistent nav.
- **Severity:** BLOCKING (life-safety). D1–D4, with `tel:` verified separately per `SPIKE-01:171`.
**Total tests:** 19 groups × 4 device families = 76 device-tests before considering iOS tab vs installed splits for T07/T02 (handled as sub-cases). Every BLOCKING test must pass before a production festival deployment.
---
# 5. Known weaknesses
Weaknesses are not hidden; each has owner/category and whether it blocks implementation. Per `ARCHITECTURE-DESIGN.md:897` declared weaknesses W-1…W-6 and `ARCHITECTURE-DESIGN.md:863` RK-1…RK-10.
## W-1 — Installation / bootstrap dependency
- **Risk:** Offline-first requires one successful online bootstrap (shell + dataset) before arrival (`DISCOVERY.md:252` C-17). Attendees who never complete prep arrive with L0 floor only (`SPIKE-07:66` minimum safe).
- **Impact:** HIGH — without prep the core promise (schedule/map/info) is not met, even though emergency still works.
- **Likelihood:** Medium (depends on distribution effectiveness — `DISCOVERY.md:346` OQ-3, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:60` A-05).
- **Mitigation:** L0–L2 with ordered/prioritised resumable downloads, honest PARTIAL messaging, tab-before-install allowed, QR/short-URL campaign, install coach with visuals (`ARCHITECTURE-DESIGN.md:428`), one-tap re-prep, organizer physical fallback (PA/signage/handout — `DISCOVERY.md:324` AMB-1, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:64` O-05).
- **Residual:** Medium — a minority of unprepared attendees remains inevitable; architecture softens but cannot eliminate it.
- **Owner/category:** Product/Ops (distribution) + Architecture (prep UX).
- **Blocks implementation?** No — invalidates product promise only for the unprepared cohort, not the build. Flagged as RK-2.
## W-2 — Browser storage is best-effort; eviction can never be prevented
- **Risk:** Storage may be wiped under pressure or by ITP/policy (`DISCOVERY.md:429` PW-1, PW-3, PW-7). Eviction is silent and all-or-nothing (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:45` B-03).
- **Impact:** HIGH — festival data lost.
- **Likelihood:** Low for installed PWAs with persist granted and regular use (`SPIKE-01:105`), Medium for tab usage or near-full devices (`SPIKE-01:56`).
- **Mitigation:** Install-first exempt, `persist()` request (P6, heuristic only), lean footprint (≤40 MB, 2× ≤80 MB bounded), free-space pre-check 2× (P4), boot light verification as detection (`SPIKE-01:154` P7), RECOVERY state with one-tap re-prep, embedded emergency floor survives everything (`SPIKE-06:81`).
- **Residual:** Low-medium — loss remains possible, recovery is fast and honest.
- **Owner/category:** Platform (browser) + Architecture (detection/recovery).
- **Blocks implementation?** No — detection/recovery is the architecture. Device tests prove the path.
## W-3 — Never-online incorrect device clock
- **Risk:** Wrong clock makes Now/Next wrong without detection when device has never been online to capture skew (`SPIKE-08:124`, `DISCOVERY.md:452` OF-7).
- **Impact:** Medium (misleading "now" during festival).
- **Likelihood:** Medium (some devices drift; airplane mode prevents correction).
- **Mitigation:** Persisted server offset when any sync occurs (`SPIKE-08:34`), monotonic anchor for mid-session jumps (`SPIKE-08:35`), sanity window warning near festival (F-3 suppressed-until-near, `SPIKE-08:104`), absolute times always shown alongside Now markers (`ARCHITECTURE-DESIGN.md:537`).
- **Residual:** Medium for the never-online subset — accepted per `DISCOVERY.md:525` OF-7 and `ARCHITECTURE-DESIGN.md:898` W-2. No network fix exists offline.
- **Owner/category:** Architecture (ClockService) + UX (warning/absolute times).
- **Blocks implementation?** No.
## W-4 — Physical-device verification requirements
- **Risk:** This Linux-host validation is spec/policy-level; iOS JSC parity, IDB atomicity under jetsam, SW re-registration, quota-near-full writes, BLOB read-back, fps/decode, `tel:` in standalone are all UNVERIFIED (`SPIKE-01:59`, `SPIKE-03:116`, `SPIKE-08:152`, `experiments/README.md:28`).
- **Impact:** HIGH if any assumption fails on target hardware.
- **Likelihood:** Unknown until measured.
- **Mitigation:** Narrow device matrix (§4) runs before production; budgets and fallbacks (overview-only, detail tile-split DD-9, Preact reconsider trigger) are predefined.
- **Residual:** Unknown pre-device; zero post-device if protocol passes.
- **Owner/category:** Architecture Validation / QA.
- **Blocks implementation?** Blocks production declaration, not implementation start — staging work proceeds on provisional origin with fixtures (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:128`).
## W-5 — Emergency content correctness and sign-off
- **Risk:** Wrong security number, moved AED/muster point, or unapproved procedure text is safety-critical (`DISCOVERY.md:468` EM-1, `DISCOVERY.md:345` OQ-2).
- **Impact:** Critical.
- **Likelihood:** Low-medium without a gate.
- **Mitigation:** Single emergency source sheet → both baseline and dataset section (no drift, `ARCHITECTURE-DESIGN.md:345`), ≤16 KB cap keeps scope life-safety-minimum, signed dataset section + provenance stamps on every screen (`SPIKE-06:60`), sign-off gate with named approver and legal review (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:62` O-02, `DISCOVERY.md:352` OQ-7), version/updatedAt in manifest.
- **Residual:** Low with gate enforced; high without it.
- **Owner/category:** Content/Ops (organizers) + Pipeline.
- **Blocks implementation?** Blocks emergency *content* publish, not scaffolding (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:128`).
## W-6 — Map asset quality / organizer-provided artwork
- **Risk:** Organizers may not have raster art at required quality/dimensions; POI placement quality varies (`DISCOVERY.md:328` AMB-3, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:105` OQ-6).
- **Impact:** Medium (map degrades to facilities list).
- **Likelihood:** Medium (illustrated raster most likely, quality unknown).
- **Mitigation:** Architecture accepts raster as-is; pipeline POI tap-tool/normalized coords (`ARCHITECTURE-DESIGN.md:560`), WebP budgets with margin (F-2), predefined fallbacks overview-only and 2×2 tile-split (`SPIKE-03:98`), SVG flip if vector supplied, facilities list is first-class (`SPIKE-03:79`).
- **Residual:** Medium — list view compensates but artwork quality caps experience.
- **Owner/category:** Content/Ops + MapService.
- **Blocks implementation?** No — placeholder art unblocks development (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:128`).
## W-7 — Vanilla-TS hand-rolled UI accretion
- **Risk:** No framework safety net; view-layer complexity could accrue bugs (`DISCOVERY.md:863` RK-7, `ARCHITECTURE-DESIGN.md:898` W-5).
- **Impact:** Medium.
- **Likelihood:** Medium as feature count grows.
- **Mitigation:** Small surface (4 destinations + status), strict layering B-1…B-7 (`ARCHITECTURE-DESIGN.md:195`), small store/router, unit tests, reconsider trigger → Preact (`ARCHITECTURE-DECISIONS.md:117`) at first sign of sustained interdependence.
- **Residual:** Low with discipline.
- **Owner/category:** Architecture/Implementation.
- **Blocks implementation?** No.
## W-8 — Bundled pure-JS Ed25519 verifier supply chain
- **Risk:** Verifier library must be audited and stay tiny; WebCrypto Ed25519 not baseline on iOS 16.4 (`ARCHITECTURE-DESIGN.md:14`, `DISCOVERY.md:401` TQ-9).
- **Impact:** High if verifier is wrong/pulled; medium on bundle.
- **Likelihood:** Low with pinned audited dep.
- **Mitigation:** Pin, audit, bundle-budget gate (≤150 KB gz JS), WebCrypto SHA-256 + pure-JS Ed25519 chosen correctly per spikes; CSP restricts to self.
- **Residual:** Low pending audit (AQ-06).
- **Owner/category:** Security + Build.
- **Blocks implementation?** Blocks production signing declaration, not scaffolding (staging uses test key).
## W-9 — Signing key custody / rotation
- **Risk:** Loss of the offline festival signing key or compromised publish credentials push bad data (`ARCHITECTURE-DESIGN.md:863` RK-6, `DISCOVERY.md:616` SE-1/SE-2, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:85` S-01, AQ-21).
- **Impact:** High.
- **Likelihood:** Low with process.
- **Mitigation:** Offline key, sealed backup, singular publisher + deputy (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:60` O-01), fingerprinted key set in shell for rotation, repo audit trail, rollback via new higher version, rotation drill pre-festival.
- **Residual:** Low with runbook + drill.
- **Owner/category:** Ops/Security.
- **Blocks implementation?** Blocks production publishing, not dev.
## W-10 — Single-origin availability
- **Risk:** All shell+data on one HTTPS origin (`DISCOVERY.md:261` C-20); CDN outage concentrates risk (`ARCHITECTURE-DESIGN.md:863` RK-10, `ARCHITECTURE-DESIGN.md:898` W-6).
- **Impact:** Low (updates are enhancement-only; last-good datasets already on devices).
- **Likelihood:** Low (static CDN).
- **Mitigation:** Static files trivially mirrorable; Transport seam also future-mitigates via LAN mirror (`ARCHITECTURE-DESIGN.md:920` DD-8); origin chosen deliberately before public link (AQ-18).
- **Residual:** Low.
- **Owner/category:** Ops/Deployment.
- **Blocks implementation?** No.
---
# 6. Architecture freeze test
20 adversarial scenarios — each: what happens, what remains functional, acceptability, any required architecture change, V1 impact. All answers are after incorporating spike changes.
1. **User never installs Lumen.** *Tab-only on iOS.* Shell works while open; storage subject to 7-day ITP eviction (`DISCOVERY.md:429` PW-1). Coach re-surfaces; degraded-persistence warning shown (`SPIKE-07:48`). Emergency L0 floor always works after any shell load; L1/L2 are available until evicted then RECOVERY. *Acceptable:* no technical fix inside web platform — install is the fix (`ARCHITECTURE-DESIGN.md:880`). *No new architecture.* V1: tab path stays.
2. **User opens Lumen once and never connects again.** *Never stages dataset.* After first shell load: L0 floor only; NOT_READY state honest, not blank (`SPIKE-07:66`). Map/info show "not downloaded" cards. *Acceptable:* expectation management is product/ops (distribution before arrival + physical fallback, `SPIKE-07:66` W-1). *No new architecture.* V1 unchanged.
3. **iOS evicts storage.** *Per-origin all-or-nothing, silent.* Boot light check (sizes + verification-record match, `SPIKE-05:83`) fails → RECOVERY with reason `missing`; emergency floor intact (`SPIKE-06:81`); one-tap re-prep when online; favourites in user DB survive unless user DB was also evicted (then RECOVERY+baseline, `SPIKE-02:69`). *Acceptable:* eviction can never be prevented (`ARCHITECTURE-DESIGN.md:898` W-4), only detected/recovered. *No new architecture beyond implemented detection.*
4. **Browser kills the service worker during an update.** SW holds no dataset state (C-21, P8). Staging runs in page context (`SPIKE-01:83`). Page persists per-file progress; resume path defined (`SPIKE-02:50` S03). SW kill cannot corrupt staging (`ARCHITECTURE-DESIGN.md:882`). *Acceptable. No change.*
5. **Page is killed during a dataset transaction.** IDB transactions are atomic per database (spec CONFIRMED, `SPIKE-01:60`); an uncommitted write leaves last committed intact (`SPIKE-01:111`). Single short txn per file (P1, F-1); activation single txn on `lumen-system` (P2, F-2). Kill before flip → old stands; during flip → uncommitted, old stands; after flip before readback → new stands but readbackPending guard (`SPIKE-02:97`) covers next-boot recheck. *Acceptable pending iOS jetsam atomicity UNVERIFIED (`SPIKE-02:111`).* V1: document and verify on device. No new architecture if device tests pass; if fails, architecture fallback is re-prep path plus retry-once behaviour (`ARCHITECTURE-DESIGN.md:672`).
6. **Device reboots during a dataset update.** Same as #5 at OS level. Committed IDB survives browser/device reboot; SW/Caches rehydrate (`SPIKE-01:90`). Cold-start path is budgets-only (`ARCHITECTURE-DESIGN.md:848` §27). *Acceptable. No change beyond device-measured cold-start timing.*
7. **Festival dataset is corrupt (bits wrong).** Per-file SHA-256 fails → discard staging, quarantine version, keep active (`SPIKE-02:60` S06, `ARCHITECTURE-DESIGN.md:667`). Corruption discovered post-activation by readback → automatic rollback when fallback slot intact, else RECOVERY+baseline (`SPIKE-02:50` S14/X1/X2). Never exposes partial dataset (invariant 4). *Acceptable. No change.*
8. **Dataset has valid hash but invalid signature (MITM / compromised hosting, DISCOVERY SE-1).** Signature checked over exact manifest bytes before parsing or downloading anything else (`SPIKE-02:29` step 3). Ed25519 verifier rejects → quarantine, never fetch files (`SPIKE-02:60` S07), diag entry. Organizer rollback via new higher signed version. *Acceptable. No change.* Future transports inherit the same verifier (`SPIKE-02:26` composition).
9. **Dataset incompatible with shell (schema or appCompatibility).** Checked before staging and at boot (`ARCHITECTURE-DESIGN.md:688`). Rejected + "please update app" when `minAppVersion > current`; boot prefers compatible slot else limited mode (floor+status only). Publishing rule C-23 (shell range first, dataset after, freeze 7 days before festival) prevents most cases. *Acceptable. No change.*
10. **User has incorrect device clock.** If any online sync occurred, `ClockService.now()` uses persisted skew (`SPIKE-08:34`); mid-session jumps flagged by monotonic anchor (`SPIKE-08:35`). Sanity window warning is suppressed until near festival or evidence of skew (F-3) so early prep is quiet (`SPIKE-08:97`). READY is time-independent (`SPIKE-05:47`), so wrong clock never bricks availability. Never-online wrong clock is *accepted residual* — absolute times remain readable (`SPIKE-08:124`). *Acceptable. No new architecture; NTP endpoint rejected (C-19).*
11. **GPS denied.** No permission requested in V1 (`ARCHITECTURE-DESIGN.md:222`). All map functionality is GPS-free; Facilities list is the primary non-visual path (`SPIKE-03:79`). *Acceptable. No change.*
12. **GPS unavailable.** Identical to #11. Hook `lat/lng` on POIs preserved for future blue dot without touching features (`ARCHITECTURE-DESIGN.md:540`). *No change.*
13. **Festival changes schedule after preparation.** New packageVersion published, monotonic. Device fetches on next open-while-online opportunistically (`SPIKE-02:29`); moved/cancelled markers render distinctly (`ARCHITECTURE-DESIGN.md:509`); dataset `generatedAt`/`fetchedAt` shows staleness. Users who never come online keep old schedule — *accepted and mitigated by organizer PA/boards* (`DISCOVERY.md:460` OF-8). *No new architecture.* Urgency beyond pull would be DD-1 annoucements (reserved, not required for core promise).
14. **Emergency information changes after preparation.** Tier-2 dataset emergency section updates via new signed package; floor frozen at shell build remains as fallback (`SPIKE-06:93`). Provenance labels show age; organizer physical channels remain urgent path (A-16, `ARCHITECTURE-DESIGN.md:891` W-3). *Acceptable. No change.*
15. **User arrives with no usable festival dataset (NOT_READY).** Shell + floor guarantee L0; PARTIAL levels each fully functional (`SPIKE-07:66`); Status enumerates exactly what's missing; one-tap prep if any connectivity appears; physical fallback at venue. *Acceptable — distribution is product/ops problem (W-1). No new architecture.*
16. **User has low storage.** `storage.estimate()` pre-check refuses staging if free < 2× package with guidance; QuotaExceeded aborts and keeps active (P3/P4); L0/L1 partial still possible when full package won't fit (`SPIKE-07:52` FP-4). Baseline always works. *Acceptable. No change.*
17. **User has low-end Android phone.** Vanilla TS + GPU-transform map + ≤1600/3072 caps + windowed lists + WebP keep within budgets (`ARCHITECTURE-DESIGN.md:848` §27, `SPIKE-03:58` F-1). Fallback overview-only if decode/transform janks (`SPIKE-03:98`). Perf is YELLOW until device protocol in `SPIKE-03:116` passes. *Acceptable pending device proof. No speculative complexity added.*
18. **User uses iOS Safari without installing (tab).** Same as #1. Works while open; eviction risk; no background sync; `tel:` still standard but verify on standalone (`SPIKE-01:171`). *Acceptable with honest messaging. No change.*
19. **User's browser clears site storage (Settings → Clear).** Identical to eviction path (FA-5/`DISCOVERY.md:460` OF-13). Next boot → RECOVERY; baseline intact; re-prep path exists. Only shell reloads from network need connectivity. *Acceptable. No change.*
20. **Future mesh proves impossible inside a browser.** Expected per feasibility analysis (`DISCOVERY.md:581`, `ARCHITECTURE-DESIGN.md:717` §20) — no BLE peripheral on iOS, no ad-hoc Wi-Fi, no background sockets. The seam is transport-agnostic: any future delivery (native companion, hardware relay, LAN mirror implementing Transport) would attach as a bridge; V1 loses nothing because HTTP transport is *complete product functionality*, not a placeholder (`ARCHITECTURE-DESIGN.md:895` 16). *Acceptable. No V1 architecture stranded.*
No scenario requires new architecture. Two scenarios expose device-dependent behaviour already tracked as UNVERIFIED (IDB atomicity under kill, map/GPU performance) — both have defined fallbacks.
---
# 7. Final architecture status
Incorporates all spike addenda. Status values per directive: GREEN = validated and suitable for implementation; YELLOW = architecturally acceptable but requires physical-device or operational validation; RED = must change before implementation (none).
| Decision | Status | Evidence | Remaining validation | Implementation impact |
|---|---|---|---|---|
| ADR-001 Offline-first | **GREEN** | SPIKE-05 time independence + SPIKE-07 L0–L2 + SPIKE-02 crash suite | None | Builds proceed as designed |
| ADR-002 PWA / install | **GREEN (functional) / YELLOW (persistence)** | Correct: install-first + single origin; L0–L2 formalised (`SPIKE-07:31`) | `persist()` grants + 7-day tab vs installed timing on D1/D2 (`SPIKE-01:159`) | Coach + honest tab warning required; no spec change |
| ADR-003 Frontend vanilla TS | **GREEN** (with trigger) | Chosen correctly; small surface; EXP-3 no disqualifying JS cost; boundaries B-1…B-7 keep data layer safe | Low-end render cost on D3; first implementation spike is the real proof (watch for sustained store→render complexity → Preact per `ARCHITECTURE-DECISIONS.md:117`) | Hand-rolled store/router + hashed assets + precache manifest |
| ADR-004 Storage (IDB + Cache) | **YELLOW** (device-conditional) | Design-logic valid: transactions atomic (spec), A/B ordering proven 16/16 (`SPIKE-02:50`), P1–P8 normative | Device tests `SPIKE-01:159` §5: iOS jetsam atomicity, quota-near-full writes, Blob read-back on D1/D3 | Must implement P1–P8 exactly; production-ready only after device pass; fallback = re-prep path already designed |
| ADR-005 Package (JSON + manifest) | **GREEN** (schema) / **YELLOW** (content) | JSON+hash+Ed25519 sufficient (F-5); required flag, no-expiration, emergency sub-versioning, user-data schema split added (`SPIKE-04:203`) | Real content must pass 5 pipeline gates + budget enforcement (`SPIKE-04:189`); stable IDs pending source sample (AQ-20) | Pipeline tooling + fixture packages; `dayKey` validated at publish |
| ADR-006 Atomic A/B updates | **YELLOW** (device-conditional) | State machine proven 14+3 scenarios PASS with F-1…F-4 (`SPIKE-02:50` + X1–X3) | IDB transaction atomicity under real kills is the sole platform assumption (`SPIKE-01:75`) | Activation = one txn + verification record + readbackPending; rollback depth 1 is accepted trade-off |
| ADR-007 Emergency baseline (3 tiers) | **GREEN** | Fixed tier-1 contents + resolution/merge + zero-IDB path + same-source generation (`SPIKE-06:12`, `SPIKE-06:60`, `SPIKE-06:75`) | Organizer content sign-off gate and real `tel:` on standalone (`SPIKE-06:81`) | Generate floor + section from same sheet at build; 16 KB cap enforced |
| ADR-008 Map | **YELLOW** (device-conditional) | Raster+DOM correct choice; memory caps tightened 1600/3072 ≤~35 MB (`SPIKE-03:58`); object-URL (F-3) + dark filter (F-4) added | Device protocol `SPIKE-03:116` §7: fps ≥30, decode, no leaks, a11y, texture-cap fallback | Lazy object URLs; one detail resident; facilities list first-class; no GPS/blue dot |
| ADR-009 Time model | **GREEN** (logic) / **YELLOW** (JSC parity) | 24/24 PASS on V8/ICU (DST, unusual zones, skew, Now/Next); F-3 sanity suppression until near (`SPIKE-08:97`); clarification F-4 (only Intl) | JSC parity at DST edges + CDN Date observation + low-end timing on D1/D3 (`SPIKE-08:152`) | `ClockService.now()` pure computation; no timers; read `Date` header opportunistically |
| ADR-010 Sync pull-only | **GREEN** | Pull on open/online/manual + monotonic + no background is the only viable model (C-19, B-06) | Online hint best-effort nature (not a bug) | `SyncService` + staging ordering; announcements schema reserved (DD-1) |
| ADR-011 Transport seam | **GREEN** | Minimal `isAvailable/fetchPointer/fetchBytes` succeeds for HTTP and any future byte-pipe; verifier-only truth | None — deliberately tiny | One interface + HttpTransport; tests inject fault transport |
| ADR-012 Mesh strategy | **GREEN** (strategy) | Future-only; costs zero in V1; out-of-browser assumption documented; no mesh fails V1 | None — feasibility study deferred until concrete opportunity (DD-7) | Nothing built; docs only |
| ADR-013 Security | **GREEN (model) / YELLOW (library audit)** | Threat model TH-1…TH-11 covered with minimal parts; WebCrypto-SHA256 + pure-JS Ed25519 correct per TQ-9; CSP `self` only; privacy by design | Bundled verifier audit + bundle gate (AQ-06); key custody/rotation drill (AQ-21, S-01) | Pin audited verifier; HSTS; no inline/eval; no secrets client-side (C-24) |
| ADR-014 Deployment (static CDN) | **GREEN (topology) / YELLOW (provider)** | Static immutable URLs + `latest.json` pointer + staging/production origins correct; cache headers per `ARCHITECTURE-DESIGN.md:807` | Provider + production origin choice (AQ-14, AQ-18) — not blocking dev; swap is cheap pre-users, costly post-users | Content repo + sign/publish pipeline + smoke re-verify |
No RED. All YELLOW are device-dependent or content/ops-dependent — no architecture must change before implementation. The only path that could turn a YELLOW to RED is a device test failure for which fallbacks are already defined (overview-only, tile-split, re-prep, Preact trigger).
---
# 8. Implementation gate
## Recommendation: **B. READY FOR IMPLEMENTATION AFTER SPECIFIED BLOCKERS**
Base: actual remaining risks after incorporating every spike's accepted change. Physical-device testing is the only architectural-level remaining blocker for a production festival deployment; it does not block starting implementation against fixtures.
### Blockers, distinguished
#### ARCHITECTURAL BLOCKERS — none.
No ADR requires redesign before code starts. The architecture is frozen at the design-logic level with all spike addenda incorporated. Starting implementation now will not incur speculative rework.
#### PHYSICAL-DEVICE VALIDATION — required before a production festival, not before implementation start
- **P-D-1 — IDB atomicity under real kills on iOS (D1/D2).** Verifies `SPIKE-01:75` / `SPIKE-02:111`. BLOCKING for production. Runnable immediately on any iPhone once a staging shell exists.
- **P-D-2 — IDB quota/Blob behaviour near-full + persistence signals on D1–D4.** `SPIKE-01:159` items 4–6, 8–10. BLOCKING (quota path) / HIGH (persistence heuristics).
- **P-D-3 — Map fps, decode, memory, leaks, texture-cap fallback on D3 (+ sanity D1/D2).** `SPIKE-03:116`. BLOCKING for map production claim; facilities list already covers degraded path.
- **P-D-4 — Intl/JSC parity at DST edges, dayKey grouping, ClockService skew + monotonic drift, F-3 sanity suppression on D1/D2 + D3.** `SPIKE-08:152`. HIGH; early-prep vs near-festival distinction must read correctly.
- **P-D-5 — SW lifecycle (kill mid-staging) on iOS.** `SPIKE-01:82` C-21 + `ARCHITECTURE-DESIGN.md:243`. BLOCKING for invariant proof.
- **P-D-6 — `tel:` from standalone on D1/D2 + D3.** `SPIKE-06:81`, `SPIKE-01:171`. HIGH for safety.
- **P-D-7 — Cold-start / boot light-check timing on D3 (and iOS sanity).** `ARCHITECTURE-DESIGN.md:851`; `SPIKE-05:83`. MEDIUM/HIGH.
All are listed in §4 with severity. None invents new architecture if they pass; each has a documented fallback if they fail.
#### PRODUCT / OPERATIONS BLOCKERS — required before content/production, not before code
- **O-1 — Emergency content sign-off + legal review (`DISCOVERY.md:345` OQ-2/OQ-7, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:62` O-02, W-5).** BLOCKING for emergency *content* publish.
- **O-2 — Distribution plan + on-site physical fallback (`DISCOVERY.md:346` OQ-3, W-1).** HIGH — determines unprepared cohort size.
- **O-3 — Production origin choice (AQ-18, AQ-14, A-11) + hosting provider/budget (OQ-8).** Must be chosen deliberately before any public staging link because storage is origin-keyed; not blocking for development on provisional origin (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:116` AQ-18).
- **O-4 — Publisher/deputy + signing key custody + rotation drill (O-01, O-02, S-01, AQ-16/AQ-17/AQ-21).** BLOCKING for production signing; staging uses test key.
#### CONTENT / DEPLOYMENT REQUIREMENTS — parallelizable with implementation
- **C-1 — Real map art at ≥1600 px (and optionally ≥3072) + POI sheet to prove budgets (`SPIKE-03:58`, `SPIKE-04:189`).** HIGH.
- **C-2 — Schedule sample to confirm dayKey, stable IDs / mint mapping, and changed-event handling (D-01, AQ-20, `SPIKE-04:189`).** HIGH.
- **C-3 — Staging dry-run festival with test edition + field devices (AQ-23).** Strongly recommended pre-festival.
### Can implementation begin?
**Yes — immediately, on a provisional staging origin with fixtures.** `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:138` explicitly states nothing above blocks shell, data-layer, sync/verifier, and feature modules against staging fixtures — by design. The only work that must complete *before* the festival is the physical-device protocol above plus the ops gates O-1…O-3; the architecture itself needs no further change.
---
*End of Architecture Validation.*