637 lines
42 KiB
Markdown
637 lines
42 KiB
Markdown
# 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.
|
||
- **Persistence correction (2026-08-31):** P1 requires the staging journal to be in each target slot database, alongside `files` and `assets`. `lumen-system` contains active/readback metadata only; it is not a source of per-file staging progress. This preserves one-database atomicity for file/asset + progress without a cross-database transaction.
|
||
- **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.
|
||
- **Persistence correction (2026-08-31):** The F-1 progress record is the authoritative `staging` journal inside the target slot database. It is committed in the same short transaction as its corresponding file or asset. The system database retains only the active pointer, verification/readback metadata, and boot metadata; no cross-database transaction is used for staging.
|
||
- **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 slot-local 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/<hash>` (immutable),
|
||
`/latest.json` (short-lived), `/editions/<ed>/packages/<v>/**` (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
|
||
```
|