docs(dates): record the true-instant date contract

- CONTEXT.md: date/time vocabulary (true instant, wall clock, camera
  offset, display zone, filename date class, ...)
- ADR-0001: dates are true instants; wall clocks are a display concern
- un-ignore docs/adr/ so decisions land in git
This commit is contained in:
Cameron Cordes committed 2026-09-24 18:23:46 -04:00
1 parent bf64d046d8
commit ee773a34ef
3 files changed
+126

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
# ADR-0001: Dates are true instants; wall clocks are a display concern
**Status**: accepted (2026-09-24)
## Context
`image_exif.date_taken` (Unix i64) accumulated four conventions in one column, indistinguishable
without joining on `date_taken_source`: EXIF rows were camera wall-clocks reinterpreted as UTC,
exiftool rows were naive dates interpreted in the server's local zone, filename rows were
deliberate wall-as-UTC, and filesystem-time rows were true instants. SMS-API and the Apollo
location track were already true UTC. Every consumer had to re-derive the missing shift
(Apollo's `_naive_to_real_utc`, the frontend `naiveAsUtcToRealUtc`, FileViewer's bare
device-local formatting, `/memories`'s per-client offset) and they disagreed: the same photo
showed different clocks in the two apps, exiftool-sourced videos (whose dates only come from
exiftool) were double-shifted, and the insight-context prompt formatted a value 4–5 hours
wrong for video rows, rolling across midnight near day edges.
## Decision
The date a media file was captured is stored as a **true instant** (Unix seconds of the
actual moment). The wall clock the camera showed is a **display value**, recovered from the
instant plus a per-row **camera offset** resolved at ingest by a cascade (explicit EXIF
offset tag → GPS position → server zone, the last recorded as an assumption). Day-level
grouping uses the owner's **display zone** (an IANA zone name). Manual edits enter as wall
clocks in the display zone and are stored as instants.
Consequences of the decision:
- Matching photo↔SMS, photo↔location, and photo↔calendar becomes instant-to-instant with no
per-consumer shift; the re-anchoring helpers in the consumers are deleted, not migrated.
- A photo's displayed time can differ from the owner's local time (a photo taken in Japan
shows the Japan wall clock); grouping still uses the owner's day. This is deliberate.
- The legacy `date_taken` column is frozen during migration (a new `date_utc` column is
populated by a per-tick backfill drain), so rollback is a client-side flip and the old
contract stays byte-identical until cutover. The legacy column is retained after cutover
for historical context.
## Considered options
- **Standardize on wall-clock-as-UTC** (fix only the exiftool branch): cheapest, but every
consumer would still carry a display-zone re-anchor as a proxy for the camera's zone, and
cross-zone photo↔SMS matching would stay approximate. Rejected.
- **Two parallel fields as a permanent contract** (wall-as-UTC `date_taken` + `date_utc`):
honest, but leaves the ambiguity in the interface forever. The dual-field shape is used
*temporarily* (migration window) only.
- **Server-side conversion per request** (keep the column as-is, convert on read): pushes
the mixed-convention join onto every query and every consumer; rejected.
## Non-obvious rules worth remembering
- **Filename date class outranks EXIF** — for files whose filename carries the capture date
(screenshots, timestamp exports), the filename is the original date of record, inverting
the global EXIF-wins rule. The class is a documented exception, not a bug.
- **Offset resolution is per-row and audited** (`date_taken_tz_source`), because a library
contains photos from many zones; a single global offset would be wrong for every trip.
- **The display zone is a zone name, not minutes** — a hardcoded offset silently breaks at
DST. (The legacy `tz_offset_minutes` default of -480 on a New York server is a surviving
symptom of this.)
- **Re-dating across a day boundary invalidates derived day data** (daily summaries, reel
pre-gen), which regenerates on its normal schedule; the backfill drain detects the day
change per row.