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
This commit is contained in:
commit
c0bfd413ff
100 changed files with 9863 additions and 0 deletions
189
SPIKE-08-TIME-MODEL.md
Normal file
189
SPIKE-08-TIME-MODEL.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue