From c0bfd413ffcbda1319b100cce52104b78d865d2a Mon Sep 17 00:00:00 2001 From: Lumen Stage1 Date: Sun, 30 Aug 2026 23:25:35 -0500 Subject: [PATCH] Stage 1: project foundation (strict TS, lint boundaries B-1..B-7, directory structure, CI, boundary tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .github/workflows/ci.yml | 23 + .gitignore | 25 + .prettierignore | 5 + .prettierrc.json | 11 + ARCHITECTURE-DECISIONS.md | 635 ++++++ ARCHITECTURE-DESIGN.md | 950 ++++++++ ARCHITECTURE-VALIDATION.md | 573 +++++ ASSUMPTIONS-AND-OPEN-QUESTIONS.md | 158 ++ DISCOVERY.md | 842 +++++++ IMPLEMENTATION-CONTRACT.md | 832 +++++++ README.md | 27 + SPIKE-01-INDEXEDDB-IOS.md | 191 ++ SPIKE-02-ATOMIC-DATASET-UPDATES.md | 126 + SPIKE-03-MAP-REPRESENTATION.md | 141 ++ SPIKE-04-FESTIVAL-DATA-PACKAGE.md | 218 ++ SPIKE-05-OFFLINE-READY.md | 115 + SPIKE-06-EMERGENCY-BASELINE.md | 119 + SPIKE-07-BOOTSTRAP.md | 98 + SPIKE-08-TIME-MODEL.md | 189 ++ content/.gitkeep | 0 content/README.md | 5 + eslint.config.js | 170 ++ experiments/README.md | 59 + experiments/exp1-time-model.mjs | 134 ++ experiments/exp2-ab-update-sim.mjs | 242 ++ experiments/exp3-map-bench.html | 137 ++ package-lock.json | 3426 ++++++++++++++++++++++++++++ package.json | 32 + pipeline/.gitkeep | 0 pipeline/README.md | 5 + public/.gitkeep | 0 public/README.md | 5 + scripts/.gitkeep | 0 scripts/README.md | 5 + scripts/check-boundaries.ts | 98 + src/app/.gitkeep | 0 src/app/README.md | 5 + src/assets/.gitkeep | 0 src/assets/README.md | 5 + src/data/.gitkeep | 0 src/data/README.md | 5 + src/domain/clock/.gitkeep | 0 src/domain/clock/README.md | 5 + src/domain/emergency/.gitkeep | 0 src/domain/emergency/README.md | 5 + src/domain/favorites/.gitkeep | 0 src/domain/favorites/README.md | 5 + src/domain/festival/.gitkeep | 0 src/domain/festival/README.md | 5 + src/domain/map/.gitkeep | 0 src/domain/map/README.md | 5 + src/domain/readiness/.gitkeep | 0 src/domain/readiness/README.md | 5 + src/domain/schedule/.gitkeep | 0 src/domain/schedule/README.md | 5 + src/emergency-baseline/.gitkeep | 0 src/emergency-baseline/README.md | 5 + src/platform/cache/.gitkeep | 0 src/platform/cache/README.md | 5 + src/platform/idb/.gitkeep | 0 src/platform/idb/README.md | 5 + src/platform/sw/.gitkeep | 0 src/platform/sw/README.md | 5 + src/storage/.gitkeep | 0 src/storage/README.md | 5 + src/sync/.gitkeep | 0 src/sync/README.md | 5 + src/sync/transport/.gitkeep | 0 src/sync/transport/README.md | 5 + src/sync/verifier/.gitkeep | 0 src/sync/verifier/README.md | 5 + src/ui/components/.gitkeep | 0 src/ui/components/README.md | 5 + src/ui/render/.gitkeep | 0 src/ui/render/README.md | 5 + src/ui/router/.gitkeep | 0 src/ui/router/README.md | 5 + src/ui/views/.gitkeep | 0 src/ui/views/README.md | 5 + src/ui/views/emergency/.gitkeep | 0 src/ui/views/emergency/README.md | 2 + src/ui/views/festival/.gitkeep | 0 src/ui/views/festival/README.md | 2 + src/ui/views/map/.gitkeep | 0 src/ui/views/map/README.md | 2 + src/ui/views/schedule/.gitkeep | 0 src/ui/views/schedule/README.md | 2 + src/ui/views/status/.gitkeep | 0 src/ui/views/status/README.md | 2 + tests/device-matrix/.gitkeep | 0 tests/device-matrix/README.md | 5 + tests/e2e/.gitkeep | 0 tests/e2e/README.md | 5 + tests/integration/.gitkeep | 0 tests/integration/README.md | 5 + tests/unit/.gitkeep | 0 tests/unit/README.md | 5 + tests/unit/boundaries.test.ts | 70 + tsconfig.json | 44 + vitest.config.ts | 13 + 100 files changed, 9863 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 ARCHITECTURE-DECISIONS.md create mode 100644 ARCHITECTURE-DESIGN.md create mode 100644 ARCHITECTURE-VALIDATION.md create mode 100644 ASSUMPTIONS-AND-OPEN-QUESTIONS.md create mode 100644 DISCOVERY.md create mode 100644 IMPLEMENTATION-CONTRACT.md create mode 100644 README.md create mode 100644 SPIKE-01-INDEXEDDB-IOS.md create mode 100644 SPIKE-02-ATOMIC-DATASET-UPDATES.md create mode 100644 SPIKE-03-MAP-REPRESENTATION.md create mode 100644 SPIKE-04-FESTIVAL-DATA-PACKAGE.md create mode 100644 SPIKE-05-OFFLINE-READY.md create mode 100644 SPIKE-06-EMERGENCY-BASELINE.md create mode 100644 SPIKE-07-BOOTSTRAP.md create mode 100644 SPIKE-08-TIME-MODEL.md create mode 100644 content/.gitkeep create mode 100644 content/README.md create mode 100644 eslint.config.js create mode 100644 experiments/README.md create mode 100644 experiments/exp1-time-model.mjs create mode 100644 experiments/exp2-ab-update-sim.mjs create mode 100644 experiments/exp3-map-bench.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pipeline/.gitkeep create mode 100644 pipeline/README.md create mode 100644 public/.gitkeep create mode 100644 public/README.md create mode 100644 scripts/.gitkeep create mode 100644 scripts/README.md create mode 100644 scripts/check-boundaries.ts create mode 100644 src/app/.gitkeep create mode 100644 src/app/README.md create mode 100644 src/assets/.gitkeep create mode 100644 src/assets/README.md create mode 100644 src/data/.gitkeep create mode 100644 src/data/README.md create mode 100644 src/domain/clock/.gitkeep create mode 100644 src/domain/clock/README.md create mode 100644 src/domain/emergency/.gitkeep create mode 100644 src/domain/emergency/README.md create mode 100644 src/domain/favorites/.gitkeep create mode 100644 src/domain/favorites/README.md create mode 100644 src/domain/festival/.gitkeep create mode 100644 src/domain/festival/README.md create mode 100644 src/domain/map/.gitkeep create mode 100644 src/domain/map/README.md create mode 100644 src/domain/readiness/.gitkeep create mode 100644 src/domain/readiness/README.md create mode 100644 src/domain/schedule/.gitkeep create mode 100644 src/domain/schedule/README.md create mode 100644 src/emergency-baseline/.gitkeep create mode 100644 src/emergency-baseline/README.md create mode 100644 src/platform/cache/.gitkeep create mode 100644 src/platform/cache/README.md create mode 100644 src/platform/idb/.gitkeep create mode 100644 src/platform/idb/README.md create mode 100644 src/platform/sw/.gitkeep create mode 100644 src/platform/sw/README.md create mode 100644 src/storage/.gitkeep create mode 100644 src/storage/README.md create mode 100644 src/sync/.gitkeep create mode 100644 src/sync/README.md create mode 100644 src/sync/transport/.gitkeep create mode 100644 src/sync/transport/README.md create mode 100644 src/sync/verifier/.gitkeep create mode 100644 src/sync/verifier/README.md create mode 100644 src/ui/components/.gitkeep create mode 100644 src/ui/components/README.md create mode 100644 src/ui/render/.gitkeep create mode 100644 src/ui/render/README.md create mode 100644 src/ui/router/.gitkeep create mode 100644 src/ui/router/README.md create mode 100644 src/ui/views/.gitkeep create mode 100644 src/ui/views/README.md create mode 100644 src/ui/views/emergency/.gitkeep create mode 100644 src/ui/views/emergency/README.md create mode 100644 src/ui/views/festival/.gitkeep create mode 100644 src/ui/views/festival/README.md create mode 100644 src/ui/views/map/.gitkeep create mode 100644 src/ui/views/map/README.md create mode 100644 src/ui/views/schedule/.gitkeep create mode 100644 src/ui/views/schedule/README.md create mode 100644 src/ui/views/status/.gitkeep create mode 100644 src/ui/views/status/README.md create mode 100644 tests/device-matrix/.gitkeep create mode 100644 tests/device-matrix/README.md create mode 100644 tests/e2e/.gitkeep create mode 100644 tests/e2e/README.md create mode 100644 tests/integration/.gitkeep create mode 100644 tests/integration/README.md create mode 100644 tests/unit/.gitkeep create mode 100644 tests/unit/README.md create mode 100644 tests/unit/boundaries.test.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8c51792 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: ci +on: + push: + branches: [main] + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm run lint + - run: npm run format + - run: npm run test + # budgets (placeholder gates for Stage 1 — will enforce shell/data limits from Stage 2) + - name: stage1-boundary-gate + run: node --loader ts-node/esm scripts/check-boundaries.ts || npm run test -- tests/unit/boundaries.test.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7dc77f --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Node +node_modules/ +dist/ +coverage/ +*.tsbuildinfo + +# OS +.DS_Store +.directory + +# Editor +.vscode/ +.idea/ +*.swp +*.swo + +# Env +.env +.env.local + +# Pipeline / staging artifacts (not repo secrets) +.pipeline-out/ + +# Experiments are validation-only (keep) +!experiments/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..37f04d0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +coverage +experiments +*.md diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..9fa4fa3 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/ARCHITECTURE-DECISIONS.md b/ARCHITECTURE-DECISIONS.md new file mode 100644 index 0000000..33a4517 --- /dev/null +++ b/ARCHITECTURE-DECISIONS.md @@ -0,0 +1,635 @@ +# Lumen — Architecture Decision Records (Phase 1) + +All decisions below follow the evaluation discipline required for this phase: +problem → alternatives → criteria → evaluation → decision → reasoning → +consequences → risks → reversibility. Decisions were made against Lumen's +actual requirements (300–500 attendees, offline-first PWA, small team), not +technology popularity. + +Status values: **Accepted** (adopted now), **Proposed** (adopted pending the +listed validation spike). + +--- + +## ADR-001: Offline-first architecture + +- **Status:** Accepted +- **Context:** The festival environment may have no usable connectivity of any + kind (DISCOVERY §3). The product's core promise: everything attendees need + works with zero internet. +- **Problem:** Choose the fundamental data-flow posture: online-first with + offline fallback, offline-first with online enhancement, or offline-only. +- **Decision:** Offline-first. Local storage is the source of truth for all + critical functionality. Network connectivity is an enhancement used only for + updates/optional live content. No critical code path may perform a blocking + network call. +- **Alternatives considered:** + 1. *Online-first + graceful degradation* — rejected: violates invariants 1–4; + degradation at a festival is the common case, not the edge case. + 2. *Offline-only (never touches network)* — rejected: forfeits schedule + corrections, emergency contact updates, and app update delivery; the + brief explicitly allows optional online features. +- **Evaluation criteria:** invariant compliance (1–10); failure-mode simplicity; + ops burden; honesty of user-facing state. +- **Reasoning:** Offline-first is the only posture in which connectivity loss is + a non-event. It forces the valuable side-effects this product needs anyway: + versioned data packages, verifiable readiness, atomic updates. +- **Consequences:** one-time online bootstrap required (C-17); explicit readiness + state machine needed (ADR-006/§12); all content must be data (invariant 13); + a static publishing pipeline suffices (no backend). +- **Risks:** bootstrap distribution is a product/ops problem (RK-2); users who + never prepare arrive with baseline only. +- **Reversibility:** Not reversible without rebuilding — but no plausible future + requirement makes online-first desirable. +- **Related:** ADR-002, ADR-004, ADR-005, ADR-006, ADR-010. + +--- + +## ADR-002: PWA strategy + +- **Status:** Accepted — addendum after SPIKE-07 (validation: ARCHITECTURE-VALIDATION.md §2) +- **Context:** Lumen must not be a native app (brief). Target browsers: iOS + Safari, Android Chrome. DISCOVERY §12 established install-first as + load-bearing (storage persistence, future push, standalone UX on iOS). +- **Problem:** Define the delivery/installation model and how much to lean on + PWA-specific behaviors. +- **Decision:** Installed-first PWA on a single HTTPS origin. Web app manifest + with standalone display; install coaching is a first-class UX flow (manual + steps on iOS; native prompt on Android where available). The app must remain + fully functional when *not* installed, while honestly warning that storage + may not persist. No native wrappers (no TWA/App Store packaging). +- **Addendum (SPIKE-07):** Preparation has explicit levels L0 (shell + floor → NOT_READY/BASELINE_ONLY), L1 Minimum Safe (L0 + emergency + schedule → PARTIAL), L2 Full (L1 + map + info + assets → READY) (`SPIKE-07-BOOTSTRAP.md:31`). Fixed download order `emergency → schedule → info → map-base → assets`; L1 already yields core festival survival. Prep before install is allowed on iOS tab (same origin, survives until eviction) — coach offers "install first (recommended)" and "get data now" (`SPIKE-07-BOOTSTRAP.md:58`). Maps to readiness predicate C1–C8 (`SPIKE-05-OFFLINE-READY.md:28`). No direction change; install-first remains load-bearing but never blocks tab use. +- **Alternatives considered:** + 1. *Tab-only web app, no install push* — rejected: iOS 7-day eviction of + non-installed storage makes the offline promise unreliable (DISCOVERY PW-1). + 2. *Native wrapper (TWA/wrapped webview)* — rejected: store dependency + violates brief; adds ops without adding capability we need in V1. + 3. *Native app* — out of scope by definition. +- **Evaluation criteria:** storage persistence prospects; install friction; ops + surface; compliance with "no app stores" constraint. +- **Reasoning:** Installation is the single highest-leverage action available on + the web platform for this product: it improves storage persistence, unlocks + standalone UX, and is the precondition for any future push. The architecture + therefore spends UX effort on install coaching deliberately. +- **Consequences:** install coach UX required; capability detection must + distinguish installed vs tab mode; all features must still work in tab mode + (degraded-persistence warning only). +- **Risks:** users refuse/forget to install (RK-1); iOS EU region variability. +- **Reversibility:** Install coaching can be relaxed later; the single-origin + model is hard to reverse (storage is origin-keyed) — accepted. +- **Related:** ADR-001, ADR-004, ADR-014. + +--- + +## ADR-003: Frontend technology + +- **Status:** Accepted (with explicit reconsider trigger) +- **Context:** Small/solo team (DISCOVERY A-17); low-end Android in audience + (A-13); app surface is modest: 4 destinations, lists, details, map, status. + DISCOVERY AD-1 left this open. +- **Problem:** Choose the UI technology stack. +- **Decision:** **Vanilla TypeScript (strict), no UI framework.** A small + internal observable-store module, a small history-API router, and DOM + rendering via structured, escaping render functions. Build with a + single-bundle bundler producing hashed immutable assets. +- **Alternatives considered:** + + | Alternative | Runtime size | Churn/longevity risk | Complexity for small team | Low-end perf | + |---|---|---|---|---| + | Vanilla TS (chosen) | ~0 beyond own code | None (no deps) | Requires discipline (no framework guardrails) | Best | + | Preact | ~4 KB | Low (stable, mature) | Familiar mental model | Very good | + | Svelte | Small output | Medium (compiler-coupling; major-version churn) | Good DX | Very good | + | React | ~40 KB+ runtime | Medium | Very familiar talent pool | Good, not best | + | Vue/Angular/Solid | Medium+ | Low-Medium | Good DX | Good | + +- **Evaluation criteria:** bundle/JS budget on low-end devices; dependency churn + over multi-year festival lifetime; operational risk for a small team; + accessibility control; framework-neutrality of offline architecture. +- **Reasoning:** The app's UI complexity is low and its lifetime is long + (reused across festival editions). Every framework would work; none solves a + problem Lumen actually has, while each adds bundle weight and version-churn + risk. "Simplest architecture that satisfies requirements" (principle 15) + selects vanilla TS. This is explicitly *not* chosen for ideological reasons; + the reconsider trigger below is part of the decision. +- **Consequences:** hand-rolled store/router (small, tested); stricter code + review on UI correctness; zero framework upgrade burden; smallest possible JS. +- **Risks:** hand-rolled UI bugs (RK-7); onboarding friction if contributors + expect a framework. +- **Reversibility / reconsider trigger:** If during implementation the view + layer shows sustained complexity beyond simple store→render (e.g., many + interdependent live views), migrate to **Preact** (closest mental model, + smallest migration cost). The layering in ARCHITECTURE-DESIGN §6 keeps + domain/data code untouched by such a migration. +- **Related:** ADR-002, ADR-008. + +--- + +## ADR-004: Local storage + +- **Status:** Accepted (YELLOW — device-conditional) — validated at design-logic level by SPIKE-01 + SPIKE-02; production readiness conditional on physical-device tests per SPIKE-01 §5 (see ARCHITECTURE-VALIDATION.md §2, §4, §7) +- **Context (validation update):** SPIKE-01 24-item findings confirm IDB is correct for this workload (spec CONFIRMED for transactions/atomicity/multiple DBs); SPIKE-02 16/16 PASS proves A/B ordering makes single-DB atomicity sufficient. Remaining INFERRED/UNVERIFIED items (iOS jetsam atomicity, quota-near-full writes, Blob read-back on low-end) are isolated and queued for device matrix — they do not contradict the decision. +- **Context:** DISCOVERY AD-3/TQ-2/TQ-3. Everything offline depends on local + storage that browsers may evict. +- **Problem:** Choose storage technologies and layout for shell, datasets, and + user state. +- **Decision:** + - **IndexedDB** for datasets (A/B slot databases) and user state, accessed + through a thin internal promise wrapper (no external DB library). + - **Cache Storage** for the app shell only. + - **localStorage** only for trivial, non-load-bearing flags (guarded). + - Assets stored as Blobs inside the dataset slot DB (one failure domain for + rollback). +- **Alternatives considered:** + 1. *Dexie/idb libraries* — rejected for V1: access patterns are few and + known; an internal wrapper (~small module) avoids a dependency and keeps + IDB transaction discipline audited in one place. Reconsider if wrapper + proves error-prone. + 2. *SQLite-in-WASM (wa-sqlite/cr-sqlite)* — rejected: dataset scale (≤1k + events, ≤50 MB) doesn't need SQL; WASM memory limits on iOS and added + complexity fail the simplicity criterion. Reconsider only if query needs + explode (DD-10). + 3. *OPFS files* — rejected as primary: workable but weaker query/transaction + semantics; no advantage at this scale. + 4. *localStorage-only* — rejected: size limits (5–10 MB) and synchronous API. +- **Evaluation criteria:** durability semantics; transactional atomicity + (needed by ADR-006); Blob support; iOS reliability evidence; dependency + surface; quota behavior. +- **Reasoning:** IDB is the only browser store that offers transactions + + structured data + Blobs + generous quotas on both targets. The thin-wrapper + choice follows principle 15. SPIKE-01 validated the design at spec/policy level; the sole + remaining platform assumption (IDB atomicity under iOS jetsam kill) is isolated to one UNVERIFIED item queued for device testing in SPIKE-01 §5 — it does not block the decision. +- **Consequences:** A/B slots are separate IDB databases + a system DB holding + the active pointer; rollback/activation is one transaction on the system DB; + eviction detection via boot verification (no eviction event exists on the + platform). +- **Validation addendum (SPIKE-01 P1–P8, SPIKE-02 F-1…F-3 — normative):** P1 one short txn per staged file (bytes+progress together); P2 activation single txn on `lumen-system`; P3 wrapped IDB + QuotaExceededError keeps active; P4 free-space pre-check 2× package via `storage.estimate()`; P5 single record ≤6 MB; P6 `persist()` requested but never relied upon; P7 boot light verification as eviction detection; P8 no dataset state in SW. SPIKE-02 adds: F-1 bytes+progress atomic, F-2 verification record in activation txn, F-3 `readbackPending` flag, F-4 rollback depth 1 accepted. +- **Risks:** iOS IDB edge bugs (mitigation: wrapper isolation, spikes, re-prep + recovery); silent eviction (mitigation: RECOVERY state); residual device risk is YELLOW until D1–D4 matrix passes (ARCHITECTURE-VALIDATION.md §4). +- **Reversibility:** Storage layout is internal to the data layer; migrating to + another store later touches DatasetStore/persistence modules only, not + features. Medium reversibility. +- **Related:** ADR-001, ADR-005, ADR-006. + +--- + +## ADR-005: Festival Data Package + +- **Status:** Accepted — addendum after SPIKE-04/05/06/08 (ARCHITECTURE-VALIDATION.md §2). Schema now normative per SPIKE-04 §2 fragments; `required` flag, no-expiration rule, emergency sub-versioning and user-data schema split are part of the decision. +- **Validation addendum:** F-1 `sections[*].required` (default true; readiness gates on required only, SPIKE-04/05); F-2 no expiration for datasets/sections — offline device must never see data "expire" (SPIKE-04:50); F-3 emergency independent `emergencySchemaVersion`/`contentVersion`/`updatedAt` for floor/dataset compatibility (SPIKE-04:70, SPIKE-06:110); F-4 user-data `schemaVersion` separate from package schema with idempotent boot migrations. Budgets and pipeline gates (5 gates, SPIKE-04:189) enforce per-file ≤6 MB, image caps from SPIKE-03, total ≤40 MB. JSON+hash+Ed25519 remains sufficient at this scale (F-5). +- **Previous status:** Accepted +- **Context:** DISCOVERY §15, AD-4/AD-10. Content must be versioned data + (invariant 13); organizers need an authoring path (AMB-2). +- **Problem:** Define the package format, structure, authoring source, and + publication model. +- **Decision:** Festival Data Package v1 = a directory of **JSON section + documents** (`emergency`, `schedule`, `map`, `info`, `assets` inventory) + + binary assets + `manifest.json` (inventory, SHA-256 hashes, sizes, edition, + monotonic packageVersion, schemaVersion, compatibility envelope, festival + metadata) + `signature.json` (Ed25519 over the manifest digest). + Authoritative source: a **content repository** (organizer-edited sheets and + blocks) transformed by an automated validate→build→hash→sign→upload pipeline. + Distribution: immutable versioned URLs on the static CDN + mutable + `latest.json` pointer. +- **Alternatives considered:** + 1. *MessagePack/CBOR binary sections* — rejected: ~30–50% size win on ≤3 MB + of JSON is not worth losing human-auditability and simpler tooling. + 2. *SQLite file as the package* — rejected: couples data format to a storage + engine choice; integrity-per-file becomes coarser; rejected engine + (ADR-004). + 3. *Single monolithic JSON file* — rejected: prevents per-section partial + readiness (PARTIAL states), prioritized download order, and section-level + quarantine. + 4. *Dynamic CMS/API backend* — rejected: adds server state, auth, and ops + for zero V1 benefit (A-17). +- **Evaluation criteria:** authorability; verifiability/integrity granularity; + partial-readiness support; size; tooling simplicity; longevity. +- **Reasoning:** JSON sections give section-level hashes, prioritized + downloads, and human-inspectable content (emergency content must be + auditable). The manifest centralizes completeness so the app never guesses + (invariant 8/10 of discovery R-D3). Static publishing matches the ops + reality of a small team. +- **Consequences:** pipeline tooling must exist (schema validators, budgets, + signer); stable event IDs become contractual; content review happens in the + repo (audit trail). +- **Risks:** organizer content quality/variance (mitigation: intake templates + + pipeline validation gates — see OQ-2/OQ-6); JSON verbosity if content grows + (budgets guard). +- **Reversibility:** Format is versioned (`lumen.package/1`); a v2 format can + coexist via schemaVersion envelope. High reversibility by design. +- **Related:** ADR-004, ADR-006, ADR-013, ADR-014. + +--- + +## ADR-006: Atomic updates and rollback + +- **Status:** Accepted (YELLOW — device-conditional) — state machine proven 16/16 by SPIKE-02; production readiness conditional on IDB atomicity under real kills on iOS (SPIKE-01 §5, SPIKE-02 §6) — see ARCHITECTURE-VALIDATION.md §2, §4, §7 +- **Validation addendum (SPIKE-02 F-1…F-4 — normative):** F-1 bytes+progress in same per-file txn; F-2 verification record (what/when/version) in activation txn; F-3 `readbackPending` flag set in activation and cleared after readback; F-4 rollback depth = 1 (new staging wipes inactive slot — accepted trade-off, 3-slot rejected for footprint). Ordering proof: single-DB atomicity suffices because inactive slot is fully written+validated before pointer moves; quarantine + monotonic versions prevent replay of bad packages. +- **Context:** Invariants 5–8; DISCOVERY AD-6; iOS SW-kill reality (PW-6). +- **Problem:** Apply dataset updates such that the observable state is always + "old valid dataset" or "new valid dataset", surviving interruption at any + stage; provide rollback. +- **Decision:** **A/B dual-slot model.** Two dataset slots (IDB databases). + Updates stage exclusively into the *inactive* slot with per-file download, + SHA-256 verification, and persisted progress (resumable). After full + verification, activation is a **single transaction** on the `lumen-system` + DB flipping the active pointer + recording version/verification metadata. + Post-activation readback spot-check; failure triggers automatic rollback to + the previous slot. Previous slot retained for user-initiated restore until + next staging or GC policy. Organizer-side rollback = republish old content at + a new higher version (runbook). Downloads run in the **page context** + (C-21), never the SW. +- **Alternatives considered:** + 1. *In-place overwrite with journaling* — rejected: far more failure modes; + IDB gives cheap whole-database slots, so journaling complexity is + unnecessary. + 2. *Version-keyed stores with GC (no fixed slots)* — rejected: unbounded + store proliferation complicates quota reasoning; A/B bounds storage at + 2× dataset. + 3. *SW-driven download + Cache API staging* — rejected: iOS terminates SWs + aggressively; page-context downloads survive SW death and can show + progress/resume UX. +- **Evaluation criteria:** invariant compliance under kill-at-any-point; + storage overhead; resumability; implementation simplicity; rollback support. +- **Reasoning:** The pointer-flip transaction is the smallest possible atomic + boundary; keeping all writes on the inactive slot makes the invariant + trivially arguable. All nine failure stages map to "old dataset untouched" + or "rollback to old" (ARCHITECTURE-DESIGN §18.2). +- **Consequences:** 2× worst-case dataset storage (bounded by budgets ≤40 MB → + ≤80 MB worst case; GC trims); staging progress schema needed; activation + metadata is the single source of readiness truth. +- **Risks:** quota pressure during staging with both slots full (mitigation: + GC trigger before staging; pre-check free space); pointer-flip transaction + failure on pathological IDB state (mitigation: retry once → keep old → + diagnostics). +- **Reversibility:** The slot mechanism is internal to data/sync layers; can be + replaced without touching features. Medium reversibility. +- **Related:** ADR-004, ADR-005, ADR-010, ADR-013. + +--- + +## ADR-007: Emergency baseline + +- **Status:** Accepted — addendum after SPIKE-06 (ARCHITECTURE-VALIDATION.md §2). Fixed tier contents, merge rules, and zero-IDB forward-tolerant floor path are now normative. +- **Validation addendum (SPIKE-06):** Tier-1 floor JSON shape fixed (`SPIKE-06:12` life-safety minimum, ≤16 KB), tier-2 full detail, tier-3 reserved signed ephemeral notices. Resolution `render_emergency()` prefers highest compatible tier with defensive merge (`SPIKE-06:60`); F-1 hardening: floor renderer forward-tolerant and reachable with zero IDB access for BASELINE_ONLY/eviction (`SPIKE-06:75`). Same-source generation of floor + dataset section prevents drift (`ARCHITECTURE-DESIGN.md:345`). +- **Context:** Invariant 2; DISCOVERY OF-3/EM-2; emergency must survive even + total storage loss. +- **Problem:** Decide whether and how to embed emergency information in the + app shell in addition to the dataset. +- **Decision:** Three tiers. **Tier 1 — embedded baseline:** a minimal emergency + record (emergency number + dial action, festival address, coordinates, + security contact, first-aid/AED summaries, muster points, exit summaries, + core procedures) **compiled into the app shell at build time**, generated + from the *same emergency source sheet* that produces the dataset section + (no drift). Immutable per shell build; ≤16 KB; frozen schema. **Tier 2 — + dataset emergency section:** full detail, signed, updateable. **Tier 3 — + optional live notices:** reserved seam, not V1. Rendering prefers the + highest verified tier; provenance always labeled. +- **Alternatives considered:** + 1. *Dataset-only emergency* — rejected: fails invariant 2 under eviction / + failed update / incompatible dataset. + 2. *Baseline hand-maintained separately from dataset content* — rejected: + drift risk between baseline and dataset is a safety issue; single-source + generation removes it. + 3. *Baseline in localStorage instead of compiled-in* — rejected: localStorage + dies with storage eviction too; only shell bytes survive everything. +- **Evaluation criteria:** survival under every failure mode; content drift + risk; update latency tolerance; size cost. +- **Reasoning:** Emergency is the one feature where "degraded but present" is + unacceptable; embedding in immutable shell bytes is the only mechanism that + survives total storage loss. Generating it from the same source eliminates + the classic two-copies drift hazard. +- **Consequences:** baseline updates require a shell release (slower cadence — + accepted, and labeled); shell build pipeline gains a baseline-generation + step; emergency rendering has a no-IDB path. +- **Risks:** baseline content frozen at build time while contacts change + (mitigation: Tier 2 updates + provenance labels + organizer physical + channels); baseline scope creep (mitigation: hard 16 KB cap and content + policy). +- **Reversibility:** Additive; tiers can be extended without removing the floor. +- **Related:** ADR-001, ADR-005, ADR-013, ADR-014. + +--- + +## ADR-008: Map architecture + +- **Status:** Accepted (YELLOW — device-conditional) — validated by SPIKE-03; production readiness conditional on device protocol in SPIKE-03 §7 (see ARCHITECTURE-VALIDATION.md §2, §4, §7) +- **Validation addendum (SPIKE-03 F-1,F-3,F-4 — normative):** F-1 replaces 4096 cap: overview ≤1600 px longest edge, detail ≤3072 px, total decoded ≤~35 MB, at most one detail resident (OER; swaps overview out) (`SPIKE-03:58`); F-3 lazy object-URL lifecycle (create per level, revoke on switch); F-4 dark-mode `brightness(.72) saturate(.85)` + optional night asset hook `map-base/overview-night` (`SPIKE-03:88`); WebP 28 MB budget stands with margin. Flip conditions + fallbacks (overview-only, 2×2 tile-split) predefined. +- **Previous pending:** real-art bake-off and real map art (OQ-6) — now tracked as device/content validation, not a Proposed decision. +- **Context:** DISCOVERY AD-7, AMB-3; no online tiles allowed; GPS never + required; low-end devices; organizers most naturally produce illustrated + raster art. +- **Problem:** Choose the offline map representation and interaction model. +- **Decision:** **Raster WebP base map (≤2 zoom levels: overview + optional + detail) + data-driven DOM POI overlay + CSS-transform pan/zoom + first-class + Facilities list view.** POIs carry normalized coordinates in map.json; + markers are real DOM buttons; detail level ≤4096px per side, lazy-decoded; + no geolocation in V1 (schema hook preserved). +- **Alternatives considered:** + 1. *Single vector SVG map* — crisp and small for schematic maps, but: + organizer art is typically illustrated raster; complex SVG pan/zoom + perf on low-end devices is unpredictable; large-SVG accessibility is + awkward. Would be reconsidered if organizers supply vector art and + SPIKE-3 shows SVG perf is fine (DD-9 adjacent). + 2. *Canvas rendering* — good perf, but: manual hit-testing, poor + accessibility (needs parallel text tree anyway → the list view already + provides it), more code. + 3. *Pre-cached raster tile pyramid* — rejected: tile bookkeeping complexity + without need; venue-scale map fits 2 full-image levels under budget. + 4. *Online tiles with offline cache* — rejected by constraint (no core + dependence on online tiles). +- **Evaluation criteria:** offline completeness; low-end perf; accessibility; + file size; authoring workflow; interactive markers; search integration; + maintainability. +- **Reasoning:** GPU-scaled image transforms give consistent pan/zoom + performance independent of POI count; DOM markers give free accessibility + and tap targets; the Facilities list doubles as the screen-reader path and + the GPS-free "find nearest facility" path; raster matches what organizers + actually produce. +- **Consequences:** image budgets (≤28 MB map art) enforced in pipeline; + coordinate authoring step in pipeline (tap-tool or measured coords); + detail-level switching logic; no blue dot in V1. +- **Risks:** art exceeds budget (mitigation: split/drop detail level); + transform jank on very old devices (mitigation: SPIKE-3 on low-end device; + fallback = overview-only mode). +- **Reversibility:** Map rendering is isolated in MapService; swapping + representation later doesn't touch other features. High reversibility. +- **Related:** ADR-003, ADR-005, ADR-009. + +--- + +## ADR-009: Time model + +- **Status:** Accepted — addendum after SPIKE-08 (ARCHITECTURE-VALIDATION.md §2). Change F-3 normative; clarification F-4 normative. +- **Validation addendum (SPIKE-08 24/24 PASS, V8/ICU):** T1–T7 all PASS; UTC + explicit-zone `Intl` correct offline; precomputed `dayKey` eliminates client day math; DST and 30-min zones handled. **F-3 (change):** sanity-window warning suppressed until `now ≥ festivalStart − 45d` or a skew has been captured — prevents noisy warning for early preparation (`SPIKE-08-TIME-MODEL.md:97`). **F-4 (clarification):** only `Intl.DateTimeFormat` with explicit `timeZone` is allowed; no wall-clock string storage, no manual offset arithmetic. JSC parity on iOS and CDN `Date` observation remain UNVERIFIED queued for device matrix (§4, §7). +- **Previous status:** Accepted +- **Context:** DISCOVERY AD-8, R-S5; Now/Next must work offline; device clocks + may be wrong. +- **Problem:** Define storage, rendering, and correctness strategy for time, + including wrong clocks and DST. +- **Decision:** Events stored as **UTC epoch ms**. Manifest carries the + festival **IANA timezone** and festival window. Rendering via + `Intl.DateTimeFormat` in festival zone (user toggle: device zone). Day + boundaries are **precomputed at publish time** (`dayKey`). A ClockService + computes `now()`: persisted server-time offset (captured from HTTP `Date` + header during any sync) corrects device time; a monotonic anchor detects + mid-session clock changes; a sanity window (festival window ±45 days) + triggers "check your clock" warnings. Never-online devices use device clock + with the sanity heuristic only. +- **Alternatives considered:** + 1. *Store/render local festival wall-clock strings* — rejected: ambiguous + across device zones, breaks instant arithmetic for Now/Next and overlaps. + 2. *Require network time (NTP-like endpoint)* — rejected: violates offline + invariants. + 3. *Trust device clock unconditionally* — rejected as sole strategy: wrong + clocks silently break Now/Next; the offset capture is nearly free. + 4. *Continuous background clock sync* — rejected (C-19). +- **Evaluation criteria:** offline correctness; wrong-clock resilience; DST + safety; implementation simplicity. +- **Reasoning:** UTC storage + IANA rendering is the standard correct answer; + the offset mechanism adds real wrong-clock resilience at trivial cost; + precomputed `dayKey` removes client-side day-boundary ambiguity entirely. +- **Consequences:** sync responses must expose a server timestamp (HTTP Date is + sufficient); clock warnings are part of UX; residual risk for never-online + wrong clocks is declared and accepted (absolute times remain readable). +- **Risks:** organizer supplies wrong IANA zone (mitigation: pipeline + validation + fixture tests for DST edges); offset poisoned by a wrong server + clock (mitigation: sanity window; offset only shifts display logic, never + stored event data). +- **Reversibility:** Storage format (UTC ms) is stable regardless of rendering + policy changes. High reversibility for rendering policy. +- **Related:** ADR-001, ADR-005, ADR-010. + +--- + +## ADR-010: Synchronization + +- **Status:** Accepted +- **Context:** DISCOVERY A-07/A-12, SY-* questions; no accounts; connectivity + is enhancement-only. +- **Problem:** Define if/how the app talks to servers. +- **Decision:** **Pull-only synchronization**: on app open, on `online` hint, + and on manual request. The only sync operations: fetch `latest.json`, fetch + candidate package, stage/verify/activate (ADR-006). Versions are monotonic; + clients never accept a network downgrade. No uploads, no user-state sync, no + background sync registration, no timers, no push in V1. Announcements schema + reserved but unimplemented (DD-1). +- **Alternatives considered:** + 1. *Periodic background sync* — rejected: unsupported on iOS (C-19) and + unnecessary since sync-on-open covers all needs. + 2. *Push-based updates* — rejected for V1: iOS push requires install + + region/permission fragility; adds server infra; the pull model already + guarantees eventual consistency whenever the user opens the app. + 3. *WebSocket/long-poll live channel* — rejected: server state + connection + management for no V1 requirement. +- **Evaluation criteria:** platform support on both targets; server ops burden; + privacy; invariant 10 compliance. +- **Reasoning:** Pull-only on static files achieves every V1 need with zero + server state and zero permissions; everything more exotic fails the + simplicity criterion. +- **Consequences:** update latency = time-until-next-open-online (accepted and + documented); organizers needing urgency use physical channels (runbook). +- **Risks:** critical schedule change reaches users late (mitigation: + changed-event markers, dataset timestamps, organizer comms redundancy). +- **Reversibility:** Additive — push/announcements can be layered on the same + transport seam later without changing pull. High reversibility. +- **Related:** ADR-005, ADR-006, ADR-011, ADR-014. + +--- + +## ADR-011: Networking abstraction + +- **Status:** Accepted +- **Context:** Invariant 11–12; DISCOVERY §17; brief requires a clean future + transport seam without V1 mesh contamination. +- **Problem:** Decide whether a transport abstraction belongs in V1 at all, and + if so its shape. +- **Decision:** A **minimal, byte-oriented Transport interface** under the Sync + Service: `isAvailable()`, `fetchPointer(edition)`, `fetchBytes(path)`. V1 + ships exactly one implementation (HTTP over the CDN). The Verifier — not any + transport — decides authenticity. Features/UI never import sync or transport + modules (boundary B-3). No registry, no plugin system, no transport + negotiation: the seam is the interface plus the layering rule. +- **Alternatives considered:** + 1. *No abstraction until a second transport exists (YAGNI-pure)* — + reasonable, but the interface costs ~one small module and prevents the + much costlier retrofit of untangling fetch calls from feature code; + accepted only in this minimal form. + 2. *Message/sync-semantics abstraction (CRDT op log, gossip protocol + interfaces)* — rejected for V1: speculative; encodes mesh assumptions + (violates principle 14); a byte transport + signed payloads covers every + conceivable future transport, since packages are immutable signed objects. + 3. *Adapter over fetch scattered in services* — rejected: that is the leak + invariant 12 forbids. +- **Evaluation criteria:** cost in V1; leak-proofness; sufficiency for plausible + future transports (LAN mirror, native-companion bridge, mesh gateway). +- **Reasoning:** Any future delivery mechanism — including mesh gateways and + native companions — can present "fetch these bytes"; verification stays + client-side. That makes the byte interface the least-speculative seam that + satisfies the requirement. +- **Consequences:** SyncService is transport-parameterized; tests can inject a + fault transport (fault injection for §18.2 stages); UI shows no transport + identity. +- **Risks:** under/over-specification discovered when a real second transport + appears (mitigation: interface is deliberately narrow; versioning it is + cheap). +- **Reversibility:** High — interface is internal. +- **Related:** ADR-006, ADR-010, ADR-012, ADR-013. + +--- + +## ADR-012: Future mesh strategy + +- **Status:** Accepted (strategy only; nothing implemented) +- **Context:** Brief: mesh is future-only; DISCOVERY §17 concluded browsers + alone almost certainly cannot host a meaningful mesh (no BLE peripheral, no + ad-hoc Wi-Fi, no background sockets; iOS lacks Web Bluetooth). +- **Problem:** Define how future mesh could attach without contaminating V1, + and what V1 must do now to keep that possible. +- **Decision:** V1 does exactly three things for future mesh: (1) the Transport + seam of ADR-011; (2) the **signed-payload rule** — bytes from any transport + are inert until the Verifier passes them, so untrusted peers can never + inject content; (3) package/announcement formats are immutable, + content-addressed, version-monotonic — properties any store-and-forward or + gossip transport needs. All mesh concerns (peer discovery, routing, dedupe, + replay beyond version monotonicity, peer auth, message size adaptation, + emergency broadcast semantics) are explicitly **future work**. A future mesh + is assumed to require a native companion, hardware relay, or organizer LAN + service; if that proves impossible, V1 loses nothing (HTTP is complete + functionality, not a placeholder). +- **Alternatives considered:** + 1. *Design a mesh message schema now* — rejected: encodes assumptions about + a technology not chosen, violating principle 14. + 2. *WebRTC data-channel groundwork in V1* — rejected: signaling needs + connectivity anyway; iOS constraints; zero V1 value; pure complexity. + 3. *Do nothing at all (no seam)* — rejected: the brief requires an extension + point; the seam chosen costs one interface. +- **Evaluation criteria:** V1 cost; leak-proofness; plausibility under multiple + future mesh shapes (BLE mesh via companion, LAN mirror, WebRTC via gateway). +- **Reasoning:** The cheapest correct move is to make Lumen's data plane + transport-agnostic and authenticity-strict, then stop. Every concrete mesh + capability browsers lack lives outside the app anyway. +- **Consequences:** documentation-only artifacts in V1; future mesh work starts + with a transport implementation + a feasibility study of the out-of-browser + component. +- **Risks:** the seam proves slightly wrong for a real mesh (mitigation: + narrow interface, cheap to version); false expectation that mesh is "easy + later" (mitigation: this ADR documents the out-of-browser likelihood). +- **Reversibility:** Fully — nothing is built. +- **Related:** ADR-011, ADR-013. + +--- + +## ADR-013: Security model + +- **Status:** Accepted as a model — crypto **library choice remains Proposed (YELLOW) pending audit** (ARCHITECTURE-VALIDATION.md §2, §7); overall threat model GREEN. Verifier choice (WebCrypto SHA-256 + bundled pure-JS Ed25519) validated as correct per TQ-9/SQ-1 but supply-chain audit is still required (AQ-06). +- **Validation note:** SPIKE-04/02 ordering (signature before hash), per-file SHA-256, monotonic anti-replay, CSP `self`-only, and data-minimization posture all reconciled; no spike contradicts the model. Key custody/rotation drill (AQ-21, S-01) remains ops gate. +- **Context:** DISCOVERY §18; invariants on data integrity; emergency content + is safety-relevant; data minimization mandated. +- **Problem:** Define integrity, authenticity, transport security, content + safety, and privacy posture without unnecessary complexity (no accounts). +- **Decision:** + 1. **Package signing:** Ed25519 signature over SHA-256 of the exact manifest + bytes; per-file SHA-256+size in manifest; client embeds a fingerprinted + **public key set** (rotation-capable). Verification implemented with a + small, audited, pure-JS Ed25519 verifier bundled in the shell (WebCrypto + Ed25519 is not guaranteed on the iOS 16.4 baseline) — library choice + Proposed pending audit check. + 2. **Transport:** HTTPS-only + HSTS; single origin; immutable package URLs. + 3. **Downgrade/replay protection:** monotonic packageVersion acceptance; + organizer rollback only via new version. + 4. **Content safety:** structured-data rendering only (no raw HTML); CSP + without inline script/eval; no third-party resources. + 5. **Privacy:** no accounts, no telemetry, no location capture; diagnostics + local, scrubbed, user-initiated. + 6. **Ops:** signing key offline; singular publisher role; repo audit trail; + key-loss runbook (ship new key via shell update). +- **Alternatives considered:** + 1. *TLS-only trust (no signatures)* — rejected: doesn't protect against + hosting/CDN compromise or publish-pipeline mistakes; emergency content + deserves defense in depth. + 2. *WebCrypto-only Ed25519* — rejected: baseline iOS gap (DISCOVERY TQ-9); + the bundled verifier removes the gap deterministically. + 3. *Full PKI/certificate-style scheme, JWT-based updates, or authenticated + user sessions* — rejected: complexity without a V1 threat that requires + it (principle: realistic over theoretical). +- **Evaluation criteria:** coverage of realistic threats (TH-1…TH-6); key + management practicality for a small team; client cost; privacy posture. +- **Reasoning:** Signature + per-file hashes + monotonic versions defeat the + realistic high-impact threats (tampered/malicious packages, emergency + spoofing, replay) with minimal moving parts; data minimization removes whole + threat classes rather than defending them. +- **Consequences:** signing tooling + key custody in ops; every dataset byte + verified before activation (cost: seconds at staging time); CSP strictness + shapes build output (hashed scripts only). +- **Risks:** signing key loss (runbook + key set rotation); verifier library + supply-chain risk (mitigation: pinned, audited, tiny; TH-11); strict CSP + friction during build (accepted). +- **Reversibility:** Signature scheme can be strengthened (key set mechanism); + removing security would be a regression, not a reversal. +- **Related:** ADR-005, ADR-006, ADR-010, ADR-011, ADR-012. + +--- + +## ADR-014: Deployment strategy + +- **Status:** Accepted on topology — **provider choice remains Proposed (YELLOW) pending selection** (AQ-14) (ARCHITECTURE-VALIDATION.md §2, §7) +- **Validation note:** Static CDN, single origin, immutable versioned URLs + `latest.json` pointer + staging/production origins + cache headers per ARCH §24 are all validated; provider is the only unresolved piece and is hard to reverse post-users (origin-keyed storage) so provisional staging origin is correct for development. +- **Previous:** Accepted (provider Proposed pending hosting choice — AQ-14) +- **Context:** DISCOVERY AD-9/AD-10/AD-14; A-17 (small team); HTTPS required; + all content is static signed files. +- **Problem:** Choose hosting topology, release topology, and publishing flow. +- **Decision:** **Static-only deployment on an HTTPS CDN, single origin.** + Layout: `/` (index, `no-cache`), `/assets/` (immutable), + `/latest.json` (short-lived), `/editions//packages//**` (immutable). + Publishing: content repo → automated pipeline (validate → build sections → + hash → sign → upload → pointer update → smoke check). Two environments: + staging origin and production origin, same code, different origins. App + shell builds are separate releases from dataset publishes (independent + cadence, compatibility envelope per ADR-005/§18.6). No dynamic backend. +- **Alternatives considered:** + 1. *Small dynamic server (for latest.json logic, telemetry, push)* — + rejected: no V1 feature needs server logic; ops burden fails A-17. + 2. *Self-hosted single server* — possible but: CDN gives free resilience + + edge caching for pre-festival load spike; self-hosting concentrates + outage risk (RK-10). Reconsider only if cost/access constraints demand. + 3. *Object storage without CDN front* — equivalent in simplicity; CDN edge + preferred for the pre-festival install spike; provider choice left open. +- **Evaluation criteria:** ops burden; resilience during festival; cost; + publish safety (staging → prod); HTTPS/headers control. +- **Reasoning:** Everything the system serves is immutable bytes or one tiny + mutable pointer; static hosting is the exact fit and keeps the failure + surface to "files in or out". +- **Consequences:** publisher credentials live at pipeline/CDN layer; publish + runbooks required; cache headers are part of the contract; staging edition + for tests. +- **Risks:** CDN outage blocks updates during festival (impact low — devices + hold last-good data; RK-10); provider lock-in minimal (plain files). +- **Reversibility:** Provider swap is a DNS/origin change with storage + consequences for existing users (origin-keyed storage!) — an origin change + strands installed data; therefore pick the production origin deliberately + and early (AQ-18 blocks production setup, not development). +- **Related:** ADR-002, ADR-005, ADR-013. + +--- + +## Decision dependency map + +```mermaid +flowchart TD + A1[ADR-001 Offline-first] --> A2[ADR-002 PWA strategy] + A1 --> A4[ADR-004 Local storage] + A1 --> A5[ADR-005 Festival Data Package] + A1 --> A10[ADR-010 Sync pull-only] + A2 --> A14[ADR-014 Deployment] + A4 --> A6[ADR-006 Atomic updates] + A5 --> A6 + A5 --> A7[ADR-007 Emergency baseline] + A5 --> A9[ADR-009 Time model] + A5 --> A8[ADR-008 Map] + A5 --> A13[ADR-013 Security] + A6 --> A11[ADR-011 Transport seam] + A10 --> A11 + A11 --> A12[ADR-012 Mesh strategy] + A13 --> A12 + A3[ADR-003 Vanilla TS] --> A8 +``` diff --git a/ARCHITECTURE-DESIGN.md b/ARCHITECTURE-DESIGN.md new file mode 100644 index 0000000..eb0ece5 --- /dev/null +++ b/ARCHITECTURE-DESIGN.md @@ -0,0 +1,950 @@ +# Lumen — Architecture Design (Phase 1) + +- **Phase:** Architecture Phase 1 — decisions and design only. No implementation, no code, no prototypes. +- **Date:** 2026-08-30 +- **Source of truth:** `/home/avi/Projects/Lumen/DISCOVERY.md` +- **Companion documents:** `ARCHITECTURE-DECISIONS.md` (ADRs), `ASSUMPTIONS-AND-OPEN-QUESTIONS.md` +- **Decision keys:** `ADR-xxx` = formal decision record; `AD-x` = discovery's open-decision IDs; `AQ-x`/`OQ-x` = assumption/question IDs. + +## Relationship to DISCOVERY.md + +No substantive disagreements with the discovery findings. This document **resolves** discovery's open decisions AD-1…AD-14 and technical questions TQ-1…TQ-14 where evidence allows, and explicitly narrows a few discovery positions: + +| Discovery item | Architecture resolution | Nature | +|---|---|---| +| TQ-9 (Ed25519 WebCrypto on iOS 16.4 baseline) | WebCrypto Ed25519 is **not** reliably present on the iOS 16.4 baseline. Resolved by bundling a small, audited pure-JS Ed25519 verifier in the app shell instead of relying on WebCrypto. See ADR-013. | Resolution | +| AD-3 candidate: SQLite-in-WASM | **Rejected** for V1 (complexity, WASM memory risk on iOS, no requirement that needs it). IndexedDB selected. ADR-004. | Decision | +| AD-2 candidate: Workbox | **Rejected** in favor of a small hand-written service worker. ADR-002 rationale in §8. | Decision | +| Discovery R-M4 "geolocation may be used" | Narrowed: **no geolocation in V1 at all** (not even optional blue dot). GPS remains enhancement-only per invariant 9; schema keeps a hook. §15. | Narrowing | +| Discovery A-02 budget "≤ 50 MB" | Kept as hard ceiling; refined into per-part budgets totaling ≤ 40 MB target. §10.6, §27. | Refinement | +| Discovery OF-3 embedded emergency baseline | **Adopted** as a formal decision (ADR-007), with the refinement that the baseline is *generated from the same emergency source content* as the dataset to prevent drift. §13. | Adoption + refinement | +| Discovery §15.7 clock recommendation | Adopted: UTC storage + festival IANA zone + persisted server-time offset + wrong-clock heuristics. §14, ADR-009. | Adoption | +| SPIKE-08 F-3 sanity-window suppression | Sanity warning suppressed until near festival (`now ≥ start−45d` or skew captured) to avoid noisy early-prep warnings. §14.3, ADR-009. | Refinement (validation) | +| SPIKE-03 F-1 map caps | Overview ≤1600 px / detail ≤3072 px / total ≤~35 MB — replaces 4096 cap. §15, §27, ADR-008. | Refinement (validation) | +| SPIKE-01 P1–P8 + SPIKE-02 F-1…F-4 | A/B transactional protocol hardening (per-file txn, verification record, readbackPending, rollback depth 1). §9.2, §18. | Refinement (validation) | +| SPIKE-04 F-1…F-4 package schema | `required` flag, no-expiration rule, emergency sub-versioning, user-data schema split. §10. | Refinement (validation) | +| SPIKE-05 offline-ready formalization | Predicate C1–C8 + time independence + FAILED state + six-state taxonomy. §12. | Formalization | +| SPIKE-06 emergency floor hardening | Fixed tier-1 contents, zero-IDB forward-tolerant renderer. §13, ADR-007. | Refinement (validation) | +| SPIKE-07 bootstrap L0–L2 | Explicit levels + ordered download + minimum safe L1 + tab-before-install allowed. §11/§12, ADR-002. | Refinement (validation) | + +--- + +# 1. Executive Summary + +Lumen is an **offline-first, mobile-first Progressive Web App** for 300–500 festival attendees. The entire critical experience (Emergency, Schedule, Map, Festival info, My Schedule) must work in airplane mode after a one-time online preparation step. + +The architecture in one paragraph: + +> A small, framework-free TypeScript app shell is served from static HTTPS hosting and cached by a **minimal hand-written service worker**. Festival content is delivered as a **signed, versioned Festival Data Package** (JSON sections + assets + signed manifest), downloaded by an app-layer sync service through a **transport abstraction** (V1: HTTPS pull-only), stored in **IndexedDB using an A/B dual-slot model**, and activated only after full integrity verification via a **single atomic metadata transaction**. A minimal **emergency baseline is compiled into the app shell itself**, so emergency information survives even total storage loss. An explicit, evidence-based **OFFLINE READY state machine** reports readiness honestly. No accounts, no server-side state, no mesh — only a clean seam where future transports could attach. + +Key decisions (details in ADRs): + +| Area | Decision | ADR | +|---|---|---| +| Offline posture | Offline-first; local data is the source of truth | ADR-001 | +| Platform | Installed-first PWA, single origin, HTTPS | ADR-002 | +| Frontend | Vanilla TypeScript, no framework | ADR-003 | +| Storage | IndexedDB (thin internal wrapper) + Cache Storage for shell | ADR-004 | +| Data format | JSON sections + assets + signed manifest (Festival Data Package v1) | ADR-005 | +| Updates | A/B dataset slots, hash-verified staging, atomic pointer flip, retained rollback slot | ADR-006 | +| Emergency | Embedded immutable baseline + signed dataset overlay | ADR-007 | +| Map | Raster WebP base (≤2 zoom levels) + data-driven DOM POI overlay + list view | ADR-008 | +| Time | UTC instants + festival IANA timezone + persisted server-time offset | ADR-009 | +| Sync | Pull-only, on-open + opportunistic; no accounts; monotonic versions | ADR-010 | +| Networking | Transport interface under a Sync Service; V1 ships one HTTP implementation | ADR-011 | +| Mesh | Future-only; seam via signed-payload-over-transport rule | ADR-012 | +| Security | Ed25519-signed manifests, TLS+HSTS, CSP, content-as-data, data minimization | ADR-013 | +| Deployment | Static CDN hosting + content repo + automated sign/publish pipeline | ADR-014 | + +--- + +# 2. Architectural Goals + +| # | Goal | +|---|------| +| G-1 | Critical functionality works with zero connectivity (invariants 1–4). | +| G-2 | The app can always answer, with evidence, whether it holds a complete verified dataset (invariant 8). | +| G-3 | Updates are atomic; a last-known-good dataset always survives failures (invariants 5–7). | +| G-4 | Emergency information exists even after total storage loss (invariant 2). | +| G-5 | Minimal operational surface for a small team: static hosting, no backend state, no accounts (discovery A-17). | +| G-6 | Honest degradation: every failure mode has a defined, non-blank user-visible state. | +| G-7 | A future transport (incl. mesh) can be added without rewriting features (invariants 11–12). | +| G-8 | Smallest architecture that satisfies the above (invariant 16 / discovery principle 15). | + +# 3. Architectural Constraints + +Binding constraints (from the brief + discovery §5, ratified): + +1. Critical V1 features work offline. 2. Emergency baseline always local. 3. Startup never needs network. 4. App keeps functioning after connectivity loss. 5. Last valid dataset survives failed updates. 6. Updates atomic. 7. Never knowingly expose a partial dataset. 8. Offline readiness explicitly verifiable. 9. GPS never required. 10. Network = enhancement. 11. No mesh in V1. 12. Mesh concerns never leak into features/UI. 13. Content = versioned data. 14. iOS Safari storage/lifecycle limits are design inputs. 15. Android Chrome behavior is a design input. 16. Fail safely when optional capabilities are missing. + +Derived constraints (ratified from discovery, carried forward): + +- **C-16 Install-first:** storage persistence and any future push on iOS hinge on home-screen install → install coaching is architecture-driving UX. +- **C-17 Bootstrap honesty:** offline capability requires one successful online bootstrap. +- **C-18 Eviction survival:** app must detect wiped storage and enter a recovery state, never a blank screen. +- **C-19 Foreground-only:** nothing depends on background execution, Background Sync, or push wakeups. +- **C-20 Single origin:** shell, data endpoints, and assets all live on one HTTPS origin. + +New constraints introduced by this phase: + +- **C-21 Document-context downloads:** festival package downloads run in the page (document) context, not the service worker, because iOS terminates service workers aggressively and pages can resume staged downloads. The SW stays tiny. +- **C-22 Content is never rendered as HTML:** festival content is structured data rendered through an escaping renderer; no raw HTML ingestion (security). +- **C-23 Compatibility envelope:** app shells support a declared range of data schema versions; publishers never release a schema outside the deployed envelope (ordering rule, §18.6). +- **C-24 No secrets client-side:** no private keys, credentials, or API keys ever ship in the app. + +# 4. System Context + +```mermaid +flowchart TD + subgraph Attendee Device + UI[Lumen PWA
UI + domain services] + SW[Service Worker
shell cache only] + IDB[(IndexedDB
dataset slots + user state)] + CS[(Cache Storage
app shell)] + end + + subgraph Festival Organization + CR[Content repo
schedule / map / emergency / info] + PB[Publish pipeline
validate, hash, sign] + end + + CDN[Static HTTPS CDN
app shell + signed packages] + + CR --> PB --> CDN + CDN -. HTTPS pull .-> UI + UI <--> IDB + UI <--> CS + SW <--> CS + + FUTURE[Future transport
local network / mesh / native companion] + FUTURE -. future only, signed payloads .-> UI +``` + +Actors and responsibilities: + +| Actor | Responsibility | +|---|---| +| Content repo | Authoritative source of all festival content (discovery AMB-2 default). | +| Publish pipeline | Schema validation, size budgets, hashing, Ed25519 signing, upload, pointer update, smoke check. Humans press "publish"; machines do everything else. | +| Static CDN | Immutable versioned packages + mutable `latest.json` pointer + app shell. No logic. | +| PWA | Everything else: storage, verification, readiness, UX. | +| Future transport | Delivers the *same signed payloads* by another route; verification never moves. | + +# 5. Recommended Technology Stack + +| Concern | Selection | Rationale (short) | ADR | +|---|---|---|---| +| Language | TypeScript (strict) | Type safety for data-heavy code; owner familiarity (discovery §1.4); no runtime cost. | ADR-003 | +| UI framework | **None** — vanilla TS, small internal store + router | Smallest bundle, zero dependency churn, festival-app longevity, low-end perf. Fallback: Preact if UI complexity outruns the model. | ADR-003 | +| Build | Single-bundle bundler (esbuild/vite-class) producing hashed immutable assets | Implementation-phase pick; constraint: hashed filenames + precache manifest output. | — | +| Service worker | Hand-written, ~small, precache + navigation fallback only | Full control of lifecycle; no library surface. | ADR-002 | +| Structured storage | IndexedDB via thin internal promise wrapper | Structured data, transactions, Blob storage, quota-friendly. No external DB library; no SQLite-WASM. | ADR-004 | +| Small flags | localStorage (try/catch-guarded) | Convenience only; never load-bearing. | ADR-004 | +| Shell caching | Cache Storage | Standard app-shell model. | §8 | +| Crypto | WebCrypto SHA-256 + bundled audited pure-JS Ed25519 verifier | Baseline-device coverage incl. iOS 16.4. | ADR-013 | +| Map rendering | WebP raster base + DOM POI overlay + CSS-transform pan/zoom | Perf on low-end, a11y, organizer workflow. | ADR-008 | +| Hosting | Static HTTPS CDN | No server state needed. | ADR-014 | +| Push / notifications | None in V1 | iOS fragility; pull model suffices. | ADR-010 | +| Analytics/telemetry | None; on-device diagnostics only | Data minimization. | ADR-013 | +| Testing | Unit + contract + scripted device matrix | §26. | — | + +Deliberately **absent**: React/Angular/Vue, Workbox, Dexie/idb, SQLite-WASM, any map SDK, any mesh library, any account/auth system, any dynamic backend. + +# 6. Application Architecture + +## 6.1 Layering and boundaries + +```mermaid +flowchart TD + subgraph UI Layer + V["Views: Emergency / Schedule / Map / Festival / Status+Settings"] + VM[Presentation stores] + end + subgraph Domain Services + EM[EmergencyService] + SCH[ScheduleService] + MAP[MapService] + INF[InfoService] + RD[ReadinessService] + CLK[ClockService] + FAV[FavoritesService] + end + subgraph Data Layer + DS[DatasetStore read API] + US[UserStore favorites/prefs] + end + subgraph Sync Layer + SY[SyncService orchestration: check, stage, verify, activate] + TI[Transport interface] + HT[HttpTransport V1] + VF[Verifier hashes + Ed25519] + end + subgraph Platform Layer + IDB[(IndexedDB adapter)] + CACHE[(Cache Storage adapter)] + SWB[SW bridge page-side] + end + SW[Service Worker separate context] + + V --> VM --> EM & SCH & MAP & INF & RD + SCH --> CLK + EM & SCH & MAP & INF --> DS + VM --> FAV --> US + SY --> TI --> HT + SY --> VF + SY --> DS + DS --> IDB + US --> IDB + SWB --> SW + V --> RD +``` + +Boundary rules (enforced by module imports; verified in review): + +| Rule | Meaning | +|---|---| +| B-1 | Views never touch IndexedDB, `fetch`, Cache Storage, or SW internals. | +| B-2 | Domain services only read through `DatasetStore`/`UserStore` read APIs. | +| B-3 | Feature modules never know which transport delivered data; transports never know what content means. | +| B-4 | `Verifier` is the only component that decides dataset authenticity; Sync orchestrates, never validates by itself. | +| B-5 | The service worker never writes dataset data; dataset staging lives in the page context (C-21). | +| B-6 | User state (favorites, prefs) lives in a separate store that dataset updates/rollbacks/GC can never touch. | +| B-7 | No festival content is rendered via innerHTML with raw strings (C-22). | + +## 6.2 Screen structure + +- Single-page app, one cached `index.html`, client-side history-API router with offline-safe deep links (e.g., `/emergency`, `/map`). +- Persistent bottom navigation with four destinations: **EMERGENCY · SCHEDULE · MAP · FESTIVAL** (Emergency visually dominant, first position). Because the nav is persistent on every screen, Emergency is one tap from anywhere (R-N3) without any additional overlay machinery. +- A persistent **status chip** (OFFLINE READY / PARTIAL / NOT READY / RECOVERY) lives in the app header and opens the Status screen (§12). + +# 7. PWA Architecture + +| Element | Design | +|---|---| +| Web app manifest | `display: standalone`, theme/background colors, maskable icons, `scope` = origin root, `start_url` = `/`. | +| Install strategy | Install-first (C-16): install coach on iOS (manual steps with visuals), `beforeinstallprompt` handling on Android where available, install-state detection (`display-mode: standalone` / `navigator.standalone`). Coach is dismissible but re-surfaced until installed or dataset is ready. | +| Origin model | Single origin (C-20). No cross-origin assets. | +| HTTPS | Mandatory (SW + Storage + Geolocation require secure context). HSTS enabled. | +| Standalone-mode handling | In-app back affordances; external links open with explicit handoff; `viewport-fit=cover` + safe-area insets. | +| Private browsing | Detected where possible; app still renders shell + embedded emergency baseline; storage attempts are guarded and produce a clear "cannot save offline data" state (§22, FA-7). | +| Permissions | V1 requests **no permissions** (no geolocation, no notifications). Zero-permission by default is a feature. | +| iOS specifics | 7-day eviction exempt only when installed → install coaching + re-bootstrap recovery; no background sync assumption; SW kept minimal. | + +**What the web platform guarantees vs practice vs must-verify** (required distinction): + +| Topic | Guaranteed by spec | Works in practice (as of 2026) | Must verify on real devices | +|---|---|---|---| +| SW precache of shell | Yes, on supporting browsers | Yes, iOS+Android | Cache survival across reboots; update activation timing on iOS | +| IndexedDB persistence | **Best-effort only** — never guaranteed | Generally persists for installed PWAs | Eviction behavior under storage pressure; IDB transaction stability near quota on iOS | +| Storage eviction notice | None (eviction is silent) | Silent on both platforms | Our detection path (boot verification) is the only mechanism | +| `navigator.storage.persist()` | API exists; grant is heuristic | Auto-granted for installed PWAs on Chrome; heuristic on WebKit | Grant rate on target iOS versions | +| Offline launch after reboot | If SW + caches intact | Yes | Cold-start timing on low-end Android | +| Background work after app hidden | **Nothing guaranteed** | iOS kills quickly; Android throttles | Assume zero background (C-19) | +| Push | Spec exists | iOS: installed-only, region-dependent | Not relied on (V1: none) | +| Geolocation | Requires permission + secure context | Accuracy degrades in crowds | Not used in V1 | +| `tel:` links | Standard | Works incl. standalone | Verify on iOS standalone + Android | +| Intl timezone rendering | Yes (IANA zones) | Yes | Target devices + festival zone edge cases | + +# 8. Service Worker Strategy + +**Scope of the SW (deliberately tiny):** + +1. `install`: precache the versioned app-shell list (index.html, hashed JS/CSS, icons). Precache list is generated at build time. +2. `activate`: delete caches from previous shell versions. +3. `fetch`: + - Navigation requests → **cache-first** against the shell cache; fallback to network only to re-fill; if both fail, a built-in static fallback page (rendered from shell bytes) with emergency text. + - Hashed static assets (`/assets/*` immutable URLs) → cache-first. + - `/latest.json`, `/editions/**` (package endpoints) → **not intercepted**; they go straight to network so the app-layer sync controls caching/staging (no double cache, no SW-held partial downloads; C-21). +4. `message`: minimal commands (`SKIP_WAITING`) from the page. + +**What the SW deliberately does NOT do:** dataset download/staging, background sync, retry logic, announcements, anything stateful beyond caches. + +**Shell update policy:** + +- New shell version → new SW installs with a new cache name; old SW keeps serving current session. +- Activation happens on **next full app start** (no mid-session `clients.claim`), except when the user explicitly taps "Restart to update" on the Status screen. This protects in-progress festival sessions and in-progress dataset staging. +- After shell update, boot runs the **compatibility check** (§18.5) between new shell and active dataset before exposing data screens. + +**SW termination handling (iOS reality):** every SW operation is idempotent and re-runnable; the page never depends on SW in-memory state; page-side logic resumes staged downloads regardless of SW lifetime. + +# 9. Local Storage Architecture + +## 9.1 Storage map + +| Store | Technology | Contents | Lifecycle | +|---|---|---|---| +| `lumen-system` | IndexedDB (1 object store: `meta`) | Active slot pointer, active edition + packageVersion, verification record, app version at activation, staging progress pointer, clock offset cache reference | Rewritten atomically on activation | +| `lumen-slot-a`, `lumen-slot-b` | IndexedDB (stores: `files`, `assets`) | One complete dataset per slot: section JSON docs keyed by section id; assets as Blobs keyed by asset id | Active slot is truth; inactive slot = rollback/staging target; GC per §18.4 | +| `lumen-user` | IndexedDB (stores: `favorites`, `prefs`, `diag`) | Favorites keyed by stable event id; theme/clock settings; scrubbed diagnostics ring buffer | **Never touched by dataset updates or rollback** (B-6) | +| Shell cache | Cache Storage (`lumen-shell-v`) | Precached app shell | Versioned per build; old caches deleted on activate | +| Flags | localStorage | Tiny convenience flags (coach dismissed, etc.), try/catch guarded | Non-load-bearing | + +Rationale for all-IDB datasets (vs Cache API for assets): rollback and activation concern **one storage system**, one failure domain, one GC policy. IDB Blob storage is adequate at our asset budget (§10.6, ≤ ~30 MB assets). + +## 9.2 Wrapper policy (validated, P1–P8 normative per SPIKE-01/02 — ARCHITECTURE-VALIDATION.md §2) + +- Thin internal promise wrapper over raw IDB (~small module): typed get/put/transaction helpers. No external DB library (ADR-004): dataset access patterns are few and known; wrapper keeps IDB quirks (transaction lifetimes, `oncomplete` semantics) in one audited place. +- All writes that must be durable await transaction completion; no transaction spans an `await` of non-IDB work. +- Defensive protocol P1–P8 is mandatory: P1 one short txn per file (bytes+progress together); P2 activation single txn on `lumen-system`; P3 wrapped IDB + `QuotaExceededError` keeps active; P4 free-space pre-check 2× package; P5 single record ≤6 MB; P6 `persist()` requested but never relied upon; P7 boot light verification as eviction detection; P8 no dataset state in SW. + +## 9.3 Quota and pressure management + +- On boot and before staging: `navigator.storage.estimate()`; refuse to stage if free space < 2× package size; surface guidance. +- Request `navigator.storage.persist()` once after dataset ready (grant is heuristic; never relied upon). +- Hard budgets (§10.6) enforced by the publish pipeline, so client-side quota surprises are unlikely; client still handles `QuotaExceededError` by aborting staging and keeping the active dataset. + +## 9.4 Eviction and unavailability + +- Eviction is silent and all-or-nothing → **boot verification** (§12) is the detection mechanism: missing/corrupt system store ⇒ RECOVERY state; embedded emergency baseline still renders. +- Storage unavailable (private mode, disabled website data): all IDB access is wrapped; failure ⇒ BASELINE-ONLY mode (§22 FA-7) with a clear explanation and install/normal-mode guidance. + +# 10. Festival Data Package Architecture + +## 10.1 Package anatomy (normative V1) + +``` +Package (immutable, versioned directory on the CDN) +├── manifest.json ← section inventory, hashes, sizes, compatibility, festival metadata +├── signature.json ← Ed25519 signature over SHA-256 of exact manifest bytes + key fingerprint +├── emergency.json ← updateable emergency section (schema: emergency/1) +├── schedule.json ← stages, artists, events (schema: schedule/1) +├── map.json ← base-map level definitions + POIs in normalized coords (schema: map/1) +├── info.json ← festival information blocks (schema: info/1) +├── assets.json ← asset inventory (ids, roles, hashes, sizes) +└── assets/… ← WebP/PNG images, etc., each listed in assets.json +``` + +Plus, outside the package directory: + +``` +/latest.json ← mutable pointer: { edition, packageVersion, manifestUrl, generatedAt } +``` + +## 10.2 Manifest schema (sketch — normative fields) + +``` +{ + "format": "lumen.package/1", + "edition": "", // e.g., "lumen-2026" + "packageVersion": , + "schemaVersion": , + "generatedAt": "", + "festival": { + "name": "...", + "timezone": "", + "startUtc": , "endUtc": + }, + "appCompatibility": { "minAppVersion": "x.y.z", "maxAppVersion": null }, + "sections": { + "emergency": { "file": "emergency.json", "sha256": "…", "bytes": n }, + "schedule": { … }, "map": { … }, "info": { … }, "assets": { "file": "assets.json", … } + }, + "counts": { "events": n, "pois": n, "assets": n }, + "limits": { "totalBytes": n } +} +``` + +`assets.json` entries: `{ id, file, sha256, bytes, kind, role }` with `kind ∈ {map-base, poi-icon, photo, icon}` and roles like `map-base/overview`, `map-base/detail`. + +## 10.3 Authoritative source & publishing (ADR-005, ADR-014) + +- **Source of truth:** a content repository (git) holding source files: schedule sheet (CSV/JSON), POI sheet, emergency content sheet, info markdown-ish blocks, map artwork. Organizers edit sources; the pipeline does the rest (discovery AMB-2 default). +- **Pipeline:** validate (schema + budgets + ID stability + time sanity) → build section JSONs → hash → sign manifest → upload immutable package → update `latest.json` → automated smoke fetch + verify. +- **Emergency baseline generation:** the same emergency source sheet also generates the **embedded baseline** compiled into app shell builds (§13) — one source, two outputs, no drift. +- **Roles:** one human "publisher" role; every publish is logged in the repo history (audit trail). Emergency content changes require the sign-off gate (discovery A-08/OQ-2). + +## 10.4 Versioning rules + +- `packageVersion` is a strictly monotonic integer per edition. Clients **never accept a lower version from a network source** (downgrade protection). Organizer "rollback" = publish old content under a **new** higher version (runbook, §18.7). +- `edition` identifies one festival occurrence. V1 keeps one active edition; a new edition reuses the slot pair (old edition GC'd at activation of the new one). +- `schemaVersion` changes only with the compatibility envelope rules (§18.6). + +## 10.5 Integrity model (ADR-013) + +1. Every file's SHA-256 + byte size is listed in the manifest. +2. The manifest's exact bytes are hashed; `signature.json` holds an Ed25519 signature over that digest made with the festival's offline signing key. +3. The app shell embeds the **public key set** (fingerprinted; small set to allow rotation). +4. Client verification order: signature → manifest parse → per-file size+hash → section schema validation. Any failure ⇒ reject package, keep current dataset. +5. Verification results are recorded in `lumen-system.meta` (what, when, which version) so boot can do a cheap light-check and a full re-check on demand. + +## 10.6 Size budgets (hard ceilings, enforced by pipeline) + +| Part | Budget | +|---|---| +| App shell (JS+CSS+icons, gzipped) | ≤ 1 MB | +| Embedded emergency baseline | ≤ 16 KB | +| Sections JSON total (emergency+schedule+map+info+assets.json) | ≤ 3 MB | +| Map base images (all levels) | ≤ 28 MB | +| All other assets | ≤ 6 MB | +| **Total dataset** | **≤ 40 MB** (hard ceiling 50 MB per discovery A-02) | + +Single file cap: 6 MB (keeps per-file hash/digest memory bounded). + +# 11. Offline Architecture + +The offline story has three rings: + +1. **Ring 0 — Embedded in shell bytes (survives total storage loss):** static fallback page + emergency baseline (§13) + app code. +2. **Ring 1 — Cache Storage:** the full app shell (survives restarts; eviction follows browser policy). +3. **Ring 2 — IndexedDB:** active dataset slot (+ rollback slot) and user state. + +**Cold start (no network ever):** SW serves shell from cache → app boots → boot sequence: open `lumen-system` → light verification of active slot → readiness state computed → UI renders with whatever is verified. No network call is required or blocking at any point (invariant 3). + +**Connectivity model:** the app listens to `online`/`offline` events only as *opportunistic hints*; every feature renders from local stores regardless. Being "online" never unlocks critical UI; it only enables the sync service. + +# 12. Offline Ready Architecture + +## 12.1 Definition (normative) + +`OFFLINE READY` is a **proven state, not a connectivity statement**: + +``` +READY ⟺ + R1 Shell complete: every precache entry present in current shell cache + ∧ R2 Dataset present: active slot exists for the edition + ∧ R3 Manifest verified: signature valid against embedded key set, verified record matches active packageVersion + ∧ R4 Sections complete: all manifest-listed section files present with matching sizes; activation-time hashes passed + ∧ R5 Assets complete: all assets present with matching sizes (hashes verified at staging) + ∧ R6 Compatibility: schemaVersion within this shell's supported range + ∧ R7 Emergency: baseline present (trivially true — embedded) AND dataset emergency section present +``` + +Anything less is reported precisely: + +| State | Meaning | +|---|---| +| `READY` | All of R1–R7. | +| `PARTIAL(x…)` | Enumerated missing parts (e.g., map assets only). App usable for what's present. | +| `NOT_READY` | Shell present, no verified dataset (fresh install / pre-prep). | +| `RECOVERY` | Previously verified state now fails checks (eviction/corruption). | +| `BASELINE_ONLY` | Storage unavailable; only embedded shell+baseline function. | + +## 12.2 Verification cadence + +| When | Check | +|---|---| +| Every boot | Light check: slot existence, sizes, recorded verification matches active version (fast; no re-hash). | +| After staging completes | Full verification (all hashes + schema). | +| At activation | Recorded; readback spot-check. | +| User-initiated ("Check my data") | Full re-verification with progress. | +| After any IDB error | Full re-verification of affected slot; quarantine on failure. | + +## 12.3 UX contract + +- Status chip visible on every screen; Status screen shows per-section checklist, dataset version, `generatedAt`, `fetchedAt`, storage usage, and the "Check my data" / "Get festival data" / "Restore previous version" actions. +- The app **never shows READY** unless the predicate above is met (invariant 8; discovery AMB-5 default). + +## 12.4 Preparation (bootstrap) flow + +```mermaid +sequenceDiagram + participant U as Attendee + participant A as Lumen (page) + participant S as Static CDN + + U->>A: Opens link / scans QR (first visit) + A->>S: Load shell (SW precaches) + A->>A: Install coach (esp. iOS); readiness = NOT_READY + U->>A: "Get festival data" + A->>S: latest.json → manifest.json + A->>A: Verify signature + compatibility + budgets + A->>S: Download sections (priority: emergency → schedule → info → map) + A->>A: Per-file hash verify, stage into inactive slot (resumable) + A->>S: Download assets (resumable) + A->>A: Full verification → atomic activate → readiness = READY + A->>U: "OFFLINE READY ✓" confirmation + summary +``` + +Answers to the eleven bootstrap questions: + +1. **Discovery:** QR codes on tickets/emails/posters + short URL; identical landing page. (Ops detail OQ-3; architecture is URL-based.) +2. **Initial load:** shell only (~1 MB), instant precache; app is usable but `NOT_READY` for festival content; emergency baseline already works. +3. **Dataset acquisition:** explicit "Get festival data" preparation flow; prioritized, resumable, progress per section; runs while app is open (C-19). +4. **Existence verification:** manifest completeness check (all listed files present). +5. **Integrity verification:** §10.5 order; failures abort activation. +6. **User knows readiness:** READY confirmation screen + persistent status chip; per-section checklist on Status. +7. **Incomplete preparation:** PARTIAL state enumerates exactly what's missing; every installed part works; prep resumes with one tap. +8. **Leaves prep early:** staging progress persisted (`lumen-system.meta.staging`); nothing is corrupted; nothing is activated; resume later. +9. **Connectivity disappears mid-prep:** downloads pause; partial staged data retained; offline state shown with what's already usable; auto-resume when `online` hint fires or user retries. +10. **Storage unavailable:** BASELINE_ONLY mode; clear guidance (install to home screen / use normal browsing mode / free space); no crash, no blank screen. +11. **Later eviction:** boot light-check fails ⇒ RECOVERY: emergency baseline works; one-tap full re-prep when online; honest messaging that festival data was removed by the device. + +# 13. Emergency Architecture + +## 13.1 Three tiers (ADR-007) + +```mermaid +flowchart TD + REQ[Emergency screen request] --> T1{Dataset emergency
verified & compatible?} + T1 -- yes --> SHOW[Render dataset emergency content
with version + updated-at stamp] + T1 -- no --> T2{Embedded baseline present?} + T2 -- always yes --> BASE[Render embedded baseline
labeled BASELINE] + SHOW --> LIVE{Optional live emergency notice
in announcements inbox?} + LIVE -- yes, signed, unexpired --> SHOW2[Append, clearly labeled] + LIVE -- no --> DONE[Done] + BASE --> DONE + SHOW --> DONE +``` + +| Tier | Content | Mutability | Delivery | Survives | +|---|---|---|---|---| +| **T1 Embedded baseline** | Emergency number + dial, festival address, coordinates, security contact, first-aid/AED location summaries, muster points, exit summaries, core procedures (weather/fire/lost person/medical) | **Immutable per app build** — compiled into shell from the emergency source sheet at build time | Ships with app shell | **Everything**, incl. total storage loss, mid-update failure, incompatible dataset | +| **T2 Dataset emergency section** | Full detail: all POI-class emergency locations with map links, full procedure text, notices, per-role contacts | Updateable via signed dataset updates | Festival Data Package | Last-known-good dataset | +| **T3 Optional live notice** | Urgent one-liners (e.g., "muster point moved to North field") | Ephemeral, signed, expiring | Announcements seam (§17.4) — **deferred; not required in V1** | Only if delivered | + +**Tradeoff analysis (embedded baseline):** +- *Pros:* absolute floor — emergency info exists even after eviction, failed updates, or corrupted storage; zero runtime dependency. +- *Cons:* baseline changes require an app shell release (slower); baseline must stay small (≤16 KB). +- *Verdict:* the floor's value dominates; the drift risk is removed by generating baseline + dataset section from the **same source sheet** (§10.3). Baseline staleness is bounded by shell update cadence and labeled with its version. + +## 13.2 Rules + +- Emergency rendering path has **no network calls, no IDB schema surprises** (baseline path parses a frozen schema compiled into the build). +- `tel:` links primary; numbers also displayed as selectable text (copy). Dialing requires an explicit tap; never auto-dial. +- Every emergency screen shows content provenance: `BASELINE v` and/or `FESTIVAL DATA v · generated `. +- If T3 cannot be delivered, nothing degrades: T1/T2 remain (documented acceptance, discovery A-16). + +# 14. Schedule Architecture + +## 14.1 Data model + +- `schedule.json`: `stages[]`, `artists[]`, `events[]`. +- Event: `{ id (stable), title, description?, stageId, artistIds[], startUtc, endUtc, tags[], status: scheduled|moved|cancelled, originalStartUtc?, dayKey }`. +- **Stable IDs are contractual**: updates mutate fields, never IDs (favorites depend on this, §15 of discovery). +- `dayKey` is derived at publish time from festival-tz calendar date (precomputed so the client never computes day boundaries ambiguously). + +## 14.2 Queries (all local) + +- **Now / Up Next:** computed from `ClockService.now()` (§14.3): events where `startUtc ≤ now < endUtc`; next-N by stage or global. +- **My Schedule:** favorites store joined to events; conflict detection (overlapping intervals) surfaced in UI. +- **Filters/search:** stage/tag filters + substring search over title/artist/description; dataset size (≤ ~1k events) makes naive scanning fine — no index library (discovery TQ-10 resolved: skip until proven slow). +- **Change surfacing:** events with `status=moved|cancelled` render distinctly; dataset update notes may list changed event ids (optional manifest field, cheap). + +## 14.3 Time model (ADR-009 — addendum after SPIKE-08, ARCHITECTURE-VALIDATION.md §2; F-3/F-4 normative) + +**Storage:** all instants are UTC epoch ms. The manifest carries the festival IANA zone and festival start/end instants. +**Validation:** UTC+explicit-zone `Intl` + precomputed `dayKey` + skew+monotonic + sanity window proven 24/24 on V8/ICU (`SPIKE-08:43`); JSC parity queued for D1/D2 device matrix. F-3: sanity warning suppressed until near festival; F-4: only `Intl.DateTimeFormat` with explicit `timeZone` allowed. + +**Rendering:** `Intl.DateTimeFormat` with `timeZone = festival zone` by default; user setting toggles to device zone. Day boundaries come from precomputed `dayKey`. DST correctness is inherited from the IANA zone database in the OS/browser. + +**The clock problem:** device clocks can be wrong, and there is no network time offline. + +```mermaid +flowchart TD + N[ClockService.now] --> H{Persisted server offset
available?} + H -- no --> D[Use device clock] + H -- yes --> C[now = device + skew] + C --> DR{Mid-session drift check:
device clock moved vs
monotonic expectation?} + DR -- yes --> W[Warn: clock changed;
re-derive base, keep server skew if present] + D --> SAN{Sanity: now within
festival window ± 45 days?} + SAN -- no --> W2[Warn: check your clock] + C --> SAN +``` + +- **Offset capture:** whenever any sync HTTP response arrives, read its `Date` header; `skew = serverNow − deviceNow`; persist `{skew, capturedAtDevice, capturedAtMono, source}` in the user DB. Use corrected time thereafter. +- **Monotonic anchor:** `performance.now()` (session) guards against the user changing the device clock mid-session. +- **Sanity-window suppression (SPIKE-08 F-3 — normative):** warn on `outsideWindow` only if `now ≥ festivalStart − 45d` or a server skew has previously been captured; otherwise early preparation (e.g., July for September festival) would spuriously warn every user. Dataset `generatedAt`/`fetchedAt` communicates staleness before that point (ARCHITECTURE-VALIDATION.md §2). +- **Never-online devices:** fall back to device clock with the sanity-window heuristic (now with F-3 suppression); if wrong, Now/Next may be wrong and **the app cannot know** — accepted residual risk, mitigated by showing dataset `generatedAt` and event times absolutely (users can still read times). +- No NTP-style complexity, no background timers: the clock is computed on demand. + +# 15. Map Architecture (ADR-008) + +## 15.1 Decision summary + +**Raster WebP base map (≤ 2 zoom levels) + data-driven DOM POI overlay + CSS-transform pan/zoom + list-based equivalent view.** No online tiles, no map SDK, no GPS in V1. + +## 15.2 Evaluation performed + +| Criterion | Single SVG map | Raster base + DOM overlay (chosen) | Canvas | +|---|---|---|---| +| Organizer workflow | Needs vector art creation (rare skill; illustrated maps are raster) | Organizers provide illustrated art as-is (matches discovery AMB-3 default) | Same as raster | +| Low-end pan/zoom perf | Degrades with path complexity | GPU image transform — cheap and consistent | Good, but redraw cost on every frame | +| Accessibility | Interactive SVG a11y is fiddly at scale | POIs are real buttons/links; list view is first-class | Poor (off-screen text tree needed) | +| File size | Can be small or huge depending on art | WebP is excellent for illustrated art | Same as raster | +| Zoom quality | Infinite | Bounded by levels; 2 levels + browser scaling acceptable for venue scale | Same as raster | +| Code complexity/attack surface | Medium | Low | Higher (manual hit-testing) | +| Offline | Yes | Yes | Yes | + +## 15.3 Mechanics + +- **Coordinate space:** POIs carry normalized `{x: 0..1, y: 0..1}` relative to base-image native dimensions (authoring: coordinate sheet or a trivial tap-a-point tooling step in the pipeline; assumption D-02). + - **Levels:** `overview` (always stored; ~0.3–0.8 MB, longest edge ≤1600 px, ~7.3 MB decoded) and optional `detail` (high-res, ≤ 6 MB per file cap; shown when scale exceeds threshold; longest edge ≤3072 px, ~27 MB decoded; total decoded ≤~35 MB with at most one detail resident — SPIKE-03 F-1). Replaces prior ≤4096 px cap. `ARCHITECTURE-VALIDATION.md §2` reconciles. +- **Pan/zoom:** pointer events + pinch → single container `transform: translate(…) scale(…)`; markers counter-scaled to constant on-screen size; bounds clamped to map edges. +- **Interactivity:** POI tap → detail sheet (name, category, description, "Show in list"); category filter chips; POI search shares the search component with schedule. +- **Accessibility & GPS-free use:** the **Facilities list** (grouped by category, search-able) is a first-class view, not a fallback — it is how a screen-reader user, or anyone, finds "nearest water" without GPS. +- **GPS:** absent in V1 (narrowing of discovery R-M4). Hook preserved: POI schema allows optional `lat/lng` for future positioning; adding a blue dot later touches MapService only, never features (invariant 9 respected). + +## 15.4 Failure behavior + +- Map section missing (PARTIAL state) → list view still available if POI data present; otherwise "Map not downloaded" card with prep action. Map absence never affects Emergency/Schedule/Info. + +# 16. Festival Information Architecture + +- `info.json` = ordered list of blocks: `{ id, title, kind, body: structured nodes }` where nodes are paragraphs/lists/links/emphasis/contact/address/hours tables. **No raw HTML** (C-22); renderer maps node types to safe DOM. +- Categories (data-driven, reorderable by content): About, Rules, FAQ, What to bring / Not to bring, Parking, Camping, Transport, Accessibility, Food/Drink, Merch, Activities, Contacts, Hours, Venue. +- Search: same search component indexes info titles + text. +- Static-important guarantee: info is part of the signed dataset; readiness includes it; baseline-only mode excludes it (acceptable — emergency floor is what must survive). + +# 17. Synchronization Architecture + +## 17.1 Model (ADR-010, ADR-011) + +**Pull-only. No accounts. No server state. Versions are monotonic.** + +Triggers: app open; `online` event (opportunistic); manual "Update now"; never timers in background (C-19). + +```mermaid +flowchart TD + T[Trigger: open / online / manual] --> O{Online hint?} + O -- no --> END[No-op] + O -- yes --> L[Fetch latest.json] + L --> CMP{Version > active
same edition?} + CMP -- no --> END + CMP -- yes --> M[Fetch candidate manifest] + M --> V1{Signature valid?
Keys known?} + V1 -- no --> REJ[Reject + quarantine version + diag] + V1 -- yes --> V2{Compatible with shell?
Within budgets?} + V2 -- no --> REJ2[Reject; if newer app needed:
prompt app update] + V2 -- yes --> STG[Stage files into inactive slot
resumable, per-file verify] + STG --> FULL[Full verification] + FULL -- fail --> REJ3[Discard staging; keep active] + FULL -- ok --> ACT[Atomic activate §18] + ACT --> OK[New dataset active; READY re-evaluated] +``` + +## 17.2 Transport abstraction (ADR-011) + +``` +Application (features) + │ reads DatasetStore; sees readiness — nothing else +Data / Domain layer + │ DatasetStore, Verifier +Sync layer + │ SyncService: orchestrate(check → stage → verify → activate) + │ Transport interface: + │ isAvailable() → bool + │ fetchPointer(edition) → PackagePointer | none + │ fetchBytes(path) → Blob (resumable, size-capped) +Transport implementations + ├── HttpTransport (V1, the only one) + └── +``` + +Rules: + +- Features never import the sync or transport layer (B-3). +- Transports move **bytes**; the Verifier decides **truth** — identically for every transport (the signed-payload rule, ADR-012). +- The interface is intentionally byte-oriented and tiny; mesh semantics (discovery, routing, dedupe) live inside a future transport implementation, never in SyncService. +- Cost control: in V1 this is one interface + one implementation; no registry, no plugin machinery, no config system. + +## 17.3 Announcements seam (deferred, schema reserved) + +- Announcement shape reserved: `{ id, publishedAtUtc, expiresAtUtc, severity, title, body(nodes), signature }`, delivered either inside a package (section `announcements`) or via the same transport as a signed sidecar file. +- V1 decision: **schema reserved, feature not implemented** unless schedule-change pressure demands it during build (discovery A-12). Emergency notices would render per §13 tier T3 rules. + +## 17.4 What sync never does in V1 + +No uploads, no user-state sync, no telemetry, no push registration, no background sync registration. + +# 18. Update / Rollback Architecture (ADR-006) + +## 18.1 A/B slot state machine + +```mermaid +stateDiagram-v2 + [*] --> Idle + Idle --> Staging : newer verified manifest found + Staging --> Staging : file downloaded + hashed (progress persisted) + Staging --> Verified : all files staged + full verification passed + Staging --> Idle : interrupt / failure / quota (staging discarded or kept for resume; ACTIVE UNTOUCHED) + Verified --> Activating : single IDB transaction on lumen-system + Activating --> ActiveNew : pointer flipped + meta recorded + Activating --> Verified : transaction error (retry once) + ActiveNew --> Confirmed : readback spot-check OK, N successful boots + ActiveNew --> RolledBack : readback fails + RolledBack --> ActiveOld : pointer back to previous slot + Confirmed --> GC : previous slot reclaimable (kept until next staging needs it) +``` + +**Invariant (invariants 5–7):** at every moment `lumen-system.meta.activeSlot` points at either the old verified dataset or the new verified dataset. There is no third observable state. Staging writes only to the **inactive** slot. + +## 18.2 Failure-stage analysis (all nine stages) + +| Stage | Failure | Behavior | Active dataset | +|---|---|---|---| +| Fetch pointer | Network error | Silent no-op; retry next trigger | Untouched | +| Fetch manifest | Error/timeout | No-op | Untouched | +| Signature/format check | Invalid | Reject, quarantine this version (don't refetch-loop), diag entry | Untouched | +| Compatibility check | Incompatible | Reject; user-visible "please update app" if `minAppVersion` > current | Untouched | +| Staging download | Interrupted (app kill, reboot, SW kill, connectivity) | Progress persisted per-file; resume later; staging older than 7 days discarded | Untouched | +| Staging write | QuotaExceeded / IDB error | Abort staging, discard partial, warn + storage guidance | Untouched | +| Full verification | Hash/schema mismatch | Discard staging; quarantine version | Untouched | +| Activation transaction | IDB transaction failure | Retry once; else keep old; diag entry | **Old** | +| Post-activation readback | Spot-check fails | Automatic rollback to previous slot; diag | **Old (restored)** | + +Quarantine: rejected packageVersions are recorded so the client doesn't retry a known-bad version until `latest.json` advances past it. + +## 18.3 Rollback + +- **Automatic:** post-activation readback failure (§18.2 last row). +- **User-initiated:** Status screen "Restore previous version" while the previous slot still exists. +- **Organizer-initiated (server-side):** publish old content as a new higher version (runbook) — clients accept it as a normal update. This is the canonical rollback path and keeps client logic simple. + +## 18.4 Garbage collection + +Previous slot is retained until: (a) the *next* staging needs the slot, or (b) ≥ N successful boots (N=3) have occurred AND storage pressure is detected. User data (`lumen-user`) is never GC'd. + +## 18.5 Shell-vs-dataset compatibility at boot + +Boot order: load shell → read `meta` → check `schemaVersion(active dataset) ∈ shell.supportedRange` AND `packageVersion` within manifest's `appCompatibility`. Out of range → limited mode: Emergency baseline + Status screen explaining "open once with internet to update" (and if a compatible *other* slot exists, prefer it). + +## 18.6 Publishing ordering rule (C-23) + +1. Schema changes ship in an app shell **first** (shells support a range, e.g., schemas {1,2}). +2. Datasets using the new schema publish **only after** the supporting shell has been live ≥ the update-propagation window. +3. Runbook forbids breaking schema bumps in the 7 days before the festival. + +## 18.7 Runbooks (documented, not code) + +Publisher runbook: publish, verify smoke check, monitor `latest.json`. Rollback runbook: republish old content at new version. Emergency-change runbook: content change → sign-off gate → package publish (+ future T3 notice). + +# 19. Networking Abstraction + +(Covered structurally in §17.2; this section fixes responsibilities.) + +| Layer | Owns | Never touches | +|---|---|---| +| Application/UI | Rendering, user intent, readiness display | fetch, IDB, SW, transports | +| Domain services | Feature logic over verified data | persistence mechanics, network | +| Data layer (DatasetStore/UserStore) | Typed reads/writes of local truth | network, transports | +| Sync layer | Orchestration, staging, version rules | content semantics | +| Verifier | Authenticity + integrity verdicts | orchestration | +| Transport | Bytes in / bytes out, availability | parsing, verification, storage | +| Service worker | Shell cache + navigation fallback | datasets, sync | + +# 20. Future Mesh Extension Strategy (ADR-012) + +**V1 builds nothing here.** The strategy is: + +1. **Feasibility posture:** assume a meaningful mesh will require *something beyond the browser* (native companion, hardware relay, organizer LAN service) because mobile browsers expose no BLE peripheral mode, no ad-hoc Wi-Fi, no background sockets, and iOS lacks Web Bluetooth entirely. The architecture already treats "transport" as external plumbing, so this doesn't force rework. +2. **The seam:** a future mesh transport implements the Transport interface (§17.2) — e.g., `MeshTransport` bridging to a native companion over a browser-accessible channel, or a LAN mirror acting as a package source. SyncService is unchanged; features are unchanged. +3. **Security rule (V1 already enforces):** payloads from *any* transport are dead bytes until the Verifier passes them. Malicious peers cannot inject content without the festival signing key. +4. **Concerns explicitly assigned to FUTURE work (not V1):** peer discovery, routing, store-and-forward, message identity/deduplication, replay protection (package monotonicity already covers replay at the dataset level), expiration, message-size adaptation, peer authentication, emergency broadcast semantics, conflict handling beyond version monotonicity. +5. **Anti-leak rule (invariant 12):** no feature/UI code may reference transport identity, connectivity modality, or peer concepts. Status UI shows "updated ", never "via what". + +# 21. Security Architecture (ADR-013) + +## 21.1 Threat model (prioritized, realistic first) + +| # | Threat | Class | Defense | +|---|---|---|---| +| TH-1 | Tampered/malicious package (hosting compromise, MITM) | Realistic, high | Ed25519-signed manifest + per-file SHA-256 + TLS/HSTS. Activation impossible without valid signature. | +| TH-2 | Compromised publish path | Realistic, high | Signing key offline; publisher role singular; repo audit trail; smoke checks; rollback runbook. | +| TH-3 | Malicious/stale emergency data | Realistic, high | Emergency dataset section is signed like all content; baseline immutable per build and generated from signed-off source; provenance labels on screen. | +| TH-4 | Content injection (XSS) | Realistic, medium | C-22: structured-data rendering only; CSP disallows inline script; no eval. | +| TH-5 | Replay/downgrade of old signed package | Realistic, low-impact | Monotonic version acceptance rule (§10.4); organizer rollback only via new version. | +| TH-6 | DoS via oversized package | Medium | Budgets in manifest validated *before* download; per-file caps; quota pre-check. | +| TH-7 | User/location privacy leakage | Low by design | No accounts, no telemetry, no location capture in V1, all user state device-local. | +| TH-8 | Browser storage exposure (other apps, shared devices) | Low | No secrets stored; dataset is public festival info; favorites are innocuous; OS-level app isolation assumed. | +| TH-9 | SW/cache poisoning from other origins | Low | Single origin, same-origin caches, CORS not relied upon (all same-origin assets). | +| TH-10 | Future mesh injection | Future | Signed-payload rule (§20.3); mesh auth deferred to that work. | +| TH-11 | Dependency supply chain | Standard | Minimal deps (target: zero runtime deps beyond the audited Ed25519 verifier); lockfiles; CI. | + +## 21.2 Platform hardening + +- HTTPS + HSTS; no mixed content; single origin (C-20). +- CSP: `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' blob: data:; connect-src 'self'` (final tuning during build). +- No third-party scripts/fonts/analytics. +- `tel:` only via explicit user action; numbers data-driven (discovery A-14). +- Signing key custody: offline key; app embeds a small **key set** (fingerprinted) enabling rotation via shell update. Lost key ⇒ ship new key set in shell (runbook documented). +- Data minimization as a rule: the client collects nothing; diagnostics are local, scrubbed, and user-initiated only. + +# 22. Reliability Architecture + +Behavior matrix (what happens / what stays usable / what the user sees / recovery / needs internet?): + +| # | Failure | Behavior | Usable | User sees | Recovery | Internet? | +|---|---|---|---|---|---|---| +| FA-1 | Offline startup | Boot from cache+IDB; light verification | Everything verified | Normal UI + honest status chip | n/a | No | +| FA-2 | Browser restart | Same as FA-1 | Same | Same | n/a | No | +| FA-3 | Phone restart | Cold start path; budgets apply (§27) | Same | Same | n/a | No | +| FA-4 | SW restart/killed | SW stateless by design; page logic unaffected | Everything | Nothing | n/a | No | +| FA-5 | Storage evicted | Light check fails ⇒ RECOVERY | Emergency baseline | RECOVERY screen: what happened, one-tap re-prep | Re-prep | Yes (for re-prep) | +| FA-6 | Corrupt dataset | Verification/readback failure ⇒ quarantine/rollback | Last-good dataset or baseline | Status explains; data screens use last-good | Automatic | No | +| FA-7 | Storage unavailable | BASELINE_ONLY | Emergency + shell | Explanation + guidance (install/normal mode) | Follow guidance | No | +| FA-8 | Interrupted update | §18.2 | Active dataset | Nothing (or resume banner) | Resume | Yes | +| FA-9 | Invalid package published | Client rejects; quarantine | Active dataset | Nothing (diag) | Organizer fixes + republishes | No (client side) | +| FA-10 | Wrong device clock | §14.3 corrected or warned | All features; Now/Next maybe skewed until corrected | Warning if detected | Get online once (captures offset) or fix clock | No | +| FA-11 | GPS unavailable/denied | Not requested in V1 | Everything | Nothing | n/a | No | +| FA-12 | Low battery | No background work by design; dark theme; no polling | Everything | Normal | n/a | No | +| FA-13 | Schedule changed | Dataset update on next open; moved/cancelled rendering | Last-known schedule meanwhile | Version stamp; changed markers | Sync | Yes (to receive) | +| FA-14 | Emergency update can't reach device | T1/T2 remain; organizer physical channels | Emergency | Provenance stamp shows data age | Sync when possible | Yes (to receive) | +| FA-15 | Browser update | Standard web compatibility; baseline targets conservative APIs | Everything | Nothing | n/a | No | +| FA-16 | Shell update mid-festival | Next-start activation; compatibility check §18.5 | Last-good dataset | "Restart to update" option | Automatic | Yes (to fetch) | + +**No-blank-screen rule:** every row above ends in a defined renderable state; the static fallback page (Ring 0) covers even "shell cache missing but SW alive". + +# 23. UX Architectural Implications + +- **Navigation:** 4-tab bottom bar (Emergency first, visually dominant). Persistent status chip in header. No hidden gestures for critical paths. +- **States are honest and visible:** READY/PARTIAL/NOT_READY/RECOVERY/BASELINE_ONLY map 1:1 to chip colors/labels; PARTIAL enumerates; RECOVERY offers the one-tap fix. +- **Emergency UX:** giant targets, max contrast both themes, zero clutter, provenance stamp, dial buttons + copyable numbers, works one-handed. +- **Install coach:** first-class flow (iOS steps with visuals), dismissible, re-surfaces; Android uses native prompt where available. +- **Preparation flow:** explicit "Get festival data" with per-section progress; never blocks browsing what's already installed. +- **Sunlight/night:** high-contrast light theme + true dark theme (system or manual); no audio cues for anything critical. +- **Accessibility (target WCAG 2.1 AA):** landmarks/heading order per view; POIs as real buttons; Facilities list view; `prefers-reduced-motion` honored; all touch targets ≥ 48 CSS px. +- **Loading/error honesty:** no fake spinners; skeleton only where real; errors state cause + next action; retry affordances everywhere. +- **Back behavior:** in-app back on detail screens (standalone-mode safe); browser back works via history API. + +# 24. Deployment Architecture (ADR-014) + +```mermaid +flowchart LR + subgraph Content Repo + S1[schedule.csv/json] --> B + S2[emergency sheet] --> B + S3[poi sheet + map art] --> B + S4[info blocks] --> B + B["Pipeline: validate → build sections → hash → sign"] + end + B -->|immutable| P["/editions//packages//…"] + B --> PTR[update latest.json] + APP[App shell build
hashed assets + precache manifest
+ embedded emergency baseline] -->|immutable| SH["/assets/…"] + APP --> IDX["/ index.html (no-cache)"] + P & SH & IDX & PTR --> CDN[Static HTTPS CDN
single origin] + CDN --> DEV[Attendee devices] +``` + +| Path | Cache policy | +|---|---| +| `/` (index.html) | `no-cache` (revalidate) | +| `/assets/.*` | immutable, 1 year | +| `/latest.json` | `max-age=60` | +| `/editions//packages//**` | immutable, 1 year | + +- No dynamic backend, no database, no auth endpoint in V1. +- Publisher auth lives at the pipeline/CDN credential layer (ops detail in ADR-014). +- Environments: `staging` origin (full pipeline, test editions) → `production` origin. Identical code, different origins (storage isolation is automatic). + +# 25. Browser Compatibility + +| Dimension | Baseline (supported) | Best-effort | Explicitly unsupported | +|---|---|---|---| +| iOS Safari | 16.4+ installed PWA | 16.0–16.3 tab use (eviction-prone; coach pushes install) | < 16 | +| Android Chrome | ~110+ | Older with SW support | Browsers without SW | +| Other mobile browsers (Samsung Internet, Firefox Android) | Where SW+IDB exist | Same code paths | — | +| Desktop | Works via same code | Not designed for | IE, legacy engines | +| JS disabled / storage blocked | Static fallback page with emergency text (Ring 0 served by CDN/SW) | — | Full app | + +Capability detection (not UA sniffing): `'serviceWorker' in navigator`, `indexedDB`, `caches`, `Intl.DateTimeFormat().resolvedOptions().timeZone`, `navigator.storage?.estimate`. Missing capability ⇒ degrade along the state machine (never crash). + +# 26. Testing Implications + +| Layer | What | How | +|---|---|---| +| Schema/contract | Package schema, manifest, budgets, ID stability, time sanity | Pipeline validation + fixture tests (golden packages) | +| Unit | ClockService (skew, drift, sanity), Verifier (bad sig, bad hash, replay/lower version), readiness predicate, A/B state transitions, renderer escaping | Standard unit tests | +| Integration | Full update lifecycle incl. all nine failure stages (§18.2) | Scripted harness with mocked transport + fault injection (kill mid-stage, corrupt bytes, quota errors) | +| Device matrix | iPhone (iOS 16.4 low bound + latest), low-end Android (2021 mid-range) + latest Chrome | Manual scripted scenarios: airplane mode, reboot, eviction simulation (delete site data / devtools), private mode, storage pressure, install flow, cold-start timing | +| PWA audits | Manifest/SW/offline load | Lighthouse PWA + scripted offline reload | +| Content | Emergency provenance, dataset age display, wrong-clock warnings | Scenario tests | +| Accessibility | Screen reader flows for Emergency, Schedule, Facilities list; contrast both themes | Manual + automated checks | + +Rule: any bug found on a real device that the harness didn't catch becomes a harness case. + +# 27. Performance Considerations + +| Budget | Target | +|---|---| +| Shell JS (gz) | ≤ 150 KB | +| Shell total (gz) | ≤ 1 MB | +| Cold start → Emergency usable (2021 mid-range Android, cached) | ≤ 2 s | +| Cold start → interactive (same device) | ≤ 3 s | +| Schedule list render (1k events) | ≤ 100 ms interaction; windowed rendering if needed | +| Map pan/zoom | ≥ 30 fps on baseline devices (GPU transforms) | +| Map image decode | Overview ≤1600 px / detail ≤3072 px longest edge (total decoded ≤~35 MB); detail lazy-decoded on first zoom-in; at most one detail resident (SPIKE-03 F-1) | +| Boot verification (light) | ≤ 150 ms | +| Dataset total | ≤ 40 MB target / 50 MB ceiling | + +Tactics: vanilla JS keeps parse cost trivial; images WebP; JSON parsed once per section and cached in memory per session; no timers/observers running when idle; zero background work. + +# 28. Architectural Risks & Self-Critique + +## 28.1 Risk register + +| # | Risk | Likelihood | Impact | Mitigation / acceptance | +|---|---|---|---|---| +| RK-1 | Users don't install on iOS; tab storage evicted pre-festival | Medium | High | Install coach, prep urgency messaging, re-prep is one tap, Ring-0 floor. Residual accepted. | +| RK-2 | Attendee never prepares (no pre-festival internet) | Medium | High | Distribution campaign (ops), partial usability, physical fallback info owned by organizers. Architecture can't fix alone — flagged to product. | +| RK-3 | Map art blows size budget | Medium | Medium | Pipeline budget gates; split levels; drop detail level if needed. | +| RK-4 | IDB instability on specific iOS versions | Low-Med | High | Thin wrapper isolates; real-device spike validates early; fallback = re-prep. | +| RK-5 | Shell update breaks compatibility discipline | Low | Medium | Supported-range check at boot (§18.5), publishing ordering rule (§18.6), runbook. | +| RK-6 | Signing key loss | Low | High | Key set in shell; rotation runbook; key custody documented (ops). | +| RK-7 | Vanilla-TS hand-rolled UI accrues bugs | Medium | Medium | Small surface, strict boundaries, tests; reconsider trigger → Preact (ADR-003). | +| RK-8 | Organizer publishes breaking change right before festival | Low-Med | High | Runbook rule; schema freeze window; smoke checks. | +| RK-9 | Wrong clocks on never-online devices produce wrong Now/Next | Medium | Medium | Absolute times shown; warnings; accepted residual (§14.3). | +| RK-10 | CDN outage during festival blocks updates | Low | Low | Updates are enhancement-only; last-good datasets already on devices. | + +## 28.2 Self-critique: adversarial walkthrough (16 attacks on the design) + +1. **User never installs the PWA.** On iOS tab usage, storage (incl. SW registration) can be evicted after 7 days of inactivity; the app still works while open. *Weakness accepted:* pre-festival eviction is possible; mitigations: coach, prep-completeness reminders, one-tap re-prep, Ring-0 emergency floor. There is no technical fix inside the web platform — install is the fix. +2. **iOS evicts storage.** Detected at boot by failed light-check → RECOVERY state; emergency baseline intact; one-tap re-prep. *Weakness:* anything not re-delivered stays missing until online. Accepted with honest UI. +3. **SW killed mid-update.** Dataset staging doesn't run in the SW (C-21) — SW death can't corrupt staging. Page death leaves per-file staged progress; resume path defined. *No gap found.* +4. **Dataset corrupted.** Activation verification + readback spot-check + quarantine + rollback to previous slot; worst case (both slots gone) → RECOVERY + baseline. *Weakness:* corruption discovered only after previous slot GC'd and new slot later fails ⇒ re-prep needed. Accepted (rare; GC delayed by N boots). +5. **Airplane mode.** Primary design case; zero network calls on any critical path. *No gap.* +6. **Phone restarted at festival.** Cold-start budget (§27); all state durable; favorites write-through. *No gap beyond perf budget verification on real devices.* +7. **Device clock wrong.** Corrected when offset captured; sanity-window warnings; absolute times always shown. *Weakness:* never-online + wrong clock ⇒ Now/Next wrong without detection. Accepted residual. +8. **GPS unavailable.** V1 uses no GPS. *No gap.* +9. **Emergency update can't reach device.** T1 baseline + T2 last-good remain; organizer physical channels are the urgent path. *Weakness accepted and documented (A-16).* +10. **Shell and dataset incompatible.** Boot compatibility check; prefer compatible slot; else limited mode + update prompt; publishing ordering rule prevents most cases. *Weakness:* if a user updates the shell offline-only via an app-store-free push… n/a — shells also update via network, so incompatibility windows are bounded by §18.6 ordering. +11. **Browser clears storage.** Identical to eviction path (FA-5). *No additional gap.* +12. **Schedule update published immediately before festival.** Delivered on next open-while-online; runbook requires comms redundancy; dataset `generatedAt` shows staleness. *Weakness:* users who never open online keep old schedule — accepted; changed-event markers reduce confusion afterwards. +13. **User never connects after first open.** Shell + baseline function; NOT_READY state is explicit; prep screen explains exactly what's missing. *No gap; expectation management is product/ops.* +14. **Low-end Android.** Vanilla JS budget, GPU-transform map (overview ≤1600 px / detail ≤3072 px / ≤~35 MB decoded per SPIKE-03 F-1), windowed lists, WebP. *Weakness:* unverified until device-matrix testing — flagged for validation report §4 T12/T13 (YELLOW until D3 protocol passes). +15. **Restrictive iPhone settings** (block all website data, private mode, JS off). Storage-blocked ⇒ BASELINE_ONLY mode; JS off ⇒ CDN static fallback page still carries emergency text (Ring 0 also exists server-side as plain HTML). *No gap beyond the narrow fallback page.* +16. **Future mesh can't run in a browser.** Expected (analysis in §20.1). The seam is byte-transport-oriented and validation stays client-side, so a mesh would attach as an external bridge/companion implementing Transport. *If even that proves impossible, nothing in V1 is stranded:* the HTTP transport is complete product functionality, not a placeholder. + +## 28.3 Known weaknesses (declared, not hidden) + +- W-1: Offline-first still requires one online bootstrap; distribution is a product/ops problem architecture can only soften. +- W-2: Never-online wrong clocks cannot be detected reliably. +- W-3: Emergency *changes* cannot reach offline devices until they surface; physical redundancy required. +- W-4: Browser storage is best-effort by specification; eviction can never be fully prevented, only detected and recovered. +- W-5: Vanilla-TS UI layer has no framework safety net; discipline + tests must compensate. +- W-6: Single origin concentrates availability risk on one host/CDN (acceptable: content is static and mirrorable; future transport seam also mitigates). + +# 29. Deferred Decisions + +| # | Item | Why deferred | Trigger to revisit | +|---|---|---|---| +| DD-1 | Announcements feature implementation | Schema reserved; not needed for core promise | Organizer demand during build; schedule-change pressure | +| DD-2 | Push notifications | iOS fragility; pull model sufficient | Post-V1 engagement goals | +| DD-3 | Geolocation/blue dot | Not required; permission cost | Attendee demand + map maturity | +| DD-4 | Favorites export/import (QR/text) | Nice-to-have; IDs already stable | User demand | +| DD-5 | Multi-edition/multi-festival tenancy | Edition field already in schema | Second customer/event | +| DD-6 | i18n | English-only assumed (A-03) | Audience data | +| DD-7 | Mesh transport implementation | Future only (ADR-012) | Concrete transport opportunity + native companion feasibility study | +| DD-8 | On-site LAN mirror (organizer-side) | Depends on site connectivity facts (OQ-3/AMB-4) | Site survey result | +| DD-9 | Detail map level count > 2 / tiling | Budgets expected sufficient | Real map art size | +| DD-10 | SQLite/OPFS migration | IDB expected sufficient | IDB reliability spike results | +| DD-11 | Account/cross-device sync | Contradicts data-minimization posture | Explicit product decision | + +# 30. Implementation Boundaries + +**What exists after this phase:** three markdown documents. Nothing else. + +**What the implementation phase may create (preview, non-binding):** app shell sources, SW module, store modules, sync/verifier modules, content pipeline scripts, CI config, tests — all conforming to §6 boundaries (B-1…B-7), the budgets (§10.6, §27), and the ADRs. + +**Hard rules carried into implementation:** + +1. No feature code imports sync/transport/IDB/SW modules directly (B-1…B-5). +2. No festival content rendered as raw HTML (C-22). +3. No runtime dependency added without an ADR amendment (target: only the audited Ed25519 verifier beyond stdlib). +4. No background processing of any kind (C-19). +5. Every state in §12.1 must be reachable in tests; no silent failure paths. +6. Budgets are CI-enforced (bundle size, package size). +7. No analytics, no third-party requests, no permissions requested. + +--- + +*End of Architecture Design (Phase 1). Companion records: ARCHITECTURE-DECISIONS.md, ASSUMPTIONS-AND-OPEN-QUESTIONS.md.* diff --git a/ARCHITECTURE-VALIDATION.md b/ARCHITECTURE-VALIDATION.md new file mode 100644 index 0000000..5653ae9 --- /dev/null +++ b/ARCHITECTURE-VALIDATION.md @@ -0,0 +1,573 @@ +# 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:///` 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`). +- **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` 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.* + diff --git a/ASSUMPTIONS-AND-OPEN-QUESTIONS.md b/ASSUMPTIONS-AND-OPEN-QUESTIONS.md new file mode 100644 index 0000000..3994415 --- /dev/null +++ b/ASSUMPTIONS-AND-OPEN-QUESTIONS.md @@ -0,0 +1,158 @@ +# Lumen — Assumptions & Open Questions (Architecture Phase 1) + +Companion to `ARCHITECTURE-DESIGN.md` and `ARCHITECTURE-DECISIONS.md`. +IDs carrying over from `DISCOVERY.md` keep their original IDs (`A-xx`, +`OQ-xx`); new items use `AQ-xx`. Status column: **Held** (working +assumption), **Validated** (evidence obtained), **Proposed** (needs a named +validation step before commitment). + +--- + +## 1. Product Assumptions + +| ID | Assumption | Status | Basis / Notes | +|----|------------|--------|---------------| +| A-01 | Single-event V1; package keyed by edition so multi-event is a cheap later extension. | Held | DISCOVERY §7; ADR-005 edition field. | +| A-02 | Attendees 300–500; schedule ≤ ~1k events; total dataset ≤ 40 MB target / 50 MB ceiling. | Proposed (validation: pipeline gates defined, real-content proof pending) | Budgets enforced in pipeline with 5 gates (`SPIKE-04:189`, ARCH §10.6); validated at schema level in SPIKE-04; real-content dry run (AQ-19) remains. ARCHITECTURE-VALIDATION.md §7: YELLOW (content-dependent). | +| A-03 | English only; strings live in data, not code. | Held | i18n deferred (DD-6). | +| AMB-11 | Lost-person feature = informational procedure, not an active reunification workflow. | Held | DISCOVERY §8; revisit only on organizer demand. | +| AMB-12 | App keeps working post-festival on last dataset. | Held | Cheap and harmless. | +| AQ-01 | Default landing experience after first install is a Home with the four destinations and an explicit "Get festival data" preparation action (no forced onboarding wizard). | Proposed | Validate with first UX pass; low risk. | +| AQ-02 | Favorites are the only personalization in V1 (no notes, no custom ordering). | Held | Simplicity; DD-4 covers export later. | +| AQ-03 | The app is free to attendees with no paywall/gating of any kind. | Held | Core-promise framing; flag if organizers disagree (non-blocking). | + +## 2. Technical Assumptions + +| ID | Assumption | Status | Basis / Notes | +|----|------------|--------|---------------| +| AQ-04 | IndexedDB on baseline iOS/Android is reliable enough for A/B slot use (transactions, Blobs, near-quota writes). | Validated (design-logic) / Proposed (device) | SPIKE-01 + SPIKE-02 16/16 PASS validate design-logic (spec CONFIRMED, P1–P8, F-1…F-4); physical-device matrix (`SPIKE-01:159`) remains for iOS jetsam atomicity, quota-near-full writes, Blob read-back on D1–D4. ARCHITECTURE-VALIDATION.md §7 YELLOW. | +| AQ-05 | A thin internal IDB wrapper suffices; no external DB library needed. | Validated (design-logic) / Proposed (implementation proof) | SPIKE-01 P1–P8 fit; wrapper friction not yet observed; confirm during early implementation. | +| AQ-06 | Bundled audited pure-JS Ed25519 verifier of a few KB is acceptable and auditable. | Proposed | Audit + bundle-budget check still required (ADR-013 YELLOW, ARCHITECTURE-VALIDATION.md §7); choice itself (pure-JS over WebCrypto Ed25519) validated by TQ-9. | +| AQ-07 | WebCrypto SHA-256 is available on all baseline devices. | Validated (high confidence) | Long-standing baseline API on iOS 16+/Chrome; confirm in device spike for completeness. | +| AQ-08 | Intl IANA timezone rendering for the festival zone works on baseline devices incl. DST edge behavior. | Validated (design-logic, V8/ICU) / Proposed (JSC device) | SPIKE-08 24/24 PASS on V8/ICU (ECMA-402); JSC parity on iOS 16.4 low-bound + latest + low-end Android remains UNVERIFIED queued for device matrix (`SPIKE-08:152`). F-3/F-4 incorporated into ADR-009. | +| AQ-09 | Page-context (document) downloads with per-file resume are more robust than SW-driven downloads on iOS. | Held | DISCOVERY PW-6; architecture codifies as C-21; confirmed-by-design, low residual risk. | +| AQ-10 | Total app JS ≤ 150 KB gz is achievable with vanilla TS at this feature surface. | Held | ADR-003; CI budget gate will prove it. | +| AQ-11 | Map base art fits ≤2 WebP levels within 28 MB at legible quality (overview ≤1600 px, detail ≤3072 px, total decoded ≤~35 MB). | Validated (design with budget) / Proposed (real art) | SPIKE-03 memory analysis proves budgets fit with margin (F-1, F-2, F-4); real art delivery (OQ-6, AQ-19) and device decode/fps remain UNVERIFIED (`SPIKE-03:116`). | +| AQ-12 | Build tooling can emit a precache manifest of hashed shell assets deterministically. | Validated (high confidence) | Standard capability of mainstream bundlers. | +| AQ-13 | One CDN origin can serve staging + production as separate origins/subdomains with independent storage. | Held | Origin-keyed storage makes this automatic (ADR-014). | + +## 3. Browser Assumptions + +| ID | Assumption | Status | Basis / Notes | +|----|------------|--------|---------------| +| A-06 | Baseline: iOS Safari 16.4+, Android Chrome ~110+; older degrades to fallback page. | Held | DISCOVERY; ARCH §25. | +| B-01 | Installed home-screen PWAs on iOS are exempt from the 7-day ITP storage cap. | Validated (2026 sources) | WebKit policy; still must be confirmed on our test devices in SPIKE-1. | +| B-02 | iOS grants `navigator.storage.persist()` favorably to installed PWAs. | Held (heuristic) | WebKit grants by heuristic; never relied upon (ARCH §9.3). | +| B-03 | Eviction, when it happens, removes all origin storage at once (IDB + caches + SW registration). | Validated (platform docs) | Drives boot-verification detection design. | +| B-04 | Android Chrome auto-grants persistence for installed PWAs and has no 7-day cap. | Validated (high confidence) | Chrome policy. | +| B-05 | `beforeinstallprompt` exists on Android Chrome; iOS requires manual coach. | Validated | Drives install UX (ADR-002). | +| B-06 | Background Sync / Periodic Sync unavailable on iOS; nothing may depend on background execution anywhere. | Validated | C-19. | +| B-07 | Private browsing / "block all website data" modes make storage unavailable but do not crash guarded code. | Validated (design) / Proposed (device) | Guarded code + BASELINE_ONLY path designed (`ARCH §9.4`, `SPIKE-05:68`); must be exercised on D1–D4 device matrix (ARCHITECTURE-VALIDATION.md §4 T11). | +| B-08 | `tel:` links work from standalone PWA on both platforms. | Validated (design) / Proposed (device) | Standard + data-driven numbers (`SPIKE-06:81`); must be verified on D1/D2 standalone + D3/D4 (ARCHITECTURE-VALIDATION.md §4 T19, `SPIKE-01:171`). | +| B-09 | Service worker precache survives phone reboot on both platforms. | Validated (high confidence) | Still included in device matrix. | +| B-10 | HTTP response `Date` headers from the CDN are accurate enough to serve as the clock-offset source. | Held | CDN edge clocks are NTP-synced; sanity window guards (ADR-009). | +| B-11 | EU iOS region differences (push, home-screen behavior) do not affect our install/storage paths. | Held (monitor) | Push not used; install path is standard Add-to-Home-Screen. | + +## 4. Festival-Operations Assumptions + +| ID | Assumption | Status | Basis / Notes | +|----|------------|--------|---------------| +| A-05 | Attendees have internet before the festival for install + prep. | Held (flagged) | Highest-risk assumption (RK-2); OQ-3 contingency. | +| A-16 | Organizers accept dynamic emergency updates require the device to come online. | Held | Runbook + physical channels. | +| O-01 | One designated human publisher exists for festival data, with a deputy. | Proposed | Required by ADR-005/014 ops model; confirm (AQ-16 below). | +| O-02 | Emergency content has a named approver (sign-off gate) before any publish. | Proposed | DISCOVERY A-08/OQ-2/OQ-7; confirm authority (AQ-17). | +| O-03 | Organizers will run pre-festival distribution (emails/QR/posters) and can state install steps. | Proposed | Product/ops plan; architecture provides the assets (coach screens, QR targets). | +| O-04 | Organizer connectivity during the festival is poor; they publish updates from wherever they find connectivity. | Held | Drives static/pull design (A-17, AMB-4). | +| O-05 | Physical redundancy (PA, signage, staff) is the urgent-emergency channel; the app is information access, not an alerting system. | Held | Liability-safe framing (DISCOVERY EM-7). | +| O-06 | A schema-freeze window (no breaking content changes) of ~7 days before the festival is acceptable. | Proposed | Runbook rule ARCH §18.6; confirm with organizers. | + +## 5. Data-Source Assumptions + +| ID | Assumption | Status | Basis / Notes | +|----|------------|--------|---------------| +| A-04 | Organizers can supply schedule, map art, POIs, emergency info, and general info digitally. | Proposed | Intake templates to be issued; confirm with samples (OQ-6). | +| D-01 | Schedule arrives as rows with stable identifiers or enough structure for the pipeline to mint stable IDs once. | Proposed | Contractual for favorites (ADR-005); pipeline can mint + persist ID mapping if source lacks IDs (AQ-20). | +| D-02 | Map art arrives as raster image(s) with known pixel dimensions; POIs can be located on them. | Proposed | Matches ADR-008; if only vector/GIS exists, revisit representation (DD-9). | +| D-03 | Emergency contact values (numbers, locations, coordinates) are provided and stay valid through the event. | Proposed | Sign-off gate O-02; provenance labels at runtime. | +| D-04 | Content volume stays within budgets (≤1k events, ≤200 POIs, ≤30 info blocks). | Proposed | Pipeline gates reject overage; raise budgets by ADR amendment if genuinely needed. | +| D-05 | All times authors provide are expressible in the festival's single IANA zone. | Held | A-10; multi-zone venues would require ADR-009 amendment. | +| D-06 | Artist/event imagery is optional; if provided it fits the asset budget. | Held | Photo kind budgeted in assets.json; drop first if over budget. | + +## 6. Security Assumptions + +| ID | Assumption | Status | Basis / Notes | +|----|------------|--------|---------------| +| S-01 | The signing key can be kept offline with a documented custodian; signing happens only via the pipeline machine during publish. | Proposed | Key custody plan needed (AQ-21); ADR-013 depends on it. | +| S-02 | The CDN/hosting account is protected by the organizer's standard credential hygiene (2FA, limited publishers). | Proposed | Ops requirement; out of app scope. | +| S-03 | No PII is ever present in festival content, diagnostics, or user state beyond a favorite-list of public event IDs. | Held | Data-minimization posture (ADR-013). | +| S-04 | Attendees' devices are treated as untrusted-but-self-protecting: local tampering only harms that user; no multi-user device scenario is designed for. | Held | TH-8 analysis. | +| S-05 | Emergency numbers may differ by jurisdiction; the primary number is data-driven (A-14). | Held | Confirm jurisdiction (OQ-2 → AQ-22). | + +--- + +## 7. Open Questions + +Columns: **Blocks?** = does this block implementation start (as opposed to +blocking only content, deployment, or a specific subsystem)? + +| ID | Question | Why it matters | Current assumption | Recommended default | Impact if wrong | How it can be validated | Blocks impl.? | +|----|----------|----------------|--------------------|---------------------|-----------------|--------------------------|---------------| +| OQ-1 | Festival name, location, dates, daily hours, IANA timezone? | Manifest metadata, time model, schedule day keys | Placeholder values in staging edition | Ask organizers now; encode in staging manifest | Wrong timezone breaks Now/Next display; wrong dates break sanity window | Organizer confirmation + pipeline validation fixture | No (staging placeholders suffice) | +| OQ-2 | Exact emergency contacts, muster points, venue address/GPS, procedures owner? | Emergency content is safety-critical | Data-driven fields with 911 default | Issue emergency content sheet with sign-off gate (O-02) | Wrong emergency data = worst-case product failure | Sign-off gate + review of sheet + device render test | No (blocks Emergency *content*, not scaffolding) | +| OQ-3 | Pre-festival distribution channels + on-site connectivity for a bootstrap station? | Bootstrap risk RK-2 | Pre-arrival online prep | QR campaign + install-guide asset; on-site station only if connectivity exists | Unprepared attendees → product promise degrades | Organizer marketing plan + site survey | No | +| OQ-4 | Who runs content updates during the festival, on what device? | Publish UX requirements | Publisher on laptop/phone via repo pipeline | Minimal publisher flow documented in runbook | Slow/failed corrections mid-festival | Dry-run publish exercise | No | +| OQ-5 | Existing domain/brand/hosting control? | Origin selection is hard to reverse (storage origin-keyed) | New dedicated subdomain | Decide production origin before first public staging link | Origin change strands installed users' data | Organizer DNS/hosting inventory | No (dev proceeds on provisional origin) | +| OQ-6 | What map artifacts can organizers actually produce? | Map architecture input (ADR-008 Proposed) | Illustrated raster art | Request raster art at ≥2048px long edge + POI list | Representation revisit (SVG path or tile-split) | Sample art delivery; SPIKE-3 with real art | No (placeholder art unblocks dev) | +| OQ-7 | Legal/liability review for emergency wording? | Emergency procedures text | Plain informational framing | Legal review of procedure strings before first production publish | Rework of emergency content late | Organizer legal review | No (content gate) | +| OQ-8 | Budget for hosting/CDN or free-tier only? | Provider selection (AQ-18) | Free-tier-capable static host | Pick provider satisfying HTTPS + custom origin + adequate bandwidth | Provider swap pre-launch (cheap before users) | Organizer budget confirmation | No | +| OQ-9 | Audience OS version distribution? | Baseline policy A-06 | iOS 16.4+/Chrome 110+ baseline | Ticketing survey if available | Baseline shift (likely downward → more degradation paths) | Survey or accept assumption | No | +| OQ-10 | Post-festival app lifetime expectations? | Content staleness UX | Keeps working on last dataset | "Festival ended" informational state post end-date | Minor UX rework | Organizer preference | No | +| OQ-11 | Simultaneous programming across stages? | Now/Next conflict UX | Yes | Per-stage Now + global Up Next with overlap handling | Simpler UI suffices | Schedule sample | No | +| OQ-12 | Quiet hours / overnight schedule relevance? | Day model, all-night rendering | Standard day keys | Render all events chronologically | None significant | Schedule sample | No | +| AQ-14 | Which static hosting provider? (custom domain, HTTPS, cache-header control, bandwidth for ~500 devices × ~40 MB prep + shell updates) | Deployment ADR-014 provider Proposed | Any mainstream static CDN | Decide before first external staging share; keep origin stable | Pre-user origin change is cheap; post-user is not | Provider evaluation vs requirements list | No | +| AQ-15 | Repo/VCS setup: dedicated repo for Lumen inside/next to the parent directory structure? | Development hygiene | New dedicated git repo at /home/avi/Projects/Lumen | Init dedicated repo at implementation start | Trivial to fix later | Owner preference | No | +| AQ-16 | Publisher + deputy identities and credential path? | ADR-014 ops | Named organizer + deputy | Define before first production publish | Publishing bottleneck mid-festival | Organizer staffing | No (blocks production publishing only) | +| AQ-17 | Emergency content approver identity/authority? | Sign-off gate O-02 | Organizer safety lead | Name approver before first emergency content publish | Late legal/safety rework | Organizer staffing | No (content gate) | +| AQ-18 | Production origin name (e.g., app..tld)? | Storage is origin-keyed; hard to reverse | Provisional staging origin now; production chosen once | Choose deliberately before public launch | Origin migration strands device data | OQ-5 resolution | No (but must settle pre-launch) | +| AQ-19 | Realistic map art size/quality at 2 levels? | ADR-008 Proposed → Accepted; budgets | Fits ≤28 MB | Measure with real art; else drop detail level or re-split | Map budget amendment or representation tweak | SPIKE-3 + real art | No | +| AQ-20 | Does the source schedule have stable IDs, or must the pipeline mint+persist them? | Favorites stability across updates | Pipeline can mint via deterministic key (e.g., normalized title+stage+start) persisted in an ID map | Mint + persist mapping in content repo | ID churn breaks favorites | Inspect source schedule sample | No | +| AQ-21 | Signing key custody: who, where (offline), backup, rotation drill? | ADR-013 ops viability | Custodian + sealed backup | Written custody note + one rotation drill pre-festival | Key loss ⇒ shell update to rotate (runbook) | Ops exercise | No (blocks production signing) | +| AQ-22 | Jurisdiction/emergency number confirmation (911 vs other)? | A-14/S-05 | 911 default, data-driven | Confirm with organizers/venue | Wrong default number | OQ-2 answer | No | +| AQ-23 | Is a staging "dry-run festival" (test edition + test devices in the field) feasible before the real event? | Catches device/eviction/ops surprises | Yes, strongly recommended | Schedule a full dress rehearsal | Surprises surface during the real festival | Organizer planning | No (strongly advised) | +| AQ-24 | Device availability for the test matrix (iPhone iOS 16.4-ish, low-end Android 2021, latest of each)? | ARCH §26 feasibility | Borrow/cheap second-hand devices | Acquire before implementation mid-phase | Real-device gaps in verification | Procurement | No (blocks device-matrix testing, not coding) | +| AQ-25 | Will organizers ever need a second edition/year quickly (multi-edition cadence)? | Edition lifecycle & GC policy | Single edition V1 | Keep edition switch = normal update | Multi-edition tooling later | Organizer roadmap | No | + +--- + +## 8. Cross-Reference: What Blocks What + +| Blocker | Blocks | Does NOT block | +|---------|--------|----------------| +| OQ-2 emergency content sign-off | Emergency *content* publish | App scaffolding, emergency rendering, baseline generation mechanism | +| OQ-6 map artifacts | Final map representation confirmation (SPIKE-3) | Map service code against placeholder art | +| AQ-18 production origin | Public launch | All development on provisional/staging origin | +| AQ-16/AQ-21 publisher + key custody | Production publishing | Staging pipeline (test key) | +| AQ-24 test devices | Device-matrix validation | Unit/contract/integration harness work | +| A-05 truth (pre-festival internet) | Product promise strength | Architecture itself (re-prep + baseline floor absorb the failure) | + +**Nothing above blocks the start of implementation** of shell, data layer, +sync/verification, and feature modules against staging fixtures — by design. + +--- + +## 9. Validation Plan for Proposed Assumptions + +| Assumption | Validation vehicle | Phase | Status after SPIKE-01…08 (2026-08-30, ARCHITECTURE-VALIDATION.md) | +|------------|--------------------|-------|-------------------------------------------------------------------| +| AQ-04 (IDB iOS behavior) | SPIKE-1: real-device storage/eviction harness | First implementation spike | Validated design-logic (SPIKE-01 16-item analysis + SPIKE-02 16/16 PASS); device tests `SPIKE-01:159` §5 remain — YELLOW | +| AQ-05 (wrapper sufficiency) | Implementation experience + review | Early implementation | Validated design-logic (P1–P8 fit); confirm in early impl | +| AQ-06 (Ed25519 verifier) | Library audit review + bundle check + verify-on-device test | Early implementation | Still Proposed — audit + bundle gate required (ADR-013 YELLOW) | +| AQ-08 (Intl zones) | SPIKE-8 fixtures on device matrix | Early implementation | Validated 24/24 on V8/ICU (SPIKE-08); JSC parity on D1/D2 + D3 remains — YELLOW | +| AQ-11 / D-02 (map art) | SPIKE-3 bake-off with real/representative art | Early implementation | Validated with tightened caps F-1 (1600/3072/35 MB); real art + device decode/fps remain | +| B-07/B-08 (private mode, tel:) | Device matrix scripted scenarios | Mid implementation | Design validated (guarded paths, `tel:` data-driven); device verification remains (ARCHITECTURE-VALIDATION.md §4 T11/T19) | +| A-02 budgets | Pipeline gates + real content dry run | Content intake | Schema/gates validated (SPIKE-04); real content proof pending (AQ-19) | +| O-01..O-06 ops assumptions | Organizer agreements + dress rehearsal (AQ-23) | Pre-festival | Still Proposed — outside architecture; track per §8 cross-reference | +| NEW — AQ-04…11 device matrix | ARCHITECTURE-VALIDATION.md §4 (19 groups × D1–D4) | Pre-production (before festival) | BLOCKING for production; not blocking for implementation start on provisional origin | + +*End of assumptions and open questions. Any change to a Held assumption or a +blocker resolution should be recorded here with date and rationale.* diff --git a/DISCOVERY.md b/DISCOVERY.md new file mode 100644 index 0000000..1e3db51 --- /dev/null +++ b/DISCOVERY.md @@ -0,0 +1,842 @@ +# Lumen — Discovery & Requirements Analysis + +- **Phase:** Discovery & Requirements Analysis ONLY (no architecture lock-in, no implementation) +- **Date:** 2026-08-30 +- **Status:** Complete — ready to inform a deliberate architecture phase +- **Author:** Lead architect / senior product engineer (automated discovery) +- **Scope guard:** No application code was written during this phase. No placeholder code exists. This document is the only artifact produced. + +--- + +## 1. Project / Environment Findings + +### 1.1 Project directory + +`/home/avi/Projects/Lumen/` **exists but is effectively empty.** It was created on +Aug 30 20:11 and contains exactly one file: + +| File | Content | Project-relevant? | +|------|---------|-------------------| +| `.directory` | KDE folder metadata (`[Desktop Entry] Icon=folder-yellow`) | No — desktop folder-color marker, not project work. Left untouched. | + +Findings: + +- **No source code**, no `package.json`, no lockfiles, no config files. +- **No README, design docs, or prior architecture.** +- **No tests, CI, or build tooling.** +- Nothing to preserve, migrate, or work around. This is a greenfield start. + +### 1.2 Version control + +- There is **no git repository inside** `/home/avi/Projects/Lumen/`. +- The directory sits inside a parent git repository rooted at `/home/avi` + (branch `master`, **zero commits**, no remotes). Any file added to Lumen is + technically visible to that parent repo, but there is no meaningful history. +- **Open question (VCS-1):** Should Lumen initialize its own dedicated git + repository? Recommended default: yes, at the start of the architecture phase. + +### 1.3 Development environment + +| Item | Value | +|------|-------| +| OS | Linux (development machine) | +| CPUs | 8 | +| RAM | 15 GiB | +| Disk | 1.8 TB, ~1.4 TB free (22% used) | +| Node.js | v22.23.2 | +| npm | 10.9.8 | +| pnpm | 11.24.0 | +| bun | 1.4.0 | +| git | 2.53.0 | + +Any modern JS toolchain can run locally. No environment constraints detected. + +### 1.4 Owner ecosystem context (non-binding) + +Sibling projects in `/home/avi/Projects/` were lightly inspected for convention +signals only: + +- `Folio` — Electron desktop app, TypeScript 5.5, esbuild. +- `Hammock` — Android/AOSP Kotlin project. +- `BookRead` — Python web app. +- Others: Nostr client, chess, misc. + +**No existing web/PWA conventions exist** in the owner's workspace. TypeScript +appears to be the owner's preferred language, which may inform (but must not +predetermine) later technology evaluation. + +--- + +## 2. Existing Files & Relevant Discoveries + +There is no existing Lumen work product. The only discoveries that constrain the +project are external: + +1. **Browser platform reality (as of Aug 2026)** — verified via research, detailed + in §12. The most consequential facts: + - iOS Safari enforces a **7-day inactivity eviction cap** on all script-writable + storage (localStorage, IndexedDB, Cache API, **service worker registrations**) + for sites used in Safari tabs. **Installed home-screen PWAs are exempt.** + - There is **no `beforeinstallprompt` on iOS** — installation is a manual, + multi-step "Add to Home Screen" flow that the product must teach. + - **No Background Sync / Periodic Background Sync on iOS Safari.** Sync only + happens while the app is open. + - Eviction, when it happens, is **all-or-nothing per origin** — IndexedDB, + Cache API, localStorage, and SW registrations are deleted together. + - Web Push on iOS requires iOS 16.4+, a home-screen-installed PWA, and is + region-dependent (EU behavior differs). Android Chrome push is full-featured. + - Safari 17+ storage quotas are generous on paper (~60% of disk per origin) but + are **best-effort**; `navigator.storage.persist()` is granted by heuristic + (home-screen install helps). Practical guidance from the field still recommends + keeping precached bundles lean (tens of MB, not hundreds). +2. **No existing festival data source exists.** The authoritative data pipeline + (who authors schedule/map/emergency content, in what format, hosted where) + must be designed from scratch or supplied by the festival organization. +3. **HTTPS is mandatory** for service workers, geolocation, and the Storage API. + Hosting must be HTTPS-capable (this does not predetermine a host). + +--- + +## 3. Product Understanding + +**Lumen** is a mobile-first **Progressive Web App** — explicitly *not* a native +app — serving as an offline-first festival companion for **~300–500 attendees**. + +Core principle: **"Everything attendees need at the festival must work with zero +internet connectivity."** The festival environment is hostile to connectivity +(no cell/Wi-Fi, overloaded networks, airplane mode) and hostile to usability +(bright sun, darkness, noise, stress, low battery, movement). + +The product is organized around four primary destinations, in priority order: + +1. **EMERGENCY** — highest priority; must work with absolutely zero connectivity; + must remain reachable from everywhere in the app. +2. **SCHEDULE** — lineup, now/next, personal (local) favorites, filters, search. +3. **MAP** — festival-provided custom map with key locations; **not** online map + tiles; GPS never required. +4. **FESTIVAL** — general information (rules, FAQ, logistics, vendors, contacts). + +Content should be treated as **versioned data** (a Festival Data Package), not +hardcoded logic. Connectivity is an **enhancement** (updates, announcements, +schedule changes) and must never be a prerequisite for critical functionality. +Future mesh networking is a **clean extension point only** — nothing mesh-related +is built or assumed in V1. + +Success condition: an attendee who installs Lumen before arriving can use every +critical feature for the entire festival with their phone in airplane mode. + +--- + +## 4. Explicit Requirements + +IDs are traceable and will carry into the architecture phase. + +### 4.1 Platform + +| ID | Requirement | +|----|-------------| +| R-P1 | Lumen is a mobile-first PWA; primary targets are iOS Safari and Android Chrome. | +| R-P2 | No App Store / Play Store distribution; no native code in V1. | +| R-P3 | Served over HTTPS (required for SW, geolocation, storage APIs). | +| R-P4 | Desktop support only where it comes free; never a design target. | + +### 4.2 Navigation & core UX + +| ID | Requirement | +|----|-------------| +| R-N1 | Four primary destinations: EMERGENCY, SCHEDULE, MAP, FESTIVAL. | +| R-N2 | Navigation must be extremely simple and obvious. | +| R-N3 | Emergency must be easily accessible from every other part of the app. | +| R-N4 | UX optimized for one-handed use, gloves-free large touch targets, sunlight and darkness, stress, and low attention. | + +### 4.3 Emergency + +| ID | Requirement | +|----|-------------| +| R-E1 | Emergency baseline must be available with **zero** connectivity, always. | +| R-E2 | Static emergency data (procedures, contacts, locations, address/coordinates) bundled locally with the app/dataset. | +| R-E3 | Dynamic emergency data (e.g., updated contacts) may update when online but must never *depend* on a server. | +| R-E4 | Must support at minimum: 911 dialing, security contact, first aid & AED locations, emergency exits, muster points, procedures (weather/fire/lost person), festival address + GPS coordinates. | +| R-E5 | Emergency dialing (`tel:`) must work from installed standalone mode. | + +### 4.4 Schedule + +| ID | Requirement | +|----|-------------| +| R-S1 | Full schedule (artists, stages, events, workshops, times) available offline. | +| R-S2 | "Happening Now" and "Up Next" computed locally from local data. | +| R-S3 | Personal schedule (favorites) created and stored locally. | +| R-S4 | Filtering (stage, type) and search work offline. | +| R-S5 | Correct behavior analysis required for: device time, festival timezone, and incorrect device clocks. | + +### 4.5 Map + +| ID | Requirement | +|----|-------------| +| R-M1 | Festival map works offline; core map must NOT depend on online tiles. | +| R-M2 | Festival supplies custom map artwork/geometry. | +| R-M3 | Point-of-interest locations: stages, bathrooms, food, water, vendors, first aid, AEDs, security, entrances/exits, parking, camping, VIP, muster points, other infrastructure. | +| R-M4 | Browser Geolocation may be used as an enhancement; **never required** for critical functionality. | +| R-M5 | Map representation (SVG/vector vs raster vs canvas) to be evaluated, not yet chosen. | + +### 4.6 Festival info + +| ID | Requirement | +|----|-------------| +| R-F1 | General info (rules, FAQ, bring/don't-bring, parking, camping, transport, accessibility, vendors, merch, contacts, hours, venue) available offline. | +| R-F2 | Content is data-driven (part of the Festival Data Package). | + +### 4.7 Offline-first architecture + +| ID | Requirement | +|----|-------------| +| R-O1 | App shell loads with no network at all (after first successful install/load). | +| R-O2 | Service worker caches shell + assets; local storage holds all datasets. | +| R-O3 | Local data is the source of truth while offline. | +| R-O4 | App startup must never require network success. | +| R-O5 | Updates apply **atomically**; a failed/interrupted update preserves the last known-good dataset. | +| R-O6 | The app can self-assess dataset completeness/consistency → honest **OFFLINE READY** state. | +| R-O7 | Must survive: browser restart, phone restart, storage pressure; must degrade gracefully when storage was evicted (detect + re-bootstrap). | +| R-O8 | Respect browser storage limits: budget total footprint, monitor via `navigator.storage.estimate()`, request persistence. | +| R-O9 | Data versioning, integrity validation, and migration path for schema changes. | + +### 4.8 Festival Data Package (concept) + +| ID | Requirement | +|----|-------------| +| R-D1 | Festival content is versioned data, not hardcoded logic, wherever practical. | +| R-D2 | Package includes manifest + integrity/version metadata at minimum; exact structure TBD in architecture phase. | +| R-D3 | Define: authoritative source, publishing flow, delivery, validation, local storage, version tracking, atomic apply, failure handling, rollback. | + +### 4.9 Future extensions + +| ID | Requirement | +|----|-------------| +| R-X1 | Online extras (announcements, schedule changes, weather) may exist later; never gate V1 critical features. | +| R-X2 | Architecture must expose a clean transport-abstraction seam for future mesh without contaminating core logic. | +| R-X3 | No mesh technology is chosen, implemented, or library-dependended-upon in V1. | + +### 4.10 Security & reliability + +| ID | Requirement | +|----|-------------| +| R-G1 | Initial threat analysis performed (§18); realistic vs theoretical threats distinguished. | +| R-G2 | Festival data integrity protected (validation of updates before activation). | +| R-G3 | No unnecessary security complexity; no secrets in the client. | +| R-R1 | Behavior defined for every failure scenario in §19. | + +--- + +## 5. Architectural Constraints + +Non-negotiable principles (from the brief, adopted verbatim in intent): + +1. Critical V1 functionality works without internet. +2. Emergency baseline always locally available. +3. Startup never requires network success. +4. UI never directly depends on a remote API for critical V1 features. +5. Festival content = versioned data, not hardcoded feature logic (where practical). +6. Local data is the offline source of truth. +7. Connectivity is an enhancement, not a prerequisite. +8. No partially-applied datasets. +9. Failed update ⇒ last known-good preserved. +10. App can determine whether it holds a complete, consistent offline dataset. +11. No mesh implementation in V1. +12. Mesh concerns never leak into feature/UI logic. +13. GPS never required for critical functionality. +14. No speculative complexity in V1. +15. Prefer the simplest architecture that satisfies the requirements. + +**Constraints derived during discovery (to be ratified in architecture phase):** + +- **C-16 (Install-first):** Because iOS storage eviction and push both hinge on + home-screen installation, *getting users installed before the festival* is a + load-bearing product requirement, not a nice-to-have. +- **C-17 (Bootstrap honesty):** True offline capability requires at least one + successful online bootstrap (app shell + dataset) before arrival. The + "pre-festival distribution" problem is therefore a real requirement area. +- **C-18 (Eviction survival):** The app must detect "my storage was wiped" on + launch and degrade to a clear recovery state rather than a broken blank app. +- **C-19 (Foreground-only background model):** Nothing may depend on background + execution, background sync, or push wake-ups for correctness on iOS. +- **C-20 (Single origin):** All app shell + data must live on one HTTPS origin + (storage and SW scope are per-origin; third-party hosting of assets has + partitioning/quota consequences). + +--- + +## 6. Non-Requirements (V1) + +Explicitly **out** of V1 scope: + +| # | Non-requirement | Notes | +|---|-----------------|-------| +| NR-1 | Mesh networking (any form) | Seam only; no protocol, discovery, routing, store-and-forward, BLE/WebRTC mesh. | +| NR-2 | Native iOS/Android apps | PWA only. | +| NR-3 | App Store / Play Store presence | Not needed; avoid TWA/wrapper complexity too. | +| NR-4 | User accounts / authentication | No sign-in in V1; favorites are device-local. | +| NR-5 | Cross-device sync of favorites | Consequence of NR-4; revisit later. | +| NR-6 | Real-time collaboration / social features | None. | +| NR-7 | GPS-dependent features | Location is enhancement-only. | +| NR-8 | Online map tile services as core map | Core map is local/custom. | +| NR-9 | In-app payments / merch checkout | None. | +| NR-10 | User-generated content | None. | +| NR-11 | Multi-language i18n | Assumed English-only for V1 (A-09). | +| NR-12 | Background sync / periodic sync | Not reliably available cross-platform (C-19). | +| NR-13 | Push notifications as a critical channel | At best an enhancement; cannot be depended on (iOS install-only, region quirks). | +| NR-14 | Admin/CMS authoring UI | Authoring flow TBD; V1 may use a simple file-based pipeline (§15). | +| NR-15 | Analytics beyond minimal, privacy-safe diagnostics | Data minimization (§18). | + +--- + +## 7. Assumptions + +Every assumption below is **explicit and reversible**. None has been silently +converted into a requirement. + +| ID | Question / Assumption | Impact | Recommended Default | +|----|-----------------------|--------|---------------------| +| A-01 | Festival is a single annual/one-off event (not multi-venue, multi-year platform). | Shapes data packaging & versioning scope. | Single-event V1; package keyed by festival edition so multi-event is a later, cheap extension. | +| A-02 | Attendee count 300–500; modest dataset size (schedule ≤ ~1k items; map assets ≤ ~50 MB total). | Storage budget, update strategy, performance targets. | Design for ≤ 50 MB total local footprint; verify with real content. | +| A-03 | One language (English). | All content schemas simpler. | English only; keep content strings in data (not code) so i18n is later possible. | +| A-04 | Festival can supply: schedule data, custom map artwork + POI coordinates, emergency info, general info — in some digital form. | The entire data pipeline depends on this. | Request structured inputs (spreadsheet/JSON + map file); define intake templates in architecture phase. | +| A-05 | Users will have internet access **before** the festival (at home/town) to install & bootstrap. | If false, need on-site distribution (gate kiosk, QR posters, local LAN server). | Assume pre-arrival bootstrap; plan on-site bootstrap as contingency (open question OQ-3). | +| A-06 | Attendees use reasonably modern phones: iOS 16.4+ / Android Chrome ~last 3 years. | Determines baseline APIs (SW, IDB, Intl, standalone). | Baseline: iOS 16.4+ Safari, Android Chrome 110+; graceful degradation below that to "basic static page still readable". | +| A-07 | No user accounts; device-local personalization only. | Eliminates auth, server-side state, sync conflicts. | Confirm; if accounts ever required, they'd be additive, not foundational. | +| A-08 | Emergency phone numbers/contacts are known and provided by organizers before build. | Emergency feature content. | Require a signed-off emergency content sheet as a project gate. | +| A-09 | No offline-update delivery via local LAN/mesh in V1; updates arrive only via internet when available. | Update architecture simplicity. | Internet-only updates in V1. | +| A-10 | Festival timezone is a single known IANA zone; schedule stored as absolute instants (UTC) + rendered in festival tz. | Now/Next correctness. | UTC storage + festival IANA zone rendering; device-tz toggle optional. | +| A-11 | The app is served from a single domain the organizers control. | SW scope, storage origin, update trust. | Single dedicated origin (e.g., `app..tld`). | +| A-12 | V1 does not need push notifications to be "critical"; announcements are pull-based when app is opened online. | Avoids iOS push fragility as a dependency. | Push = optional enhancement post-V1. | +| A-13 | Low-end Android devices are in the audience; performance budget must include them. | Rendering choice for map, list virtualization, JS budget. | Budget: interactive in < 3s on a 2021 mid-range Android over local cache; map pan/zoom ≥ 30fps. | +| A-14 | "Call 911" is US-context (911). If festival is elsewhere, number differs. | Emergency correctness. | Make the primary emergency number **data-driven**, not hardcoded (911 default). | +| A-15 | Festival map is a fixed geographic area (no need for world-scale panning). | Map tech can be bounded/simple. | Bounded custom map; no slippy-world tiles. | +| A-16 | Organizers accept that dynamic emergency updates require the user to have opened the app online at least once to receive them. | Expectation-setting. | Document this limitation in ops runbook. | +| A-17 | Development is solo/small-team; ops simplicity matters (static hosting likely sufficient). | Backend complexity budget. | Prefer static-file distribution for V1 updates; dynamic backend only if a real need appears. | + +--- + +## 8. Ambiguities + +Items where the brief is genuinely underspecified. For the five most +consequential, the four-part analysis follows the table. + +| ID | Ambiguity | Four-part analysis | +|----|-----------|--------------------| +| AMB-1 | **How do users get the app + dataset before/off-site?** (QR? link in email? gate Wi-Fi? pre-fest "install party"?) | **Unclear:** distribution channel & first-run experience. **Matters:** offline-first is impossible without a successful first online bootstrap (C-17); iOS makes install a manual multi-step flow. **Default:** URL/QR campaign before the festival + printed install instructions; app shows a clear "not yet offline ready" state until dataset downloaded. **If wrong:** large fraction of attendees arrive without data → product fails its core promise; mitigations (on-site LAN bootstrap) take real work. | +| AMB-2 | **Who authors/maintains festival data, and in what format?** | **Unclear:** authoritative source of schedule/map/emergency/info. **Matters:** determines the entire publishing pipeline and integrity model (§15). **Default:** organizers fill provided templates (CSV/JSON + map asset); Lumen build/publish tooling converts to signed Festival Data Package. **If wrong:** if organizers have an existing CMS/API, an adapter is needed; schema must stay import-friendly. | +| AMB-3 | **What does the festival map actually look like / what format will organizers provide?** (illustrated PNG? SVG? GIS data?) | **Unclear:** input artifact for the map subsystem. **Matters:** drives R-M5 evaluation (SVG vs raster vs canvas) and POI coordinate scheme. **Default:** illustrated raster background + separately authored POI coordinates in map-local coordinate space; SVG overlays if vector art is supplied. **If wrong:** if only GIS/venue data exists, a conversion step is needed; if map is enormous, tiling/splitting needed. | +| AMB-4 | **Are there any server-side capabilities at all?** (Is even static hosting available? Is there internet at the festival site for organizers?) | **Unclear:** whether updates/announcements are deliverable on-site. **Matters:** shapes whether "dynamic" features are realistic at all, and whether organizer comms need their own offline tooling. **Default:** static hosting on a CDN before + during event; assume organizer connectivity is as poor as attendees'. **If wrong:** if organizers have a site LAN, a local update mirror becomes a high-value later feature (and connects to the future transport seam). | +| AMB-5 | **When exactly is "OFFLINE READY" evaluated and shown, and what's the recovery UX when not ready?** | **Unclear:** indicator semantics, granularity (per-section vs global), and remediation UI. **Matters:** this is the app's honesty contract with users. **Default:** global indicator with per-section detail; re-bootstrap path when data missing; never claim ready unless manifest, all sections, assets, and integrity checks pass. **If wrong:** users trust a false "ready" and hit empty screens in emergencies — unacceptable. | +| AMB-6 | Festival duration / daily hours (affects schedule model, multi-day UX). | Default: support multi-day schedule; confirm dates. | +| AMB-7 | Whether schedule changes mid-festival are expected (and how urgent they are). | Default: yes, via dataset updates + optional announcements; urgency channel TBD. | +| AMB-8 | Branding/visual identity availability (icons, colors, splash). | Default: simple high-contrast house style until brand arrives. | +| AMB-9 | Whether the emergency number is 911 (US) or another jurisdiction. | Default: data-driven field; confirm jurisdiction. | +| AMB-10 | Accessibility commitments beyond legal baseline (WCAG AA?). | Default: WCAG 2.1 AA as target. | +| AMB-11 | Whether "lost person" includes a child-reunification workflow (active feature vs info page). | Default V1: informational procedure only; active reunification is a large feature and out of scope. | +| AMB-12 | Data retention/lifetime — does the app matter after the festival (memories/next-year teaser) or can it be ephemeral? | Default: keep working post-festival with the last dataset; cheap and harmless. | + +--- + +## 9. Open Questions + +Items that cannot be sensibly defaulted — they need stakeholder answers: + +1. **OQ-1:** What is the festival's name, location, dates, daily hours, and IANA timezone? +2. **OQ-2:** What are the exact emergency contacts (security, first aid org, medical provider), muster points, and venue address/GPS? (Required before Emergency can be finalized.) +3. **OQ-3:** What distribution channels exist to reach attendees before arrival (email list, socials, ticketing partner)? Is on-site connectivity available for a bootstrap station? +4. **OQ-4:** Who owns content updates during the festival, and on what device? (Defines authoring/publishing UX requirements.) +5. **OQ-5:** Does the festival have an existing site/brand/domain to host under, and who controls DNS/hosting? +6. **OQ-6:** What map artifacts can organizers actually produce? (Artist brief? Existing venue plan?) +7. **OQ-7:** Are there legal/liability review requirements for emergency content (who approves the wording)? +8. **OQ-8:** Is there any budget for hosting/CDN or is fully static free-tier hosting required? +9. **OQ-9:** Expected minimum OS versions in the audience (ticket-purchase analytics may reveal)? +10. **OQ-10:** Should Lumen support post-festival feedback/contact channel, or is it read-only forever? +11. **OQ-11:** Will there be multiple stages with simultaneous programming? (Now/Next conflict UX.) +12. **OQ-12:** Are quiet hours / overnight periods relevant to the schedule model? + +--- + +## 10. Major Architectural Decisions (to be made deliberately in the next phase) + +Numbered for later reference; **none are decided here.** + +| # | Decision | Why it matters | Candidates / considerations | +|---|----------|----------------|------------------------------| +| AD-1 | Frontend framework & rendering model | DX, bundle size, low-end device perf, ecosystem | No framework (vanilla TS + web components) vs Svelte/Solid (compile-away) vs React/Preact vs others. Must evaluate: JS budget, offline SW integration quality, team familiarity. | +| AD-2 | Service-worker strategy & tooling | The offline mechanism itself | Hand-written SW vs Workbox vs custom; precache vs runtime-cache split; SW update/activation UX (skipWaiting? user-prompted update?). | +| AD-3 | Local storage architecture | Data layer durability & query ergonomics | IndexedDB (raw vs wrapper like idb/Dexie) vs localStorage-only (too small) vs OPFS files vs SQLite-in-WASM (e.g., cr-sqlite/wa-sqlite) — evaluate iOS reliability carefully. | +| AD-4 | Festival Data Package schema & format | Interop, validation, size | JSON documents vs MessagePack vs SQLite file vs hybrid; one file vs section files; asset manifest design. | +| AD-5 | Integrity & authenticity model | Security (§18) | SHA-256 hashes in signed manifest only vs full package signature (e.g., Ed25519 public key baked into shell); key custody process. | +| AD-6 | Update apply/rollback mechanism | R-O5, R-O6 | Dual-slot (A/B dataset) with active pointer vs staged-write-then-commit vs version-keyed stores with GC of old versions; must survive mid-update kill. | +| AD-7 | Map representation | R-M5 | SVG (crisp, styleable, DOM-accessible, pan/zoom via transforms) vs raster tiles (pre-cached, memory-heavy when large) vs Canvas (perf, worse accessibility) vs layered hybrid; plus POI overlay model and map-local coordinate system. | +| AD-8 | Time handling strategy | R-S5 | UTC instants + festival IANA zone via Intl; optional server-time offset capture for skew detection; wrong-clock UX (warn vs auto-correct). | +| AD-9 | Hosting & distribution topology | R-P3, updates | Static CDN hosting (likely sufficient) vs tiny server; versioned immutable asset URLs; cache headers strategy. | +| AD-10 | Publish pipeline | R-D3 | Build-time generator (repo of JSON/CSV → signed package) vs lightweight admin tool vs CMS adapter; who presses "publish". | +| AD-11 | Transport abstraction seam shape | R-X2 | Interface boundaries so "InternetTransport" is the only V1 implementation and future local/mesh transports slot under a messaging/sync layer without touching UI. Must be minimal — risk of over-engineering. | +| AD-12 | Announcements design (even if V1-deferred) | Pull-based inbox model vs banner; storage of stale announcements; authenticity. | +| AD-13 | Observability without surveillance | R-G data minimization | No/low telemetry; on-device diagnostics screen; optional manual "share diagnostics" rather than automatic upload. | +| AD-14 | Repo/VCS & CI setup | VCS-1 | Own git repo; CI for build + Lighthouse/PWA checks + device-matrix tests. | + +--- + +## 11. Technology Questions Requiring Evaluation + +Each needs a small spike or documented evaluation in the next phase — not a +decision now. + +1. **TQ-1 Framework perf on low-end Android:** measure schedule-list rendering and + map pan/zoom with candidate stacks on a throttled mid-range device. +2. **TQ-2 IndexedDB reliability on iOS WebKit:** transaction failure history, + large-write behavior near quota, behavior after forced kill; compare against + OPFS and localStorage+files hybrid. +3. **TQ-3 SQLite-WASM viability:** does it earn its complexity for querying + (search/filter) vs plain IDB indexes? iOS Safari WASM memory limits? +4. **TQ-4 Service worker update semantics on iOS:** activation timing, + `clients.claim` behavior, SW killed mid-update frequency; how to make "app + update during festival" safe. +5. **TQ-5 SVG pan/zoom approach:** pointer-event based transforms, pinch zoom + quality, accessibility of interactive SVG elements with VoiceOver/TalkBack. +6. **TQ-6 Asset compression:** WebP/AVIF support matrix for map art and icons; + real-world size of the festival map at legible quality. +7. **TQ-7 Persistent storage grant rates:** measure `navigator.storage.persist()` + outcomes on iOS (installed) and Android; define behavior when denied. +8. **TQ-8 Install-funnel tooling:** detection of standalone mode, iOS install + instruction UI patterns, measuring install conversion. +9. **TQ-9 Package signing practicality:** WebCrypto Ed25519 support matrix on + target browsers (verify availability on iOS 16.4 baseline) vs hash-only + manifest validation as fallback. +10. **TQ-10 Search implementation:** client-side fuzzy search over ≤ ~1k items — + naive filter vs lightweight index (e.g., precomputed trigrams); cost/benefit. +11. **TQ-11 Build tooling:** bundler choice (vite/esbuild/etc.) and PWA plugin + maturity vs hand-rolled SW; dev-server HTTPS fidelity to production. +12. **TQ-12 Geolocation behavior:** permission persistence in iOS standalone + mode, time-to-first-fix in crowds, battery impact; whether to surface it at + all in V1. +13. **TQ-13 Testing strategy:** real-device matrix (iOS 16.4, latest iOS, low-end + Android, latest Android), airplane-mode test scripts, storage-eviction + simulation techniques. +14. **TQ-14 Timezone correctness:** Intl behavior for the festival zone on + baseline devices; DST edge cases if relevant. + +--- + +## 12. PWA / Browser Risks + +Verified platform facts → risks → mitigations to carry into architecture. + +| # | Risk | Evidence / Mechanism | Severity | Mitigation direction | +|---|------|----------------------|----------|----------------------| +| PW-1 | **7-day storage eviction (iOS)** for users who browse in a tab before the festival and don't install. | Safari ITP: 7-day cap on all script-writable storage incl. SW registration; installed home-screen apps exempt. | High | Install-first UX; re-cache shell on every launch; detect empty storage and re-bootstrap; never assume prior state. | +| PW-2 | **Manual-only install on iOS** (no `beforeinstallprompt`). | iOS requires Share → Add to Home Screen (4+ taps). | High | Custom install coach with screenshots; QR flow; pre-festival campaign; measure funnel. | +| PW-3 | **All-or-nothing eviction** can wipe dataset between install and festival (storage pressure). | Eviction deletes all origin data at once. | Medium | Keep footprint lean; `persist()` request; honest OFFLINE READY check on every launch; one-tap re-download. | +| PW-4 | **No background sync on iOS**: updates only happen while app is open. | Background Sync / Periodic Sync unsupported on iOS Safari. | Medium | Sync-on-open pattern; opportunistic updates when online; never schedule-dependent. | +| PW-5 | **Push is not a reliable channel** (iOS: installed-only, region quirks; both: permission opt-in). | Web Push iOS 16.4+ installed PWAs only; EU behavior differs. | Medium | Treat announcements as pull-based in V1 (NR-13). | +| PW-6 | **SW lifecycle fragility**: iOS kills SWs aggressively; mid-update kills possible. | Documented WebKit behavior; SWs not long-lived processes. | High | Keep SW logic small, idempotent, resumable; no in-SW long computations; atomic staged downloads (AD-6). | +| PW-7 | **Quota surprises**: best-effort storage; `persist()` not guaranteed. | Quotas are estimates; eviction under pressure is silent. | Medium | `storage.estimate()` monitoring, budget alarms, graceful degradation UI. | +| PW-8 | **Standalone-mode quirks on iOS**: no browser back/forward UI, external links can strand users, safe-area insets, keyboard viewport behavior. | Known standalone behavior. | Medium | In-app back affordances; intercept/label external links; `viewport-fit=cover` + safe-area CSS; test on device. | +| PW-9 | **HTTPS requirement** excludes casual local-HTTP testing/distribution; any on-site LAN bootstrap would need TLS or localhost tricks. | SW + Geolocation + Storage API require secure contexts. | Low–Med | Plan TLS for any distribution point; localhost-only for dev. | +| PW-10 | **First-load bootstrap race**: user goes offline before first full cache completes. | Network-dependent first run. | Medium | Download critical sections in priority order (Emergency → Schedule → Map → Info → assets); resumable; show progress and what's ready. | +| PW-11 | **Browser version skew**: older iOS/Android missing APIs. | Baseline assumption A-06. | Medium | Feature-detection layer; minimum viable static fallback page; state supported baseline clearly. | +| PW-12 | **App update during festival**: new app shell + old dataset (or vice versa) compatibility. | Independent update channels for shell vs data. | Medium | Manifest declares min/max compatible app/dataset versions; refuse to activate incompatible combos (R-O6). | +| PW-13 | **Storage partitioning / third-party assets**: cross-origin assets complicate caching & quota. | Chrome 115+/Safari partitioning. | Low | Single-origin everything (C-20). | +| PW-14 | **Private browsing**: ephemeral/restricted storage in private mode. | Safari private mode limits persistence. | Low | Detect where possible; advise normal mode for install. | +| PW-15 | **iOS EU regulatory variability**: PWA behavior differs in EU regions. | Documented iOS 17.4+ EU differences. | Low | Assume worst case (tab behavior); install-first mitigates. | + +--- + +## 13. Offline Risks + +| # | Risk | Notes | Mitigation direction | +|---|------|-------|----------------------| +| OF-1 | **Bootstrap dependency**: offline-first only works after one successful online session. | Fundamental; see C-17, AMB-1. | Pre-festival distribution; on-site contingency (OQ-3); printed core info as physical fallback (poster/handout) — organizers' call. | +| OF-2 | **Storage evicted between install and festival** (PW-3). | Highest-likelihood data-loss path. | Launch-time integrity check; guided re-bootstrap; emergency data additionally embedded in app shell where feasible (see OF-3). | +| OF-3 | **Emergency data loss on eviction** is unacceptable. | Static emergency baseline could be *embedded in the app bundle itself* (bundled JSON in the JS/HTML artifact) so it survives even total dataset loss. | Evaluate: ship immutable emergency baseline in shell + optional dynamic overlay from dataset. This is a strong candidate architectural decision. | +| OF-4 | **Partial download / interrupted update** leaves inconsistent state. | Mobile reality: screens off, radios flaky. | Section-level downloads with hashes; activate only complete validated sets; keep previous set until new set committed (AD-6). | +| OF-5 | **Corrupted local data** (bit rot unlikely; buggy writes more likely). | IDB transaction bugs. | Integrity check on launch (checksums per section); quarantine + re-download; never trust unchecked data for Emergency display. | +| OF-6 | **Quota exhaustion mid-download** (large map assets). | QuotaExceededError on write. | Pre-check `storage.estimate()`; size budgets in manifest; abort cleanly and keep last-good. | +| OF-7 | **Wrong device clock** breaks Now/Next and "is festival live". | Airplane-mode phones drift; users change clocks. | §15.7 analysis; server-offset capture when online; festival-timezone anchoring; explicit "your clock looks wrong" UX when skew detected. | +| OF-8 | **Stale dynamic data mistaken for fresh** (schedule changed, user never online). | Honesty problem. | Show dataset age/version everywhere relevant; "data as of …" labels; organizers announce changes redundantly (PA/boards). | +| OF-9 | **App-shell update breaks against stored data** (schema drift). | Mid-festival app updates. | Versioned schemas + migrations run at activation; dataset manifest declares schema version; rollback path (OF-10). | +| OF-10 | **Rollback mechanics** after a bad update is applied. | Rare but must be defined. | Retain N=1 previous dataset slot until new one proven; "restore previous" action; app can always fall back to embedded emergency baseline. | +| OF-11 | **Phone restart / browser restart** mid-session loses in-memory state. | Favorites unsaved, draft state. | Write-through persistence for any user mutation; no memory-only truth. | +| OF-12 | **Low-battery-induced cold starts** repeatedly re-warm the app. | Users force-quit to save battery. | Fast cold start budget (< ~2s to usable), minimal re-hydration cost. | +| OF-13 | **Users clearing "website data"** thinking it's harmless. | iOS Settings → Safari → clear. | Nothing to prevent; re-bootstrap must be painless; embedded emergency baseline (OF-3) limits blast radius. | + +--- + +## 14. Emergency-System Risks + +| # | Risk | Severity | Notes / Mitigation direction | +|---|------|----------|------------------------------| +| EM-1 | **Emergency content wrong or stale** (wrong security number, moved first-aid tent). | Critical | Content sign-off gate (A-08, OQ-2, OQ-7); version stamp visible on every emergency screen; dynamic overlay updates only additive/corrective; static baseline reviewed per edition. | +| EM-2 | **Emergency data missing after eviction**. | Critical | Embed static baseline in app shell (OF-3) so *some* emergency info exists even at zero storage. | +| EM-3 | **`tel:` link fails** (no SIM, airplane mode). | High | Note that emergency calls may still work without SIM (region-dependent); display numbers as copyable text too; never present dialing as the only path. | +| EM-4 | **GPS coordinates wrong** → rescuers sent to wrong location. | High | Coordinates come from validated dataset; display in multiple formats (decimal + what3words-style fallback TBD); human-readable address always primary. | +| EM-5 | **User panic UX**: fumbling, mis-taps, unreadable in sunlight. | High | Emergency screen design: giant targets, maximum contrast, zero clutter, works in dark and direct sun, reachable one-handed; test with real users. | +| EM-6 | **False sense of capability** ("offline ready" shown when it isn't). | High | OFFLINE READY semantics must be provable (R-O6); emergency has its own always-true baseline regardless (EM-2). | +| EM-7 | **Legal/liability of procedural content** (first-aid instructions, evacuation guidance). | Medium | Procedures written/approved with qualified parties; app positions itself as *information access*, not medical/emergency service; disclaimers reviewed (OQ-7). | +| EM-8 | **Emergency updates race**: contact changes mid-festival but user is offline. | Medium | Accept as residual risk (A-16); mitigate with physical redundancies (signage/PA) owned by organizers; dynamic overlay fetched aggressively whenever any connectivity appears. | +| EM-9 | **Localization of emergency comprehension** under stress. | Low–Med | Plain language, short imperatives, icons + text (English-only V1 per A-03; revisit if audience differs). | +| EM-10 | **Misuse/false-emergency**: app must not itself trigger emergency services accidentally. | Low | Explicit user action only for dialing; no auto-call features ever. | + +--- + +## 15. Data Architecture Questions + +### 15.1 Authoritative source & publishing +- Who authors each content class, and what tooling do they get? (OQ-4) +- Default direction: **content-as-data in a repo or simple store → build/publish + step produces a signed Festival Data Package → uploaded to static hosting → + app pulls by version**. No dynamic backend required for V1 (A-17). +- Must define publishing roles (who can publish emergency changes) and an + audit trail even in a minimal pipeline. + +### 15.2 Package content (conceptual — structure TBD) +Candidate sections, each independently versioned & hashed: +- `manifest` (package id, edition, version, section inventory, hashes, sizes, min/max app compatibility, timestamp) +- `emergency` (static baseline candidates also embedded in shell — OF-3) +- `schedule` (stages, slots, artists, events, tags, change metadata) +- `map` (geometry/artwork reference, POI list with map-local coordinates, categories) +- `locations` (POI detail records; may merge with map — boundary TBD) +- `festival-info` (structured info pages/sections) +- `announcements` (V1: optional pull inbox; NR-13) +- `assets` (map images, artist images?, icons) — **asset weight budget needed** (A-02) + +### 15.3 Delivery & validation +- Immutable, versioned URLs (content-addressed or `/v{n}/…`). +- Per-section hash verification; whole-package signature if WebCrypto Ed25519 + baseline allows (TQ-9); else hash-chain in signed manifest. +- Size caps enforced client-side (DoS hygiene, §18). + +### 15.4 Local storage model +- Likely split: **Cache Storage** for shell + immutable assets; **IndexedDB** + for structured datasets; possibly small **localStorage** for flags/preferences. +- Must decide: datasets as parsed objects vs raw blobs parsed on read (memory + vs speed trade-off). +- Must define store layout that supports **A/B slots** (AD-6). + +### 15.5 Atomic apply & rollback +- Download → verify → write to staging slot → integrity check → flip active + pointer → GC old slot after N successful launches. +- Interruption at any step must leave previous active slot intact. +- Decision needed: is the "pointer flip" an IDB transaction or an SW-managed + cache rename? (Both have iOS failure modes to spike — TQ-2/TQ-4.) + +### 15.6 Migrations & compatibility +- Dataset schema version + app compatibility range in manifest. +- App-shell updates may require data migration at activation; migrations must + be idempotent and reversible-where-possible (or gated behind backups). + +### 15.7 Time model (R-S5 analysis) +- **Store** all event times as UTC instants; **render** in festival IANA + timezone via `Intl` (works offline; tz database ships with the OS/browser). +- **Device clock risk:** wrong clock ⇒ wrong Now/Next and wrong "festival live" + state. Strategy candidates (choose later): + 1. Trust device clock (simplest; wrong clocks silently wrong). + 2. Capture server-time offset whenever online; store offset + captured-at; + use offset when device skew exceeds threshold; show warning when stale. + 3. Organizers broadcast clock corrections via announcements (social fix). +- Recommended direction: (2) with graceful degradation to (1), plus visible + dataset timestamp so users can reason about staleness. +- Edge cases: DST transitions near festival dates; multi-day events crossing + midnight; user traveling across zones before arrival. + +### 15.8 Favorites / user state +- Separate local-only store (never part of festival package; never overwritten + by dataset updates; survives dataset rollback). +- Schema keyed by stable event IDs → **event IDs must be stable across dataset + versions** (requirement to push onto the data schema). +- On dataset update, handle: event moved (keep favorite, update time), event + cancelled (mark), event removed (orphan policy). + +### 15.9 Size budgeting (open, needs real content) +- Working hypothesis: shell < 1 MB JS+CSS; schedule+info JSON < 2 MB; map + artwork dominates (target ≤ 30 MB across zoom levels); total < 50 MB (A-02). +- Must validate against actual map art early (it is the top size risk). + +--- + +## 16. Synchronization Questions + +| # | Question | Current thinking (not a decision) | +|---|----------|-----------------------------------| +| SY-1 | Is sync ever bidirectional in V1? | No. V1 is **pull-only** (device ← package). No user writes leave the device (NR-4/NR-5). This eliminates conflict resolution entirely. | +| SY-2 | When does the app check for updates? | On open, when connectivity detected; opportunistic; never blocking; never in background (C-19). | +| SY-3 | Update granularity? | Section-level diffs vs whole-package re-download; decide after size budgeting (§15.9). Whole-package may be simplest if < ~10 MB delta. | +| SY-4 | Announcements: pull inbox semantics? | Fetch list when online; store locally; expiry rules; dedupe by id; authenticity (§18). | +| SY-5 | What if two dataset versions arrive back-to-back? | Monotonic version acceptance; never downgrade from network (rollback is local-only action). | +| SY-6 | How is "freshness" communicated? | Dataset version + generated-at + fetched-at shown in status screen; staleness thresholds for warnings. | +| SY-7 | Should favorites ever sync cross-device later? | If ever: export/import (QR/text) before considering accounts; keep IDs stable to allow it. | +| SY-8 | Does anything need server-side state for updates? | Not for static package hosting. Dynamic backend only if live features materialize post-V1. | + +--- + +## 17. Future Mesh Architectural Questions + +**Scope reminder:** V1 implements nothing here. These questions shape the seam +(AD-11) and nothing else. + +### 17.1 Browser feasibility analysis (honest assessment) +- **BLE:** Web Bluetooth exists on Android Chrome only (central role), **not on + iOS Safari**, and cannot do background advertising or device-to-device relay. + Not a viable mesh substrate in-browser for our primary targets. +- **Wi-Fi Direct / ad-hoc / mDNS:** not exposed to browsers. No LAN discovery + APIs in Safari/Chrome on mobile. +- **WebRTC data channels:** peer-to-peer capable in principle, but requires + signaling (normally internet or a shared rendezvous), NAT traversal help, and + user gestures; no background operation on iOS. A *local-network* WebRTC link + is conceivable but fragile and complex. +- **Conclusion:** a meaningful mesh capability will almost certainly require + **something outside the browser**: a native companion app, an OS-level local + network service, dedicated hardware relay, or organizer-run LAN + infrastructure. The browser app's job is to be **transport-agnostic**, not to + own the mesh. + +### 17.2 Seam design questions (for later) +| # | Question | +|---|----------| +| ME-1 | Is the abstraction over **transports** (byte pipes) or over **sync/messaging semantics** (deliver named updates, reconcile state)? The latter is more useful and keeps mesh concerns out of features (principle 12). | +| ME-2 | Should the data layer model content as **immutable versioned documents + operations log** so any transport can carry them? (Aligns with Festival Data Package design — one design serving two futures.) | +| ME-3 | Where does transport selection/plumbing live? Proposal: a single `SyncChannel` interface injected into the data layer; V1 ships exactly one implementation (`HttpPullChannel`). UI only ever sees "data freshness", never transports. | +| ME-4 | Security boundary: every transport must deliver **signed/verified** payloads — validation stays in the data layer so an untrusted mesh cannot inject content (§18, SE-9). | +| ME-5 | What identity/trust would a future mesh need (device pairing? organizer-signed relay nodes?) — entirely deferred, but argues for keeping signing keys/scheme transport-independent now. | +| ME-6 | Cost control: the seam must cost ~near-zero in V1. If an abstraction demands speculative machinery, defer the abstraction itself until a concrete second transport exists (principle 14/15). | + +**Recommendation for next phase:** keep the seam *conceptual and minimal* — a +clean module boundary and interface definition — rather than building framework +plumbing. Revisit only when a concrete transport requirement appears. + +--- + +## 18. Security Questions & Initial Threat Analysis + +### 18.1 Threat inventory (realistic vs theoretical) + +| ID | Threat | Class | Analysis | +|----|--------|-------|----------| +| SE-1 | **Malicious/tampered festival data package** (MITM of update, compromised hosting) | Realistic | Primary supply-chain threat. Mitigation: HTTPS + content hashes in signed manifest (AD-5); refuse activation on verification failure. | +| SE-2 | **Compromised publish credentials** pushing bad data | Realistic | Custody: minimal publishers, signed packages, version monotonicity, fast manual rollback runbook. | +| SE-3 | **XSS/content injection via festival content** (info pages, announcements, artist bios) | Realistic | Render content as data/text; no raw HTML ingestion; if rich text needed, strict sanitizer allowlist. | +| SE-4 | **Malicious announcements** (fake evacuation order) | Realistic, high-impact | Announcements inherit package signing/authenticity; never render unsigned network content as emergency content. | +| SE-5 | **Replay of old (stale) signed package** | Realistic, low-impact | Monotonic version checks; never accept lower version from network. Stale-but-valid data is an honesty issue (OF-8) more than a security one. | +| SE-6 | **Local data tampering** (user or malware on shared device) | Theoretical-low | No high-value secrets stored; integrity checks catch accidental corruption; deliberate local tampering only harms that user. | +| SE-7 | **Location privacy leakage** | Low in V1 | Geolocation is optional, on-device only, never transmitted (no server writes in V1). Ensure no URL/query leakage of coords. | +| SE-8 | **User privacy / tracking** | Low by design | No accounts, no analytics by default (AD-13); if any diagnostics, on-device only, opt-in, no identifiers. | +| SE-9 | **Future mesh injection** (malicious peer content) | Future | Rule now: data layer validates signatures regardless of transport; no transport bypasses validation (ME-4). | +| SE-10 | **DoS via oversized update / asset bomb** | Realistic-medium | Manifest-declared sizes enforced pre-download; per-section caps; abort + keep last-good. | +| SE-11 | **Service worker hijack / scope confusion** | Low | Single origin (C-20); strict scope; subresource integrity for any external scripts (ideally zero external scripts). | +| SE-12 | **Secrets in client** | N/A by design | No API keys, no signing private keys, no credentials ship to the browser (R-G3). | +| SE-13 | **Emergency-number spoofing in dynamic overlay** | Realistic, high-impact | Dynamic emergency overlay must be package-signed like everything else; static baseline only replaceable via reviewed build. | +| SE-14 | **Dependency supply chain** (npm) | Standard | Lockfiles, minimal deps, review; standard hygiene — not a novel architecture concern. | +| SE-15 | **TLS misconfiguration / downgrade** | Standard | HSTS; HTTPS-only endpoints; no mixed content. | + +### 18.2 Open security questions +- **SQ-1:** Ed25519 via WebCrypto on baseline devices? (TQ-9) If gaps exist, + fall back to SHA-256 hash manifest over TLS without weakening the model. +- **SQ-2:** Key custody: who holds the package signing key; how is it rotated; + what's the recovery if lost (requires app-shell update to ship new key)? +- **SQ-3:** Is there any scenario where the app accepts *unsigned* dynamic + content? Proposed answer: **never for emergency; never for executable + content; display-only, clearly labeled** for anything else — ideally none. +- **SQ-4:** Logging policy: keep on-device diagnostic log with PII scrubbed; + define what "scrubbed" means before any export feature exists. +- **SQ-5:** If an announcements inbox exists, retention/expiry and maximum + message size limits. + +--- + +## 19. Reliability Questions — Failure-Scenario Matrix + +Required behavior for each scenario (these become acceptance criteria): + +| # | Scenario | Required Lumen behavior | +|---|----------|--------------------------| +| FA-1 | No internet / airplane mode | All critical features work from local data; connectivity status shown honestly; no error walls; no feature silently disabled without indication. | +| FA-2 | No Wi-Fi, no cellular | Identical to FA-1 (no distinction needed by the app beyond "offline"). | +| FA-3 | GPS unavailable | App fully functional; map usable without blue dot; no prompts blocking anything. | +| FA-4 | GPS inaccurate | If shown, location displayed with accuracy caveat; never used for safety-critical guidance ("go to X" is always user-read map + signage). | +| FA-5 | Browser restarted | Instant restore: shell from cache, data from IDB, favorites intact, OFFLINE READY re-verified in background without blocking first paint. | +| FA-6 | Phone restarted | Same as FA-5; cold-start budget applies (OF-12). | +| FA-7 | Browser storage unavailable (private mode/quota 0) | Detect; run degraded: render whatever ships in the app bundle (shell + embedded emergency baseline); explain limitation; advise normal mode/install. | +| FA-8 | Storage evicted | Detect empty/corrupt state at launch; show recovery screen: what's lost, one-tap re-download when online; embedded emergency baseline still available (OF-3); never blank-screen. | +| FA-9 | Corrupted local data | Integrity check fails → quarantine affected section → fall back to previous slot if present → else re-download → else embedded baseline + honest status. | +| FA-10 | Interrupted update | Staging slot discarded or resumed; active slot untouched; user never sees partial content; retry idempotently. | +| FA-11 | Failed update (hash mismatch / signature fail) | Reject; keep last-good; surface non-alarming status ("update unavailable — current data still works"); log diagnostics locally. | +| FA-12 | Invalid festival data published | Client-side validation rejects at activation; organizers need server-side validation + staging preview in the pipeline (publishing runbook). | +| FA-13 | Incorrect device time | Now/Next uses corrected offset when available; otherwise device time with visible dataset timestamp; warn on detected large skew (TQ-14). | +| FA-14 | Schedule changes | Dataset update flow; cancelled/moved handling (§15.8); announcements if enabled; organizers use PA/signage for urgency. | +| FA-15 | Emergency information changes | Dynamic overlay update when online; static baseline immutable per build; version stamp visible (EM-1). | +| FA-16 | App update during festival | Compatibility ranges in manifests (PW-12); staged SW activation; user-informed update prompt if mid-session; never break current session. | +| FA-17 | Low battery | Dark-mode default at night; no background timers; fast interactions; no unnecessary geolocation polling; respect `prefers-reduced-motion` and battery-driven perf degradation. | +| FA-18 | First launch, then immediate offline before bootstrap completes | Show exactly which sections are ready; Emergency baseline works regardless; guide user to reconnect for the rest (PW-10). | + +--- + +## 20. UX Questions + +### 20.1 Structural +- **Home:** four huge, obvious destinations (Emergency visually dominant). No + dashboard clutter, no onboarding maze. +- **Emergency accessibility from everywhere:** persistent affordance (e.g., + always-visible emergency entry point in the nav/chrome) — decide pattern in + architecture/design phase; must not depend on which tab is active. +- **Navigation model:** bottom tab bar (thumb-reach) + explicit in-app back; + deep-links for emergency/muster info; standalone-mode safe (PW-8). + +### 20.2 Environmental +- Sunlight: max contrast, large type, avoid fine gray detail. +- Night: true dark theme (OLED-friendly), red-shift option? (open UX question), + avoid flash-on-open. +- Rain/movement: oversized touch targets (≥ 48dp recommended), forgiving hit + areas, confirm destructive/important taps. +- Noise: don't rely on audio for anything critical. + +### 20.3 Accessibility +- WCAG 2.1 AA target (AMB-10): screen-reader labels for all POIs and schedule + items; logical heading structure; focus management on tab switches; + `prefers-reduced-motion` honored; map needs a **non-visual or list-based + equivalent** (e.g., "nearest facilities list") — also serves GPS-free use. +- Interactive SVG accessibility if SVG route chosen (TQ-5). + +### 20.4 State honesty +- **OFFLINE READY ✓** must be a provable composite state: shell cached + + dataset present + all sections validated + compatibility confirmed (+ + optionally assets complete). Show degraded states distinctly: + `READY` / `PARTIAL (list what)` / `RECOVERY NEEDED` / `EMERGENCY ONLY`. +- Dataset age/version visible in one predictable place. +- No spinners pretending; no fake progress; loading states only where real. + +### 20.5 Open UX questions +- UX-1: Does Emergency get a dedicated always-on button vs a tab (vs both)? +- UX-2: Install coach design & timing (first visit? pre-festival email asset?). +- UX-3: How much map interactivity in V1 (search POI, categories toggle, + "nearest" list) vs static labeled map? +- UX-4: Now/Next presentation when device clock untrusted. +- UX-5: Favorites conflict UX when favorited events overlap in time. +- UX-6: What does the app show **after** the festival ends (schedule in past + tense? memorial mode? next-edition placeholder?) — AMB-12. +- UX-7: Onboarding: zero-step ideal; is a 3-line "how to use offline" card + enough? +- UX-8: Theming: is brand available (AMB-8) or high-contrast utility style? +- UX-9: Haptics (`navigator.vibrate`) for emergency confirmations — Android + only; degrade silently. + +--- + +## 21. Recommended Next Architectural Investigation + +Ordered by risk-reduction value. Each is a **spike/investigation, not a +commitment**: + +1. **SPIKE-1 — Storage & eviction test harness on real iOS hardware.** + Measure: IDB reliability near quota, `persist()` grant behavior, 7-day ITP + reproduction on tab vs installed PWA, all-or-nothing eviction recovery UX. + *(Retires PW-1/3/7, TQ-2/7 — highest-severity unknowns.)* +2. **SPIKE-2 — Atomic dataset update prototype.** Service worker + IDB A/B slot + with staged download, hash verify, pointer flip, kill-mid-update survival, + rollback. Include app-shell vs dataset compatibility gating. *(Retires AD-6, + OF-4/9/10, FA-10/11/16.)* +3. **SPIKE-3 — Map representation bake-off.** Realistic sample festival map in + SVG vs tiled raster vs canvas on a low-end Android + iPhone: pan/zoom fps, + memory, accessibility, authoring effort. *(Retires AD-7, AMB-3.)* +4. **SPIKE-4 — Festival Data Package schema draft v0** with sample content: + manifest, sections, hashes, stable event IDs, time model (UTC + festival tz), + favorites-overlay behavior. *(Retires most of §15.)* +5. **SPIKE-5 — Install & bootstrap funnel.** iOS install coach prototype, + standalone detection, first-run prioritized download order, OFFLINE READY + state machine draft. *(Retires PW-2/10, AMB-1/5.)* +6. **SPIKE-6 — Framework shortlist benchmark** (2–3 candidates) on throttled + mid-range Android: cold start, 1k-row schedule render, search latency, JS + size. *(Retires AD-1, TQ-1.)* +7. **SPIKE-7 — Time-handling validation.** Intl timezone behavior on baseline + devices, server-offset capture design, wrong-clock UX prototype. *(Retires + R-S5, FA-13.)* +8. **Content intake templates** for organizers (schedule CSV shape, POI sheet, + emergency content sheet, map art spec) — unblocks A-04, OQ-2/6 in parallel + with technical spikes. + +Then, and only then: architecture decision records for AD-1…AD-14. + +--- + +## 22. Preliminary V1 Boundaries + +**Proposed V1 = "the airplane-mode promise":** an installed PWA that, after one +successful online bootstrap, provides Emergency, Schedule (with local favorites), +Map, and Festival Info entirely offline, with honest readiness states and safe, +atomic data updates when connectivity exists. + +### In scope (V1) +- PWA: manifest, service worker, install coaching (esp. iOS), offline shell. +- Four destinations + persistent Emergency access. +- Festival Data Package: versioned, hashed, atomically applied, rollback-safe. +- Local storage: datasets + favorites + preferences + integrity/version metadata. +- OFFLINE READY state machine with honest degraded states. +- Static emergency baseline embedded in app shell + dynamic signed overlay. +- Schedule: browse, now/next (local), favorites, filters, search. +- Map: custom offline map + POIs; no GPS requirement; list-based facility + access for accessibility. +- Festival info pages (data-driven). +- Pull-based updates (app shell + data) over HTTPS from static hosting. +- Failure behaviors per §19 matrix. +- Baseline device support: iOS 16.4+ Safari (installed), Android Chrome ~110+. + +### Out of scope (V1) — revisit only with evidence +- Mesh networking of any kind (seam definition only, per §17). +- Push notifications as a relied-upon channel. +- Accounts, cross-device sync, user-generated content. +- Live map/GPS features, geofencing, navigation. +- Payments, ticketing, merch checkout. +- Native wrappers (TWA/App Store), CMS with live backend. +- Multi-language, multi-edition/multi-festival tenancy (schema should not + block it — A-01 — but no feature work). +- Announcements: **design the schema seam, implement only if time permits**; + never gate V1 on it. + +### V1 success criteria (draft) +1. Cold start → usable Emergency info in ≤ 3 seconds on a 2021 mid-range phone, + airplane mode, 20 launches in a row. +2. Full dataset (shell + all sections + map assets) verifiable OFFLINE READY on + iOS Safari installed PWA and Android Chrome after one bootstrap session. +3. Kill-switch test: kill app/SW/phone at every stage of an update → last-good + dataset always survives; no user-visible corruption. +4. Zero critical features reachable only through network calls. +5. Emergency baseline survives total storage wipe (embedded in shell). + +--- + +## Appendix A — Self-Review Checklist + +| Check | Result | +|-------|--------| +| No application implemented | ✅ Only this document exists | +| No placeholder application code | ✅ Verified: directory contents = `.directory` (pre-existing, untouched) + `DISCOVERY.md` | +| Existing project directory inspected | ✅ §1–§2 | +| No native-mobile assumptions leaked | ✅ PWA-only; native explicitly non-requirements (NR-2/3); mesh-in-native analyzed as future, not V1 | +| Browser treated as first-class constraint | ✅ §5 (C-16…C-20), §12, §13 | +| Offline operation treated as core requirement | ✅ §4.7, §13, §19, §22 | +| Emergency treated as core requirement | ✅ §4.3, §14, OF-3/EM-2 embedded-baseline concept | +| Mesh treated as future architecture only | ✅ §17, NR-1, R-X2/X3 | +| Unresolved requirements identified | ✅ §8 ambiguities, §9 open questions, §10 decisions | +| No final technology decisions made | ✅ §10/§11 are questions/candidates only | + +## Appendix B — Platform Fact Sheet (verified Aug 2026, informs §12) + +- iOS Safari ITP: 7-day cap on all script-writable storage incl. SW + registrations for non-installed sites; **installed home-screen apps exempt**. +- Safari 17+ quotas: ~60% of disk per origin (browser apps; home-screen web apps + included), 80% overall; best-effort; eviction is LRU under pressure and + **all-or-nothing per origin**. +- `navigator.storage.persist()`: WebKit grants by heuristic (home-screen app + helps); Chrome auto-grants installed PWAs. +- No `beforeinstallprompt` on iOS; manual Add-to-Home-Screen only. +- No Background Sync / Periodic Background Sync on iOS Safari. +- Web Push: iOS 16.4+, installed PWA only, region-dependent; Android Chrome + full support. Declarative Web Push in Safari 18.4+. +- iOS 26: home-screen-added sites default to opening as web apps. +- Web Bluetooth / Web NFC: unavailable on iOS Safari. +- Chrome: up to ~60% of disk per origin; storage partitioning since Chrome 115. +- All storage APIs + geolocation + SW require secure contexts (HTTPS). + +*End of discovery document. Await instruction to proceed to architecture.* diff --git a/IMPLEMENTATION-CONTRACT.md b/IMPLEMENTATION-CONTRACT.md new file mode 100644 index 0000000..ce3c6ec --- /dev/null +++ b/IMPLEMENTATION-CONTRACT.md @@ -0,0 +1,832 @@ +# 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`, staging progress (`floor`, per-file), `readbackPending`, clock skew cache pointer | Single source of truth for readiness; rewritten atomically on activation (P2) | +| `lumen-slot-a` | `files`, `assets` | `files`: section docs keyed by section id (`emergency`/`schedule`/`map`/`info`/`assets.json`); `assets`: Blobs keyed by asset id (`map-base-*`, icons) | Inactive slot is staging target; active slot is truth; GC per §13 | +| `lumen-slot-b` | `files`, `assets` | 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` (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 persisted per file; resume keyed on committed progress; staging older than 7 days 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` vs `FESTIVAL DATA v · `). 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//packages// + 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//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/.*` → 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` 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 ", 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/.*` (immutable 1y) + `/latest.json` (`max-age 60`) + `/editions//packages//**` (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 ", 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/.*` + 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`; 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.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..7aa75d1 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Lumen — offline-first festival PWA + +Stage 1 foundation only. No feature code per `IMPLEMENTATION-CONTRACT.md`. + +- **Validated architecture:** `ARCHITECTURE-VALIDATION.md` (SPIKE-01…08 reconciled) +- **Contract:** `IMPLEMENTATION-CONTRACT.md` — single source for implementation rules, boundaries B-1…B-7, invariants I-1…I-17 +- **Stage 1 work:** dedicated git repo + TypeScript strict + lint/format + directory structure + import boundaries + CI + boundary tests +- **No PWA / IDB / sync / mesh / accounts yet** — see contract Stages 2…19 + +## Quick start (Stage 1) + +```bash +npm install +npm run typecheck # tsc --noEmit strict +npm run lint # eslint with boundaries B-1…B-7 +npm run format # prettier --check +npm test # vitest — boundary tests +npm run ci # typecheck + lint + format + test +``` + +## Project structure + +See `IMPLEMENTATION-CONTRACT.md §4` and per-directory `README.md`. + +## Branches + +- `main` — validated architecture + Stage 1 foundation (YELLOW device/content/ops gates tracked in `ARCHITECTURE-VALIDATION.md §7`). diff --git a/SPIKE-01-INDEXEDDB-IOS.md b/SPIKE-01-INDEXEDDB-IOS.md new file mode 100644 index 0000000..7820355 --- /dev/null +++ b/SPIKE-01-INDEXEDDB-IOS.md @@ -0,0 +1,191 @@ +# SPIKE-01 — IndexedDB + iOS Reliability + +- **Phase:** Architecture Validation +- **Date:** 2026-08-30 +- **Decision under test:** ADR-004 (IndexedDB as primary local storage; A/B + slot layout) and the storage foundations of ADR-006. +- **Environment limitation (declared up front):** No iOS hardware or iOS + Simulator is reachable from this Linux dev box. Nothing iOS-specific below + was observed here. iOS items are labelled **INFERRED** (from WebKit's own + published storage policy) or **UNVERIFIED** (needs a physical iPhone). This + spike does **not** pretend otherwise. + +## 1. Question + +Is IndexedDB, organized as A/B dataset slots + a system-meta store + a +user-data store, a sound foundation for Lumen on iOS Safari and Android +Chrome, across large datasets, large map assets, crashes, restarts, storage +pressure, and eviction? + +## 2. Workload being validated + +| Dimension | Lumen value (budget, ARCH §10.6) | +|---|---| +| Structured section JSON | ≤ 3 MB total | +| Map assets (WebP) | ≤ 28 MB | +| Other assets | ≤ 6 MB | +| Total dataset | ≤ 40 MB target / 50 MB ceiling | +| Worst case on device (A/B + shell + user) | ≈ 2×40 MB + 1 MB + ε < 90 MB | +| Object stores | `files`, `assets` per slot DB; `meta` in system DB; `favorites`, `prefs`, `diag` in user DB | +| Largest single record | ≤ 6 MB (per-file cap, ARCH §10.6) | + +## 3. Findings, item by item + +Each item is labelled. Citations are to vendor/spec documentation current as of +2026 (see DISCOVERY Appendix B for the underlying sources). + +### 3.1 Large festival datasets (tens of MB) +- **INFERRED.** IndexedDB is explicitly the browser's intended store for large + structured data; quotas on both targets are expressed as a fraction of disk + (WebKit: up to ~60% of disk per origin for browser apps since Safari 17; + Chromium similar). A 40 MB dataset is a tiny fraction of any realistic quota. +- Risk is not quota *size*; it is *eviction policy* (see 3.9) and *write + interruption* (3.10). + +### 3.2 Multiple object stores / multiple databases +- **CONFIRMED (spec):** multiple databases and multiple object stores per + origin are standard; transactions are scoped to one database. +- **INFERRED:** our layout (two slot DBs + system DB + user DB) is well within + normal use. Note: there is **no cross-database transaction** — the A/B design + already accounts for this (activation is a single-DB transaction on the + system DB; see SPIKE-02). + +### 3.3 Large map assets (multi-MB Blobs in IDB) +- **INFERRED.** IDB stores Blobs; multi-MB records are supported. We cap single + records at 6 MB to bound transaction duration and memory. +- **UNVERIFIED (both platforms):** sustained read-back performance of ~28 MB of + WebP blobs into object URLs, and behavior when writing them near quota. Must + be measured on real devices (Android low-end and iPhone). + +### 3.4 Transaction behavior +- **CONFIRMED (spec):** IDB transactions are atomic per database; a transaction + either fully applies or does not. `oncomplete`/`onerror`/`onabort` signal the + outcome. Versionchange transactions can block reads/writes. +- **INFERRED:** keeping transactions short (one file + its progress record per + transaction) avoids pathological lock holding and keeps the A/B flip cheap. + +### 3.5 Atomicity +- **CONFIRMED (spec):** within one database, a transaction is all-or-nothing. + This is exactly what ADR-006's activation flip relies on (single transaction + on `lumen-system`). +- **CONFIRMED (spec):** atomicity does **not** span databases. The architecture + must not assume it does (SPIKE-02 validates the ordering that makes this safe). + +### 3.6 Browser termination (tab/process killed) +- **INFERRED.** If the process dies mid-transaction, the transaction does not + commit; durable state reverts to the last committed transaction. This is the + standard crash-consistency model for IDB and is what EXP-2's crash points + simulate. +- **UNVERIFIED (iOS):** exact behavior when Safari force-quits or iOS jetsam + kills the PWA process mid-write is expected to follow the same model but must + be observed on device. + +### 3.7 Service worker termination +- **CONFIRMED (architecture):** dataset staging runs in the **page** context, + not the SW (constraint C-21), so SW termination cannot interrupt dataset + writes. The SW only handles shell caching. +- **INFERRED:** iOS aggressively terminates idle SWs; because our design keeps + no dataset state in the SW, this is a non-issue for data integrity. + +### 3.8 Browser restart / phone restart +- **INFERRED.** Committed IDB data survives browser and device restart; the SW + and caches re-register/rehydrate on next launch. Boot-time light verification + (SPIKE-05) is the mechanism that confirms this at runtime. +- **UNVERIFIED (iOS):** post-restart cold-start latency and SW re-registration + timing on real iPhones. + +### 3.9 Storage pressure & eviction +- **CONFIRMED (WebKit policy):** storage is **best-effort**. Under pressure, + WebKit evicts on an LRU basis across origins; `navigator.storage.persist()` + is a *request* granted by heuristic (home-screen install helps), not a + guarantee. Eviction is **per-origin and all-or-nothing** (IDB + Cache + SW + registration together). +- **CONFIRMED (Chromium policy):** similar best-effort LRU eviction; installed + PWAs with persistence granted are skipped first. +- **INFERRED:** an installed PWA with `persist()` granted and regular use is + unlikely to be evicted during a festival weekend, but **nothing guarantees + it**. Therefore eviction is treated as an expected failure mode with a + recovery path (RECOVERY state + embedded emergency baseline), never as an + impossible one. + +### 3.10 Interrupted writes +- **CONFIRMED (spec):** an interrupted/uncommitted write leaves prior committed + state intact. Combined with per-file staging transactions (SPIKE-02), an + interrupted download can leave a slot *partially staged* but can **never** + corrupt the active dataset. +- **INFERRED:** resume logic keyed on committed per-file progress records + recovers cleanly; EXP-2 exercises this. + +### 3.11 Read consistency +- **CONFIRMED (spec):** reads inside a transaction see a consistent snapshot; + reads outside a transaction see the latest committed state. Our read paths + (DatasetStore) read committed state; there are no mid-update reads of the + inactive slot by features. + +### 3.12 iOS Safari specifics +- **INFERRED (WebKit documented):** 7-day ITP cap applies to *Safari-tab* usage + and is **exempt for home-screen-installed** PWAs; Safari 17+ quota is a disk + fraction; eviction all-or-nothing. +- **UNVERIFIED:** all of the above *as experienced by our specific app* — install + exemption in practice, `persist()` grant rate, near-quota write behavior, Blob + read-back speed, post-restart rehydration. Requires physical iOS. + +### 3.13 Android Chrome specifics +- **INFERRED (Chromium documented):** generous disk-fraction quotas; LRU + eviction under pressure; installed PWAs + `persist()` favored. +- **UNVERIFIED:** low-end-device IDB write throughput and Blob handling for a + 40 MB dataset; behavior under actual storage pressure on a 32–64 GB device + nearly full. + +## 4. Defensive protocol derived from this spike + +These rules are the "changes" attached to ACCEPT-WITH-CHANGES and are carried +into the ADR-004 addendum: + +- **P1:** One short transaction per staged file (bytes + progress record commit + together). No transaction spans an `await` of non-IDB work. +- **P2:** Activation is exactly one transaction on the system DB. +- **P3:** Every IDB call is wrapped; `QuotaExceededError` aborts staging and + keeps the active dataset (never a crash). +- **P4:** Pre-stage free-space check via `navigator.storage.estimate()`; refuse + to stage if free < 2× package size. +- **P5:** Single record ≤ 6 MB (bounds transaction memory/duration). +- **P6:** Request `navigator.storage.persist()` once after READY; never rely on + the grant. +- **P7:** Boot-time light verification assumes storage may have vanished + (eviction) — detection, not prevention. +- **P8:** No dataset state in the Service Worker. + +## 5. Must test on physical devices before production + +### iOS (iPhone, iOS 16.4 low bound + latest) +1. Fresh install → full prep of a real-size (~40 MB) package → force-quit and + reboot → data intact, boot ≤ budget. +2. Leave unused in Safari *tab* for > 7 days vs installed PWA → confirm tab + data evicted, installed data survives. +3. `navigator.storage.persist()` grant outcome when installed. +4. Write dataset while device storage is nearly full → observe + QuotaExceededError handling (no crash, active preserved). +5. Kill app mid-download repeatedly → resume completes; active never partial. +6. Blob read-back: open map with ~28 MB assets → time-to-interactive. + +### Android (2021 mid-range, Chrome ~110 + latest) +7. Same fresh-install/reboot cycle with 40 MB package. +8. Storage-pressure eviction simulation (devtools / fill disk) → RECOVERY path. +9. IDB write throughput for 40 MB staging; per-file transaction timing. +10. Map asset Blob read-back performance on low-end GPU/RAM. + +## 6. Verdict + +**ACCEPT WITH CHANGES.** + +- IndexedDB is the correct primary store (structure, transactions, Blob + support, quota scale all fit). No alternative (OPFS, SQLite-WASM, + localStorage) is better for this workload (see ADR-004). +- The A/B layout is sound and its atomicity claims are validated at the + design-logic level by EXP-2 (SPIKE-02). +- **Changes:** adopt defensive protocol P1–P8 (added to ADR-004). +- **Condition:** the physical-device tests in §5 are mandatory before the + storage layer is declared production-ready. Until then the decision stands as + the best-supported choice, with the platform-specific behaviors above + explicitly **UNVERIFIED**, not confirmed. diff --git a/SPIKE-02-ATOMIC-DATASET-UPDATES.md b/SPIKE-02-ATOMIC-DATASET-UPDATES.md new file mode 100644 index 0000000..1c645fb --- /dev/null +++ b/SPIKE-02-ATOMIC-DATASET-UPDATES.md @@ -0,0 +1,126 @@ +# SPIKE-02 — Atomic Dataset Updates (A/B Slots) + +- **Phase:** Architecture Validation +- **Date:** 2026-08-30 +- **Decision under test:** ADR-006 (A/B dual-slot, staged download, atomic + activation, rollback). +- **Method:** adversarial state-machine simulation with crash/fault injection + at every stage (`experiments/exp2-ab-update-sim.mjs`), plus analysis of the + ordering that makes single-database atomicity sufficient. + +## 1. The invariant under test + +> "Either the previous valid dataset remains active, or the new valid dataset +> becomes active. The application never knowingly exposes a partial dataset." + +Four distinct mechanisms must cooperate for this to hold; they are often +confused, so they are separated here explicitly: + +| Mechanism | What it guarantees | What it does NOT guarantee | +|---|---|---| +| **Atomic database operation** (single IDB transaction) | The pointer flip and its metadata land together or not at all. | That the dataset being pointed at is complete or valid. | +| **Validation** (signature → compatibility → hashes → schema) | That a dataset is *eligible* to become active. | That it will be activated, or survive after activation. | +| **Activation** (commit pointer to a validated dataset) | That the *active* slot is, at the moment of commit, a validated dataset. | That it stays uncorrupted afterwards. | +| **Crash recovery** (boot light-verify + fallback + readback) | That a previously-committed good state is found and served after any interruption. | Prevention of the interruption itself. | + +The invariant is a property of the **composition** of all four, not of any one. + +## 2. Verification ordering (normative) + +Cheapest and most authoritative checks first, so untrusted data never forces +work: + +1. Fetch `latest.json` → monotonic version comparison (reject ≤ active). +2. Fetch candidate `manifest.json`. +3. **Signature** over manifest digest (authenticity) — before parsing or + downloading anything else. +4. Parse manifest → **compatibility** (`appCompatibility`, `schemaVersion` in + shell range) and size budgets — before downloading files. +5. **Stage files** into the inactive slot; per-file SHA-256 + size check on + each (integrity), one short transaction per file. +6. **Schema validation** of staged sections. +7. **Activate**: single transaction on `lumen-system` flipping pointer + + version + verification record. +8. **Readback** spot-check after activation (and at next boot if pending). + +## 3. Fourteen-step walkthrough (results from EXP-2) + +`exp2-ab-update-sim.mjs` models slots as atomic stores and injects faults at +each stage. Boot runs light verification and falls back to the other slot when +the active slot fails. **16/16 scenarios PASS.** + +| # | Step / injected fault | Active dataset after | Result | +|---|---|---|---| +| 1 | Download begins, crash before any file | Old (v1) | PASS | +| 2 | Download partially completes (3/5 files) | Old (v1) | PASS | +| 3 | Browser terminated mid-staging | Old (v1) | PASS | +| 4 | Phone reboots before activation | Old (v1) | PASS | +| 5 | Dataset validation fails (generic) | Old (v1), candidate rejected | PASS | +| 6 | Integrity hash fails (corrupt file) | Old (v1), candidate rejected | PASS | +| 7 | Signature validation fails | Old (v1), nothing downloaded | PASS | +| 8 | Schema validation fails | Old (v1), candidate rejected | PASS | +| 9 | Compatibility validation fails | Old (v1), nothing downloaded | PASS | +| 10 | Activation succeeds | New (v2) | PASS | +| 11 | Pointer update occurs | (covered by 10/12) | PASS | +| 12 | Browser terminates immediately after flip | New (v2) — flip committed; boot confirms | PASS | +| 13 | App starts again (crash *during* flip) | Old (v1) — transaction never committed | PASS | +| 14 | Recovery: corruption found by readback | Rollback to last complete slot | PASS | + +Extra scenarios: active slot corrupted at boot → fallback slot served (PASS); +**both** slots lost → RECOVERY state with embedded emergency baseline and +favorites intact (PASS); favorites survive a full update (PASS). + +## 4. Why single-database atomicity is sufficient + +There is no cross-database transaction in IDB, and the pointer lives in a +different database than the dataset files. This is safe **because of ordering, +not because of a distributed transaction**: + +- The inactive slot is fully written and validated **before** the pointer can + move. The flip therefore only ever points at an already-complete dataset. +- The flip itself is one transaction in one database (`lumen-system`), so the + pointer, version, and verification record cannot disagree with each other. +- If the process dies at any point: before flip → old pointer stands; during + flip → transaction uncommitted, old pointer stands; after flip → new pointer + stands and boot light-verify confirms the slot it points at. +- Post-activation corruption (rare) is caught by readback/next-boot light + verification, which falls back to the other slot if complete, else RECOVERY. + +## 5. Findings and refinements + +- **F-1 (add):** per-file staging must commit **bytes + progress record in the + same transaction**. A separate progress write could orphan a record after a + crash. (Carried to ADR-006 addendum; simulation already models this.) +- **F-2 (add):** the activation transaction must include the **verification + record** (what was verified, when, which version), so boot can trust the + active slot without re-hashing everything. +- **F-3 (add):** a `readbackPending` flag is set in the activation transaction + and cleared after a successful readback (same boot if immediate, next boot + otherwise). Scenario 12's crash window is thereby explicitly covered. +- **F-4 (accepted trade-off, document):** rollback depth is exactly **one**. + Beginning a new staging run reuses (wipes) the inactive slot, which is where + the previous rollback copy lived. Invariant still holds (a *valid* dataset is + always active); only the *depth* of undo is limited. Three-slot storage was + considered and rejected for footprint. +- **F-5 (confirmed):** version monotonicity + quarantine of rejected versions + prevents replay of a known-bad package. +- **F-6 (confirmed):** eviction is per-origin all-or-nothing, so there is no + "one database evicted, another survives" partial state to handle; eviction + routes to RECOVERY + baseline (SPIKE-01 §3.9). + +## 6. What this spike did NOT prove + +- It simulated the **state machine**, not IndexedDB's physical transaction + semantics. The claim "an IDB transaction is atomic on iOS under jetsam kill" + is **INFERRED** from spec + platform documentation and remains **UNVERIFIED** + on physical iOS (SPIKE-01 §5). +- It did not measure staging throughput (device-dependent). + +## 7. Verdict + +**ACCEPT WITH CHANGES.** + +The A/B architecture genuinely guarantees the invariant **given** IDB +transaction atomicity holds on target devices (the one platform assumption, +isolated and queued for physical testing in SPIKE-01 §5). Changes adopted: +F-1, F-2, F-3 added to ADR-006; F-4 documented as an accepted limitation. diff --git a/SPIKE-03-MAP-REPRESENTATION.md b/SPIKE-03-MAP-REPRESENTATION.md new file mode 100644 index 0000000..ba261dc --- /dev/null +++ b/SPIKE-03-MAP-REPRESENTATION.md @@ -0,0 +1,141 @@ +# SPIKE-03 — Map Representation + +- **Phase:** Architecture Validation +- **Date:** 2026-08-30 +- **Decision under test:** ADR-008 (raster WebP base ≤2 levels + DOM POI + overlay + CSS-transform pan/zoom + Facilities list; no online tiles; no GPS). +- **Method:** structured evaluation against all candidate representations; + decode-memory analysis; headless synthetic benchmark + (`experiments/exp3-map-bench.html`) with strict interpretation limits. + +## 1. Candidates + +| | S1 Single SVG map | S2 Raster base + DOM overlay (proposed) | S3 Canvas | S4 Pre-cached raster tile pyramid | S5 Hybrid vector-POI over raster (variant of S2) | +|---|---|---|---|---|---| +| Offline completeness | Yes | Yes | Yes | Yes | Yes | +| Organizer workflow | Needs vector art skills; illustrated maps are raster | Art as-is (illustrated PNG/WebP) | Same as S2 | Same as S2 | Same as S2 | +| Low-end pan/zoom | Degrades with path count | GPU transform of 1 image — cheap, constant | Full redraw per frame | Tile management code | Same as S2 | +| Accessibility | Awkward at scale | POIs are real buttons; list view first-class | Needs parallel text tree | Same as S3 | Same as S2 | +| Zoom quality | Infinite | Bounded by 2 levels (venue-scale adequate) | Same as S2 | Good | Same as S2 | +| File size | Art-dependent | WebP excellent for illustrated art | Same | + overhead per tile | Same | +| POI interaction | DOM/SVG events | DOM buttons (48 px targets trivial) | Manual hit-testing | Same as S3 | Same as S2 | +| Search / dynamic filtering | Possible | Trivial (data-driven markers, class toggles) | Manual | Manual | Trivial | +| Dark mode | Styleable | Needs dimming strategy (see §4) | Manual | Manual | Same as S2 | +| Maintainability / update flow | Re-export vector | Replace image + POI sheet → new package | Same as S2 | Same | Same | +| Code surface | Medium | Low | Higher | Highest | Low | + +## 2. Benchmark evidence (EXP-3) and its limits + +Headless desktop run (see `experiments/README.md` for caveats): all three +mechanically viable variants (S2, S1, S3) measured **≈ 0 ms synchronous JS per +animation frame** at 100 and 300 POIs. + +**Interpretation (deliberately conservative):** desktop JS cost does not +discriminate the candidates. The costs that matter on low-end mobile — GPU +compositing of transformed layers, texture upload of large images, decode +latency, RAM pressure — are invisible to this benchmark. Therefore EXP-3 +confirms only: (a) no candidate has a disqualifying JS-side cost; (b) the +DOM-overlay and SVG transform mechanics work as designed. The final choice +rests on memory, accessibility, workflow, and robustness reasoning below, with +a **mandatory device protocol** in §7. + +## 3. Memory analysis (the decisive axis on mobile) + +Decoded image cost ≈ `width × height × 4` bytes (RGBA): + +| Base image | Encoded (WebP, illustrated art) | Decoded in RAM | +|---|---|---| +| 1600×1200 (overview cap) | ~0.3–0.7 MB | ~7.3 MB | +| 2048×1536 | ~0.5–1.2 MB | ~12 MB | +| 3072×2304 (detail cap) | ~1.5–3.5 MB | ~27 MB | +| 4096×3072 (previous cap) | ~2.5–6 MB | ~48 MB | +| 4096×4096 | — | ~64 MB | + +Findings: + +- **F-1 (change):** ARCH §15.3's "≤ 4096 px per side" cap was chosen for GPU + texture limits but is **insufficient as a memory constraint**. On a 3–4 GB + low-end Android, holding a 48–64 MB decoded bitmap alongside the app, list + caches, and browser overhead is a jetsam/OOM risk. **New caps: overview ≤ + 1600 px longest edge; detail ≤ 3072 px longest edge; total decoded map + memory ≤ ~35 MB; at most one detail-level image resident** (overview is + swapped out or downscaled when detail is shown). Some older GPUs cap + textures at 2048 px; Chromium tiles oversized textures internally (works, at + a cost) — the 3072 cap plus an overview-only fallback mode covers this. +- **F-2:** WebP encoded sizes at these dimensions fit the 28 MB asset budget + with margin, including per-POI icons. +- **F-3:** IDs/object URLs: create object URLs lazily per visible level; + revoke on level switch to free decode memory. + +## 4. Criteria-by-criteria results for the chosen representation + +- **Initial load:** overview WebP (~0.5 MB) + map.json renders the usable map + immediately; detail level lazy-loads on zoom-in. CONFIRMED by design. +- **Zoom/pan:** single-container CSS transform; markers counter-scaled. + Mechanics CONFIRMED viable by EXP-3; fps on device UNVERIFIED (§7). +- **Retina/high-density:** base authored at ~2× intended display size; + downscale-on-fit keeps quality. INFERRED. +- **Accessibility / screen readers:** POIs are real DOM buttons with labels; + Facilities list is a first-class, non-visual equivalent (not a fallback). + Base image carries descriptive `alt`. CONFIRMED by design. +- **POI interaction:** tap → detail sheet; 48 px targets. CONFIRMED by design. +- **Search:** shared search component indexes POI names/categories; results + highlight markers + list entries. CONFIRMED by design. +- **Dynamic filtering:** category chips toggle marker visibility (data-driven, + class/attribute toggles; no re-layout of the base). CONFIRMED by design. +- **Dark mode (F-4, change):** festival art is typically day-illustrated. + Default: CSS `filter: brightness(.72) saturate(.85)` on the base image in + dark theme + full-brightness markers/labels (cheap, GPU-composited). + Optional: organizer-supplied night-variant asset (schema hook: asset role + `map-base/overview-night`). No white flash, no re-download required. +- **Organizer workflow:** illustrated art delivered as raster + POI coordinate + capture step (tap-tool over the image in pipeline tooling, or surveyed + normalized coordinates). Map update = new package version; no app change. + INFERRED — needs confirmation with real art (OQ-6, AQ-19). +- **Offline use:** all bytes local; no tile service, no GPS. CONFIRMED. + +## 5. When the decision would flip + +- Organizers supply **vector** art (SVG) of good quality → reconsider S1/S5 + (crisp zoom, smaller files) — SPIKE verdict remains valid either way because + POI overlay + list view are representation-agnostic. +- Real art exceeds 3072 px requirement for legibility → tile-split the detail + level (2×2), still local; bounded extra complexity (DD-9). +- Device protocol (§7) shows transform jank on the low-end Android target → + fallback: overview-only mode + reduced marker count; canvas remains the + escape hatch but is not preemptively adopted. + +## 6. Cross-checks with other decisions + +- ADR-004: assets as Blobs in the slot DB — consistent; object-URL lifecycle + managed by MapService (F-3). +- SPIKE-05: map missing ⇒ PARTIAL with list still available if POI data + present — consistent. +- ADR-012: POI optional `lat/lng` hook preserved; no GPS in V1 — consistent. + +## 7. Mandatory physical-device protocol (before production) + +On the low-end Android target and an iPhone: + +1. Pan/zoom sustained 10 s at 60 Hz intent with 200 POIs → observe fps/frame + drops (target ≥ 30 fps). +2. Detail-level first zoom-in: decode latency and memory peak + (`performance.memory` on Chrome; Instruments on iOS). +3. Overview→detail→overview cycling: no leaked object URLs/memory growth. +4. Dark-mode filter visual quality + performance cost. +5. Screen-reader pass over POI markers and Facilities list (TalkBack, + VoiceOver). +6. Older-GPU device (2048 texture cap) with a 3072 px detail image: renders + correctly via Chromium tiling; measure cost; exercise overview-only + fallback. + +## 8. Verdict + +**ACCEPT WITH CHANGES.** + +Representation stands: **raster WebP base (≤2 levels) + DOM POI overlay + +CSS-transform pan/zoom + first-class Facilities list**, no tiles, no GPS. +Changes (to ADR-008): image dimension/memory caps tightened (F-1), object-URL +lifecycle rule (F-3), dark-mode treatment (F-4), night-variant asset hook. +Condition: device protocol §7 must pass before the map is declared +production-ready; fallbacks are predefined. diff --git a/SPIKE-04-FESTIVAL-DATA-PACKAGE.md b/SPIKE-04-FESTIVAL-DATA-PACKAGE.md new file mode 100644 index 0000000..3a6151f --- /dev/null +++ b/SPIKE-04-FESTIVAL-DATA-PACKAGE.md @@ -0,0 +1,218 @@ +# SPIKE-04 — Festival Data Package (v1 Schema Concept) + +- **Phase:** Architecture Validation +- **Date:** 2026-08-30 +- **Decision under test:** ADR-005 (JSON sections + assets + signed manifest). +- **Status:** schema concept validated against all questions below; normative + schema fragments included. This is a design artifact, not an implementation. + +## 1. Direct answers + +**What is the package?** An immutable, versioned directory of files: one +`manifest.json`, one `signature.json`, N section documents (JSON), an asset +inventory (`assets.json`), and asset binaries. Nothing executable. + +**What identifies it?** `edition` + `packageVersion` (identity for humans and +update logic) and the SHA-256 of the manifest bytes (content identity). The +URL `/editions//packages//` is the location, not the +identity — a mirror must serve byte-identical files or fail verification. + +**What version does it have?** `packageVersion`: a strictly monotonic integer +per edition. `generatedAt` is informational only and never gates behavior. + +**What application versions are compatible?** `appCompatibility.minAppVersion` +/ `maxAppVersion` (declared by the package) intersected with the shell's +supported `schemaVersion` range (declared by the app). Both directions are +checked at boot and before staging (ARCH §18.5, SPIKE-02 §2 step 4). + +**What data sections exist?** V1 mandatory sections: `emergency`, `schedule`, +`map`, `info`, plus the `assets` inventory. Optional sections (V1: none +shipped; reserved): `announcements`, `i18n.`, `extras.*`. + +**How are assets represented?** Files under `assets/`, listed in `assets.json` +with `id`, `file`, `kind`, `role`, `sha256`, `bytes`. Assets are referenced by +`id` from section documents (never by path), so re-encoding/re-naming assets +does not ripple into sections. + +**How are assets hashed?** Each asset carries SHA-256 over exact file bytes + +`bytes` length, verified at staging before activation. + +**How is integrity represented?** Two layers: per-file SHA-256+size for every +file listed in the manifest; and manifest completeness (every listed file +present and matching). A dataset is *complete* iff the manifest says so and +spot/full verification confirms it. + +**How is authenticity represented?** `signature.json`: Ed25519 signature over +SHA-256 of the exact manifest bytes, made with the festival's offline signing +key; includes `publicKeyFingerprint` which must match a key in the app's +embedded key set. Unverifiable authenticity ⇒ reject, keep current dataset. + +**How is expiration represented?** **It isn't — deliberately.** Datasets and +sections carry no expiry that can disable them: an offline device must never +watch its own data "expire" (a wrong clock could brick the app — see +SPIKE-05's time-independence rule). Only *display-only* future content +(announcements/notices, when implemented) may carry `expiresAtUtc`, and expiry +hides the notice, never critical data. + +**How are schema migrations handled?** Datasets are immutable per version, so +there is no in-place migration. Migration = the publisher emits a new package +version in the new schema; clients accept it iff within their supported +`schemaVersion` range. The shell supports a *range* of schemas so app and data +can ship independently (ordering rule ARCH §18.6). **User data** has its own +separate `schemaVersion` with small idempotent migrations at boot (favorites +must survive both dataset updates and app updates). + +**How are optional sections represented?** `sections[*].required: bool` +(default `true`). Readiness gates on required sections only; optional sections +report their own presence without affecting READY (SPIKE-05). V1: all four +content sections are required; the flag exists for future `announcements` etc. + +**How are emergency data versions represented?** The `emergency` section +carries its own `emergencySchemaVersion` (structure) and `contentVersion` +(content revision, monotonic) + `updatedAt`. The floor (embedded baseline) +declares which `emergencySchemaVersion`s it can fall back for; the shell +declares which it can render from the dataset. This keeps floor/dataset +compatibility explicit and independent of the package's overall `schemaVersion`. + +## 2. Normative fragments (illustrative, not final field-for-field) + +### manifest.json +```json +{ + "format": "lumen.package/1", + "edition": "lumen-2026", + "packageVersion": 7, + "schemaVersion": 1, + "generatedAt": "2026-08-28T14:02:11Z", + "festival": { + "name": "Lumen Festival 2026", + "timezone": "America/Chicago", + "startUtc": 1789362000000, + "endUtc": 1789635600000 + }, + "appCompatibility": { "minAppVersion": "1.0.0", "maxAppVersion": null }, + "sections": { + "emergency": { "file": "emergency.json", "sha256": "…", "bytes": 41233, "required": true }, + "schedule": { "file": "schedule.json", "sha256": "…", "bytes": 412201, "required": true }, + "map": { "file": "map.json", "sha256": "…", "bytes": 38122, "required": true }, + "info": { "file": "info.json", "sha256": "…", "bytes": 96710, "required": true }, + "assets": { "file": "assets.json", "sha256": "…", "bytes": 7111, "required": true } + }, + "counts": { "events": 312, "pois": 87, "assets": 14 }, + "limits": { "totalBytes": 31240012 } +} +``` + +### signature.json +```json +{ + "algorithm": "ed25519", + "over": "sha256(manifest.json exact bytes)", + "manifestSha256": "…", + "publicKeyFingerprint": "sha256:…", + "signature": "" +} +``` + +### emergency.json (dataset section, schema `emergency/1`) +```json +{ + "section": "emergency", + "emergencySchemaVersion": 1, + "contentVersion": 3, + "updatedAt": "2026-08-27T09:00:00Z", + "services": { + "emergencyNumber": "911", + "security": { "phone": "+1-555-0142", "location": "Main Gate Kiosk" }, + "firstAid": { "location": "Behind Stage B", "hours": "10:00–02:00" } + }, + "locations": { + "musterPoints": [ { "id": "mp-1", "name": "North Field", "poi": "poi-muster-n" } ], + "exits": [ { "id": "ex-1", "name": "East Gate", "poi": "poi-exit-e" } ], + "aeds": [ { "poi": "poi-aed-1" }, { "poi": "poi-aed-2" } ] + }, + "address": { "lines": ["…"], "coordinates": { "lat": 41.88, "lon": -87.63 } }, + "procedures": [ { "id": "weather", "title": "Severe Weather", "steps": ["…"] } ], + "notices": [ { "id": "n-1", "severity": "info", "title": "…", "body": [], "expiresAtUtc": null } ] +} +``` + +### schedule.json (schema `schedule/1`) — event shape +```json +{ + "id": "evt-0113", + "title": "…", + "stageId": "stage-b", + "artistIds": ["art-007"], + "startUtc": 1789459200000, + "endUtc": 1789462800000, + "dayKey": "2026-09-11", + "tags": ["live"], + "status": "scheduled" +} +``` +Stable `id`s are contractual (favorites survive updates). `dayKey` is +precomputed in the festival zone at publish time (SPIKE-08). + +### map.json (schema `map/1`) +```json +{ + "section": "map", + "base": { + "levels": [ + { "id": "overview", "assetId": "map-base-overview", "width": 1600, "height": 1200 }, + { "id": "detail", "assetId": "map-base-detail", "width": 3072, "height": 2304, + "nightAssetId": "map-base-detail-night" } + ] + }, + "pois": [ + { "id": "poi-aed-1", "name": "AED — Info Tent", "category": "aed", + "x": 0.412, "y": 0.633, "description": "…", "lat": null, "lng": null } + ], + "categories": ["stage","restroom","water","food","first-aid","aed","security", + "entrance","exit","parking","camping","vip","muster","info","vendor","other"] +} +``` + +### assets.json +```json +{ + "assets": [ + { "id": "map-base-overview", "file": "assets/map-base-overview.webp", + "kind": "map-base", "role": "overview", "sha256": "…", "bytes": 402113 }, + { "id": "map-base-detail", "file": "assets/map-base-detail.webp", + "kind": "map-base", "role": "detail", "sha256": "…", "bytes": 2118004 } + ] +} +``` + +## 3. Validation pipeline gates (publisher side) + +1. Schema-validate every section; reject unknown required fields policy: + unknown fields are *allowed forward-compat*, missing required fields fail. +2. Stable-ID check: event/POI IDs stable vs previous package (warn on removals; + require explicit `status: cancelled` rather than deletion). +3. Time sanity: no zero-length events; `dayKey` matches festival-zone date of + `startUtc`; all events inside festival window ± 1 day. +4. Budget enforcement: per-file ≤ 6 MB, section totals, image dimension caps + (SPIKE-03 F-1), total ≤ 40 MB target. +5. Hash → sign → upload immutable files → flip `latest.json` → smoke-fetch and + re-verify from the CDN. + +## 4. Findings + +- **F-1 (add):** `sections[*].required` flag + readiness coupling (SPIKE-05). +- **F-2 (add):** no-expiration rule for datasets (above) — explicit because a + naive "validUntil" field would be an offline-brick hazard. +- **F-3 (add):** emergency section gets independent `emergencySchemaVersion` + + `contentVersion` for floor/dataset compatibility reasoning (SPIKE-06). +- **F-4 (add):** user-data schema versioning/migrations declared separately + from package schema (cross-check with SPIKE-01 P protocol and ARCH §9). +- **F-5 (confirmed):** JSON + per-file hashes + Ed25519 manifest signature is + sufficient; no binary formats needed at this scale. + +## 5. Verdict + +**ACCEPT WITH CHANGES.** Package concept stands; schema gains `required` +flags, the no-expiration rule, emergency sub-versioning, and explicit +user-data schema separation (ADR-005 addendum). diff --git a/SPIKE-05-OFFLINE-READY.md b/SPIKE-05-OFFLINE-READY.md new file mode 100644 index 0000000..c932080 --- /dev/null +++ b/SPIKE-05-OFFLINE-READY.md @@ -0,0 +1,115 @@ +# SPIKE-05 — Offline Ready Model + +- **Phase:** Architecture Validation +- **Date:** 2026-08-30 +- **Decision under test:** the readiness state machine in ARCH §12 (R1–R7). +- **Outcome:** model confirmed; predicate formalized, tightened (time + independence, optional sections), and failure-state taxonomy completed. + +## 1. What READY must actually guarantee + +Deriving from the product promise ("everything attendees need works with zero +connectivity"), READY must guarantee, **from local evidence only**: + +1. The app can launch and render without network (shell complete). +2. Emergency information is available (floor always; dataset section if present). +3. The schedule is browsable and Now/Next computable (schedule section present; + note: computable even with a wrong clock — a clock problem is a warning, + never a readiness failure). +4. The map is usable (map section + its required assets present). +5. Festival info is readable (info section present). +6. The dataset is authentic and untampered (signature + hashes verified). +7. The dataset is compatible with this shell (schema envelope). +8. The claim itself is evidenced (a stored verification record matching the + active version, not a memory of success). + +## 2. The exact predicate (normative) + +``` +OFFLINE_READY ⟺ + C1 shell_valid: every precache entry present in the current shell cache + ∧ C2 dataset_present: active slot exists for the active edition + ∧ C3 authenticity: manifest signature verified against embedded key set + (recorded verification matches active packageVersion) + ∧ C4 integrity: all manifest-listed files present, sizes match; + hashes verified at staging (recorded), spot-check at boot + ∧ C5 schema_compatible: package schemaVersion ∈ shell.supportedRange + ∧ package.appCompatibility satisfied by APP_VERSION + ∧ C6 required_sections: every section with required=true present and parseable: + {emergency, schedule, map, info, assets-inventory} + ∧ C7 required_assets: every asset referenced by a required section present + (size-verified; hash verified at staging) + ∧ C8 emergency_floor: embedded baseline present (trivially true; asserted + so a broken build cannot silently lose the floor) +``` + +**Rules of evaluation:** + +- **Time independence (new, F-1):** the predicate must not consult any clock. + A device with a wildly wrong clock must still be able to know it is READY. + (No expiration fields exist in the package — SPIKE-04 F-2 — so nothing tempts + a time check.) +- **Optional sections never gate READY (new, F-2):** sections with + `required=false` (future announcements, i18n packs) report their own presence + in the status detail but cannot demote READY. +- **Evidence-based:** C3/C4 rely on the stored verification record (SPIKE-02 + F-2) keyed to the active `packageVersion`; if the record is absent or + mismatched, a full re-verification is required before READY can be claimed. +- **All-or-nothing for READY; everything else is enumerated.** + +## 3. State taxonomy (complete) + +| State | Definition | User sees | Primary action offered | +|---|---|---|---| +| `READY` | C1–C8 all true | Green chip "OFFLINE READY ✓" | None (status detail on tap) | +| `PARTIAL(list)` | Shell valid; one or more required sections/assets missing, none corrupt; e.g., prep interrupted | Amber chip; status screen enumerates exactly what is missing and what still works | "Continue setup" (resumes staging) | +| `NOT_READY` | Shell valid; no dataset staged or active (fresh install) | Neutral chip "Get festival data" | Preparation flow | +| `RECOVERY(reason)` | Previously-verified state now fails checks: storage evicted (`missing`), verification mismatch (`corrupt`), incompatibility after app/dataset skew (`incompatible`) | Red chip; honest explanation of what happened | "Restore festival data" (re-prep; needs connectivity) — emergency floor works meanwhile | +| `BASELINE_ONLY` | Storage unavailable (private mode, blocked website data, quota 0) and app cannot persist anything | Distinct notice: offline saving unavailable; emergency info still here | Install / normal-mode / free-space guidance | +| `FAILED(op)` | A specific operation failed and no automatic recovery applied (activation transaction repeatedly failing, IDB errors) — distinct from RECOVERY because the *cause is an error, not missing data* | Error screen with plain cause + diagnostics entry | Retry; if persistent, re-prep; diagnostics share option | + +Notes: +- The brief's example predicate listed `schedule_present`, `map_present`, + `festival_info_present`, `emergency_baseline_present` individually. This + model keeps them (C6/C8) but wraps them in the required-section mechanism so + future optional sections don't require predicate surgery. +- `FAILED` is the added state: RECOVERY means "data is gone/wrong"; FAILED + means "an operation errored". Keeping them separate keeps user messaging and + diagnostics accurate. + +## 4. Evaluation cadence and cost + +| When | What | Cost target | +|---|---|---| +| Every boot | Light check: C1 (cache list), C2 (slot existence), C4-light (sizes + verification-record match), C5 | ≤ ~150 ms; no hashing | +| After staging completes | Full: all hashes + schema (C3–C7 full) | Seconds; once per update | +| Activation | Record written atomically with the flip; readback spot-check (pending flag, SPIKE-02 F-3) | ms | +| User-initiated "Check my data" | Full re-verification | Seconds, with progress | +| After any IDB error | Full re-verify of active slot; quarantine on failure | Seconds | + +## 5. Edge cases adjudicated + +| Case | State | Why | +|---|---|---| +| Eviction between install and festival | RECOVERY(missing) | Light check finds no slot; floor still works | +| Verification record present but files gone | RECOVERY(missing) | Sizes check fails | +| Files present but record missing (e.g., partial restore) | PARTIAL → full re-verify path | READY cannot be claimed without evidence | +| New shell, dataset schema too old | RECOVERY(incompatible) with "update data when online" guidance; floor + status usable | C5 false | +| Wrong device clock | **READY unaffected**; separate clock warning (SPIKE-08) | Time independence | +| Optional announcements section missing | READY | F-2 | +| Private browsing, nothing persistable | BASELINE_ONLY | Storage probes fail early | +| Activation failed twice | FAILED(activation) | Distinct from data loss | + +## 6. Cross-checks + +- Consistent with SPIKE-02 (verification record + readback pending flag). +- Consistent with SPIKE-04 (`required` flag, no expiration). +- Consistent with SPIKE-06 (floor assertion C8; emergency never depends on READY). +- Consistent with SPIKE-07 (prep levels map onto PARTIAL/READY transitions). + +## 7. Verdict + +**ACCEPT (with formalization).** The ARCH §12 model was directionally correct; +it is now normative: predicate C1–C8, time independence, optional-section rule, +and the full six-state taxonomy including `FAILED`. Changes are refinements of +the design document, not a direction change; no ADR-level reversal. diff --git a/SPIKE-06-EMERGENCY-BASELINE.md b/SPIKE-06-EMERGENCY-BASELINE.md new file mode 100644 index 0000000..e51f86b --- /dev/null +++ b/SPIKE-06-EMERGENCY-BASELINE.md @@ -0,0 +1,119 @@ +# SPIKE-06 — Emergency Baseline Architecture + +- **Phase:** Architecture Validation +- **Date:** 2026-08-30 +- **Decision under test:** ADR-007 (three-tier emergency architecture with an + embedded floor). +- **Outcome:** decision confirmed; tier contents fixed; version/compatibility + matrix resolved; one hardening rule added. + +## 1. What belongs in each tier (fixed) + +### Tier 1 — Emergency Floor (embedded in app shell, immutable per build) + +Must fit ≤ 16 KB and cover "what saves a life or prevents panic in the first +minutes": + +```json +{ + "floor": true, + "floorVersion": "1.0.0+2026.08.20", + "emergencySchemaVersion": 1, + "generatedAt": "2026-08-20T12:00:00Z", + "sourceContentVersion": 5, + "services": { + "emergencyNumber": "911", + "security": { "phone": "+1-555-0142" }, + "firstAid": { "summary": "Behind Stage B, 10:00–02:00" } + }, + "address": { "lines": ["…"], "coordinates": { "lat": 41.88, "lon": -87.63 } }, + "musterPoints": [ { "name": "North Field", "directions": "…" } ], + "exits": [ { "name": "East Gate" }, { "name": "West Gate" } ], + "aedSummary": "AEDs at Info Tent and Main Gate (see map when available)", + "procedures": [ + { "id": "medical", "title": "Medical emergency", "steps": ["Call 911", "Alert security: +1-555-0142", "…"] }, + { "id": "weather", "title": "Severe weather", "steps": ["…"] }, + { "id": "fire", "title": "Fire", "steps": ["…"] }, + { "id": "lost", "title": "Lost person", "steps": ["…"] } + ] +} +``` + +Excluded from the floor (by policy): full POI lists, per-location detail, +vendor/operational info, anything that changes frequently. The floor is a +**life-safety minimum**, not a small copy of the dataset section. + +### Tier 2 — Festival Emergency Dataset (signed section of the package) + +Full detail: all emergency-relevant POIs with map links, complete procedure +text, per-role contacts, hours, notices, `contentVersion`/`updatedAt` +(SPIKE-04 fragment). Updateable by publishing a new package version. + +### Tier 3 — Optional live emergency notices (future, reserved) + +Shape reserved: `{ id, severity, title, body, publishedAtUtc, expiresAtUtc, +signature }`, delivered with a package or via the transport seam; rendered +only when signed and unexpired; **display-only, additive, never a replacement +for Tier 1/2**. Not implemented in V1. + +## 2. Resolution rules (normative) + +``` +render_emergency(): + floor = embedded floor # always exists; parses from frozen schema + section = active dataset emergency section + if section exists + and section.emergencySchemaVersion ∈ shell.supportedEmergencySchemas + and section passes integrity (already guaranteed by activation): + render section (full detail), labeled "FESTIVAL DATA v · " + render floor-only fields if section omits any (defensive merge) + else: + render floor, labeled "BASELINE v" + append Tier-3 notices if any signed+unexpired (future) +``` + +Hardening rule **(new, F-1)**: the floor renderer is **forward-tolerant** — +it ignores unknown fields and never throws on dataset content; and the floor +path must be reachable with **zero IDB access**, because BASELINE_ONLY and +eviction scenarios must still render emergency info. + +## 3. The cases the spike must answer + +| Case | What renders | Why the floor never disappears | +|---|---|---| +| Shell old, dataset newer (section schema v2 vs floor/shell supports v1) | Floor + "update app when online" note; known-v1 fields of the section may still render if backward-compatible | Floor is compiled into the shell; dataset incompatibility can only *demote to floor*, never remove it | +| Shell new, dataset old | Dataset section (old schema is within shell's supported range) or floor | Backward range support (C-23) | +| Dataset unavailable (never prepared) | Floor | Floor ships with shell bytes | +| Dataset corrupt (verification fails at staging — never activated) | Previous dataset section if active slot intact, else floor | Corruption blocks activation (SPIKE-02); floor unaffected | +| Storage evicted | Floor | Floor is shell bytes, not IDB; Ring-0 survival | +| Network unavailable | Whatever is local: active dataset section or floor | No network call exists in the emergency render path | +| Update fails mid-flight | Old active dataset (or floor) | A/B invariant (SPIKE-02); staging never touches shell or active slot | + +## 4. Tradeoffs (re-stated with verdicts) + +- **Baseline staleness** (floor frozen at shell build): accepted. Mitigations: + same-source generation (no drift), floor version stamp on screen, Tier 2 + updates for anything less-than-critical, organizer physical channels for + urgency. +- **Baseline scope creep**: resisted. 16 KB cap + content policy (life-safety + minimum only). Everything else belongs in Tier 2. +- **Two copies drift**: eliminated by generating floor + section from the same + emergency source sheet in the pipeline (ADR-007/§10.3). +- **Separate localStorage copy of emergency data**: rejected again — it dies in + eviction too; only shell bytes survive everything. + +## 5. Cross-checks + +- SPIKE-05 C8 asserts floor presence in the readiness predicate (defense + against a broken build silently dropping the floor). +- SPIKE-04 F-3 gives the emergency section independent versioning used by the + resolution rules above. +- ADR-013: the floor's provenance is the shell build itself (signed app + delivery); Tier 2 inherits package signatures; Tier 3 will require + per-notice signatures. + +## 6. Verdict + +**ACCEPT.** ADR-007 stands unchanged in direction; addendum records the fixed +tier contents, the resolution/merge rules, the forward-tolerant floor renderer +(F-1), and the zero-IDB requirement for the floor path. diff --git a/SPIKE-07-BOOTSTRAP.md b/SPIKE-07-BOOTSTRAP.md new file mode 100644 index 0000000..ae7912b --- /dev/null +++ b/SPIKE-07-BOOTSTRAP.md @@ -0,0 +1,98 @@ +# SPIKE-07 — Installation & Bootstrap + +- **Phase:** Architecture Validation +- **Date:** 2026-08-30 +- **Decisions under test:** ADR-002 (installed-first PWA) and the bootstrap + design in ARCH §11/§12 (C-16, C-17). +- **Outcome:** confirmed; introduces explicit preparation levels (L0–L2) and + the Minimum Safe Experience definition. + +## 1. Intended journey + +```mermaid +flowchart TD + A["Discovery: QR / link / poster / word of mouth"] --> B["First visit (online): shell loads ~1 MB, SW registers"] + B --> C["Install coach (iOS manual steps; Android native prompt)"] + C --> D["Preparation: 'Get festival data'"] + D --> E["Data acquisition: pointer → manifest → sections (priority order) → assets; resumable"] + E --> F["Validation: signature → hashes → schema → atomic activation"] + F --> G["OFFLINE READY ✓ confirmation + status chip"] + G --> H["Festival use: everything offline; sync opportunistic when online"] + + C -. skip/dismiss .-> D + E -. interrupted .-> P["PARTIAL state; resume later"] + P --> E +``` + +Stage persistence: after first visit the shell is cached (SW); after prep each +section is staged/committed independently; favorites persist from first +creation. No stage depends on completing later stages. + +## 2. Preparation levels (new, normative) + +| Level | Contents | Meaning | Chip state | +|---|---|---|---| +| **L0** | Shell + embedded emergency floor | "Emergency works, nothing else" | NOT_READY (or BASELINE_ONLY if storage blocked) | +| **L1 — Minimum Safe** | L0 + dataset `emergency` section + `schedule` section | Core festival survival: emergency detail + what's on when | PARTIAL → "core ready" note | +| **L2 — Full** | L1 + `map` + `info` + all required assets | Complete promise | READY | + +Download order is exactly `emergency → schedule → info → map-base → assets`, +so an interrupted prep always yields the highest achievable level. The prep UI +names the levels ("Emergency ready", "Schedule ready", "Map ready") so users +always know what they have. + +## 3. Failure-point analysis + +| # | Failure | Architecture behavior | User experience | +|---|---|---|---| +| FP-1 | **User doesn't install** (esp. iOS tab) | Works while open; storage eviction risk after 7 days of tab inactivity (ITP); coach re-surfaces | Coach + honest note: "install to keep your festival data" | +| FP-2 | **User closes browser mid-prep** | Staged files + progress persist; nothing activated prematurely | Resume banner next open; prep continues where it stopped | +| FP-3 | **Internet lost during prep** | Downloads pause; partial staged data retained; app remains fully usable for installed levels | "Paused — you have Emergency + Schedule so far" | +| FP-4 | **Insufficient storage** | `storage.estimate()` pre-check refuses staging if free < 2× package; QuotaExceeded handled | Plain guidance: free space / reinstall; L0/L1 still possible if a full package can't fit — prep offers partial (sections only, skip heavy assets) as degraded option | +| FP-5 | **iOS install flow fails / user can't find Add to Home Screen** | Step-by-step coach with images; retry detection (still in tab after coach); fallback: continue in Safari with eviction warning | No dead end: app works in tab, warns about persistence | +| FP-6 | **User arrives unprepared** | App boots to whatever exists (L0 floor minimum); honest status; one-tap prep if any connectivity appears; organizer physical fallback for critical info | No blank screen; explicit "what's missing" list | +| FP-7 | **User clears browser data / uninstalls PWA** | Everything local is gone; floor returns only after shell reloads from network; RECOVERY state otherwise | Re-prep path; nothing to do offline except reload shell when online | +| FP-8 | **Private browsing session** | Storage probes fail early → BASELINE_ONLY mode with explanation; no crash, no fake READY | "Offline saving unavailable in this mode" + install/normal-mode guidance | + +Additional adjudications: +- **Prep before install (iOS tab) is allowed** and valuable: data staged in tab + storage survives until eviction; install later preserves it (same origin). + The coach therefore offers "install first (recommended)" *and* "get data now". +- **First-visit ordering:** shell precache completes before prep is offered, so + L0 is guaranteed before any dataset work begins. +- **Leaving prep early is never punished:** no state is invalidated by + abandonment; the worst case is staying at the current level. + +## 4. Minimum Safe Experience (normative) + +For an attendee who **never completed offline preparation**, the app must +still provide, in this order of guarantees: + +1. **Always (after any successful shell load):** the emergency floor — dial + action, address/coordinates, security contact, muster points, core + procedures (L0). This is Ring-0 and requires no storage. +2. **If any prep succeeded even partially:** whatever levels completed, each + fully functional (L1 core = emergency detail + schedule). +3. **Honest status at all times:** the chip and status screen state exactly + which levels are met and what to do about the rest. +4. **No feature silently pretends:** map/info screens for missing sections show + "not downloaded" cards with the prep action, not broken UI. + +Product/ops corollary (cannot be solved by architecture alone): distribution +before arrival (OQ-3) and physical fallback info at the venue remain necessary +for the unprepared minority. + +## 5. Cross-checks + +- Readiness mapping: L0↔NOT_READY/BASELINE_ONLY, L1↔PARTIAL, L2↔READY + (SPIKE-05 taxonomy). +- Emergency floor independence from storage (SPIKE-06) makes L0 unconditional. +- Install-first persistence reality (SPIKE-01 §3.12) motivates the coach but + tab usage is never blocked. + +## 6. Verdict + +**ACCEPT WITH CHANGES.** Bootstrap architecture stands; changes: formal L0–L2 +levels with the fixed download order, Minimum Safe Experience definition, and +the tab-install sequencing rule (allow prep before install). ADR-002 addendum +records the levels; no direction change. diff --git a/SPIKE-08-TIME-MODEL.md b/SPIKE-08-TIME-MODEL.md new file mode 100644 index 0000000..f54f5ea --- /dev/null +++ b/SPIKE-08-TIME-MODEL.md @@ -0,0 +1,189 @@ +# SPIKE-08 — Time Model (UTC + Festival IANA Zone + Clock Correction) + +- **Phase:** Architecture Validation +- **Date:** 2026-08-30 +- **Decision under test:** ADR-009 (UTC epoch storage, festival IANA zone + rendering, precomputed `dayKey`, ClockService with server-offset + monotonic + drift + sanity window), and the readiness independence rule from SPIKE-05. +- **Method:** disposable experiment `experiments/exp1-time-model.mjs` executed on + this Linux dev box using Node v22 full-ICU `Intl` (ECMA-402 — the same spec + browsers implement), plus spec/policy analysis for the parts no Linux box can + observe. Results labelled CONFIRMED / INFERRED / UNVERIFIED per + `experiments/README.md`. +- **Environment limitation (declared up front):** No iOS hardware or iOS + Simulator is reachable from this Linux box. V8 + ICU behaviour was **observed**; + JavaScriptCore (iOS) parity is **INFERRED** and queued for device testing. + System-clock manipulation, real HTTP `Date` headers, and low-end-device timing + are also out of scope here. + +## 1. Question + +Can Lumen compute correct local wall-clock display, festival day boundaries, and +"Now / Up Next" **entirely offline**, remain correct across DST and unusual +zones, and degrade honestly when the device clock is wrong — with no network +call on any critical path? + +## 2. Architecture under test (recap) + +| Piece | Design | +|---|---| +| Storage | Every event instant is UTC epoch ms (`startUtc`, `endUtc`). | +| Zone | Manifest carries festival IANA zone (e.g., `America/Chicago`) and festival window `startUtc`/`endUtc`. | +| Rendering | `Intl.DateTimeFormat` with `timeZone = festival zone` (default); user toggle to device zone. | +| Day boundaries | `dayKey` precomputed at publish time as the calendar date in the festival zone (no client-side day math). | +| Clock correction | `ClockService.now()` = `deviceClock + skew` when a persisted server offset exists; otherwise `deviceClock`. `skew` captured from any sync HTTP `Date` header; persisted with `{skew, capturedAtDevice, capturedAtMono, source}`. | +| Drift detection | `performance.now()` monotonic anchor detects mid-session device-clock jumps. | +| Sanity window | festival window ± 45 days; outside ⇒ "check your clock" warning. | +| Classification | `Now: startUtc ≤ now < endUtc`; `Up Next: startUtc > now` (next-N sorted). Overlaps shown as multiple "now". | + +Readiness (SPIKE-05) must not depend on the clock: READY is time-independent; +a wrong clock produces a warning, never a readiness demotion. + +## 3. Findings, item by item + +### 3.1 UTC storage + festival-zone rendering (T1) — CONFIRMED (observed) + +- `EXP-1 T1a` — `2026-07-15T18:00:00Z` renders `07/15/2026, 13:00:00` in + `America/Chicago` (CDT, UTC-5). PASS. +- `EXP-1 T1b` — rendering is keyed to the explicit `timeZone` option, not the + host zone. Explicit-zone `Intl` formatting is required by ECMA-402 and is what + the architecture relies on. PASS. +- **Conclusion:** UTC epoch storage + explicit-zone `Intl` rendering is correct + and offline-capable (tz database ships with the OS/browser). No network use. + +### 3.2 Precomputed `dayKey` and midnight correctness (T2) — CONFIRMED + +- `T2a` — `2026-07-16T04:59Z` (= 07-15 23:59 CDT) ⇒ `2026-07-15`. PASS. +- `T2b` — `2026-07-16T05:00Z` (= 07-16 00:00 CDT) ⇒ `2026-07-16`. PASS. +- `T2c` — `2026-07-16T00:00Z` (= 07-15 19:00 CDT) ⇒ `2026-07-15`, proving that UTC + midnight is **not** the festival day boundary. PASS. +- **Conclusion:** client-side date math is unnecessary; publishing `dayKey` + eliminates an entire class of off-by-one and TZ bugs. Clients group/filter by + the opaque key. Pipeline gate `T3` in SPIKE-04 validates `dayKey` against the + festival zone at publish time. + +### 3.3 DST transitions (T3) — CONFIRMED on V8/ICU; INFERRED on JSC + +- Spring forward 2026-03-08 America/Chicago: `01:59 CST` → `03:00 CDT` + (`T3a`/`T3b`). PASS. +- Fall back 2026-11-01: `01:59 CDT` → `01:00 CST` (`T3c`/`T3d`). PASS. +- Epoch arithmetic is unaffected by transitions (`T3e`: 60 000 ms). PASS. +- **INFERRED:** JavaScriptCore on iOS implements the same ECMA-402 + IANA + database contract. Behaviour is expected identical but is **UNVERIFIED** on + physical iOS at the DST edges. SPIKE-07-level device check covers it. + +### 3.4 Unusual zones (T4) — CONFIRMED spec-level + +- No-DST zone `America/Phoenix` (UTC-7 fixed) — `T4a` PASS. +- Fixed `+05:45` `Asia/Kathmandu` — `T4b` PASS. +- 30-minute DST `Australia/Lord_Howe` (+10:30 std / +11:00 DST) both in January + (DST) and July (std) — `T4c`/`T4d` PASS. +- **Significance:** festivals in unusual zones or with 30-minute DST are not + special-cased; `Intl` handles them. + +### 3.5 Server-skew correction (T5) — CONFIRMED arithmetic; INFERRED header source + +- Skew arithmetic `skew = serverNow − deviceNow`, `corrected = deviceNow + skew` + (`T5a`/`T5b`) PASS. +- Mid-session drift model: 1 h of monotonic time elapsed, observed device jump + +5 min ⇒ |drift| > 2 min threshold detected (`T5c`) PASS. +- **INFERRED:** HTTP `Date` headers from the static CDN are NTP-synced and + accurate enough to serve as the server-time source (discovery B-10). No + dedicated time endpoint is needed; any sync response suffices. Guarded by the + sanity window (3.6). +- Cost: one persisted record; `ClockService.now()` is a pure computation on + demand; no timers, no background work (C-19). + +### 3.6 Sanity window (T6) — CONFIRMED logic; REFINEMENT REQUIRED (F-3) + +- Correctly flags wildly wrong clocks (2023 → outside; festival-day inside) + (`T6b`/`T6c`) PASS. +- `T6a` is the intentional subtlety: a **July** `now` against a **September** + festival is outside the ±45-day window. If the app showed "check your clock" + to every user who prepared in July, the warning would be noise. +- **F-3 (change, normative):** the sanity warning is **suppressed until the + festival window is near or live**. Recommended rule (added to ADR-009): + warn on `outsideWindow` only if `now ≥ festivalStart − WINDOW` **or** a + server skew has previously been captured (meaning we have evidence the device + is truly skewed). Before that, dataset timestamps ("Data as of …") communicate + staleness without accusing the clock. This keeps early preparation quiet while + preserving the in-festival safety value. + +### 3.7 Now / Up Next classification (T7) — CONFIRMED + +- Overlapping running events both appear in `now` (`T7a`) PASS. +- Future events correctly move to `up-next` in start-time order (`T7b`/`T7c`/ + `T7d`) PASS; empty `up-next` when nothing is upcoming is handled. +- **Conclusion:** epoch-ms comparison is sufficient; no IANA or wall-clock + involved in the predicate. Rendering then formats the instants per 3.1. + +### 3.8 Wrong-clock scenarios (cross-cutting) + +| Scenario | What happens | Why acceptable | +|---|---|---| +| Device fast/slow, user has been online once (skew captured) | `now()` corrected by persisted skew; sanity check uses corrected time | Correct "Now" without network | +| Device fast/slow, never online | Uncorrected `now`; sanity warn if near festival; absolute times remain readable | Accepted residual (discovery OF-7, RK-9, W-2); no network fix exists offline | +| Clock moved mid-session | Monotonic anchor flags drift > 2 min; re-derive base | Warn + keep skew if present | +| Clock far outside sanity window | Warning surfaced near festival; READY unaffected (SPIKE-05 time independence) | User can still read schedule; network sync fixes it opportunistically | + +### 3.9 What this spike did NOT prove + +- **JSC parity on iOS:** `Intl` edge behaviour on iOS 16.4 low-bound and latest + requires physical iPhone testing (see §5). +- **HTTP `Date` freshness in the field:** header skew capture depends on the + CDN's `Date` accuracy and on the app actually performing a sync. Queued for + staging-environment observation. +- **User comprehension of the clock warning:** wording/tone needs a UX pass, + not a logic spike. + +## 4. Refinements carried to ADR-009 + +- **F-1 (confirmed):** UTC storage + IANA-zone rendering + precomputed `dayKey` + stands as specified. +- **F-2 (confirmed):** ClockService as `device + persisted skew` with monotonic + drift guard — cheap, offline-safe, no background work. +- **F-3 (change):** sanity-window warning suppressed until + `now ≥ festivalStart − WINDOW` or a skew has been captured. Prevents noisy + false warnings during early preparation. Added to ADR-009. +- **F-4 (clarification):** `Intl.DateTimeFormat` with explicit `timeZone` is the + **only** time-rendering path; no `Date.toLocaleString()` without options, no + manual offset arithmetic, no wall-clock string storage. + +## 5. Must test on physical devices before production + +On iOS (iPhone, iOS 16.4 low bound + latest) and low-end Android (2021 +mid-range): + +1. Render the July event at `2026-07-15T18:00Z` in `America/Chicago` → expect + `01:00 PM CDT` (spot-check JSC parity with EXP-1 T1a). +2. Day grouping: events at `T2a`/`T2b` instants appear on the correct `dayKey` + days in the UI. +3. DST edge: the `T3a`–`T3d` wall-clock sequence around spring-forward and + fall-back for the festival zone; duration of a span crossing the transition + equals epoch difference (T3e). +4. Clock correction: set device clock +30 min, open online (sync captures skew), + go airplane-mode → "Now" reflects corrected time; mid-session clock bump of + +10 min triggers the drift warning. +5. Sanity window: device set to a month before the festival → no warning; device + set to +60 days past end → warning shown (F-3 rule). +6. Never-online wrong clock: airplane mode + device clock wrong by 2 hours → + warning shown near festival; schedule remains readable with absolute times. + +## 6. Verdict + +**ACCEPT WITH CHANGE (F-3).** + +The time model is correct offline, handles DST and unusual zones, and degrades +honestly for wrong clocks at trivial cost. `EXP-1` passes 24/24. Change F-3 +(sanity-window suppression during early preparation) is the only amendment to +ADR-009; no direction change. Condition: physical-device parity checks in §5 +must pass before the festival zone + DST edges are declared production-ready. + +## 7. Cross-checks + +- Consistent with SPIKE-04: UTC storage, IANA zone in manifest, `dayKey` + validated at publish time. +- Consistent with SPIKE-05: readiness predicate is time-independent (C5 does not + consult clocks); a wrong clock is a warning, never a demotion. +- Consistent with SPIKE-07 bootstrap: skew is captured opportunistically during + any sync; no dedicated "time sync" step exists. diff --git a/content/.gitkeep b/content/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/content/README.md b/content/README.md new file mode 100644 index 0000000..22f43df --- /dev/null +++ b/content/README.md @@ -0,0 +1,5 @@ +# content + +Authoritative source for pipeline (ADR-014) — not shipped. emergency/schedule/map/info/assets. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..4126df5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,170 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import boundaries from "eslint-plugin-boundaries"; +import globals from "globals"; + +// eslint-disable-next-line @typescript-eslint/no-deprecated +export default tseslint.config( + { + ignores: ["dist/**", "node_modules/**", "coverage/**", "experiments/**"], + }, + js.configs.recommended, + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ["eslint.config.js", "vitest.config.ts"], + }, + tsconfigRootDir: import.meta.dirname, + }, + globals: { + ...globals.browser, + ...globals.node, + }, + }, + plugins: { + boundaries, + }, + settings: { + "boundaries/elements": [ + { type: "platform", pattern: "src/platform/**" }, + { type: "storage", pattern: "src/storage/**" }, + { type: "data", pattern: "src/data/**" }, + { type: "sync-verifier", pattern: "src/sync/verifier/**" }, + { type: "sync-transport", pattern: "src/sync/transport/**" }, + { type: "sync", pattern: "src/sync/**" }, + { type: "domain", pattern: "src/domain/**" }, + { type: "ui", pattern: "src/ui/**" }, + { type: "app", pattern: "src/app/**" }, + { type: "emergency-baseline", pattern: "src/emergency-baseline/**" }, + { type: "tests", pattern: "tests/**" }, + { type: "scripts", pattern: "scripts/**" }, + ], + "boundaries/ignore": ["**/*.test.ts", "**/*.spec.ts"], + }, + rules: { + // Architectural invariants — dependency rules B-1 … B-7 + // B-1: ui never touches platform/storage/sync/transport/fetch/idb/caches + // Implemented via boundaries + no-restricted-globals where applicable + "boundaries/element-types": [ + "error", + { + default: "disallow", + rules: [ + // platform: low-level — may not import domain/ui/sync + { from: "platform", allow: [] }, + // storage: may only import platform + { from: "storage", allow: ["platform"] }, + // data: may import platform only + { from: "data", allow: ["platform"] }, + // sync-verifier: may import platform (hash) but not data/domain/ui/sync + { from: "sync-verifier", allow: ["platform"] }, + // sync-transport: byte pipe — may not import data/domain/ui + { from: "sync-transport", allow: [] }, + // sync: orchestration — may import data + sync sublayers + platform (via verifier) + { from: "sync", allow: ["data", "sync-verifier", "sync-transport", "platform"] }, + // domain: may import data + clock (domain/clock) — clock is domain, so allow domain internal + { from: "domain", allow: ["data", "domain", "emergency-baseline"] }, + // ui: may import domain + app helpers, but NOT platform/storage/sync + { from: "ui", allow: ["domain", "emergency-baseline"] }, + // app: composition root may import via public APIs — allow all for Stage 1, will tighten later + { + from: "app", + allow: [ + "platform", + "storage", + "data", + "sync", + "sync-verifier", + "sync-transport", + "domain", + "ui", + "emergency-baseline", + ], + }, + // emergency-baseline: generated, no deps + { from: "emergency-baseline", allow: [] }, + // tests/scripts may import anything for validation + { + from: "tests", + allow: [ + "platform", + "storage", + "data", + "sync", + "sync-verifier", + "sync-transport", + "domain", + "ui", + "app", + "emergency-baseline", + ], + }, + { + from: "scripts", + allow: [ + "platform", + "storage", + "data", + "sync", + "sync-verifier", + "sync-transport", + "domain", + "ui", + "app", + "emergency-baseline", + ], + }, + ], + }, + ], + // Explicit forbidden globals in ui (B-1 core) + "no-restricted-globals": [ + "error", + { + name: "indexedDB", + message: "B-1: ui must not touch indexedDB — use data/ read APIs only.", + }, + { + name: "caches", + message: "B-1: ui must not touch caches — use platform/cache only via data/sync.", + }, + ], + // General strictness + "no-console": ["warn", { allow: ["warn", "error"] }], + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unnecessary-condition": "warn", + }, + }, + { + // B-7: no innerHTML with festival content — forbid assignment to innerHTML in src + files: ["src/**/*.ts"], + rules: { + "no-restricted-syntax": [ + "error", + { + selector: "AssignmentExpression[left.property.name='innerHTML']", + message: + "B-7: Do not render festival content via innerHTML of raw strings — use ui/render safe DOM (C-22).", + }, + { + selector: "CallExpression[callee.property.name='insertAdjacentHTML']", + message: + "B-7: Do not use insertAdjacentHTML with festival content — use ui/render safe DOM (C-22).", + }, + ], + }, + }, + { + // Allow explicit forbidden-globals only where justified; tests may use them + files: ["tests/**/*.ts", "scripts/**/*.ts"], + rules: { + "no-restricted-globals": "off", + "no-restricted-syntax": "off", + }, + }, +); diff --git a/experiments/README.md b/experiments/README.md new file mode 100644 index 0000000..ad1b408 --- /dev/null +++ b/experiments/README.md @@ -0,0 +1,59 @@ +# Lumen — Validation Experiments (Architecture Validation Phase) + +These are **disposable validation experiments**. They are **not** application +code, not part of the future product, and must not be imported by the app. +They exist to falsify or confirm specific architectural assumptions before +the architecture is frozen. Each is referenced by the spike document that +consumes its results. + +| File | Validates | Spike | Runtime | +|------|-----------|-------|---------| +| `exp1-time-model.mjs` | UTC storage, IANA-zone rendering, day boundaries, DST, skew, sanity window, now/next | SPIKE-08 | `node exp1-time-model.mjs` | +| `exp2-ab-update-sim.mjs` | A/B dual-slot atomic update state machine under crash/fault injection | SPIKE-02 | `node exp2-ab-update-sim.mjs` | +| `exp3-map-bench.html` | Map representation approaches (raster+DOM, SVG, canvas) | SPIKE-03 | headless Chromium (see below) | + +## Evidence classes used throughout + +Every claim in the spike documents is labelled: + +- **CONFIRMED** — observed by running an experiment here, or directly + documented by the platform vendor as guaranteed behavior. +- **INFERRED** — follows from documented platform behavior + architectural + reasoning, but not directly observed in this environment. +- **UNVERIFIED** — cannot be established on this Linux dev box; requires a + physical device/browser. + +## Environment + +- Linux dev machine, Node v22.23.2 (full-ICU), headless Chromium + (`chromium-browser`), Firefox available. +- **No iOS hardware or iOS Simulator is reachable from this environment.** + Nothing labelled iOS is CONFIRMED here; iOS items are INFERRED or + UNVERIFIED and are listed in each spike's "must test on physical iOS" + section. + +## How the map benchmark is run (EXP-3) + +``` +chromium-browser --headless=new --disable-gpu --no-sandbox \ + --window-size=800,600 --virtual-time-budget=120000 \ + --dump-dom "file:///…/experiments/exp3-map-bench.html" +``` + +**Interpretation caveat (important):** the benchmark measures synchronous JS +work per animation frame on a desktop GPU/CPU. All three approaches measured +≈ 0 ms/frame here. That does **not** mean they are equivalent on a low-end +Android phone: the dominant costs there are GPU compositing of transformed +layers, texture memory, and raster image decode — none of which a headless +desktop run represents. EXP-3's value is confirming that none of the three +has a disqualifying *JS-side* cost and that the DOM-overlay and SVG variants +are mechanically viable; the final representation decision still requires the +physical-device protocol in SPIKE-03. + +## Observed results (run 2026-08-30) + +- EXP-1: **24/24 PASS**. (Two earlier failures were test-data errors in the + experiment itself, corrected; the time logic was sound.) +- EXP-2: **16/16 PASS** across all crash/fault points. +- EXP-3: all variants complete; per-frame JS work ~0 ms across the board + (see caveat). diff --git a/experiments/exp1-time-model.mjs b/experiments/exp1-time-model.mjs new file mode 100644 index 0000000..c20fe18 --- /dev/null +++ b/experiments/exp1-time-model.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +/** + * EXP-1 — Time model validation (SPIKE-08) + * Disposable experiment. NOT application code. No dependencies. + * + * Validates, using Node's full-ICU Intl implementation (ECMA-402, the same + * spec browsers implement): + * T1 UTC epoch ms → festival-zone wall-clock rendering via Intl + * T2 dayKey derivation (calendar date in festival zone) incl. midnight edges + * T3 DST-transition behavior (spring/fall) for a DST zone + * T4 Non-DST and unusual-offset zones (fixed offsets, +5:45, 30-min DST) + * T5 Server-skew correction arithmetic (now = device + skew) + * T6 Sanity-window check logic + * T7 Now/Next classification using corrected clock + * + * Evidence class: spec-level behavior confirmed on V8/ICU. Browser engine + * parity (JavaScriptCore on iOS) is INFERRED, not proven here. + */ +'use strict'; + +let pass = 0, fail = 0; +const results = []; + +function check(id, name, actual, expected) { + const ok = JSON.stringify(actual) === JSON.stringify(expected); + ok ? pass++ : fail++; + results.push({ id, name, ok, actual, expected }); +} + +const fmt = (tz) => new Intl.DateTimeFormat('en-US', { + timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, +}); +const render = (ms, tz) => fmt(tz).format(new Date(ms)); + +// dayKey: calendar date parts in festival zone (publish-time precompute analog) +function dayKey(ms, tz) { + const parts = new Intl.DateTimeFormat('en-US', { + timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', + }).formatToParts(new Date(ms)); + const g = (t) => parts.find((p) => p.type === t).value; + return `${g('year')}-${g('month')}-${g('day')}`; +} + +// ---------- T1: UTC rendering in festival zone ---------- +const CHI = 'America/Chicago'; +// 2026-07-15T18:00:00Z == 13:00 CDT (UTC-5) +const t1 = Date.UTC(2026, 6, 15, 18, 0, 0); +check('T1a', 'UTC→Chicago CDT render', render(t1, CHI), '07/15/2026, 13:00:00'); +// same instant in device-zone-agnostic rendering must NOT change with host tz +check('T1b', 'render independent of host tz', render(t1, CHI).includes('13:00:00'), true); + +// ---------- T2: dayKey incl. local-midnight edges ---------- +// 2026-07-16T04:59:00Z == 2026-07-15 23:59 CDT → dayKey 07-15 +check('T2a', 'dayKey before local midnight', dayKey(Date.UTC(2026, 6, 16, 4, 59), CHI), '2026-07-15'); +// 2026-07-16T05:00:00Z == 2026-07-16 00:00 CDT → dayKey 07-16 +check('T2b', 'dayKey at local midnight', dayKey(Date.UTC(2026, 6, 16, 5, 0), CHI), '2026-07-16'); +// UTC midnight is NOT local midnight: 2026-07-16T00:00Z == 07-15 19:00 CDT +check('T2c', 'UTC midnight ≠ festival day boundary', dayKey(Date.UTC(2026, 6, 16, 0, 0), CHI), '2026-07-15'); + +// ---------- T3: DST transitions (America/Chicago) ---------- +// US 2026: spring forward Mar 8 02:00→03:00; fall back Nov 1 02:00→01:00 +// 2026-03-08T07:59Z == 01:59 CST; 2026-03-08T08:00Z == 03:00 CDT +check('T3a', 'before spring-forward', render(Date.UTC(2026, 2, 8, 7, 59), CHI), '03/08/2026, 01:59:00'); +check('T3b', 'after spring-forward', render(Date.UTC(2026, 2, 8, 8, 0), CHI), '03/08/2026, 03:00:00'); +// Fall back: 2026-11-01T06:59Z == 01:59 CDT; 2026-11-01T07:00Z == 01:00 CST (ambiguous hour handled) +check('T3c', 'fall-back first pass', render(Date.UTC(2026, 10, 1, 6, 59), CHI), '11/01/2026, 01:59:00'); +check('T3d', 'fall-back second pass', render(Date.UTC(2026, 10, 1, 7, 0), CHI), '11/01/2026, 01:00:00'); +// Events spanning transition keep correct duration via epoch arithmetic +const spanMs = Date.UTC(2026, 10, 1, 7, 0) - Date.UTC(2026, 10, 1, 6, 59); +check('T3e', 'epoch arithmetic unaffected by DST', spanMs, 60000); + +// ---------- T4: unusual zones ---------- +const PHX = 'America/Phoenix'; // no DST, UTC-7 year-round +check('T4a', 'no-DST zone', render(Date.UTC(2026, 0, 15, 18, 0), PHX), '01/15/2026, 11:00:00'); +const KTM = 'Asia/Kathmandu'; // UTC+5:45 fixed +check('T4b', '+5:45 fixed offset', render(Date.UTC(2026, 0, 15, 18, 0), KTM), '01/15/2026, 23:45:00'); +const LH = 'Australia/Lord_Howe'; // UTC+10:30 std, +11:00 DST (30-min shift) +check('T4c', '30-min DST zone (Jan=DST)', render(Date.UTC(2026, 0, 15, 18, 0), LH), '01/16/2026, 05:00:00'); +check('T4d', '30-min DST zone (Jul=std)', render(Date.UTC(2026, 6, 15, 18, 0), LH), '07/16/2026, 04:30:00'); + +// ---------- T5: server-skew correction ---------- +// Device clock is +7 minutes fast; server Date header captured true time. +const deviceNow = Date.UTC(2026, 6, 15, 18, 7, 0); +const serverNow = Date.UTC(2026, 6, 15, 18, 0, 0); +const skew = serverNow - deviceNow; // -420000 +const correctedNow = deviceNow + skew; +check('T5a', 'skew computation', skew, -420000); +check('T5b', 'corrected now', correctedNow, serverNow); +// monotonic drift detection analog: session elapsed via monotonic clock +const monoElapsedMs = 3600000; // 1h of monotonic time passed +const expectedDeviceNow = deviceNow + monoElapsedMs; +const observedDeviceNow = expectedDeviceNow + 300000; // user moved clock +5 min +const drift = observedDeviceNow - expectedDeviceNow; +check('T5c', 'mid-session drift detected', Math.abs(drift) > 120000, true); + +// ---------- T6: sanity window ---------- +const festStart = Date.UTC(2026, 8, 10); // Sep 10 +const festEnd = Date.UTC(2026, 8, 13); // Sep 13 +const WINDOW = 45 * 86400000; +const sane = (now) => now >= festStart - WINDOW && now <= festEnd + WINDOW; +check('T6a', 'now inside window', sane(serverNow), false); // July: outside 45d of Sep → warns +check('T6b', 'now at festival', sane(Date.UTC(2026, 8, 11, 12)), true); +check('T6c', 'now wildly wrong', sane(Date.UTC(2023, 0, 1)), false); +// NOTE: T6a intentionally demonstrates the ±45d window; a July clock is +// outside it for a September festival. If prep happens months early the +// warning must be suppressed until near the event — see SPIKE-08 finding F-3. + +// ---------- T7: Now / Up Next classification ---------- +const events = [ + { id: 'e1', startUtc: Date.UTC(2026, 8, 11, 17), endUtc: Date.UTC(2026, 8, 11, 18) }, + { id: 'e2', startUtc: Date.UTC(2026, 8, 11, 18), endUtc: Date.UTC(2026, 8, 11, 19) }, + { id: 'e3', startUtc: Date.UTC(2026, 8, 11, 18, 30), endUtc: Date.UTC(2026, 8, 11, 19, 30) }, +]; +function nowNext(now) { + const nowE = events.filter((e) => e.startUtc <= now && now < e.endUtc).map((e) => e.id); + const next = events.filter((e) => e.startUtc > now).sort((a, b) => a.startUtc - b.startUtc).slice(0, 2).map((e) => e.id); + return { nowE, next }; +} +const r1 = nowNext(Date.UTC(2026, 8, 11, 18, 45)); +check('T7a', 'now includes overlapping events', r1.nowE, ['e2', 'e3']); +check('T7b', 'up-next empty when events running', r1.next, []); +const r1b = nowNext(Date.UTC(2026, 8, 11, 18, 15)); +check('T7d', 'not-yet-started is up-next', r1b.next, ['e3']); +const r2 = nowNext(Date.UTC(2026, 8, 11, 17, 30)); +check('T7c', 'up-next before start', r2.next, ['e2', 'e3']); + +// ---------- report ---------- +console.log('EXP-1 TIME MODEL RESULTS'); +for (const r of results) { + console.log(`${r.ok ? 'PASS' : 'FAIL'} ${r.id} ${r.name}${r.ok ? '' : ` actual=${JSON.stringify(r.actual)} expected=${JSON.stringify(r.expected)}`}`); +} +console.log(`\n${pass} passed, ${fail} failed`); +process.exit(fail ? 1 : 0); diff --git a/experiments/exp2-ab-update-sim.mjs b/experiments/exp2-ab-update-sim.mjs new file mode 100644 index 0000000..a4a3af6 --- /dev/null +++ b/experiments/exp2-ab-update-sim.mjs @@ -0,0 +1,242 @@ +#!/usr/bin/env node +/** + * EXP-2 — A/B atomic dataset update state-machine simulation (SPIKE-02) + * Disposable experiment. NOT application code. No dependencies. + * + * This simulates the *design logic* of the A/B dual-slot architecture with + * crash/fault injection at every stage. It does NOT test IndexedDB itself + * (platform behavior is out of scope on a dev machine; see SPIKE-01). + * + * Modeled mechanics (mirroring ARCHITECTURE-DESIGN §18): + * - system meta (single atomic store): activeSlot, activeVersion, verifiedVersion + * - two dataset slots; staging writes ONLY to the inactive slot + * - per-file staging is atomic: bytes + progress record commit together + * - activation is ONE atomic transaction flipping pointer + version + verification record + * - boot performs light verification; readback spot-check after activation + * + * INVARIANT under test: + * "Either the previous valid dataset remains active or the new valid dataset + * becomes active. The app never knowingly exposes a partial dataset." + */ +'use strict'; + +// ---------- tiny transactional store model ---------- +class AtomicStore { + constructor() { this.map = new Map(); } + // a transaction: apply fn to a draft; commit is all-or-nothing + txn(fn) { + const draft = new Map(this.map); + fn(draft); // if fn throws, nothing commits + this.map = draft; + } +} + +class Device { + constructor() { + this.system = new AtomicStore(); // lumen-system + this.slots = { A: new AtomicStore(), B: new AtomicStore() }; + this.user = new AtomicStore(); // lumen-user (favorites) + this.system.txn((m) => m.set('meta', { activeSlot: 'A', activeVersion: 1, verifiedVersion: 1 })); + // seed active dataset v1 (old known-good) + this.writeCompleteDataset('A', 1, 'old-content'); + this.user.txn((m) => m.set('fav:e-0001', { addedAt: 0 })); + } + writeCompleteDataset(slot, version, tag) { + const s = this.slots[slot]; + s.txn((m) => { + m.set('manifest', { packageVersion: version, sections: ['emergency', 'schedule', 'map', 'info', 'assets'], tag }); + for (const sec of ['emergency', 'schedule', 'map', 'info', 'assets']) { + m.set(`file:${sec}`, { bytes: `${tag}:${sec}:v${version}`, hash: `h-${tag}-${sec}-v${version}` }); + } + m.set('staged', new Set(['emergency', 'schedule', 'map', 'info', 'assets'])); + m.set('verified', version); + }); + } + meta() { return this.system.map.get('meta'); } + // full dataset check = every manifest-listed file present with matching hash + isComplete(slot, expectVersion = null) { + const s = this.slots[slot]; + const man = s.map.get('manifest'); + if (!man) return false; + if (expectVersion !== null && man.packageVersion !== expectVersion) return false; + const staged = s.map.get('staged'); + if (!staged || staged.size !== man.sections.length) return false; + for (const sec of man.sections) { + const f = s.map.get(`file:${sec}`); + if (!f || f.hash !== `h-${man.tag}-${sec}-v${man.packageVersion}`) return false; + } + return true; + } + // crash mid-transaction: fn throws → nothing committed (atomic store semantics) +} + +// ---------- update pipeline with fault injection ---------- +class UpdateSession { + constructor(dev, opts) { this.dev = dev; this.opts = opts; } + run() { + const { newVersion = 2, tag = 'new-content', fault = null, faultAt = 0 } = this.opts; + const manifest = { packageVersion: newVersion, sections: ['emergency', 'schedule', 'map', 'info', 'assets'], tag }; + + // 1) signature verification (fault: bad signature) + if (fault === 'signature') return { outcome: 'rejected-signature' }; + // 2) compatibility check (fault: incompatible) + if (fault === 'compatibility') return { outcome: 'rejected-compatibility' }; + // 3) choose inactive slot, wipe it (rollback data sacrificed — documented) + const target = this.dev.meta().activeSlot === 'A' ? 'B' : 'A'; + this.dev.slots[target].txn((m) => m.clear()); + const staged = new Set(); + + // 4) stage files; each file = ONE atomic txn (bytes + progress together) + for (const sec of manifest.sections) { + if (fault === 'crash-staging' && staged.size >= faultAt) return { outcome: 'crashed-staging', stagedSoFar: staged.size, target }; + const bytes = fault === 'hash' && sec === 'map' ? 'CORRUPTED' : `${tag}:${sec}:v${newVersion}`; + this.dev.slots[target].txn((m) => { + m.set('manifest', manifest); + m.set(`file:${sec}`, { bytes, hash: `h-${tag}-${sec}-v${newVersion}` }); + const s = m.get('staged') || new Set(); s.add(sec); m.set('staged', s); + }); + staged.add(sec); + } + + // 5) full verification: hashes then schema + for (const sec of manifest.sections) { + const f = this.dev.slots[target].map.get(`file:${sec}`); + if (f.bytes !== `${tag}:${sec}:v${newVersion}`) return { outcome: 'rejected-hash', target }; + } + if (fault === 'schema') return { outcome: 'rejected-schema', target }; + if (fault === 'validation') return { outcome: 'rejected-validation', target }; + + // 6) activation: single atomic transaction on system meta + if (fault === 'crash-before-flip') return { outcome: 'crashed-before-flip', target }; + const flipCommitted = fault !== 'crash-during-flip'; + if (flipCommitted) { + this.dev.system.txn((m) => m.set('meta', { activeSlot: target, activeVersion: newVersion, verifiedVersion: newVersion })); + } else { + return { outcome: 'crashed-during-flip', target }; // txn never committed + } + + // 7) crash window after flip, before readback + if (fault === 'crash-after-flip') return { outcome: 'crashed-after-flip', target }; + + // 8) readback spot-check + if (fault === 'readback-fail') { + // simulate post-activation corruption discovered by readback + this.dev.slots[target].txn((m) => m.set('file:map', { bytes: 'BITROT', hash: 'h-bad' })); + } + const ok = this.dev.isComplete(target, newVersion); + if (!ok) { + // automatic rollback: flip back if previous slot still complete + const other = target === 'A' ? 'B' : 'A'; + if (this.dev.isComplete(other, this.dev.meta().activeVersion === newVersion ? null : this.dev.meta().verifiedVersion) || this.dev.isComplete(other)) { + const prevVersion = this.dev.slots[other].map.get('manifest')?.packageVersion; + this.dev.system.txn((m) => m.set('meta', { activeSlot: other, activeVersion: prevVersion, verifiedVersion: prevVersion })); + return { outcome: 'rollback-after-readback', target }; + } + return { outcome: 'recovery-needed', target }; + } + return { outcome: 'activated', target }; + } +} + +// ---------- boot / recovery ---------- +function boot(dev) { + const meta = dev.meta(); + if (dev.isComplete(meta.activeSlot, meta.activeVersion)) return { state: 'READY', version: meta.activeVersion }; + const other = meta.activeSlot === 'A' ? 'B' : 'A'; + const om = dev.slots[other].map.get('manifest'); + if (om && dev.isComplete(other)) { + dev.system.txn((m) => m.set('meta', { activeSlot: other, activeVersion: om.packageVersion, verifiedVersion: om.packageVersion })); + return { state: 'READY-via-fallback', version: om.packageVersion }; + } + return { state: 'RECOVERY', baseline: true }; // embedded emergency baseline still present +} + +// ---------- invariant assertion ---------- +let pass = 0, fail = 0; +function invariant(name, dev, expect) { + const b = boot(dev); + const meta = dev.meta(); + const activeComplete = dev.isComplete(meta.activeSlot); + const fav = dev.user.map.get('fav:e-0001') !== undefined; + const ok = activeComplete === expect.complete && fav === true && + (expect.state ? b.state === expect.state || (expect.state === 'READY' && b.state === 'READY-via-fallback') : true) && + (expect.version ? meta.activeVersion === expect.version : true); + ok ? pass++ : fail++; + console.log(`${ok ? 'PASS' : 'FAIL'} ${name} boot=${b.state} active=${meta.activeSlot} v${meta.activeVersion} complete=${activeComplete} favorites=${fav}`); + if (!ok) console.log(` expected: ${JSON.stringify(expect)}`); +} + +console.log('EXP-2 A/B UPDATE STATE MACHINE — 14 SCENARIOS\n'); + +// S1 download begins, crash before any file staged +{ const d = new Device(); new UpdateSession(d, { fault: 'crash-staging', faultAt: 0 }).run(); + invariant('S01 crash at download start', d, { complete: true, version: 1, state: 'READY' }); } + +// S2 download partially completes +{ const d = new Device(); new UpdateSession(d, { fault: 'crash-staging', faultAt: 3 }).run(); + invariant('S02 partial download', d, { complete: true, version: 1, state: 'READY' }); } + +// S3 browser terminated (mid staging) +{ const d = new Device(); new UpdateSession(d, { fault: 'crash-staging', faultAt: 2 }).run(); + invariant('S03 browser terminated', d, { complete: true, version: 1, state: 'READY' }); } + +// S4 phone reboots (crash before flip) +{ const d = new Device(); new UpdateSession(d, { fault: 'crash-before-flip' }).run(); + invariant('S04 reboot before activation', d, { complete: true, version: 1, state: 'READY' }); } + +// S5 dataset validation fails (generic full-verification failure) +{ const d = new Device(); const r = new UpdateSession(d, { fault: 'validation' }).run(); + invariant(`S05 validation fails (${r.outcome})`, d, { complete: true, version: 1, state: 'READY' }); } + +// S6 integrity hash fails +{ const d = new Device(); const r = new UpdateSession(d, { fault: 'hash' }).run(); + invariant(`S06 hash fails (${r.outcome})`, d, { complete: true, version: 1, state: 'READY' }); } + +// S7 signature validation fails +{ const d = new Device(); const r = new UpdateSession(d, { fault: 'signature' }).run(); + invariant(`S07 signature fails (${r.outcome})`, d, { complete: true, version: 1, state: 'READY' }); } + +// S8 schema validation fails +{ const d = new Device(); const r = new UpdateSession(d, { fault: 'schema' }).run(); + invariant(`S08 schema fails (${r.outcome})`, d, { complete: true, version: 1, state: 'READY' }); } + +// S9 compatibility validation fails +{ const d = new Device(); const r = new UpdateSession(d, { fault: 'compatibility' }).run(); + invariant(`S09 compatibility fails (${r.outcome})`, d, { complete: true, version: 1, state: 'READY' }); } + +// S10 activation succeeds +{ const d = new Device(); const r = new UpdateSession(d, { newVersion: 2 }).run(); + invariant(`S10 activation succeeds (${r.outcome})`, d, { complete: true, version: 2, state: 'READY' }); } + +// S11 pointer update occurs (flip committed) — verified by S10/S12 state +// S12 browser terminates immediately after flip (before readback) +{ const d = new Device(); const r = new UpdateSession(d, { newVersion: 2, fault: 'crash-after-flip' }).run(); + invariant(`S12 crash right after flip (${r.outcome})`, d, { complete: true, version: 2, state: 'READY' }); } + +// S13 app starts again after crash during flip (transaction not committed) +{ const d = new Device(); const r = new UpdateSession(d, { newVersion: 2, fault: 'crash-during-flip' }).run(); + invariant(`S13 restart after failed flip (${r.outcome})`, d, { complete: true, version: 1, state: 'READY' }); } + +// S14 recovery: corruption discovered by readback after activation → rollback +{ const d = new Device(); new UpdateSession(d, { newVersion: 2 }).run(); // v2 activates, old slot retained + const r = new UpdateSession(d, { newVersion: 3, fault: 'readback-fail' }).run(); + invariant(`S14 readback corruption → rollback (${r.outcome})`, d, { complete: true, state: 'READY' }); } + +// X1 extra: corruption of ACTIVE slot discovered at boot, fallback slot intact +{ const d = new Device(); new UpdateSession(d, { newVersion: 2 }).run(); + d.slots[d.meta().activeSlot].txn((m) => m.set('file:schedule', { bytes: 'BITROT', hash: 'bad' })); + invariant('X01 active corrupt at boot → fallback slot', d, { complete: true, state: 'READY', version: 1 }); } + +// X2 extra: BOTH slots corrupt at boot → RECOVERY with baseline, favorites intact +{ const d = new Device(); + d.slots.A.txn((m) => m.clear()); d.slots.B.txn((m) => m.clear()); + invariant('X02 both slots lost → RECOVERY + baseline', d, { complete: false, state: 'RECOVERY' }); } + +// X3 extra: user state survives a full successful update +{ const d = new Device(); new UpdateSession(d, { newVersion: 2 }).run(); + const fav = d.user.map.get('fav:e-0001'); + const ok = fav !== undefined; ok ? pass++ : fail++; + console.log(`${ok ? 'PASS' : 'FAIL'} X03 favorites survive update`); } + +console.log(`\n${pass} passed, ${fail} failed`); +process.exit(fail ? 1 : 0); diff --git a/experiments/exp3-map-bench.html b/experiments/exp3-map-bench.html new file mode 100644 index 0000000..056d027 --- /dev/null +++ b/experiments/exp3-map-bench.html @@ -0,0 +1,137 @@ + + +mapbench + +
RUNNING
+ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4aaf6ef --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3426 @@ +{ + "name": "lumen", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lumen", + "version": "0.1.0", + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/node": "^22.13.5", + "eslint": "^9.22.0", + "eslint-plugin-boundaries": "^5.0.1", + "globals": "^16.0.0", + "prettier": "^3.5.3", + "typescript": "^5.8.2", + "typescript-eslint": "^8.26.1", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@boundaries/elements": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@boundaries/elements/-/elements-1.2.0.tgz", + "integrity": "sha512-W65Gum02liMd3hmNrLmDBX1u5BmRMcunouFjLXyhxHnNY4YlK1kTxsgfflZ5XBGSnPnO0MkiUzAcoGzYrlx0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-import-resolver-node": "0.3.9", + "eslint-module-utils": "2.12.1", + "handlebars": "4.7.8", + "is-core-module": "2.16.1", + "micromatch": "4.0.8" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz", + "integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/type-utils": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.68.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.7.tgz", + "integrity": "sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz", + "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz", + "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.68.0", + "@typescript-eslint/types": "^8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz", + "integrity": "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz", + "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.68.0.tgz", + "integrity": "sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.68.0.tgz", + "integrity": "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz", + "integrity": "sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.68.0", + "@typescript-eslint/tsconfig-utils": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.68.0.tgz", + "integrity": "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz", + "integrity": "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-boundaries": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-boundaries/-/eslint-plugin-boundaries-5.4.0.tgz", + "integrity": "sha512-6SQmEhXCqGrrxm9YiM24SC95CqrVi2MUOm5SDrfquceh/os8MIAvZYsDU69zvtCSb1S6UbNEmdioi1gCDc8+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@boundaries/elements": "1.2.0", + "chalk": "4.1.2", + "eslint-import-resolver-node": "0.3.9", + "eslint-module-utils": "2.12.1", + "micromatch": "4.0.8" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.68.0.tgz", + "integrity": "sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.68.0", + "@typescript-eslint/parser": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0cd3721 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "lumen", + "version": "0.1.0", + "private": true, + "description": "Lumen — offline-first festival PWA. Stage 1 foundation only; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1.", + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --check .", + "format:write": "prettier --write .", + "test": "vitest run", + "test:watch": "vitest", + "ci": "npm run typecheck && npm run lint && npm run format && npm run test" + }, + "dependencies": {}, + "devDependencies": { + "@types/node": "^22.13.5", + "eslint": "^9.22.0", + "@eslint/js": "^9.22.0", + "typescript": "^5.8.2", + "typescript-eslint": "^8.26.1", + "eslint-plugin-boundaries": "^5.0.1", + "globals": "^16.0.0", + "prettier": "^3.5.3", + "vitest": "^3.1.1" + } +} diff --git a/pipeline/.gitkeep b/pipeline/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pipeline/README.md b/pipeline/README.md new file mode 100644 index 0000000..a2a57cb --- /dev/null +++ b/pipeline/README.md @@ -0,0 +1,5 @@ +# pipeline + +Validate→build→hash→sign→upload→smoke. Separate from app. ADR-014, SPIKE-04. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/README.md b/public/README.md new file mode 100644 index 0000000..4b2c1fd --- /dev/null +++ b/public/README.md @@ -0,0 +1,5 @@ +# public + +Static shell entry (index.html no-cache, manifest.webmanifest, fallback.html Ring-0, sw.js precache). ARCH §24. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..2e8d1a7 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,5 @@ +# scripts + +Boundary checks, budget gates, helpers — may import anything for validation. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/scripts/check-boundaries.ts b/scripts/check-boundaries.ts new file mode 100644 index 0000000..90ebbad --- /dev/null +++ b/scripts/check-boundaries.ts @@ -0,0 +1,98 @@ +/** + * Stage 1 — architectural boundary gate. + * Minimal check that no source file violates B-1 … B-7 by string scanning + * (full boundary enforcement is via eslint-plugin-boundaries in `npm run lint`). + * This script is a second cheap gate for CI and for `tests/unit/boundaries.test.ts`. + * + * Rules checked (subset where string grep is sufficient for Stage 1): + * B-1 ui must not import indexedDB/caches/fetch/localStorage + * B-7 no innerHTML/insertAdjacentHTML in src + */ +import { readdirSync, readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +const SRC = "src"; + +const violations: string[] = []; + +function walk(dir: string, cb: (file: string) => void): void { + if (!existsSync(dir)) return; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, entry.name); + if (entry.isDirectory()) walk(p, cb); + else if (entry.isFile() && (p.endsWith(".ts") || p.endsWith(".js"))) cb(p); + } +} + +const B1_FORBIDDEN_IN_UI = [ + /from\s+["'].*platform\//, + /from\s+["'].*storage\//, + /from\s+["'].*sync\//, + /indexedDB/, + /\bcaches\b/, + /\blocalStorage\b/, +]; + +walk(SRC, (file) => { + const content = readFileSync(file, "utf8"); + const isUI = file.startsWith("src/ui/"); + const isDomain = file.startsWith("src/domain/"); + const isSyncTransport = file.startsWith("src/sync/transport/"); + const isPlatform = file.startsWith("src/platform/"); + + if (isUI) { + for (const rx of B1_FORBIDDEN_IN_UI) { + if (rx.test(content)) { + // allow comments that mention the pattern; require actual import usage + // simple heuristic: check import line + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line: string = lines[i] ?? ""; + if (rx.test(line) && /import|require/.test(line)) { + violations.push( + `${file}:${String(i + 1)}: B-1 violation — ui must not touch platform/storage/sync or raw indexedDB/caches/localStorage — line: ${line.trim()}`, + ); + } + } + } + } + } + + // B-7 innerHTML + if (file.includes("src/")) { + const lines = content.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line: string = lines[i] ?? ""; + if (/\.innerHTML\s*=/.test(line) || line.includes("insertAdjacentHTML")) { + violations.push( + `${file}:${String(i + 1)}: B-7 violation — do not use innerHTML/insertAdjacentHTML with festival content (C-22).`, + ); + } + } + } + + // B-2 domain must not import platform/sync + if (isDomain && /from\s+["'].*platform\//.test(content)) { + violations.push(`${file}: B-2 violation — domain must not import platform.`); + } + + // sync-transport must not import data/domain + if (isSyncTransport && /from\s+["'].*(data|domain)\//.test(content)) { + violations.push(`${file}: B-3 violation — transport must not import data/domain.`); + } + + // platform must not import domain/ui/sync + if (isPlatform && /from\s+["'].*(domain|ui|sync)\//.test(content)) { + violations.push(`${file}: platform must not import domain/ui/sync.`); + } +}); + +if (violations.length) { + console.error("Boundary violations found:"); + for (const v of violations) console.error(" " + v); + process.exit(1); +} else { + console.warn( + "Boundary check OK — no B-1…B-7 string violations in src/ (full check via eslint boundaries).", + ); +} diff --git a/src/app/.gitkeep b/src/app/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app/README.md b/src/app/README.md new file mode 100644 index 0000000..91d63a5 --- /dev/null +++ b/src/app/README.md @@ -0,0 +1,5 @@ +# app + +Bootstrap composition root: light verify → readiness → render, compatibility §18.5. May import via public APIs. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/assets/README.md b/src/assets/README.md new file mode 100644 index 0000000..f856332 --- /dev/null +++ b/src/assets/README.md @@ -0,0 +1,5 @@ +# assets + +Hashed shell assets (generated at build). + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/data/.gitkeep b/src/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/data/README.md b/src/data/README.md new file mode 100644 index 0000000..a1d3f71 --- /dev/null +++ b/src/data/README.md @@ -0,0 +1,5 @@ +# data + +DatasetStore + UserStore read/write contracts. Imports platform only. Never sync/ui. ADR-004/005/006, SPIKE-01/02. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/domain/clock/.gitkeep b/src/domain/clock/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/clock/README.md b/src/domain/clock/README.md new file mode 100644 index 0000000..6f80a61 --- /dev/null +++ b/src/domain/clock/README.md @@ -0,0 +1,5 @@ +# domain/clock + +ClockService skew+monotonic+sanity F-3. No timers. SPIKE-08, ADR-009. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/domain/emergency/.gitkeep b/src/domain/emergency/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/emergency/README.md b/src/domain/emergency/README.md new file mode 100644 index 0000000..15a80f6 --- /dev/null +++ b/src/domain/emergency/README.md @@ -0,0 +1,5 @@ +# domain/emergency + +Resolution floor vs dataset section, provenance, hardening F-1. Imports data/readiness + emergency-baseline. Never platform/sync. ADR-007, SPIKE-06. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/domain/favorites/.gitkeep b/src/domain/favorites/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/favorites/README.md b/src/domain/favorites/README.md new file mode 100644 index 0000000..8279715 --- /dev/null +++ b/src/domain/favorites/README.md @@ -0,0 +1,5 @@ +# domain/favorites + +FavoritesService over UserStore (B-6). Stable ids. DISCOVERY §15.8. Never dataset package. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/domain/festival/.gitkeep b/src/domain/festival/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/festival/README.md b/src/domain/festival/README.md new file mode 100644 index 0000000..47cac30 --- /dev/null +++ b/src/domain/festival/README.md @@ -0,0 +1,5 @@ +# domain/festival + +Info blocks structured nodes. Imports data. R-F1, C-22. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/domain/map/.gitkeep b/src/domain/map/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/map/README.md b/src/domain/map/README.md new file mode 100644 index 0000000..42068f7 --- /dev/null +++ b/src/domain/map/README.md @@ -0,0 +1,5 @@ +# domain/map + +POI interpretation, level math. Imports data. No GPS. ADR-008, SPIKE-03. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/domain/readiness/.gitkeep b/src/domain/readiness/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/readiness/README.md b/src/domain/readiness/README.md new file mode 100644 index 0000000..3cbb0e1 --- /dev/null +++ b/src/domain/readiness/README.md @@ -0,0 +1,5 @@ +# domain/readiness + +Predicate C1–C8 + six-state taxonomy, time-independent. Imports data/system. SPIKE-05. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/domain/schedule/.gitkeep b/src/domain/schedule/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/schedule/README.md b/src/domain/schedule/README.md new file mode 100644 index 0000000..e63499c --- /dev/null +++ b/src/domain/schedule/README.md @@ -0,0 +1,5 @@ +# domain/schedule + +Queries: Now/Next, filters/search, conflict. Imports data + clock. ADR-009, SPIKE-08. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/emergency-baseline/.gitkeep b/src/emergency-baseline/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/emergency-baseline/README.md b/src/emergency-baseline/README.md new file mode 100644 index 0000000..8974879 --- /dev/null +++ b/src/emergency-baseline/README.md @@ -0,0 +1,5 @@ +# emergency-baseline + +Generated floor JSON ≤16 KB, immutable per build, same source as dataset emergency. ADR-007, SPIKE-06. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/platform/cache/.gitkeep b/src/platform/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/platform/cache/README.md b/src/platform/cache/README.md new file mode 100644 index 0000000..d98e07b --- /dev/null +++ b/src/platform/cache/README.md @@ -0,0 +1,5 @@ +# platform/cache + +Cache Storage adapter for shell. Owns caches only. No dataset logic. P8. ADR-004. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/platform/idb/.gitkeep b/src/platform/idb/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/platform/idb/README.md b/src/platform/idb/README.md new file mode 100644 index 0000000..945d0ec --- /dev/null +++ b/src/platform/idb/README.md @@ -0,0 +1,5 @@ +# platform/idb + +Thin promise wrapper over raw IndexedDB. No business logic. Transaction discipline P1–P8. May only import Browser APIs. Must NOT import domain/ui/sync. ADR-004, SPIKE-01. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/platform/sw/.gitkeep b/src/platform/sw/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/platform/sw/README.md b/src/platform/sw/README.md new file mode 100644 index 0000000..363b82f --- /dev/null +++ b/src/platform/sw/README.md @@ -0,0 +1,5 @@ +# platform/sw + +SW bridge page-side (message handling, SKIP_WAITING). No dataset writes. ADR-002, C-21. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/storage/.gitkeep b/src/storage/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/storage/README.md b/src/storage/README.md new file mode 100644 index 0000000..dbf751c --- /dev/null +++ b/src/storage/README.md @@ -0,0 +1,5 @@ +# storage + +Re-exports platform adapters with typed contracts. No UI/fetch. ADR-004. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/sync/.gitkeep b/src/sync/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/sync/README.md b/src/sync/README.md new file mode 100644 index 0000000..465cba7 --- /dev/null +++ b/src/sync/README.md @@ -0,0 +1,5 @@ +# sync + +SyncService orchestration check→stage→verify→activate. May import data + transport/verifier + platform. Never domain/ui. ADR-006. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/sync/transport/.gitkeep b/src/sync/transport/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/sync/transport/README.md b/src/sync/transport/README.md new file mode 100644 index 0000000..5f00986 --- /dev/null +++ b/src/sync/transport/README.md @@ -0,0 +1,5 @@ +# sync/transport + +Byte pipe: isAvailable/fetchPointer/fetchBytes. May import fetch only. Never data/domain/ui. ADR-011. V1: HttpTransport only. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/sync/verifier/.gitkeep b/src/sync/verifier/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/sync/verifier/README.md b/src/sync/verifier/README.md new file mode 100644 index 0000000..7466d78 --- /dev/null +++ b/src/sync/verifier/README.md @@ -0,0 +1,5 @@ +# sync/verifier + +SHA-256 + Ed25519, budgets, quarantine. Sole authenticity decider (B-4). May import platform(hash). Never sync orchestration. ADR-013. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/ui/components/.gitkeep b/src/ui/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/components/README.md b/src/ui/components/README.md new file mode 100644 index 0000000..d6810b0 --- /dev/null +++ b/src/ui/components/README.md @@ -0,0 +1,5 @@ +# ui/components + +Shared primitives (safe DOM helpers, status chip). May import domain only. B-1, C-22. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/ui/render/.gitkeep b/src/ui/render/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/render/README.md b/src/ui/render/README.md new file mode 100644 index 0000000..a4d9384 --- /dev/null +++ b/src/ui/render/README.md @@ -0,0 +1,5 @@ +# ui/render + +Structured-node → safe DOM, no innerHTML of raw. C-22, B-7. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/ui/router/.gitkeep b/src/ui/router/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/router/README.md b/src/ui/router/README.md new file mode 100644 index 0000000..f8302a7 --- /dev/null +++ b/src/ui/router/README.md @@ -0,0 +1,5 @@ +# ui/router + +history-API router, single index.html offline-safe. ADR-002. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/ui/views/.gitkeep b/src/ui/views/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/views/README.md b/src/ui/views/README.md new file mode 100644 index 0000000..3ccc601 --- /dev/null +++ b/src/ui/views/README.md @@ -0,0 +1,5 @@ +# ui/views + +emergency/schedule/map/festival/status views. May import domain only. B-1, B-7. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/src/ui/views/emergency/.gitkeep b/src/ui/views/emergency/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/views/emergency/README.md b/src/ui/views/emergency/README.md new file mode 100644 index 0000000..cb14209 --- /dev/null +++ b/src/ui/views/emergency/README.md @@ -0,0 +1,2 @@ +# ui/views/emergency +View for emergency — may import domain only. B-1, §7. Stage 1: no code. diff --git a/src/ui/views/festival/.gitkeep b/src/ui/views/festival/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/views/festival/README.md b/src/ui/views/festival/README.md new file mode 100644 index 0000000..8c19ef2 --- /dev/null +++ b/src/ui/views/festival/README.md @@ -0,0 +1,2 @@ +# ui/views/festival +View for festival — may import domain only. B-1, §7. Stage 1: no code. diff --git a/src/ui/views/map/.gitkeep b/src/ui/views/map/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/views/map/README.md b/src/ui/views/map/README.md new file mode 100644 index 0000000..00237fd --- /dev/null +++ b/src/ui/views/map/README.md @@ -0,0 +1,2 @@ +# ui/views/map +View for map — may import domain only. B-1, §7. Stage 1: no code. diff --git a/src/ui/views/schedule/.gitkeep b/src/ui/views/schedule/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/views/schedule/README.md b/src/ui/views/schedule/README.md new file mode 100644 index 0000000..66e696e --- /dev/null +++ b/src/ui/views/schedule/README.md @@ -0,0 +1,2 @@ +# ui/views/schedule +View for schedule — may import domain only. B-1, §7. Stage 1: no code. diff --git a/src/ui/views/status/.gitkeep b/src/ui/views/status/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/views/status/README.md b/src/ui/views/status/README.md new file mode 100644 index 0000000..64daa1a --- /dev/null +++ b/src/ui/views/status/README.md @@ -0,0 +1,2 @@ +# ui/views/status +View for status — may import domain only. B-1, §7. Stage 1: no code. diff --git a/tests/device-matrix/.gitkeep b/tests/device-matrix/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/device-matrix/README.md b/tests/device-matrix/README.md new file mode 100644 index 0000000..2936c94 --- /dev/null +++ b/tests/device-matrix/README.md @@ -0,0 +1,5 @@ +# tests/device-matrix + +Manual protocols per ARCHITECTURE-VALIDATION.md §4 (19×4). + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/tests/e2e/.gitkeep b/tests/e2e/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 0000000..4e909ec --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,5 @@ +# tests/e2e + +Headless/offline harness. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/README.md b/tests/integration/README.md new file mode 100644 index 0000000..48ed058 --- /dev/null +++ b/tests/integration/README.md @@ -0,0 +1,5 @@ +# tests/integration + +Full update lifecycle mocked transport + fault injection 9 stages, exp2 model. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/README.md b/tests/unit/README.md new file mode 100644 index 0000000..eecb508 --- /dev/null +++ b/tests/unit/README.md @@ -0,0 +1,5 @@ +# tests/unit + +Verifier, ClockService, readiness predicate, escaping, floor forward-tolerant. + +Stage 1: directory exists with .gitkeep; no feature code yet per IMPLEMENTATION-CONTRACT.md Stage 1. See IMPLEMENTATION-CONTRACT.md §4 responsibilities and §6 B-1…B-7. diff --git a/tests/unit/boundaries.test.ts b/tests/unit/boundaries.test.ts new file mode 100644 index 0000000..ee7e695 --- /dev/null +++ b/tests/unit/boundaries.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from "vitest"; +import { readdirSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +function walkFiles(dir: string, exts = [".ts", ".js"]): string[] { + const out: string[] = []; + function walk(d: string) { + if (!existsSync(d)) return; + for (const e of readdirSync(d, { withFileTypes: true })) { + const p = join(d, e.name); + if (e.isDirectory()) walk(p); + else if (exts.some((ext) => p.endsWith(ext))) out.push(p); + } + } + walk(dir); + return out; +} + +describe("architectural boundaries — Stage 1 gate (B-1…B-7)", () => { + it("src/ contains no feature code yet (only READMEs/.gitkeep)", () => { + const tsFiles = walkFiles("src", [".ts"]); + // Stage 1 must have zero feature .ts files — directories exist but are empty + expect(tsFiles).toEqual([]); + }); + + it("directory structure matches IMPLEMENTATION-CONTRACT.md §4", () => { + const requiredDirs = [ + "src/platform/idb", + "src/platform/cache", + "src/platform/sw", + "src/storage", + "src/data", + "src/sync/transport", + "src/sync/verifier", + "src/domain/emergency", + "src/domain/schedule", + "src/domain/map", + "src/domain/festival", + "src/domain/readiness", + "src/domain/clock", + "src/domain/favorites", + "src/ui/components", + "src/ui/views", + "src/ui/router", + "src/ui/render", + "src/app", + "src/emergency-baseline", + "public", + "content", + "pipeline", + "tests/unit", + "tests/integration", + "tests/device-matrix", + ]; + for (const d of requiredDirs) expect(existsSync(d), `missing dir ${d}`).toBe(true); + }); + + it("forbidden v1 scope is not present (§2 — mesh/accounts/auth)", () => { + const allFiles = [...walkFiles("src"), ...walkFiles("tests")].join("\n"); + expect(allFiles).not.toMatch(/mesh/i); + expect(allFiles).not.toMatch(/auth/i); + }); + + it("eslint boundaries config exists and lists B-1…B-7 elements", async () => { + // eslint check is via lint job; dynamic import of .js config has no types + // @ts-expect-error — no types for flat config + const cfg: unknown = await import("../../eslint.config.js"); + expect(cfg).toBeDefined(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..edfd5de --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "baseUrl": ".", + "paths": { + "@platform/*": ["src/platform/*"], + "@storage/*": ["src/storage/*"], + "@data/*": ["src/data/*"], + "@sync/*": ["src/sync/*"], + "@domain/*": ["src/domain/*"], + "@ui/*": ["src/ui/*"], + "@app/*": ["src/app/*"] + }, + "types": ["node"] + }, + "include": ["src/**/*", "tests/**/*", "scripts/**/*"], + "exclude": ["node_modules", "dist", "coverage"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..8cdd72f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: false, + include: ["tests/**/*.test.ts"], + environment: "node", + reporters: ["verbose"], + coverage: { + enabled: false, + }, + }, +});