Dates: reads + insight context on the new contract (mixed-row safe) #119

Closed
opened 2026-09-24 22:58:16 +00:00 by cameron · 1 comment
Owner

Parent

ImageApi #113 — SPEC: true-instant date contract (ADR-0001).

What to build

The server speaks the new date contract on every read and in every AI context. /photos, /memories, and the image metadata responses carry the new fields (true instant, camera offset, offset-source) beside the frozen, byte-identical legacy field. Day grouping (memories buckets, day-level queries) prefers the instant in the display zone; rows not yet drained fall back through their known per-source convention — so a converted row and an unconverted row mid-drain both land in the right day, and the long-standing double-shift of container/filesystem rows in /memories is fixed before the drain ever runs.

The insight context follows the same rule as the UI: the chat bootstrap and the photo summary format the camera wall clock (instant minus camera offset, or the display zone when the row has no offset), so the date the AI is told is the date the owner sees. The SMS, location, and calendar context windows are anchored on the true instant with no shift — a 9pm shot pulls 9pm messages and 9pm location regardless of the camera's zone.

Acceptance criteria

  • /photos, /memories, and metadata responses include the new fields for converted rows (null when unconverted); the legacy field is byte-identical for every row throughout.
  • Day grouping mid-drain: a seeded converted row and a seeded unconverted row each group into their correct display-zone day (handler-seam test with mixed fixtures).
  • An exiftool/container row and an fs_time row group into the same day as an EXIF row whose camera wall clock is the same instant (the /memories double-shift is gone pre-drain).
  • The insight chat bootstrap and the photo summary format the camera wall clock (instant − offset); a row with no offset formats in the display zone. The formatted string equals the wall clock the metadata endpoint reports (prompt/UI agreement).
  • The SMS/location/calendar windows query by the true instant with zero offset math; a photo with a +9h camera offset (e.g. Japan) anchored at local 21:00 pulls the correct evening's messages and location.
  • A video's insight context date matches its metadata/UI date (no 4–5 h offset, no midnight rollover).
  • Tests at the HTTP contract seam (existing handler-test pattern) for the response shape and mixed-row grouping; waterfall-seam tests for the formatting rule.

Blocked by

  • ImageApi #115 (container/fs/manual branches)
  • ImageApi #116 (filename class conventions + priority)
## Parent ImageApi #113 — SPEC: true-instant date contract (ADR-0001). ## What to build The server speaks the new date contract on every read and in every AI context. `/photos`, `/memories`, and the image metadata responses carry the new fields (true instant, camera offset, offset-source) beside the frozen, byte-identical legacy field. Day grouping (memories buckets, day-level queries) prefers the instant in the display zone; rows not yet drained fall back through their **known per-source convention** — so a converted row and an unconverted row mid-drain both land in the right day, and the long-standing double-shift of container/filesystem rows in `/memories` is fixed *before* the drain ever runs. The insight context follows the same rule as the UI: the chat bootstrap and the photo summary format the **camera wall clock** (instant minus camera offset, or the display zone when the row has no offset), so the date the AI is told is the date the owner sees. The SMS, location, and calendar context windows are anchored on the true instant with no shift — a 9pm shot pulls 9pm messages and 9pm location regardless of the camera's zone. ## Acceptance criteria - [ ] `/photos`, `/memories`, and metadata responses include the new fields for converted rows (null when unconverted); the legacy field is byte-identical for every row throughout. - [ ] Day grouping mid-drain: a seeded converted row and a seeded unconverted row each group into their correct display-zone day (handler-seam test with mixed fixtures). - [ ] An exiftool/container row and an fs_time row group into the same day as an EXIF row whose camera wall clock is the same instant (the `/memories` double-shift is gone pre-drain). - [ ] The insight chat bootstrap and the photo summary format the camera wall clock (instant − offset); a row with no offset formats in the display zone. The formatted string equals the wall clock the metadata endpoint reports (prompt/UI agreement). - [ ] The SMS/location/calendar windows query by the true instant with zero offset math; a photo with a +9h camera offset (e.g. Japan) anchored at local 21:00 pulls the correct evening's messages and location. - [ ] A video's insight context date matches its metadata/UI date (no 4–5 h offset, no midnight rollover). - [ ] Tests at the HTTP contract seam (existing handler-test pattern) for the response shape and mixed-row grouping; waterfall-seam tests for the formatting rule. ## Blocked by - ImageApi #115 (container/fs/manual branches) - ImageApi #116 (filename class conventions + priority)
cameron self-assigned this 2026-09-26 15:03:09 +00:00
Author
Owner

Resolved — PR #125 (branch feature/date-reads-insights, commit af87765).

All seven acceptance criteria met; 1,410 tests pass, cargo fmt --check + cargo clippy --all-targets clean. Two-axis review (spec + standards) done in-branch; spec's four test-gap notes and standards' duplicated-code + naming findings fixed before handoff.

What landed:

  • src/date_view.rs (new, pure — no DB/IO): row_true_instant (window anchor), row_display_wall (prompt/UI date), row_display_day (grouping key — always the owner's day), filename_true_instant. Converted rows read the triple directly; unconverted rows fall back to the per-source convention table in the module docs (exif/filename-wall/manual = wall in server or display zone; exiftool-video = one server-zone round-trip read back as UTC; exiftool-photo/ts/fs_time = legacy as-is or its render).
  • AC-1: /memories items, GpsPhotoSummary, and the photos/EXIF response paths carry date_utc / camera_offset_minutes / date_taken_tz_source — null while unconverted.
  • AC-2 / AC-3: /memories groups by render(true_instant, owner_display_zone) with exact MM-DD/%W/month matching in Rust (day_in_span + sqlite_w_week), over a full years-back SQL pre-filter (memories_span_windows). The container double-shift is gone — the mixed-fixture seam test (memories::api::memories_mixed_rows_group_into_owner_month) seeds converted + unconverted-exif + unconverted-container rows and asserts all three land in the owner's month with the correct per-source created.
  • AC-4: insight prompt date = row_display_wall (camera wall when the row carries an offset — a Japan photo tells the AI the Japan day, with a midnight rollover relative to the owner's zone asserted in japan_offset_row_ai_date_is_camera_wall_and_anchor_unshifted); metadata/UI and prompt now agree by construction (same function).
  • AC-5: the timestamp flowing to the SMS/location/calendar windows is row_true_instant — zero offset math (InsightGenerator::resolve_date_anchor shared by both agentic and non-agentic paths).
  • AC-6: unconverted_video_row_ai_date_matches_display_wall pins the container convention at the chat seam.
  • Reels: selector + pregen take an IANA display zone (REEL_PREGEN_DISPLAY_TZ) instead of a fixed offset; fixed_tz_offset deleted.
  • The legacy date_taken column is untouched by this PR (no migrations, no writes) — the #120 cutover flips the remaining consumers.

Deferred (P2 judgement calls, noted for #120/follow-up): bundling the (display_zone, server_zone) data clump into a DateContext type; date_from/date_to range filters and tool_store_fact::valid_from stay on legacy until the big-bang flip.

Note: merge is pending the Jenkins CI infra fix (Core Repos/ImageApi/pipeline/pr-master is failing for unrelated reasons); local gates are all green.

**Resolved — PR #125** (branch `feature/date-reads-insights`, commit `af87765`). All seven acceptance criteria met; 1,410 tests pass, `cargo fmt --check` + `cargo clippy --all-targets` clean. Two-axis review (spec + standards) done in-branch; spec's four test-gap notes and standards' duplicated-code + naming findings fixed before handoff. What landed: - **`src/date_view.rs`** (new, pure — no DB/IO): `row_true_instant` (window anchor), `row_display_wall` (prompt/UI date), `row_display_day` (grouping key — always the owner's day), `filename_true_instant`. Converted rows read the triple directly; unconverted rows fall back to the per-source convention table in the module docs (exif/filename-wall/manual = wall in server or display zone; exiftool-video = one server-zone round-trip read back as UTC; exiftool-photo/ts/fs_time = legacy as-is or its render). - **AC-1**: `/memories` items, `GpsPhotoSummary`, and the photos/EXIF response paths carry `date_utc` / `camera_offset_minutes` / `date_taken_tz_source` — null while unconverted. - **AC-2 / AC-3**: `/memories` groups by `render(true_instant, owner_display_zone)` with exact MM-DD/%W/month matching in Rust (`day_in_span` + `sqlite_w_week`), over a full years-back SQL pre-filter (`memories_span_windows`). The container double-shift is gone — the mixed-fixture seam test (`memories::api::memories_mixed_rows_group_into_owner_month`) seeds converted + unconverted-exif + unconverted-container rows and asserts all three land in the owner's month with the correct per-source `created`. - **AC-4**: insight prompt date = `row_display_wall` (camera wall when the row carries an offset — a Japan photo tells the AI the Japan day, with a midnight rollover relative to the owner's zone asserted in `japan_offset_row_ai_date_is_camera_wall_and_anchor_unshifted`); metadata/UI and prompt now agree by construction (same function). - **AC-5**: the timestamp flowing to the SMS/location/calendar windows is `row_true_instant` — zero offset math (`InsightGenerator::resolve_date_anchor` shared by both agentic and non-agentic paths). - **AC-6**: `unconverted_video_row_ai_date_matches_display_wall` pins the container convention at the chat seam. - **Reels**: selector + pregen take an IANA display zone (`REEL_PREGEN_DISPLAY_TZ`) instead of a fixed offset; `fixed_tz_offset` deleted. - The legacy `date_taken` column is untouched by this PR (no migrations, no writes) — the #120 cutover flips the remaining consumers. **Deferred (P2 judgement calls, noted for #120/follow-up):** bundling the `(display_zone, server_zone)` data clump into a `DateContext` type; `date_from`/`date_to` range filters and `tool_store_fact::valid_from` stay on legacy until the big-bang flip. Note: merge is pending the Jenkins CI infra fix (`Core Repos/ImageApi/pipeline/pr-master` is failing for unrelated reasons); local gates are all green.
cameron added the wayfinder:task label 2026-09-30 00:47:20 +00:00
Sign in to join this conversation.