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

10 KiB
Raw Permalink Blame History

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.