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

189 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.