feat(dates): filename date class — per-pattern conventions + filename-over-EXIF priority (#116)

The filename date class is now first-class (ADR-0001):

- memories::extract_date_from_filename returns FilenameDate:
  Wall(NaiveDateTime) for the screenshot/dash/compact patterns,
  Utc(DateTime<Utc>) for the unix-timestamp patterns. No patterns
  added or removed; every existing pattern carries its convention.
- Waterfall inversion in both resolve_date_taken and
  resolve_dates_batch: an in-class name is the original date of
  record and resolves BEFORE EXIF / container dates are consulted
  (shared filename_class_resolved step; in-class files skip
  exiftool entirely). Wall patterns run the offset cascade over the
  kamadak prior's evidence (empty evidence -> server-zone
  assumption, so a class file always resolves); timestamp patterns
  are true instants (tz_source utc_direct, no offset).
- Legacy date_taken stays byte-identical: wall-as-UTC for wall
  patterns, epoch for timestamp patterns, via
  FilenameDate::legacy_datetime / legacy_timestamp at the seven
  consumer call sites.
- Out-of-class files keep the global EXIF-wins waterfall unchanged.
- CLAUDE.md waterfall section rewritten (step 0 = filename class;
  the EXIF-beats-filename note becomes out-of-class-only).

Tests: five new waterfall-seam tests (wall no-evidence ->
assumed_server; wall outranks EXIF with offset-tag evidence;
wall GPS evidence; timestamp utc_direct; in-class video name
resolves without exiftool) plus enum-shape assertions in every
extractor test. Code-reviewed on both axes (standards: no
violations, 3 of 4 P2 notes applied; spec: all acceptance
criteria verified, no scope creep).

716 integration + 639 lib tests pass; fmt + clippy clean.
This commit is contained in:
Cameron Cordes committed 2026-09-25 16:54:46 -04:00
1 parent 6f73a31e08
commit a97bf53603
7 files changed
+358 -143

No files matched your search

+1 -1
View File
@@ -2037,7 +2037,7 @@ fn resolve_date_taken_for_context(
return from_exif;
}
crate::memories::extract_date_from_filename(file_path)
.map(|dt| dt.format("%Y-%m-%d").to_string())
.map(|fd| fd.legacy_datetime().format("%Y-%m-%d").to_string())
}
/// Validate a stored→effective backend transition for a chat continuation.
+3 -3
View File
@@ -23,7 +23,7 @@ use crate::database::{
LocationHistoryDao, SearchHistoryDao,
};
use crate::libraries::Library;
use crate::memories::extract_date_from_filename;
use crate::memories::{FilenameDate, extract_date_from_filename};
use crate::otel::global_tracer;
use crate::tags::TagDao;
use crate::utils::{earliest_fs_time, normalize_path, retry_with_backoff, retry_with_backoff_if};
@@ -1062,7 +1062,7 @@ impl InsightGenerator {
log::warn!("No date_taken in EXIF for {}, trying filename", file_path);
extract_date_from_filename(&file_path)
.map(|dt| dt.timestamp())
.map(FilenameDate::legacy_timestamp)
.or_else(|| {
let full_path = self.resolve_full_path(&file_path)?;
File::open(&full_path)
@@ -4153,7 +4153,7 @@ Return ONLY the summary, nothing else."#,
} else {
log::warn!("No date_taken in EXIF for {}, trying filename", file_path);
extract_date_from_filename(&file_path)
.map(|dt| dt.timestamp())
.map(FilenameDate::legacy_timestamp)
.or_else(|| {
let full_path = self.resolve_full_path(&file_path)?;
File::open(&full_path)