Lumen/IMPLEMENTATION-CONTRACT.md
Lumen Stage1 fcc18ddcc8
Some checks failed
ci / check (push) Has been cancelled
Checkpoint: current Lumen state
2026-09-23 18:58:21 -05:00

832 lines
85 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 — Implementation Contract
- **Phase:** Implementation Preparation — handoff from Architecture Validation to coding agent
- **Date:** 2026-08-30
- **Validated architecture freeze:** `ARCHITECTURE-VALIDATION.md:1` (SPIKE-01…08 reconciled)
- **Source of truth chain:** `DISCOVERY.md` → `ARCHITECTURE-DESIGN.md` + `ARCHITECTURE-DECISIONS.md` + `ASSUMPTIONS-AND-OPEN-QUESTIONS.md` → `ARCHITECTURE-VALIDATION.md` → this contract
- **Scope guard:** No application code, no `package.json`, no `node_modules`, no PWA/DB/network implementation exists when this contract was written. A future coding agent must not reinterpret the architecture to simplify it.
---
# 0. How to read this contract
Every rule below traces to a validated ADR or spike. Where the contract says "normative" the rule was proven or hardened by a spike. Where it says "provisional" the architecture allows implementation to proceed but production still requires the physical-device or content checks listed in `ARCHITECTURE-VALIDATION.md:4` and `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:9`.
If an implementation conflict is found, stop and document it — do not silently change the architecture (`RULES FOR FUTURE AI CODING AGENTS`).
---
# 1. V1 scope
V1 ships an **offline-first, mobile-first Progressive Web App** for 300–500 attendees (`DISCOVERY.md:103`).
Included:
- Four destinations: **EMERGENCY · SCHEDULE · MAP · FESTIVAL** (`DISCOVERY.md:145` R-N1, `ARCHITECTURE-DESIGN.md:207`)
- Emergency 3-tier model (floor + dataset section + reserved T3) (`ARCHITECTURE-DECISIONS.md:272`, `SPIKE-06:12`)
- Schedule: browse, Now/Next, My Schedule (favorites), filters, search — all offline (`DISCOVERY.md:162` R-S1…S5)
- Map: raster WebP ≤2 levels + DOM POI overlay + Facilities list — all offline, no GPS in V1 (`ARCHITECTURE-DECISIONS.md:315`, `SPIKE-03:58`)
- Festival info: structured blocks — all offline (`DISCOVERY.md:182` R-F1)
- Pull-only sync on open / online hint / manual (`ARCHITECTURE-DECISIONS.md:406`)
- Honest readiness states: READY/PARTIAL/NOT_READY/RECOVERY/BASELINE_ONLY/FAILED (`SPIKE-05:60`)
- Bootstrap L0–L2 preparation (`SPIKE-07:31`)
- Time model: UTC + festival IANA zone + ClockService (`ARCHITECTURE-DECISIONS.md:362`)
Trace: ADR-001…ADR-010, `ARCHITECTURE-DESIGN.md:25` executive summary.
# 2. Explicitly excluded functionality (V1)
Do not build these in V1 unless a later ADR explicitly approves them:
- Mesh networking in any form (seam only, `DISCOVERY.md:270` NR-1, ADR-012) — see §27
- Native apps / App Store wrapping (NR-2, NR-3)
- User accounts / cross-device sync of favorites (NR-4, NR-5; `DISCOVERY.md:240` constraints 11–12)
- Real-time collaboration/social (NR-6), payments (NR-9), UGC (NR-10)
- GPS-required features / blue dot, online tiles as core map (NR-7, NR-8, ADR-008) — hook preserved only
- Push as critical channel (NR-13, `ARCHITECTURE-DECISIONS.md:406` pull-only)
- Background Sync / Periodic Sync (`DISCOVERY.md:433` PW-4, `ARCHITECTURE-DESIGN.md:78` C-19)
- Admin/CMS UI (NR-14; pipeline is file-based per ADR-014), private analytics (NR-15)
- Multi-language i18n (NR-11, DD-6): keep strings in data but English-only
Violations fail the Definition of Done `§40`.
# 3. Technology stack (normative)
| Concern | Selection | Trace |
|---|---|---|
| Language | TypeScript strict | ADR-003 (`ARCHITECTURE-DECISIONS.md:86`) |
| UI framework | None — vanilla TS + tiny store + history router + escaping renderers | ADR-003; fallback trigger → Preact only if store→render proves unworkable (`ARCHITECTURE-DECISIONS.md:86`) |
| Build | Single-bundle bundler that emits hashed immutable assets + precache manifest (esbuild/vite-class) — implementation pick | `ARCHITECTURE-DESIGN.md:126` |
| Service worker | Hand-written, tiny (~precache + navigation fallback only) — no Workbox | ADR-002 (`ARCHITECTURE-DESIGN.md:14`) |
| Structured storage | IndexedDB via thin internal promise wrapper; Cache Storage for shell only; localStorage guarded non-load-bearing | ADR-004 (`ARCHITECTURE-DECISIONS.md:129`, `SPIKE-01:140` P1–P8) |
| Crypto | WebCrypto SHA-256 + bundled audited pure-JS Ed25519 verifier (WebCrypto Ed25519 not baseline on iOS 16.4) | ADR-013 (`ARCHITECTURE-DESIGN.md:14`), TQ-9 — YELLOW pending audit `ARCHITECTURE-VALIDATION.md:522` |
| Map | WebP raster + DOM buttons + CSS-transform pan/zoom + Facilities list | ADR-008 + SPIKE-03 |
| Hosting | Static HTTPS CDN, single origin | ADR-014 (`ARCHITECTURE-DESIGN.md:104`), `DISCOVERY.md:261` C-20 |
| Push/analytics | None in V1 | ADR-010 / ADR-013 |
Forbidden without ADR amendment: React/Angular/Vue, Workbox, Dexie/idb, SQLite-WASM, any map SDK, any mesh lib, any auth system, any dynamic backend (`ARCHITECTURE-DESIGN.md:144`). Target: only the audited Ed25519 verifier beyond stdlib (`ARCHITECTURE-DESIGN.md:930` rule 3).
# 4. Project structure (recommended)
Structure follows the layering in `ARCHITECTURE-DESIGN.md:148` §6 and the boundaries B-1…B-7 `ARCHITECTURE-DESIGN.md:195`. A coding agent may rename leaves but must keep the boundaries and allowed/forbidden sets.
```
src/
platform/ # Cross-cutting low-level adapters
idb/ # Thin promise wrapper over raw IDB (ADR-004, SPIKE-01 P1–P8) — no business logic
cache/ # Cache Storage adapter for shell
sw/ # SW bridge (page side, message handling)
storage/ # Re-exports platform adapters with typed contracts — no UI, no fetch
data/ # DatasetStore + UserStore — read APIs + writes (typed, transactional)
sync/ # SyncService orchestration (check→stage→verify→activate) + staging state
transport/ # Transport interface + HttpTransport (V1 only)
verifier/ # SHA-256 + Ed25519, quarantine, budgets, schema checks
domain/ # Domain services, pure logic over data layer — no persistence, no network
emergency/ # Resolution: floor vs dataset section, provenance, hardening F-1
schedule/ # Queries: Now/Next, filters, search, conflict detection
map/ # POI interpretation, level math, Facilities list queries
festival/ # Info blocks
readiness/ # ReadinessService predicate C1–C8 + state taxonomy (SPIKE-05)
clock/ # ClockService (skew + monotonic + sanity F-3)
favorites/ # FavoritesService over UserStore (B-6)
ui/ # Views + presentation stores + router + escaping renderer
components/ # Shared primitives (safe DOM helpers, status chip, etc.)
views/ # emergency/ / schedule/ / map/ / festival/ / status/
router/ # history-API router (one index.html, offline-safe deep links)
render/ # Structured-node → safe DOM (C-22, no innerHTML of raw strings)
app/ # App bootstrap, boot sequence (light verify → readiness → render), composition root
emergency-baseline/ # Compiled-in floor JSON (generated, immutable per build)
assets/ # Hashed shell assets (generated)
public/
index.html # Cached shell entry (no-cache per ARCH §24)
manifest.webmanifest
fallback.html # Ring-0 static fallback (emergency text, no JS needed)
sw.js # Hand-written SW (generated precache list injected at build)
content/ # Not shipped — authoritative source for pipeline (ADR-014)
emergency/ schedule/ map/ info/ assets/
pipeline/ # Validate→build→hash→sign→upload→smoke (separate from app)
tests/
unit/ # Verifier, ClockService, readiness predicate, A/B transitions, escaping
integration/ # Full update lifecycle with mocked transport + fault injection (9 stages)
e2e/ # Headless/offline harness where useful
device-matrix/ # Scripted manual protocols per ARCHITECTURE-VALIDATION.md §4
```
### Directory responsibilities
| Directory | Owns | May import | Must never import |
|---|---|---|---|
| `platform/idb`, `platform/cache` | Transaction discipline, durability, quota | Browser APIs | Any domain/ui/sync logic |
| `data/` | Typed local truth (read/write contracts) | `platform/` | `sync/`, `ui/` |
| `sync/` | Orchestration, staging, version logic, activation txn | `data/`, `sync/verifier`, `sync/transport` | `domain/`, `ui/` internals |
| `sync/verifier` | Authenticity/integrity verdicts + budgets + schema | `platform/` (hash), bundled verifier | Rollback/policy (lives in `sync/`) |
| `sync/transport` | Bytes in/out, availability | `fetch` (or equivalent) | `data/`, `domain/`, `ui/` |
| `domain/*` | Feature logic over `data/` read APIs + `clock/` | `data/` read, `clock/` | `platform/`, `sync/`, `network` |
| `ui/` | Rendering, routing, user intent, readiness display | `domain/`, `readiness/` | `platform/`, `storage/`, `sync/transport`, raw `fetch`, `indexedDB` |
| `app/` | Boot sequence, system-meta read, compatibility check (§18.5), composition | Everything via its public APIs | — |
| `public/sw.js` | Shell-cache precache + navigation fallback only | `caches` | Datasets, sync, IDB |
This encodes `ARCHITECTURE-DESIGN.md:704` responsibilities and the mesh anti-leak rule (invariant 12).
# 5. Architectural layers (validity: `ARCHITECTURE-DESIGN.md:148`)
Layers are UI → Domain → Data → Sync → Transport → Platform + SW (separate context). The layering diagram is `ARCHITECTURE-DESIGN.md:148`. Implementation must not collapse layers even if it seems convenient.
# 6. Dependency rules (normative, per `ARCHITECTURE-DESIGN.md:195` B-1…B-7 + `ARCHITECTURE-DESIGN.md:930` hard rules)
| ID | Rule |
|---|---|
| B-1 | Views never touch `indexedDB`, `fetch`, `caches`, `localStorage`, or SW internals. |
| B-2 | Domain services read only through `data/` read APIs (`DatasetStore`/`UserStore`). |
| B-3 | Feature modules never know which transport delivered data; transports never know content meaning (invariant: mesh concerns never leak). |
| B-4 | Only `sync/verifier` decides authenticity/integrity; Sync orchestrates, never validates alone. |
| B-5 | SW never writes dataset data; staging lives in page context (C-21, P8, `ARCHITECTURE-DESIGN.md:83`). |
| B-6 | User state (`lumen-user`) is never touched by dataset updates/rollback/GC (`SPIKE-02:69` X3). |
| B-7 | No festival content rendered via `innerHTML` of raw strings (C-22, `ARCHITECTURE-DESIGN.md:195`). |
Hard rules carried into implementation `ARCHITECTURE-DESIGN.md:928`: no feature→persistence/transport imports; no raw-HTML ingestion; no runtime dep without ADR amendment; no background work (C-19); every readiness state reachable in tests; budgets CI-enforced; no analytics/third-party/permissions.
# 7. UI / application boundaries
- Single-page app, one cached `index.html`, history-API router (`ARCHITECTURE-DESIGN.md:207`).
- Persistent bottom nav EMERGENCY · SCHEDULE · MAP · FESTIVAL (Emergency first, visually dominant) — satisfies R-N3 one-tap emergency (`ARCHITECTURE-DESIGN.md:207`).
- Persistent status chip (READY/PARTIAL/NOT_READY/RECOVERY/BASELINE_ONLY/FAILED) in header → Status screen per `SPIKE-05:60`.
- Views → VM/presentational stores → `domain/*` → `data/` read APIs. Never direct persistence.
# 8. Local storage rules
- Storage is best-effort, silent eviction, per-origin all-or-nothing (`SPIKE-01:97`, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:43` B-03). Detection—not prevention—is the architecture: boot light verify (§20) + RECOVERY + baseline.
- Do not assume `navigator.storage.persist()` is granted (`ARCHITECTURE-DESIGN.md:285`); request it once after READY (P6) but never rely on grant.
- Do not handle storage pressure with retry loops; pre-check free space via `storage.estimate()` (P4).
- Do not keep long transactions; one short txn per file (P1) — no `await` of non-IDB work inside a txn.
# 9. IndexedDB schema requirements (normative, ADR-004 + SPIKE-01/02)
| Database | Stores | Keys / contents | Lifecycle |
|---|---|---|---|
| `lumen-system` | `meta` (single object store) | Active slot pointer (`A`/`B`), `activeEdition`, `activePackageVersion`, verification record (what/when/which, fingerprint, manifestSha256), `appVersionAtActivation`, `readbackPending`, clock skew cache pointer | Single source of truth for active/readback metadata; rewritten atomically on activation (P2); no per-file staging progress |
| `lumen-slot-a` | `files`, `assets`, `staging` | `files`: section docs keyed by section id (`emergency`/`schedule`/`map`/`info`/`assets.json`); `assets`: Blobs keyed by asset id (`map-base-*`, icons); `staging`: authoritative slot-local journal | Inactive slot is staging target; file/asset + journal commit in one transaction; active slot is truth; GC per §13 |
| `lumen-slot-b` | `files`, `assets`, `staging` | Same as above | Same |
| `lumen-user` | `favorites` (by stable event id), `prefs`, `diag` (scrubbed ring buffer) | Favorites `id → { addedAt }`; `prefs` (theme, clock, `displayMode` flags); `diag` (quarantined versions, recent errors) | Never GC'd by dataset logic (B-6); own idempotent migrations (package schema separate, `SPIKE-04:210`) |
| `lumen-shell-v<build>` (Cache) | Cache Storage | Precached shell (hashed JS/CSS/icons, `index.html`, `fallback.html`) | Versioned per build; old caches deleted on SW `activate` |
| (localStorage) | Guarded flags only | Coach dismissed, etc. — `try/catch` | Non-load-bearing |
Notes: assets stored as Blobs in the slot DB to keep rollback in one failure domain (`ARCHITECTURE-DESIGN.md:275`, `SPIKE-01:140`). Dataset JSON parsed once per section per session; no persistent derived indexes.
# 10. A/B dataset rules (normative, ADR-006 + SPIKE-02)
Invariant: at every observable moment `lumen-system.meta.activeSlot` points at either the old verified dataset or the new verified dataset — there is no third state (`ARCHITECTURE-DESIGN.md:659`).
- Two dataset slot databases; updates stage exclusively into the **inactive** slot (`ARCHITECTURE-DESIGN.md:659`).
- Per-file staging is one short txn: `bytes + progress record` together (P1, F-1 `SPIKE-02:89`); no txn spans non-IDB await.
- Staging progress is authoritative in the target slot's `staging` journal; resume is keyed on committed slot-local progress; staging older than 7 days is discarded (`ARCHITECTURE-DESIGN.md:669`).
- Beginning a new staging run wipes the inactive slot — rollback depth is exactly **1** (three-slot rejected for footprint, `SPIKE-02:100` F-4). Invariant holds (valid dataset always active); only undo depth is bounded.
- Downloads run in **page context**, never SW (C-21, P8).
Violations (e.g., staging into active slot or cross-DB txn) contradict `SPIKE-02:73` ordering proof and fail `§40` acceptance.
# 11. Dataset validation rules (normative, ADR-005/013 + SPIKE-02/04)
Cheap + authoritative checks first; untrusted data never forces work.
1. Fetch `latest.json` → monotonic version comparison: reject `≤ active` from any network source (downgrade protection, `ARCHITECTURE-DESIGN.md:349`). Purely informational `generatedAt` never gates.
2. Fetch candidate `manifest.json`.
3. **Signature** over exact manifest bytes (Ed25519, `signature.json` `manifestSha256` + `publicKeyFingerprint`) — verified against embedded key set (fingerprinted, rotation-capable, `ARCHITECTURE-DESIGN.md:358`) — **before parsing or downloading anything else** (`SPIKE-02:29` step 3). Fail → quarantine version, diag entry, keep active (S07 PASS).
4. Parse manifest → **compatibility** (`appCompatibility.minAppVersion/maxAppVersion` ∧ `schemaVersion ∈ shell.supportedRange` — `SPIKE-04:22`, `ARCHITECTURE-DESIGN.md:688`) and **budgets** (per-file ≤6 MB, totals, image caps) — before downloading files. Fail → user-visible "please update app" if app too old (`ARCHITECTURE-DESIGN.md:668`), else quarantine.
5. **Stage files** into inactive slot; per-file SHA-256 + size check on each (`SPIKE-02:29` step 5).
6. **Schema validation** of staged sections (strict on required, allow unknown forward-compat fields, `SPIKE-04:189` gate 1).
7. Full verification of all staged files → only then eligible to activate.
No step may be skipped, reordered, or weakened without ADR amendment. Every rejection is quarantined so a known-bad version is not re-fetched until `latest.json` advances (`ARCHITECTURE-DESIGN.md:675`).
# 12. Dataset activation rules (normative, ADR-006 + SPIKE-02 F-2/F-3)
- Activation is **exactly one transaction** on `lumen-system` (P2) that **atomically** flips `activeSlot` + `activePackageVersion` + **verification record** (F-2 `SPIKE-02:89`) and sets `readbackPending`. No cross-database transaction is attempted — single-DB atomicity suffices because inactive slot is already complete before flip (`SPIKE-02:73`).
- On txn failure: retry once → keep old → diag (`ARCHITECTURE-DESIGN.md:672`).
- After commit: **readback spot-check** synchronously if possible, otherwise left pending and checked at next boot (`SPIKE-02:97` F-3). Spot-check reads the slot the pointer now points at.
A partially staged, un-verified, quarantined, or incompatible dataset can never be activated (invariants §3 #4–#5).
# 13. Dataset rollback rules (normative, `ARCHITECTURE-DESIGN.md:677` §18.3)
- **Automatic:** post-activation readback fails, or next-boot light verify of active slot fails while the other slot is still complete → flip back to previous slot (`SPIKE-02:50` S14, X1). If both slots lack a complete dataset → RECOVERY + baseline (`SPIKE-02:69` X2) — favorites in `lumen-user` remain.
- **User-initiated:** Status screen "Restore previous version" while previous slot exists (until next staging or GC).
- **Organizer-initiated (canonical):** republish old content at a **new higher packageVersion** (runbook, `ARCHITECTURE-DESIGN.md:682`) — clients accept it as a normal monotonic update. Local rollback is depth-1 only (F-4); server-side re-publish is the general rollback.
GC: previous slot retained until next staging needs it or `N≥3` successful boots AND storage pressure (`ARCHITECTURE-DESIGN.md:685`). User data never GC'd.
# 14. Emergency baseline rules (normative, ADR-007 + SPIKE-06)
Three tiers — no tier may be omitted or merged silently:
| Tier | Content | Mutability | Delivery | Survives |
|---|---|---|---|---|
| T1 Floor | Emergency number + dial, address/coordinates, security contact, first-aid/AED summaries, muster/exits summaries, core procedures (`medical`/`weather`/`fire`/`lost`) — ≤16 KB, life-safety minimum only | Immutable per shell build, compiled from the **same emergency source sheet** that produces the dataset section (no drift, `ARCHITECTURE-DESIGN.md:345`) | Shell bytes | **Everything** — eviction, failed update, incompatible dataset, BASELINE_ONLY |
| T2 Dataset section | Full detail (all emergency POIs with map links, full procedure text, per-role contacts, `contentVersion`/`updatedAt`, `emergencySchemaVersion`) | Updateable via signed dataset publish | Package `emergency.json` | Last-good dataset |
| T3 Notices (reserved) | `{id,severity,title,body,publishedAtUtc,expiresAtUtc,signature}` — display-only, additive | Ephemeral, signed, expiring | Package or transport seam — **deferred in V1** | Only if delivered |
Resolution `SPIKE-06:60`: prefer highest verified compatible tier with defensive merge (floor-only fields rendered even when section omits them); provenance always labeled (`BASELINE v<floor>` vs `FESTIVAL DATA v<package> · <generatedAt>`). Floor renderer is **forward-tolerant** (ignores unknown fields, never throws) and reachable with **zero IDB access** (F-1 `SPIKE-06:75`) — this is what makes invariants §41 #1–#2 hold.
Hard limits: 16 KB cap CI-enforced; floor version stamp validated (`ARCHITECTURE-DESIGN.md:367`). No `localStorage` copy — dies with eviction (`SPIKE-06:102`).
# 15. Festival Data Package rules (normative, ADR-005 + SPIKE-04)
An immutable versioned directory on the CDN:
```
Package /editions/<edition>/packages/<packageVersion>/
manifest.json → inventory, hashes, sizes, compatibility, festival metadata
signature.json → Ed25519 over sha256(exact manifest bytes) + publicKeyFingerprint
emergency.json → emergency/1 (+ independent emergencySchemaVersion/contentVersion)
schedule.json → schedule/1 (stages, artists, events with stable id, dayKey)
map.json → map/1 (levels + POIs normalized x/y + categories)
info.json → info/1 (ordered blocks of structured nodes)
assets.json → inventory {id,file,sha256,bytes,kind,role}
assets/... → WebP/PNG binaries, each listed in assets.json
/editions/<edition>/latest.json → mutable pointer {edition,packageVersion,manifestUrl,generatedAt}
```
Normative schema fragments: `SPIKE-04:79` (manifest, signature, emergency/schedule/map/assets). Rules:
- Format `lumen.package/1` (`SPIKE-04:79`).
- `packageVersion` strictly monotonic per edition; never accept `≤ active` from network (`ARCHITECTURE-DESIGN.md:349`). Organizer rollback = new higher version.
- `schemaVersion` envelope with ordering rule `ARCHITECTURE-DESIGN.md:690` C-23: schemas ship in shell range `{1,2}` first, datasets follow, freeze 7 days before festival.
- `sections[*].required` bool (default true) gates readiness (`SPIKE-04:64`, `SPIKE-05:47` F-2).
- **No expiration** for datasets/sections (`SPIKE-04:50` F-2) — only future announcements/notices may carry `expiresAtUtc` and expiry hides the notice, never critical data (time-independence, `SPIKE-05:47`).
- No binary sections (F-5 `SPIKE-04:203`); per-file ≤6 MB; total ≤40 MB target / 50 MB ceiling `ARCHITECTURE-DESIGN.md:363` enforced by pipeline.
- Pipeline gates (publisher side, all blocking): 1) schema + unknown-fields forward-compat, 2) stable IDs (removals require `status: cancelled`), 3) time sanity (`dayKey` matches festival-zone date, no zero-length, within window ±1d), 4) budgets/dimensions, 5) hash→sign→upload→flip→smoke-fetch (`SPIKE-04:189`).
Identity vs location: `edition + packageVersion` and `sha256(manifest)` are identity; URL is location (`SPIKE-04:15`).
# 16. Service worker responsibilities (normative, `ARCHITECTURE-DESIGN.md:241`)
Scope deliberately tiny (`ARCHITECTURE-DESIGN.md:243`):
- `install`: precache versioned shell list (generated at build, hashed `index.html` + JS/CSS + icons).
- `activate`: delete previous shell caches.
- `fetch`: navigation → cache-first against shell cache, fallback to network to re-fill; if both fail, serve built-in static fallback page (emergency text, Ring-0). Hashed `/assets/<hash>.*` → cache-first. **Package endpoints (`/latest.json`, `/editions/**`) are NOT intercepted — pass through to network so app-layer sync controls staging (`ARCHITECTURE-DESIGN.md:243` C-21).**
- `message`: minimal `SKIP_WAITING` handling only.
Shell update policy `ARCHITECTURE-DESIGN.md:255`: new SW installs under new cache name; serves old session; activation on **next full app start** (no mid-session `clients.claim`), except explicit user "Restart to update" on Status screen. Then §18.5 compatibility check (`ARCHITECTURE-DESIGN.md:688`) runs before data screens.
Every SW operation is idempotent/re-runnable; page never depends on SW in-memory state (`ARCHITECTURE-DESIGN.md:261`).
# 17. Service worker non-responsibilities (normative)
The SW **must not** do any of: dataset download/staging, background sync, retry logic, announcements, anything stateful beyond caches (`ARCHITECTURE-DESIGN.md:253`), any IDB access, any verification. Dataset data must never live in SW state — P8 `SPIKE-01:140` + C-21 `ARCHITECTURE-DESIGN.md:83`. A coding agent that moves staging into the SW violates invariants §41 #3–#4.
# 18. Cache Storage responsibilities
- Owns **shell only**: `lumen-shell-v<build>` versioned per build, enumerated in readiness predicate C1 (`SPIKE-05:28`).
- Immutable hashed assets cached with long TTL; `index.html` with `no-cache` (revalidate) per deploy layout `ARCHITECTURE-DESIGN.md:807`.
- Old caches purged on SW activate — no manual GC.
# 19. IndexedDB responsibilities
- Owns **datasets + user state** as in §9; plus `lumen-system.meta` as the atomic activation/verification record.
- Enforces P1–P5: single-file atomic, single-txn flip + verification, 6 MB cap, pre-stage free-space check, wrapped Quota handling.
- Boot light verify C1…C5 is the detection mechanism for eviction/corruption; heavy hashing lives only in full verification (after staging, user-initiated "Check my data", after IDB error) per `SPIKE-05:82` cadence.
# 20. Offline Ready state machine (normative, SPIKE-05 + §12)
## 20.1 Predicate (time-independent — `SPIKE-05:47` F-1)
```
OFFLINE_READY ⇔
C1 shell_valid every precache entry present in current shell cache
∧ C2 dataset_present active slot exists for active edition
∧ C3 authenticity manifest signature verified against embedded key set; recorded verification matches active packageVersion
∧ C4 integrity manifest-listed files present, sizes match; hashes verified at staging (recorded), spot-check at boot
∧ C5 schema_compatible package schemaVersion ∈ shell.supportedRange ∧ appCompatibility satisfied
∧ C6 required_sections sections with required=true present+parseable {emergency,schedule,map,info,assets.json}
∧ C7 required_assets assets referenced by required sections present (size at boot, hash at staging)
∧ C8 emergency_floor embedded baseline present (asserted; defense against broken build dropping floor)
```
Rules: predicate never consults clocks (`SPIKE-08` parity); optional sections (`required=false`) never gate READY (`SPIKE-05:47` F-2); evidence-based via stored verification record (`SPIKE-02:89` F-2) — missing/mismatched record → no READY until full re-verify; all-or-nothing for READY.
## 20.2 States (six-state taxonomy, `SPIKE-05:60`)
| State | Definition | Chip | Action offered |
|---|---|---|---|
| READY | C1–C8 all true | Green "OFFLINE READY ✓" | None (status detail on tap) |
| PARTIAL(list) | Shell valid; required sections/assets missing (none corrupt) — e.g., prep interrupted | Amber | "Continue setup" (resumable staging) |
| NOT_READY | Shell valid; no staged/active dataset (fresh install) | Neutral "Get festival data" | Preparation flow |
| RECOVERY(reason=`missing`/`corrupt`/`incompatible`) | Previously verified now fails (eviction, corruption, app/dataset skew) | Red | "Restore festival data" (re-prep; needs online) — floor works meanwhile |
| BASELINE_ONLY | Storage entirely unavailable (private mode, blocked "website data", quota 0) | Distinct notice | Install/normal-mode/free-space guidance |
| FAILED(op) | Activation txn repeatedly failed or IDB error — distinct from RECOVERY (data gone vs error) | Error screen | Retry; re-prep; diagnostics share |
Edge cases adjudicated `SPIKE-05:90`: eviction→RECOVERY, file-gone→RECOVERY, record-missing→PARTIAL→full re-verify, incompatible→RECOVERY(incompatible), wrong clock→READY unaffected (warning separate), optional missing→READY, private→BASELINE_ONLY, activation fail twice→FAILED.
## 20.3 Cadence and budgets
| When | Check | Cost target |
|---|---|---|
| Every boot | Light check C1+C2+C4-light+C5 (no hashing) | ≤~150 ms `SPIKE-05:82` |
| After staging | Full C3–C7 (all hashes + schema) | Seconds; once per update |
| Activation | Recorded with flip; readback spot-check (pending flag) | ms |
| User "Check my data" | Full re-verification with progress | Seconds |
| After any IDB error | Full re-verify active slot; quarantine on failure | Seconds |
## 20.4 Preparation mapping (SPIKE-07)
L0 (shell+floor) ↔ NOT_READY/BASELINE_ONLY; L1 (L0 + emergency + schedule) ↔ PARTIAL ("core ready"); L2 (L1 + map+info+assets) ↔ READY (`SPIKE-07:31`, `SPIKE-05:108` cross-check). Download order `emergency→schedule→info→map-base→assets` maximises achievable level when interrupted (`SPIKE-07:39`).
## 20.5 UX contract
Status chip visible on every screen; Status screen shows per-section checklist, dataset version, `generatedAt`/`fetchedAt`, usage, plus "Check my data" / "Get festival data" / "Restore previous version" (`ARCHITECTURE-DESIGN.md:424`). App never shows READY unless predicate holds.
# 21. Schedule / time rules (normative, ADR-009 + SPIKE-08)
- **Storage:** `startUtc`/`endUtc` UTC epoch ms on every event (`DISCOVERY.md:297` A-10, ADR-009). Includes `dayKey` precomputed at publish as festival-zone calendar date (`SPIKE-08:43` T2).
- **Zone:** manifest carries `festival.timezone` (IANA) + `festival.startUtc/endUtc` (`SPIKE-04:79`).
- **Rendering:** `Intl.DateTimeFormat` with `timeZone = festival zone` by default; toggle to device zone. **Only** explicit-zone Intl is allowed (clarification F-4 `SPIKE-08:148`); never `Date.toLocaleString()` without options, manual offsets, or wall-clock strings. `dayKey` groups days; no client day math.
- **Queries (§14.2 `ARCHITECTURE-DESIGN.md:508`):**
- `Now: startUtc ≤ now < endUtc`; `Up Next: startUtc > now` (next-N sorted). Overlaps shown as multiple `now` (`SPIKE-08:43` T7).
- Favorites join via stable event `id` (contractual — §24); `status: scheduled|moved|cancelled` rendering.
- Filters/search: naive scan over ≤~1k events; no index lib unless proven slow (`DISCOVERY.md:391` TQ-10 decision documented, `ARCHITECTURE-DESIGN.md:508`).
- Change markers when `status=moved|cancelled` or manifest change notes.
- **ClockService (`ARCHITECTURE-DESIGN.md:523`, `SPIKE-08:26`):**
```
now() = deviceClock + skew if persisted offset exists else deviceClock
skew = serverNow − deviceNow captured from any sync HTTP Date header, persisted {skew,capturedAtDevice,capturedAtMono,source}
drift = |observedDevice − (base + monotonicElapsed)| > 2 min → warn + re-derive base
sanity = now within festival window ±45d → else warn, but ONLY if now ≥ festivalStart−45d or skew was captured (F-3 suppression SPIKE-08:97) → READY unaffected
```
No NTP endpoint, no timers, no background work (C-19); computed on demand (`SPIKE-08:43` T5/T6). HTTP `Date` is NTP-synced CDN source, INFERRED accurate (`SPIKE-08:84` B-10).
- **DST/unusual zones:** Rendering inherits IANA database; epoch arithmetic correct across spring-forward/fall-back (`SPIKE-08:43` T3/T4 validated 24/24 on V8/ICU; JSC parity UNVERIFIED queued for device T15 `ARCHITECTURE-VALIDATION.md:301`).
# 22. Map architecture (normative, ADR-008 + SPIKE-03 + §15)
Decision: **Raster WebP base (≤2 levels) + data-driven DOM POI overlay + CSS-transform pan/zoom + first-class Facilities list** (`ARCHITECTURE-DESIGN.md:543`). No tiles, no SDK, no GPS in V1.
- **Levels:** `overview` longest edge ≤1600 px (~0.3–0.7 MB encoded, ~7.3 MB decoded), optional `detail` ≤3072 px (~1.5–3.5 MB encoded, ~27 MB decoded), total decoded ≤~35 MB, **at most one detail resident** (overview swapped out/downscaled when detail shown) — F-1 `SPIKE-03:58` replaces prior 4096 cap (`ARCHITECTURE-DESIGN.md:571`). Per-file ≤6 MB cap applies.
- **Levels in `map.json`:** `SPIKE-04:157` — `levels: {overview assetId width height, detail assetId width height nightAssetId?}` with `nightAssetId` optional hook for F-4 dark mode. Alternatives SVG/Canvas/tile-pyramid rejected per matrix `SPIKE-03:11`.
- **Coordinate space:** POIs normalized `x:0..1, y:0..1` relative to base image (`ARCHITECTURE-DESIGN.md:561`, D-02). Authorship via tap-tool or measured coords; optional `lat/lng` hook preserved but unused in V1 (invariant 9).
- **Pan/zoom:** pointer-events + pinch → single container `transform: translate() scale()`; markers counter-scaled constant on-screen; bounds clamped (`ARCHITECTURE-DESIGN.md:561`).
- **Interactivity/accessibility:** POIs are real `button`s with labels and 48 px targets; Facilities list is first-class, not fallback, and is the screen-reader path (`SPIKE-03:79`). Search shares schedule component (highlight + list). Category chip filtering via class toggles, no re-layout of base.
- **Dark mode (F-4 `SPIKE-03:88`):** default `filter: brightness(.72) saturate(.85)` on base in dark, markers full-brightness (GPU-composited); optional `map-base/overview-night` asset — no white flash, no re-download.
- **Object URLs (F-3 `SPIKE-03:68`):** lazy-create per visible level; revoke on switch — leak freedom is part of acceptance.
- **Flip conditions:** vector art supplied → reconsider S1/S5; art >3072 px legible → 2×2 tile-split (DD-9, `SPIKE-03:98`); fps jank low-end → overview-only fallback.
- **Failure behavior:** map section missing → list view if POIs present, else "Map not downloaded" card (`ARCHITECTURE-DESIGN.md:568`).
# 23. Festival information architecture
- `info.json` ordered blocks `{id,title,kind,body: structured nodes}` where nodes ∈ {paragraphs,lists,links,emphasis,contact,address,hours} (`ARCHITECTURE-DESIGN.md:572`). **No raw HTML (C-22), no `innerHTML` of raw strings — renderer maps node types → safe DOM** (`ARCHITECTURE-DESIGN.md:573`, ADR-013 TH-4).
- Categories data-driven, reorderable: About, Rules, FAQ, What to bring/Not, Parking, Camping, Transport, Accessibility, Food/Drink, Merch, Activities, Contacts, Hours, Venue (`ARCHITECTURE-DESIGN.md:573`).
- Search indexes titles + text via shared search component.
- Signed as required section; readiness gates on it (`SPIKE-04:30`); BASELINE_ONLY excludes it — floor is what must survive, per `ARCHITECTURE-DESIGN.md:573` static-important note.
# 24. User preferences / favorites architecture (normative, `DISCOVERY.md:545` §15.8, `SPIKE-04:154`)
- Store: `lumen-user.favorites` keyed by **stable event id** (`SPIKE-04:154` — contractual across versions). **Never part of package**; never overwritten by updates; survives rollback and GC (B-6, `SPIKE-02:69` X3 PASS).
- Event model `SPIKE-04:140`: `{id stable, title, stageId, artistIds[], startUtc, endUtc, dayKey, tags[], status: scheduled|moved|cancelled, originalStartUtc?}` — publisher must not churn ids; pipeline mints via deterministic key if source lacks ids (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:74` D-01, AQ-20). Removals require `cancelled` rather than deletion (`SPIKE-04:189` gate 2).
- On dataset update: event moved → keep fav with new time; `cancelled` → mark; removed → orphan policy (show as "no longer scheduled" but keep fav entry, do not crash).
- Favorites: write-through persistence, no memory-only truth (`DISCOVERY.md:456` OF-11). Includes overlap conflict surfacing (`ARCHITECTURE-DESIGN.md:508`). Duplicated export later (DD-4).
- `prefs` (in `lumen-user`) includes theme/clock `displayMode` toggles; clock skew cache reference per §21. Small flags in `localStorage` only for non-load-bearing coach dismissed etc., guarded `try/catch` (ADR-004).
# 25. Networking abstraction (normative, ADR-011 + §17.2)
```
Application (features) ──→ reads DatasetStore; sees readiness — nothing else
Data/Domain ──→ DatasetStore, Verifier
Sync layer ──→ SyncService: orchestrate(check→stage→verify→activate)
Transport interface isAvailable() / fetchPointer(edition) / fetchBytes(path) byte-oriented, tiny
Transport impls ──→ HttpTransport (V1, the only one) / future transports implement same iface
Verifier ──→ hashes + Ed25519 (sole decider)
Service worker ──→ shell cache + navigation fallback — exclusive
```
Rules: features never import sync/transport (B-3); transports move bytes, Verifier decides truth identically for every transport (signed-payload rule, ADR-012); seam is one interface + one impl — no registry/plugin/negotiation (`ARCHITECTURE-DESIGN.md:628`). SyncService is transport-parameterized for fault-injection testing (`ARCHITECTURE-DESIGN.md:628`).
# 26. V1 networking limitations (normative, C-19 + ADR-010)
- Triggers: **on app open**, **on `online` event (opportunistic hint)**, and **manual "Update now"** (`ARCHITECTURE-DESIGN.md:584` §17.1). Nothing runs while app hidden; no timers, no periodic sync.
- Model: **pull-only**. Only operations: `fetch latest.json` → monotonic comparison → `fetch candidate manifest` → verify → stage. No uploads, no user-state sync, no telemetry, no push registration, no Background Sync registration (`ARCHITECTURE-DESIGN.md:637` §17.4).
- Announcements schema reserved but not implemented unless demanded during build (DD-1, `ARCHITECTURE-DESIGN.md:631` §17.3). Emergency tier-T3 notices are reserved with that shape.
- Rejected: periodic background sync (unsupported on iOS PW-4), push-based updates (iOS fragility), WebSocket/long-poll (no V1 need) (`ARCHITECTURE-DECISIONS.md:406`).
- Latency = time-until-next-open-online is the accepted trade-off, documented and communicated via `generatedAt`/`fetchedAt` and changed-event markers.
# 27. Future mesh boundary (normative, ADR-012 + DISCOVERY §17.1/§20)
- V1 does three things for future mesh and then stops: 1) Transport seam `§25`, 2) **signed-payload rule** — bytes from any transport are inert until Verifier passes them, so untrusted peers can never inject content (ME-4 `DISCOVERY.md:596`, `ARCHITECTURE-DESIGN.md:718` §20.3), 3) package/announcement formats immutable, content-addressed, version-monotonic (`ARCHITECTURE-DESIGN.md:717` §20.1–2). Cost in V1 is ~one small module (`ARCHITECTURE-DECISIONS.md:482`).
- Feasibility posture: meaningful mesh will require something outside the browser (native companion, hardware relay, organizer LAN) because mobile browsers expose no BLE peripheral, ad-hoc Wi-Fi, background sockets, and iOS lacks Web Bluetooth (`DISCOVERY.md:581`, `ARCHITECTURE-DESIGN.md:717`). A future transport attaches as e.g. `MeshTransport` bridging to a companion over a browser-accessible channel, or a LAN mirror acting as a package source. **If even that proves impossible, V1 loses nothing — HTTP is complete product functionality** (`ARCHITECTURE-VALIDATION.md:475` §6 scenario 20).
- **Deferred to future work (not V1):** peer discovery, routing, store-and-forward, dedupe/replay beyond monotonic, peer auth, message-size adaptation, emergency broadcast semantics, conflict handling beyond version monotonicity (`ARCHITECTURE-DESIGN.md:718` §20.4).
- Anti-leak: no feature/UI code may reference transport identity, connectivity modality, or peer concepts; Status UI shows "updated <when>", never "via what" (`ARCHITECTURE-DESIGN.md:718` §20.5).
# 28. Security requirements (normative, ADR-013 + TH-1…TH-11 `ARCHITECTURE-DESIGN.md:728`)
| Threat | Defense (must implement) |
|---|---|
| TH-1 tampered/malicious package (MITM, compromised hosting, `DISCOVERY.md:616` SE-1) | Ed25519-signed manifest (SHA-256 of exact manifest bytes) + per-file SHA-256+size (`ARCHITECTURE-DESIGN.md:358`), plus HTTPS+HSTS (`ARCHITECTURE-DESIGN.md:743`), single origin C-20; activation impossible without valid signature (ordering `§11`) |
| TH-2 publish path compromise | Signing key offline; singular publisher + deputy (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:60` O-01); repo audit trail; smoke re-verify; rollback via new version |
| TH-3 malicious/stale emergency | Emergency section signed; baseline immutable per build + same-source; provenance labels on every screen (`SPIKE-06:60`) |
| TH-4 XSS/content injection | C-22 structured-data rendering only; CSP `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' blob: data:; connect-src 'self'` (`ARCHITECTURE-DESIGN.md:743`); no inline script, no `eval`, no `innerHTML` of raw |
| TH-5 replay/downgrade | Monotonic `packageVersion` acceptance; never accept ≤ active from network (quarantine) |
| TH-6 DoS oversized | Budgets in manifest validated before download + per-file 6 MB caps + quota pre-check (`ARCHITECTURE-DESIGN.md:358`, `SPIKE-01:140` P4) |
| TH-7/TH-8 privacy/storage | No accounts, no telemetry, no location capture; no secrets stored; dataset public; favorites innocuous (data minimization `ARCHITECTURE-DECISIONS.md:524`) |
| TH-9 SW/cache poisoning | Single origin, same-origin caches, no CORS relied upon |
| TH-10 mesh injection | Signed-payload rule §27 — mesh auth deferred but rule enforced from V1 |
| TH-11 supply chain | Minimal deps (target zero beyond audited verifier), lockfiles, CI, pin |
Platform hardening `ARCHITECTURE-DESIGN.md:743`: HTTPS+HSTS, no mixed content, no third-party scripts/fonts/analytics, `tel:` data-driven + explicit tap only (A-14), key set (fingerprinted) for rotation, lost-key runbook (ship new set via shell).
Device matrix must verify `tel:` in standalone (T19, `SPIKE-01:171`) and CSP allows blob object URLs for map assets (`ARCHITECTURE-DESIGN.md:743` `img-src blob:` — must include).
# 29. Privacy requirements (normative, ADR-013 + NR-15)
- No accounts, no auth, no session/token (NR-4).
- No telemetry/ analytics collection by default (NR-15, `ARCHITECTURE-DECISIONS.md:524` 4): diagnostics are on-device, scrubbed, and user-initiated — manual "share diagnostics" only, no automatic upload.
- No location capture in V1 (`ARCHITECTURE-DESIGN.md:222` zero-permission).
- No PII in content or favorites (favorites = `public event id` list, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:87` S-03).
- Data minimization as a rule (`ARCHITECTURE-DECISIONS.md:524`).
# 30. Error handling requirements
- Every failure maps to a defined renderable state — **no-blank-screen rule** `ARCHITECTURE-DESIGN.md:775` §22. Contract in §20.2 + §31.
- IDB errors (`QuotaExceededError`, txn abort) are caught, wrapped, and keep active dataset — never crash (`SPIKE-01:140` P3, `ARCHITECTURE-DESIGN.md:672` retry once).
- Network errors (fetch pointer/manifest timeout) are silent no-ops; retry next trigger (`ARCHITECTURE-DESIGN.md:664`).
- Hash/signature/schema mismatches → quarantine + diag, never activate (`ARCHITECTURE-DESIGN.md:667`).
- `localStorage` guarded `try/catch` (non-load-bearing) per ADR-004.
- Renderer forward-tolerant for floor path; dataset content unknowns tolerated (`SPIKE-06:75`).
# 31. Recovery requirements (normative, `ARCHITECTURE-DESIGN.md:755` §22 + SPIKE-05/02)
Behavior matrix is the contract (`ARCHITECTURE-DESIGN.md:755` FA-1…FA-16). Coding agent must implement verbatim:
FA-1 offline startup / FA-2 browser restart / FA-3 phone restart → boot from `caches`+IDB, light verify, budgets `§33`. FA-4 SW killed → page unaffected (C-21). FA-5 evicted → RECOVERY + floor + one-tap re-prep (needs online to re-prep). FA-6 corrupt → quarantine/rollback `§13` + baseline. FA-7 unavailable → BASELINE_ONLY. FA-8 interrupted → resume `§10`. FA-9 invalid package published → reject+quarantine, client-side no online needed to reject. FA-10 wrong clock → corrected or warned per `§21` (F-3). FA-11/12 GPS/low battery → no degradation. FA-13 schedule changed → update on next online, last-good meanwhile. FA-14 emergency unreachable → T1/T2 remain + physical channels (A-16). FA-15 browser update → baseline targets conservative. FA-16 shell mid-festival → next-start activation + §18.5 compatibility check.
Every row ends renderable; Ring-0 static `fallback.html` covers shell-cache-missing case (`ARCHITECTURE-DESIGN.md:775`).
# 32. Accessibility requirements (normative, WCAG 2.1 AA per `DISCOVERY.md:334` AMB-10, `ARCHITECTURE-DESIGN.md:783`)
- Landmarks/heading order per view; visible focus.
- POIs are real `button`s/links (not canvas hit-testing); Facilities list is the primary screen-reader path (`SPIKE-03:79`).
- Emergency: giant targets, max contrast both themes, zero clutter, one-handed reachable (`ARCHITECTURE-DESIGN.md:783` + `DISCOVERY.md:470` EM-5); `tel:` with selectable-text copy fallback.
- Touch targets ≥48 CSS px (`ARCHITECTURE-DESIGN.md:783`).
- `prefers-reduced-motion` honoured (no vestigial autoplay).
- High-contrast light + true dark themes; map handles F-4 dark filter without breaking markers (`SPIKE-03:88`).
- In-app back affordances (standalone safe), history-API back (`ARCHITECTURE-DESIGN.md:783`).
- Screen-reader pass required on device T13 `ARCHITECTURE-VALIDATION.md:283` using VoiceOver/TalkBack.
Do not weaken this for convenience — `§40` fails if a11y is degraded to save code.
# 33. Performance budgets (hard ceilings, CI-enforced — `ARCHITECTURE-DESIGN.md:848` §27 + `ARCHITECTURE-DESIGN.md:363` §10.6)
| Part | Target / Ceiling | Enforcement |
|---|---|---|
| Shell JS (gz) | ≤150 KB | Bundler gate (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:32` AQ-10) |
| Shell total (JS+CSS+icons, gz) | ≤1 MB | Same |
| Emergency floor | ≤16 KB | Build asserted (`ARCHITECTURE-DESIGN.md:367`) |
| Sections JSON total (emergency+schedule+map.json+info+assets.json) | ≤3 MB | Pipeline gate `SPIKE-04:189` + client budget check |
| Map base images (all levels) | ≤28 MB (overview ≤1600 px / detail ≤3072 px, total decoded ≤~35 MB) | Pipeline + client `§22` |
| Other assets (icons, etc.) | ≤6 MB | Same |
| Total dataset | ≤40 MB target / **50 MB hard ceiling** (`DISCOVERY.md:297` A-02) | Pipeline reject above 40 MB; client refuses above 50 MB |
| Single file | ≤6 MB | Hash/digest memory bounded `SPIKE-01:140` P5 |
| Cold start → Emergency usable (D3 cached, mid-range 2021 Android) | ≤2 s | Device matrix T06 |
| Cold start → interactive | ≤3 s | T06 |
| Schedule list (1k events) | ≤100 ms interaction; window rendering if needed | Unit/perf |
| Map pan/zoom | ≥30 fps | T13 |
| Boot light verify | ≤150 ms | T03 |
Tactics: vanilla JS parse trivial, WebP, single JSON parse per section per session, no idle timers/observers, zero background work (`ARCHITECTURE-DESIGN.md:858`).
Silently increasing any budget violates `§40`.
# 34. Browser support (normative, `ARCHITECTURE-DESIGN.md:819` §25)
| Dimension | Baseline (must support) | Best-effort | Explicitly unsupported |
|---|---|---|---|
| iOS Safari | 16.4+ installed PWA | 16.0–16.3 tab (eviction-prone, coach pushes install) | <16 |
| Android Chrome | ~110+ (including 2021 mid-range) | Older with SW+IDB | Without SW |
| Other (Samsung Internet, Firefox Android) | Where SW+IDB exist | Same code paths | — |
| Desktop | Works via same code | Not a target | IE, legacy |
| JS disabled / storage blocked | Static `fallback.html` with emergency text (Ring-0) | — | Full app |
Capability detection (not UA sniffing): `'serviceWorker' in navigator`, `indexedDB`, `caches`, `Intl.DateTimeFormat`, `navigator.storage.estimate`. Missing → degrade per `§20`/`§31` (never crash) `ARCHITECTURE-DESIGN.md:829`.
# 35. Testing requirements (normative, `ARCHITECTURE-DESIGN.md:832` §26)
| Layer | What | How |
|---|---|---|
| Pipeline / schema / contract | Package schema, manifest, budgets, stable IDs, dayKey/time sanity (≤40 MB, ≤6 MB/file, dimension caps, no-zero-length, `dayKey` matches zone), forward-compat unknown fields | Pipeline validation + golden-package fixture tests |
| Unit | ClockService (skew/drift/sanity + F-3 suppression), Verifier (bad sig, bad hash, replay/lower version, quarantine), readiness predicate C1–C8 (`SPIKE-05:28`), A/B state transitions (14+3 scenarios), escaping renderer, forward-tolerant floor | Standard unit suite |
| Integration (sync) | Full update lifecycle including all 9 failure stages `ARCHITECTURE-DESIGN.md:664` §18.2 — mocked transport + fault injection (kill mid-stage, corrupt bytes, quota errors); readbackPending + rollback; favorites survive (X3) | Scripted harness modeled on `exp2-ab-update-sim.mjs:1` |
| PWA audits | Manifest/SW/offline load | Lighthouse PWA + scripted offline reload |
| Content | Emergency provenance stamps, dataset age/staleness, wrong-clock warnings (absolute times always shown) | Scenario tests |
| Accessibility | Emergency/Schedule/ Facilities list screen-reader flows + contrast both themes + 48 px targets | Automated + manual (VoiceOver/TalkBack) — must run on devices T13 |
Rule: any bug found on a real device that harness missed becomes a harness case (`ARCHITECTURE-DESIGN.md:843`). Every readiness state `§20.2` must be reachable in tests (`ARCHITECTURE-DESIGN.md:930` rule 5).
# 36. Physical-device validation requirements (blocking for production)
Required to declare storage/map/time/SW production-ready. Defined in `ARCHITECTURE-VALIDATION.md:301` §4 (19 groups × 4 devices, 76 device-tests) and queued throughout spikes as UNVERIFIED.
Device families: D1 iPhone 16.4 low-bound (tab + installed), D2 iPhone latest installed, D3 low-end Android 2021 mid-range Chrome ~110+ (primary perf), D4 modern Android Chrome latest (`ARCHITECTURE-VALIDATION.md:158`).
Minimum required before a real festival: T01 first visit → T02 install → T03 offline launch → T04 airplane simulation → T05 browser restart → T06 phone restart → T07 `persist()`/exemption → T08 storage pressure → T09 happy update → T10 interrupted updates (kill at S01–S14 points `SPIKE-02:50`) → T11 recovery (eviction/corrupt/private/incompatible) → T12 map rendering (1600/3072/35 MB) → T13 map interaction (≥30 fps, leaks, a11y) → T14 GPS absence → T15 incorrect clock (5 steps per `SPIKE-08:152`, including F-3 near-festival gating) → T16 SW lifecycle (C-21) → T17 shell update (next-start + §18.5) → T18 dataset compatibility → T19 emergency floor in every failure state.
Severity per `ARCHITECTURE-VALIDATION.md:171`: T01,T03–T06,T08–T13,T16–T19 BLOCKING; T02 BLOCKING on iOS; T07/T14/T15 graded HIGH/MEDIUM as marked. Device matrix is **BLOCKING for production** but does not block starting implementation on fixtures (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:138`, `ARCHITECTURE-VALIDATION.md:493`).
# 37. Deployment assumptions (normative, ADR-014 + DISCOVERY AMB-4)
- Static HTTPS CDN, **single origin** (`DISCOVERY.md:261` C-20, `ARCHITECTURE-DESIGN.md:88`) with layout `/` (`no-cache`) + `/assets/<hash>.*` (immutable 1y) + `/latest.json` (`max-age 60`) + `/editions/<ed>/packages/<v>/**` (immutable 1y) `ARCHITECTURE-DESIGN.md:807`.
- Two environments: `staging` origin + `production` origin, same code, different origins (storage isolation automatic) — `ARCHITECTURE-DESIGN.md:817`.
- No dynamic backend, DB, or auth endpoint in V1; publisher auth at pipeline/CDN credential layer (`ARCHITECTURE-DESIGN.md:815`).
- Origin is hard to reverse post-users (origin-keyed storage strands data) — choose production origin deliberately before any public staging link (AQ-18 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:116`); provisional staging origin is fine for development.
- Provider choice (AQ-14 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:112`) and budget (OQ-8) are **provisional** — pick before public staging share, keep stable.
- Publisher runbook: publish → smoke re-verify → monitor `latest.json`; rollback runbook: republish old content at new higher `packageVersion`; emergency runbook gated by sign-off (`ARCHITECTURE-DESIGN.md:697`).
# 38. Content / data assumptions (normative, ADR-005/014 + ASSUMPTIONS §5)
- Single annual edition (A-01 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:15`), keyed `edition` for cheap multi-event later; budgets `§33`.
- Organizers can supply schedule/map art/POIs/emergency/info digitally (`DISCOVERY.md:297` A-04, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:73` A-04) via intake templates (CSV/JSON + raster art) or existing CMS adapter must stay import-friendly (AMB-2 `DISCOVERY.md:324`).
- Event/POI IDs stable (`SPIKE-04:189` gate 2, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:74` D-01) — pipeline mints deterministically if source lacks ids and persists mapping (AQ-20); favorites depend on it.
- Map art: raster with known dimensions, POIs locatable normalized `x/y` (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:75` D-02); if only vector/GIS arrives, representation flip `SPIKE-03:98` DD-9.
- Emergency contacts/coordinates valid through event, provenance `contentVersion`/`updatedAt` + `emergencySchemaVersion` (`SPIKE-04:70`, `SPIKE-06:110`); sign-off gate O-02 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:62`.
- Single festivals IANA zone (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:78` D-05); multi-zone would amend ADR-009.
- Volumes ≤1k events / ≤200 POIs / ≤30 info blocks within budgets (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:77` D-04).
- Offline LAN/mesh update delivery not in V1 (A-09).
- **Provisional until real content arrives:** map art quality, exact schedule sample; pipeline gates prove but cannot pass without samples (`ARCHITECTURE-VALIDATION.md:501`).
# 39. Feature acceptance criteria (normative per DISCOVERY requirements)
| Area | Requirement | Must hold |
|---|---|---|
| Platform | R-P1…P4 `DISCOVERY.md:132` | Mobile-first PWA, iOS Safari + Android Chrome; HTTPS; no native; desktop only incidental. |
| Navigation | R-N1…N4 `DISCOVERY.md:143` | 4 destinations; nav obvious; Emergency one tap from anywhere (persistent); one-handed 48 px, high-contrast day/night. |
| Emergency | R-E1…E5 `DISCOVERY.md:152` | All tiers; `tel:` from standalone (`SPIKE-01:171`); dial requires explicit tap; address/coordinates/GPS multiple formats; procedures/muster/exits/AEDs; provenance stamps. |
| Schedule | R-S1…S5 `DISCOVERY.md:162` | Full schedule offline ≤~1k; Now/Next via ClockService; favorites local stable ids; filters/search offline; R-S5 time model `§21` + device-clock handling + dayKey groups. |
| Map | R-M1…M5 `DISCOVERY.md:172` | Offline raster+POI+list `§22`; organizer raster+POIs supplied; geolocation never required (M4 narrowed per `ARCHITECTURE-DESIGN.md:18`); representation validated 1600/3072. |
| Festival | R-F1…F2 `DISCOVERY.md:182` | Info blocks offline, data-driven, not hardcoded. |
| Offline | R-O1…O9 `DISCOVERY.md:189` | Shell without network; SW caches shell; local data truth; startup never needs network; atomic updates; OFFLINE READY evidenced `§20`; survive restarts/pressure/eviction; storage budgets `§33`; versioning/migrations. |
| Package | R-D1…D3 `DISCOVERY.md:203` | Versioned signed package `§15`; manifest inventory; source/pipeline/delivery/version/tracking/atomic/rollback defined `§§10–13`. |
| Extensions | R-X1…X3 `DISCOVERY.md:211` | Online enhancements never gate V1; transport seam (§25) behind SyncService; no mesh in V1. |
| Security | R-G1…G3 `DISCOVERY.md:219` | TH-1…TH-11 `§28` defended; package-signed; no secrets client-side (C-24). |
Failure matrix `ARCHITECTURE-DESIGN.md:755` FA-1…FA-16 must be satisfied for each feature; §31 is the contract.
# 40. Definition of done (checklist before merging or declaring READY)
- [ ] No rule in `§6` B-1…B-7 violated (verified by module-import lint + review).
- [ ] No direct `innerHTML` of festival content; escaping renderer + CSP `self`-only; no inline/eval (`ARCHITECTURE-DESIGN.md:743`).
- [ ] Emergency floor compiled from same sheet as dataset section; ≤16 KB; forward-tolerant; zero-IDB path renders in every failure state; provenance visible (`SPIKE-06:60`, `ARCHITECTURE-DESIGN.md:367`).
- [ ] IDB protocol P1–P8 `SPIKE-01:140` implemented exactly; no txn spans non-IDB await; `QuotaExceededError` path keeps active; pre-check 2×.
- [ ] A/B ordering `§§10–13` holds; per-file atomic; verification record + `readbackPending` + rollback depth 1; page-context downloads only (C-21).
- [ ] Validation ordering `§11`: signature before hash before schema before activate; budgets checked before download; quarantine + monotonic anti-replay.
- [ ] Shell/dataset compatibility dual-checked at boot (§18.5 `ARCHITECTURE-DESIGN.md:688`) and respects C-23 ordering `ARCHITECTURE-DESIGN.md:690` (freeze 7d before festival).
- [ ] Readiness predicate C1–C8 `§20.1` computed locally; every state `§20.2` reachable in tests; READY is time-independent (`SPIKE-05:47`); chip always honest.
- [ ] ClockService: UTC+I`ANA + `dayKey`, skew capture, monotonic guard, F-3 suppression `SPIKE-08:97`, only explicit-zone Intl (F-4).
- [ ] Map: 1600/3072/35 MB budgets, lazy object URLs (F-3), dark filter (F-4), DOM buttons 48 px + Facilities list, a11y `§32`.
- [ ] Schedule: Now/Next, favorites with stable ids, filters/search, `§21` queries, status markers.
- [ ] Info: structured nodes → safe DOM `§23`.
- [ ] Sync pull-only at open/`online`/manual `§26`; HTTP transport only; transport seam empty for future mesh `§27`.
- [ ] Security `§28`: signed manifest + per-file SHA-256, CSP, HSTS, no secrets, key set rotation-capable; verifier library audit recorded (AQ-06) even if not blocking staging.
- [ ] Privacy `§29`: no accounts, no telemetry, no location capture, manual diagnostics only.
- [ ] Recovery `§31`: every FA row renderable; Ring-0 fallback, no blank screen.
- [ ] Performance `§33` budgets CI-gated (shell ≤150 KB gz / 1 MB, dataset ≤40 MB, single file ≤6 MB, decode ≤35 MB, cold 2 s/3 s, boot ≤150 ms, map ≥30 fps).
- [ ] Unit + contract + integration (fault-injected) + content + a11y tests `§35`; any device bug that harness missed becomes a harness case `ARCHITECTURE-DESIGN.md:843`.
- [ ] Device-matrix hand-off documented per `§36` — remaining UNVERIFIED items tracked as YELLOW in `ARCHITECTURE-VALIDATION.md:476` §7 (IDB jetsam, map/GPU, JSC parity, `tel:`/SW).
Implementation may not be declared done for a production festival until the BLOCKING items in `§36` and ops gates `§37` (origin, key custody runbook) have been exercised.
---
# ARCHITECTURAL INVARIANTS (implementation-oriented, non-negotiable)
These copy the validated invariants into rules a coding agent cannot break. Each cites the validated freeze (`ARCHITECTURE-VALIDATION.md:301` §3, DISCOVERY constraints `DISCOVERY.md:230` §5, and specific hard rules).
| # | Invariant (implement as stated) | Trace |
|---|---|---|
| I-1 | **Emergency baseline is Ring-0 shell bytes and never uses IndexedDB.** Tier-1 floor is compiled into the shell at build time from the same source as the dataset emergency section; its renderer parses a frozen schema, is forward-tolerant, and must succeed with zero IDB access in every state (NOT_READY, PARTIAL, READY, RECOVERY, BASELINE_ONLY, FAILED, eviction, corrupt, incompatible, offline). | ADR-007 `ARCHITECTURE-DESIGN.md:465`, `SPIKE-06:12`, `SPIKE-06:75` F-1, C8 `SPIKE-05:43` |
| I-2 | **No emergency path may fetch the network.** Rendering preferences (floor vs dataset section), provenance stamps, and `tel:` number display must be satisfiable from shell + verified active slot only. Online is an enhancement for receiving updates, never a prerequisite for displaying emergency info. | DISCOVERY C17, ADR-001 `ARCHITECTURE-DECISIONS.md:14`, `SPIKE-06:81`, `ARCHITECTURE-DESIGN.md:492` |
| I-3 | **Schedule works fully offline.** Full browse, day groups by `dayKey`, Now/Next via ClockService, My Schedule (favorites), stage/type filters, and search over ≤~1k events all over the active slot — zero network calls, zero hidden APIs. | DISCOVERY R-S1…S4, R-O1…R-O9, ADR-009, `ARCHITECTURE-DESIGN.md:508`, `SPIKE-08:43` |
| I-4 | **Map works fully offline.** Overview (+ optional detail) WebP bases + POI normalized coords + Facilities list + search/filter all from active slot Blobs and JSON — no online tiles, no map SDK, no network. Factors: capped decode `§22`, no GPS. | DISCOVERY R-M1…M5, ADR-008 `SPIKE-03:58`, `ARCHITECTURE-DESIGN.md:543` |
| I-5 | **Festival information works fully offline.** All info blocks rendered as structured safe DOM from the active slot — no remote fetches while festival is running. | DISCOVERY R-F1, `ARCHITECTURE-DESIGN.md:572` |
| I-6 | **Favorites / My Schedule works fully offline and survives every data transition.** `lumen-user` is separate from `lumen-slot-*` and `lumen-system`; updates/rollback/GC never touch it; writes are write-through. | DISCOVERY `DISCOVERY.md:545` §15.8, `SPIKE-02:69` X3, `ARCHITECTURE-DESIGN.md:275` B-6 |
| I-7 | **GPS is optional and V1 has no blue dot.** Never request location permission in V1; find-nearest-facility is solved by the Facilities list and categorised POIs, not GPS. `lat/lng` on POIs is an optional hook only. | DISCOVERY `DISCOVERY.md:172` R-M4 narrowed `ARCHITECTURE-DESIGN.md:18`, invariant 9, `SPIKE-03:112` |
| I-8 | **Network loss cannot break the core app.** After first successful shell+dataset bootstrap, every critical path (boot, nav, emergency, schedule, map, festival info, readiness display, favorites) renders from Cache Storage + IDB with no blocking fetch — online is only `SyncService` enhancement (`ARCHITECTURE-DESIGN.md:383`). | DISCOVERY C19/C20 + ADR-001, C-19 `ARCHITECTURE-DESIGN.md:78`, FA-1 `ARCHITECTURE-DESIGN.md:755` |
| I-9 | **No partial dataset can ever become the active dataset.** Staging exclusively into the inactive slot; per-file hash verified; full verification before pointer move; active is observable only as "old verified" or "new verified" (`SPIKE-02:50` 14 crash points, ordering proof `SPIKE-02:73`). | DISCOVERY C8, ADR-006, `ARCHITECTURE-DESIGN.md:659` |
| I-10 | **No invalid dataset (bad sig, bad hash, bad schema, incompatible, over-budget) can ever become active.** Cheap+authoritative validation order `§11`; Verifier is sole decider (B-4); quarantine prevents refetch-loop; incompatible bubbles "please update app" (`ARCHITECTURE-DESIGN.md:668`). | DISCOVERY C22, R-G2, ADR-013 TH-1/TH-3/TH-5, `SPIKE-02:60` S06–S09 |
| I-11 | **Existing valid data must survive every failed or interrupted update.** Kill at any of the 9 failure stages → old active untouched or restored via rollback/readbackPending (`ARCHITECTURE-DESIGN.md:664` §18.2, `SPIKE-02:50` S01–S14, `SPIKE-02:97` F-3, `ARCHITECTURE-DESIGN.md:672` retry once). | DISCOVERY C9, ADR-006 F-4, `ARCHITECTURE-DESIGN.md:659`, W-4 `ARCHITECTURE-DESIGN.md:898` |
| I-12 | **Dataset authenticity/integrity must be proved before activation.** Ed25519 over `sha256(exact manifest bytes)` against embedded key set + per-file SHA-256+size + schema; recorded in `lumen-system.meta` verification record (F-2) and spot-checked at boot. | ADR-005/013, `ARCHITECTURE-DESIGN.md:358`, `SPIKE-02:29`, `SPIKE-04:203` F-5, `SPIKE-01:140` P2 |
| I-13 | **Shell and dataset compatibility must be checked at two points.** Before staging (manifest `appCompatibility` + `schemaVersion`) and at boot (`schemaVersion ∈ shell.supportedRange` + `appCompatibility`) per `ARCHITECTURE-DESIGN.md:688` §18.5; ordering rule C-23 `ARCHITECTURE-DESIGN.md:690` — shell range ships first, datasets follow, freeze 7d before festival. |
| I-14 | **V1 has no mesh networking — exactly one transport exists.** No WebRTC/BLE/ad-hoc/HP code, no discovery/routing/dedupe in V1. The only transport is `HttpTransport` implementing the byte interface `§25`. | DISCOVERY `DISCOVERY.md:268` NR-1, NR-12, ADR-011/012, `ARCHITECTURE-DESIGN.md:704` |
| I-15 | **V1 has no user accounts.** No login, JWT, session, or server-side identity. All state stays device-local. | DISCOVERY `DISCOVERY.md:270` NR-4, ADR-010 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:60` A-07, `ARCHITECTURE-DESIGN.md:930` rule 7 |
| I-16 | **UI code must never directly manipulate persistence or transport.** Views/components/router may import only `domain/*` read APIs and `readiness/` — never `platform/`, `storage/`, `sync/`, `fetch`, `indexedDB`, `caches`. Enforce via import lint and review (B-1…B-3 `ARCHITECTURE-DESIGN.md:195`, hard rules `ARCHITECTURE-DESIGN.md:928`). |
| I-17 | **Future transport/mesh concerns remain behind the transport boundary.** No feature domain service or UI may import `sync/transport` nor branch on transport identity; Verifier decides truth for every transport equally (signed-payload rule `SPIKE-06:53` ME-4, `ARCHITECTURE-DESIGN.md:718`). Status shows "updated <when>", never "via what". | ADR-011/012, `DISCOVERY.md:596` ME-4, `ARCHITECTURE-DESIGN.md:704`/`ARCHITECTURE-DESIGN.md:718` §20.5 |
Violations of any I-1…I-17 fail `§40` Definition of Done.
---
# IMPLEMENTATION SEQUENCE (do not attempt to build everything simultaneously)
Prerequisites use `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:138` and `ARCHITECTURE-VALIDATION.md:493` — every stage is buildable against a provisional staging origin and fixtures. Do not block on production origin (AQ-18), real map art (OQ-6/AQ-19), or emergency sign-off (OQ-2) — model them with placeholders that respect budgets/schemas.
## Stage 1 — Project foundation
- **Prerequisites:** Contract read; architecture docs understood; decision to use provisional staging origin.
- **Work:** Dedicated git repo (VCS-1 `DISCOVERY.md:34`), TypeScript strict base, lint (import boundaries for B-1…B-7 + §6), formatter, commit hooks, CI skeleton.
- **Tests:** CI runs lint + typecheck on empty src.
- **Acceptance:** Import-lint for forbidden edges (`ui`→`platform`, etc.) passes; no app code yet.
- **Must NOT yet:** PWA, DB, any feature code; no runtime deps except possibly the audited verifier placeholder.
## Stage 2 — Application shell
- **Prerequisites:** Stage 1.
- **Work:** `index.html` (single entry), manifest (`display: standalone`, icons), build emits hashed `/assets/<hash>.*` + precache manifest deterministically (AQ-12 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:36` Validated), production `no-cache` vs immutable headers `ARCHITECTURE-DESIGN.md:807`. Install coach visuals scaffolded (iOS manual steps + Android `beforeinstallprompt` detection, `ARCHITECTURE-DESIGN.md:217`).
- **Tests:** Lighthouse PWA (shell installable), scripted offline reload (`ARCHITECTURE-DESIGN.md:838`).
- **Acceptance:** `tests` PWA audits pass; budgets `§33` bundle gate visible.
- **Must NOT yet:** IDB, dataset, sync.
## Stage 3 — PWA / service worker
- **Prerequisites:** Stage 2 shell hashes stable.
- **Work:** Hand-written `public/sw.js` `§16` (install/activate/fetch/message). Cache name `lumen-shell-v<build>`; navigation cache-first with `fallback.html`; hashed assets cache-first; package endpoints passthrough (C-21). `SKIP_WAITING` + next-start activation + in-app "Restart to update" affordance `ARCHITECTURE-DESIGN.md:255`.
- **Tests:** SW lifecycle harness: install → activate → old SW serves current session → next-start flip; idempotent ops; package passthrough not cached. Offline launch without IDB still renders shell+floor.
- **Acceptance:** T01/T03 blue/green path passes on Chromium headless; T16 readiness for device kill test deferred to `§36`.
- **Must NOT yet:** Any IDB write from SW — keep P8 `SPIKE-01:140`.
## Stage 4 — Data model
- **Prerequisites:** §8 budgets known.
- **Work:** Festival Data Package types (`SPIKE-04:79` manifests + section schemas), emergency floor schema (`SPIKE-06:12`), map/POI categories, info structured nodes. Define `schemaVersion` envelope and `sections[*].required` `SPIKE-04:64`, no-expiration rule `SPIKE-04:50`, stable-id contract `SPIKE-04:189`.
- **Tests:** Contract fixtures — golden packages pass gates `SPIKE-04:189`; stable-id invariant tests; dayKey contract `SPIKE-08:43`.
- **Acceptance:** Pipeline can validate/reject a fixture package deterministically; no UI yet.
## Stage 5 — Local persistence
- **Prerequisites:** §4 types.
- **Work:** `platform/idb` thin wrapper (`SPIKE-01:140` P1–P8), `data/` stores, `lumen-system/meta` single-txn discipline (P2), `lumen-user` migrations, no txn spans await, `QuotaExceededError` keeps active (P3), 6 MB single-record cap (P5), `storage.estimate()` free-space (P4), persist request (P6). Assets as Blobs in slot DB `ARCHITECTURE-DESIGN.md:275`.
- **Tests:** Wrapper atomicity tests `SPIKE-01:60`; QuotaError injection keeps active; boot light verify detects missing/corrupt; heavy BLOB read-back timing instrumentation.
- **Acceptance:** Unit tests mirror `exp2-ab-update-sim.mjs:1` infrastructure at harness level (without claiming device proof).
- **Must NOT yet:** Verifier/crypto — reads only.
## Stage 6 — Festival Data Package (content pipeline stub)
- **Prerequisites:** §4 types + §5 stores exist (even without real art).
- **Work:** `content/` templates + pipeline script `validate→build→hash→sign→upload→smoke` (`ARCHITECTURE-DESIGN.md:697` runbook). Share same emergency source sheet for floor + dataset section (`ARCHITECTURE-DESIGN.md:345`) using test emergency data. Mutable `latest.json`.
- **Tests:** Pipeline gates `SPIKE-04:189` on synthetic data; signed fixture → smoke fetch re-verifies.
- **Acceptance:** A staging `lumen-2026` edition with Budget ≤40 MB produces a smoke-verified signed package reachable via immutable URLs; test key only.
- **Must NOT yet:** Real organizer content — provisional placeholder.
## Stage 7 — Dataset validation
- **Prerequisites:** Stages 5–6.
- **Work:** `sync/verifier` (SHA-256 via WebCrypto + bundled pure-JS Ed25519 verifier placeholder). Implements ordering `§11` before any download, quarantine store, budgets checked before download (`ARCHITECTURE-DESIGN.md:358`, `SPIKE-02:29`).
- **Tests:** Bad signature → keep old, no file fetched (S07), bad hash (S06), bad schema (S08), incompatible (S09), replay lower version, bundled verifier smoke against `signature.json` `SPIKE-04:79`. Enforces quarantine no-loop.
- **Acceptance:** Every rejection keeps active; Verifier is sole decider (B-4).
## Stage 8 — A/B activation
- **Prerequisites:** Stages 5–7.
- **Work:** `sync/` staging into inactive slot (per-file atomic, resumable 7-day discard), verification record + `readbackPending` (F-2/F-3 `SPIKE-02:89`), single-txn flip `§12`, retry-once, automatic rollback on spot-check/next-boot, user-initiated restore (`ARCHITECTURE-DESIGN.md:677`).
- **Tests:** Fault-injection integration exactly covering `SPIKE-02:50` S01–S14 + X1–X3 (kill between files, before/during/after flip, corrupt at boot, both-gone RECOVERY, favorites survive). Mirrors `exp2-ab-update-sim.mjs:1`.
- **Acceptance:** 16/16 analogous PASS at state-machine level; coverage does not claim iOS jetsam proof — that is device T10 `§36`.
## Stage 9 — Offline Ready
- **Prerequisites:** Stages 3 (C1), 5/8 (C2–C8).
- **Work:** `domain/readiness` predicate C1–C8 `§20.1`, six-state taxonomy `§20.2`, cadence `§20.3` (light at boot ≤150 ms, full after staging / user check / error), persistence of `readbackPending`, chip mapping, Status screen checklist/version/usage.
- **Tests:** Predicate truth table (`SPIKE-05:90` edge cases: eviction→RECOVERY, incompatible→RECOVERY, private→BASELINE_ONLY, wrong clock→READY+warning, optional missing→READY, activation fail→FAILED). Every state reachable `ARCHITECTURE-DESIGN.md:930` rule 5.
- **Acceptance:** Time-independent READY (`SPIKE-05:47` F-1) — wrong clock produces warning `§21`, never demotion.
## Stage 10 — Emergency
- **Prerequisites:** Ready §9 + baseline generation from content pipeline §6.
- **Work:** Compiled `emergency-baseline/` ≤16 KB (`ARCHITECTURE-DESIGN.md:367`), three-tier resolution `SPIKE-06:60`, forward-tolerant zero-IDB renderer `SPIKE-06:75` F-1, provenance stamps, `tel:` (primary) + copyable text, explicit tap to dial (`DISCOVERY.md:470` EM-5/EM-10), persistent nav one-tap reach (`ARCHITECTURE-DESIGN.md:207`).
- **Tests:** Every `§20.2` state → Emergency renders floor or highest compatible tier; floor with zero-IDB (BASELINE_ONLY); incompatible demotes to floor with guidance; signature/compatibility failure never removes floor (`SPIKE-06:81` 7 cases).
- **Acceptance:** Emergency coverage is Ring-0 — invariants I-1/I-2 pass in all failure states `§31`.
## Stage 11 — Schedule
- **Prerequisites:** §9 readiness, `§21` ClockService.
- **Work:** `domain/schedule` + `views/schedule` + `domain/favorites`. Stages/artists/events with stable ids + `dayKey`; Now/Next (§21), per-stage/global up-next, conflict detection, stage/tag filters, substring search (naive, ≤1k) (`ARCHITECTURE-DESIGN.md:508`). `status: scheduled|moved|cancelled` rendering, changed-event notes. ClockService integration: device+skew → monotonic guard → F-3 suppressed sanity → absolute times always visible `SPIKE-08:97`.
- **Tests:** Intl zone rendering T1a/b, dayKey T2a–c, DST T3a–e, unusual zones T4a–d (`SPIKE-08:43`); skew arithmetic T5a–c; sanity T6a–c with F-3; overlap T7a–d — unit suite mirroring `exp1-time-model.mjs:1`. Display stays inside festival zone vs device toggle.
- **Acceptance:** `§21` queries work offline; wrong-never-online clock is accepted residual with warnings, not READY demotion.
## Stage 12 — Map
- **Prerequisites:** §9 + `§22` types; provisional art meeting caps suffices for build even without real art (placeholder art does not block dev `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:105` OQ-6).
- **Work:** `domain/map` + `views/map` — two-level WebP decoding (≤1600/≤3072/≤35 MB, one resident), DOM POI buttons, CSS transform pan/zoom with counter-scaled markers, Facilities list first-class (`SPIKE-03:79`), search highlight, category filter, object-URL lifecycle F-3, dark filter F-4 (`SPIKE-03:88`).
- **Tests:** Headless mechanics OK (EXP-3 caveat `experiments/README.md:36`); unit decode-budget guards; filter/search; navigation without GPS.
- **Acceptance:** Correct at mechanism level on headless; production proof deferred to device T12/T13 `§36` (`SPIKE-03:116` 6-item protocol: ≥30 fps, no leaks, a11y VoiceOver/TalkBack, texture-cap fallback).
## Stage 13 — Festival information
- **Prerequisites:** Structured blocks types `§23`.
- **Work:** `domain/festival` + `views/festival` — renderer for structured nodes (no raw HTML, C-22). Category-driven nav, shared search index, readability (day/night contrast).
- **Tests:** Escaping/structured-node mapping + search index; no `innerHTML` of content lint passes (B-7).
- **Acceptance:** Signed required section; degraded BASELINE_ONLY correctly excludes it while emergency stays.
## Stage 14 — User state
- **Prerequisites:** Earlier favorite model must align with stable ids (§24).
- **Work:** `domain/favorites` over `lumen-user.favorites` (write-through), `prefs` (theme/clock), scrubbed `diag` ring buffer, flags for coach. Idempotent `lumen-user` migrations separate from package schema (`SPIKE-04:210`).
- **Tests:** Favorite survives A/B updates/rollback (`SPIKE-02:69` X3); moved/cancelled/orphan handling; no cross-slot GC into `lumen-user`.
- **Acceptance:** X3 analog passes at integration level.
## Stage 15 — Synchronization
- **Prerequisites:** Ready `§9` + verifier `§7` + activation `§8` + `§25` seam.
- **Work:** `SyncService` pull-only on open/`online`/manual `§26` — `latest.json` monotonic check → manifest verify → budget/compat → stage → full verify → flip; quarantine; budget + compatibility + hash rejection paths. `Transport` seam with `HttpTransport` only (`DISCOVERY.md:596` ME-4).
- **Tests:** §18.2 nine-stage fault tests (`ARCHITECTURE-DESIGN.md:664`): pointer error, manifest error, bad sig (quarantine), incompatible, interrupted download, quota, full-verify mismatch, activation txn fail, post-activation readback rollback.
- **Acceptance:** Every row in `§31` FA-8/FA-9/FA-13 satisfied; no background work (C-19, `ARCHITECTURE-DESIGN.md:78`).
## Stage 16 — Accessibility
- **Prerequisites:** Views exist (10–14).
- **Work:** Landmarks, heading order, focus management per `§32`; map Facilities list; emergency targets/contrast; 48 px targets; reduced-motion; standalone safe-area handling (`ARCHITECTURE-DESIGN.md:217`).
- **Tests:** Automated a11y (headings, landmarks, contrast) + manual screen-reader passes on Emergency/Schedule/Facilities per `§32` / `§36` T13.
- **Acceptance:** No feature merges without a11y checklist in `§40`.
## Stage 17 — Performance
- **Prerequisites:** Meaningful budgets from cached staging.
- **Work:** CI gates enforcing `§33` (bundle JS 150 KB gz / shell 1 MB, dataset 40 MB / file 6 MB, decode 35 MB, cold 2 s/3 s, boot 150 ms, map 30 fps). Optimisations: windowed schedule list if needed, single JSON parse per section, no idle timers `ARCHITECTURE-DESIGN.md:858`.
- **Tests:** Light perf harness for boot verify, schedule 1k, WebP decode memory meter.
- **Acceptance:** Every gate green; any exceedance requires ADR amendment — do not silently raise budgets (`RULES`).
## Stage 18 — Device testing
- **Prerequisites:** All above buildable with fixtures; at least one staging edition smoke-verified (stage 6).
- **Work:** Execute the full physical-device matrix `§36` (19 groups × D1–D4, 76 tests). Devices: D1 iOS 16.4, D2 iOS latest, D3 low-end Android 2021 ~Chrome 110+, D4 modern Android `ARCHITECTURE-VALIDATION.md:158`. Scripts cover T01–T19 as worded there.
- **Tests:** Device tests themselves — every BLOCKING must pass before production festival; bugs that harness missed feed back into harness (`ARCHITECTURE-DESIGN.md:843`).
- **Acceptance:** UNVERIFIED items from spikes (IDB jetsam, quota/Blob, map/GPU, JSC Intl, `tel:`, SW re-registration, cold-start) become CONFIRMED by observation or fall back to documented degraded paths (overview-only, tile-split, re-prep).
## Stage 19 — Deployment
- **Prerequisites:** Pipeline producing signed packages; verifier audit (AQ-06) and key custody drill (AQ-21) in progress.
- **Work:** Chosen provider (AQ-14) and production origin (AQ-18, hard to reverse) + HSTS/CSP/headers `ARCHITECTURE-DESIGN.md:807`, two envs `staging`/`production` `ARCHITECTURE-DESIGN.md:817`, runbooks `ARCHITECTURE-DESIGN.md:697` (publish smoke re-verify, rollback via new version, emergency gate). Provisional staging remains usable until public link (`ASSUMPTIONS-AND-OPEN-QUESTIONS.md:116`).
- **Tests:** Staging → production dry-run festival (AQ-23 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:121`) with test edition + field devices.
- **Acceptance:** Staging smoke-verified package activates on real devices; rollback via `latest.json` advance is exercised; emergency floor provenance correct.
---
# RULES FOR FUTURE AI CODING AGENTS
1. **Read before you code.** Read — in full — `ARCHITECTURE-VALIDATION.md`, `ARCHITECTURE-DECISIONS.md`, `ARCHITECTURE-DESIGN.md`, `ASSUMPTIONS-AND-OPEN-QUESTIONS.md`, all eight SPIKE docs, and `DISCOVERY.md` before touching code.
2. **Do not invent infrastructure.** Static CDN + import maps only; no backend/DB/auth/mesh service because none exists in the architecture (`ARCHITECTURE-DESIGN.md:815`). If you need one, file an ADR and wait for approval.
3. **Do not add dependencies without justification.** Target only the audited Ed25519 verifier beyond stdlib (`ARCHITECTURE-DESIGN.md:930` rule 3). Any new runtime dep requires an ADR addendum with size impact (JSP 150 KB gz, `§33`).
4. **Do not silently replace decisions.** If vanilla TS + tiny store feels inconvenient, use the Preact reconsider trigger in ADR-003 (`ARCHITECTURE-DECISIONS.md:86`) via a documented proposal — do not just switch.
5. **Do not introduce accounts/authentication.** V1 has no login (`§2`, `INVARIANT I-15`); favorites are device-local. Any future account needs explicit approval and must not touch emergency/schedule paths.
6. **Do not introduce mesh in V1.** Seam costs one interface `§25`; no WebRTC/BLE/ad-hoc code, no discovery/routing (`DISCOVERY.md:270` NR-1, `INVARIANT I-14`). A future transport is the only extension.
7. **Do not create a hidden network dependency.** No critical view, domain service, readiness check, emergency render, or boot path may `fetch` or await a server. Sync runs only as `SyncService` enhancement with `online` hint (`INVARIANT I-8`, C-19 `ARCHITECTURE-DESIGN.md:78`).
8. **Do not put festival content directly into UI components.** Content lives in the signed Festival Data Package `§15` and is read via `data/` APIs; no hard-coded schedules, POIs, or emergency strings in components. `§6` B-2, `DISCOVERY.md:230` C13, failures `FA-6`/`FA-9`.
9. **Do not duplicate emergency data by hand.** Floor and dataset section share the same source sheet (`ARCHITECTURE-DESIGN.md:345`, `SPIKE-06:12`). Manual copy-paste creates drift — generate, do not duplicate `§14` (and enforce the 16 KB cap).
10. **Do not bypass validation or A/B activation.** Check signature before hash before schema before activate, quarantine, and flip in a single `lumen-system` txn `§§11–12` — every rejection path in `§31`. B-4 `ARCHITECTURE-DESIGN.md:195`. Shortcuts fail `INVARIANTS I-9…I-12` and `§40`.
11. **Do not drop the defensive IDB protocol.** P1–P8 `SPIKE-01:140`: short txns, `QuotaExceededError` keeps active, free-space 2× pre-check, 6 MB cap, no dataset in SW, boot light detection.
12. **Do not weaken accessibility for convenience.** A11y is BLOCKING in `§36` T13 and `§40` checklist; POIs remain real `button`s with 48 px and Facilities list is first-class, not fallback `SPIKE-03:79`.
13. **Do not silently raise budgets.** Map 1600/3072/35 MB (`SPIKE-03:58`), dataset 40 MB, file 6 MB, bundle 150 KB gz / 1 MB, boot 150 ms, cold 2 s/3 s, map 30 fps — all in `§33`. Negotiate an ADR if genuinely needed; do not inflate in code.
14. **Do not treat UNVERIFIED as confirmed.** IDB jetsam atomicity (`SPIKE-01:75`), JSC Intl parity (`SPIKE-08:152`), SW lifecycle (`SPIKE-01:82`), storage pressure/Blob (`SPIKE-01:159`), map decode (`SPIKE-03:116`), `tel:` standalone (`SPIKE-01:171`) are queued for `§36` device matrix. A desktop pass does not clear them.
15. **When a conflict is discovered, stop and document.** File a gap note citing the conflicting lines (e.g., `ARCHITECTURE-DECISIONS.md:129` vs `SPIKE-01:140`) and propose a patch — do not silently pick a side. The valid resolver is the validation report `ARCHITECTURE-VALIDATION.md:1` which already reconciles all eight spikes.
16. **Respect the ordering and layering contracts.** Per-file atomic + quarantine (F-1, `§10`), activation single txn (P2/F-2 `§12`), time-sane rendering via explicit-zone `Intl` only (F-4 `§21`), content-as-data via structured nodes (C-22 `§23`), transport byte-seam (`§25`), and `INVARIANTS I-16/I-17` behind the transport boundary.
---
# CROSS-TRACE: every major rule → validated decision
| Rule in contract | Source decision(s) | Spike reconciliation |
|---|---|---|
| V1 scope / NL-1…NR-15 §1/§2 | ADR-001…ADR-014, `ARCHITECTURE-DESIGN.md:25` | `ARCHITECTURE-VALIDATION.md:40` §2 (no spike contradicts scope) |
| Stack vanilla TS / no framework §3 | ADR-003 `ARCHITECTURE-DECISIONS.md:86` | SPIKE-03 EXP-3 `SPIKE-03:28` + `ARCHITECTURE-VALIDATION.md:56` (no dedicated spike, YELLOW device perf) |
| Thin IDB wrapper + Cache for shell §3/§8/§9 | ADR-004 `ARCHITECTURE-DECISIONS.md:129` | SPIKE-01 P1–P8 `SPIKE-01:140`, ACCEPT WITH CHANGES |
| Package JSON+assets+signed manifest §15 | ADR-005 `ARCHITECTURE-DECISIONS.md:177` | SPIKE-04 F-1…F-5 `SPIKE-04:203`, ACCEPT WITH CHANGES |
| A/B inactive staging + single-txn activation §10/§12 | ADR-006 `ARCHITECTURE-DECISIONS.md:225` | SPIKE-02 F-1…F-4 `SPIKE-02:89` 16/16 PASS, proof `SPIKE-02:73` |
| Emergency 3 tiers (floor compiled) §14 | ADR-007 `ARCHITECTURE-DECISIONS.md:272` | SPIKE-06 `SPIKE-06:12` + F-1 `SPIKE-06:75` |
| Map raster+DOM+list §22 | ADR-008 `ARCHITECTURE-DECISIONS.md:315` | SPIKE-03 F-1 `SPIKE-03:58`, F-3 `SPIKE-03:68`, F-4 `SPIKE-03:88` |
| Time UTC+IANA+ClockService §21 | ADR-009 `ARCHITECTURE-DECISIONS.md:362` | SPIKE-08 24/24 `SPIKE-08:43`, F-3 `SPIKE-08:97`, F-4 `SPIKE-08:148` |
| Sync pull-only §26 | ADR-010 `ARCHITECTURE-DECISIONS.md:406` | C-19/B-06/PW-4 `DISCOVERY.md:433` + cheapest-first ordering `SPIKE-02:29` |
| Transport byte seam §25 | ADR-011 `ARCHITECTURE-DECISIONS.md:441` | `DISCOVERY.md:596` ME-4 + signed-payload rule |
| Mesh future-only §27 | ADR-012 `ARCHITECTURE-DECISIONS.md:482` | `ARCHITECTURE-VALIDATION.md:475` scenario 20 + feasibility `DISCOVERY.md:581` |
| Security: sig+hash+key set+CSP §28 | ADR-013 `ARCHITECTURE-DECISIONS.md:524` | TQ-9/SQ-1 pure-JS verifier correct per `ARCHITECTURE-DESIGN.md:14`, SPIKE-02 rejection paths `SPIKE-02:60` |
| Static CDN single origin §37 | ADR-014 `ARCHITECTURE-DECISIONS.md:575` | SPIKE-04 gates `SPIKE-04:189` + origin-keyed strand risk `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:116` |
| Offline Ready C1–C8 + six states §20 | `ARCHITECTURE-DESIGN.md:388` §12 + ADR-006 | SPIKE-05 `SPIKE-05:28`/`SPIKE-05:60` + time independence `SPIKE-05:47` |
| Invariants I-1…I-17 | `ARCHITECTURE-VALIDATION.md:301` §3 + `DISCOVERY.md:230` §5 | Every freeze-test scenario `ARCHITECTURE-VALIDATION.md:524` §6 proven after spikes |
| Device matrix §36 | `ARCHITECTURE-DESIGN.md:832` §26 → `ARCHITECTURE-VALIDATION.md:156` §4 | All §5 UNVERIFIED queues in spikes |
| Hard rules B-1…B-7 `§6` | `ARCHITECTURE-DESIGN.md:195` + `ARCHITECTURE-DESIGN.md:930` | Reinforced by `SPIKE-01:140` P8 + `SPIKE-06:75` |
---
# INTENTIONALLY PROVISIONAL (do not pretend otherwise)
Per `ARCHITECTURE-VALIDATION.md:476` §7 YELLOW tables and `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:9` validation plan:
- **Physical-device YELLOW (not blocking start, blocking production):** IDB atomicity near-quota/jetsam (AQ-04), map decode/fps/leaks/texture-cap (AQ-11, `SPIKE-03:116`), JSC/DayKey/ClockService on iOS (AQ-08, `SPIKE-08:152`), SW re-registration, `tel:` in standalone, `persist()` heuristics. These are `§36` T07/T08/T10/T12/T13/T15/T16, not architecture gaps.
- **Content YELLOW (not blocking start):** A-02 budgets `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:16` with pipeline gates `SPIKE-04:189` + real schedule/map samples (D-01/D-02/AQ-11) `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:73`, OQ-6 illustrated art.
- **Ops YELLOW (not blocking start on provisional origin):** production origin (AQ-18 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:116`), provider (AQ-14 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:112`), emergency sign-off gate O-02/OQ-2 `DISCOVERY.md:345`, legal review, key custody drill AQ-21/S-01 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:85` (verifier audit AQ-06 `ASSUMPTIONS-AND-OPEN-QUESTIONS.md:29`).
The only class that could ever turn YELLOW to RED is a device-matrix failure for which fallbacks are already defined (overview-only mode, 2×2 tile-split DD-9 `ARCHITECTURE-DESIGN.md:918`, Preact trigger ADR-003, re-prep path).
---
# FIRST IMPLEMENTATION PHASE (upon approval)
Stage 1 — Project foundation (`IMPLEMENTATION SEQUENCE Stage 1`): dedicated repo at `/home/avi/Projects/Lumen` (per VCS-1 `DISCOVERY.md:34`), TypeScript strict base, lint rule enforcing B-1…B-7 `§6`, commit hooks, CI skeleton — no deps yet. This is gated only by the decision to proceed; no provisional item blocks it.
---
*End of Implementation Contract. No application source was created to write it. Cross-check instruction: verify every row above appears in `ARCHITECTURE-VALIDATION.md` and reconcile any drift by preferring the validation report.*