- 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
10 KiB
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.mjsexecuted on this Linux dev box using Node v22 full-ICUIntl(ECMA-402 — the same spec browsers implement), plus spec/policy analysis for the parts no Linux box can observe. Results labelled CONFIRMED / INFERRED / UNVERIFIED perexperiments/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
Dateheaders, 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:00Zrenders07/15/2026, 13:00:00inAmerica/Chicago(CDT, UTC-5). PASS.EXP-1 T1b— rendering is keyed to the explicittimeZoneoption, not the host zone. Explicit-zoneIntlformatting is required by ECMA-402 and is what the architecture relies on. PASS.- Conclusion: UTC epoch storage + explicit-zone
Intlrendering 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
dayKeyeliminates an entire class of off-by-one and TZ bugs. Clients group/filter by the opaque key. Pipeline gateT3in SPIKE-04 validatesdayKeyagainst 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) —T4aPASS. - Fixed
+05:45Asia/Kathmandu—T4bPASS. - 30-minute DST
Australia/Lord_Howe(+10:30 std / +11:00 DST) both in January (DST) and July (std) —T4c/T4dPASS. - Significance: festivals in unusual zones or with 30-minute DST are not
special-cased;
Intlhandles 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
Dateheaders 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. T6ais the intentional subtlety: a Julynowagainst 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
outsideWindowonly ifnow ≥ festivalStart − WINDOWor 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-nextin start-time order (T7b/T7c/T7d) PASS; emptyup-nextwhen 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:
Intledge behaviour on iOS 16.4 low-bound and latest requires physical iPhone testing (see §5). - HTTP
Datefreshness in the field: header skew capture depends on the CDN'sDateaccuracy 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
dayKeystands as specified. - F-2 (confirmed): ClockService as
device + persisted skewwith monotonic drift guard — cheap, offline-safe, no background work. - F-3 (change): sanity-window warning suppressed until
now ≥ festivalStart − WINDOWor a skew has been captured. Prevents noisy false warnings during early preparation. Added to ADR-009. - F-4 (clarification):
Intl.DateTimeFormatwith explicittimeZoneis the only time-rendering path; noDate.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):
- Render the July event at
2026-07-15T18:00ZinAmerica/Chicago→ expect01:00 PM CDT(spot-check JSC parity with EXP-1 T1a). - Day grouping: events at
T2a/T2binstants appear on the correctdayKeydays in the UI. - DST edge: the
T3a–T3dwall-clock sequence around spring-forward and fall-back for the festival zone; duration of a span crossing the transition equals epoch difference (T3e). - 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.
- Sanity window: device set to a month before the festival → no warning; device set to +60 days past end → warning shown (F-3 rule).
- 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,
dayKeyvalidated 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.