Scanner EXIF dates are recorded as hard, day-precision evidence #74
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: HIGH - this is writing wrong data today
Found independently by two audit agents and re-verified directly.
The bug
backend/app/services/exif_extractor.py:58tries date tags in order:For a scanned print,
DateTimeDigitizedandDateTimeare the scan date, not the capturedate.
backend/app/services/ingest.py:117-130then writes whatever it found as:and promotes the photo to
needs_review. There is no plausibility check against the collection'sdate range.
So a scanner that stamps EXIF mints confident, day-precise, maximum-reliability evidence
saying a 1975 print is from 2025.
Two things that make it harder to undo
ExifResultnever records which tag matched, andraw_valueholds only the date string. After ingest you cannot distinguish a genuine capture date from a
scanner timestamp — so this is not cleanly repairable without re-reading every original file.
exif_extractor.py:71-76builds thetag id with
next((v for k, v in ExifTags.TAGS.items() if ExifTags.TAGS[k] == "Make"), None),which yields the name string
"Make", then looks it up in an integer-keyed dict. AlwaysNone. It is marked "future use" and intended to feed format-rule evidence — it would havefailed silently whenever that was built. It also performs two pointless O(n) scans per photo.
Why this is urgent
hardevidence is exactly what the constraint propagation engine (#19) will trust and spread toneighbouring photos. This poisons the mechanism the product is built on. Fix before ingesting
more.
It also contradicts the spec's own trust table (
docs/circa-spec.md§10.1), which rates EXIFfrom a digital camera as High, and reserves
hardfor format rules.Scope
The collection is mostly scanned prints, but will include some genuinely digital photos from
more recent years — so EXIF evidence must stay, and be made scan-aware rather than disabled.
DateTimeOriginalathighreliability. Treat bareDateTime/DateTimeDigitizedasprobable scan dates: record them at
lowwith an explanatory note, and do not promote thephoto on their basis alone.
of ingest time, is almost certainly a scan date — record at
lowwith a note rather thanpromoting.
ExifTags.TAGScorrectly, or the numeric ids directly);make/model is a useful format-rule signal later.
CIRCA_EXIF_TRUSTsetting (full/scan_aware/off) so behaviour can be tunedper collection without a code change.
Done when
DateTimeOriginaland the digitization tags are treated differentlyoutside the collection range
References
backend/app/services/exif_extractor.py:39-78backend/app/services/ingest.py:114-130docs/circa-spec.md§10.1Blocks: #19 (constraint propagation) should not run on poisoned evidence.
Done in
be78124. All six "done when" items covered, 36 tests inbackend/tests/test_exif_trust.py.The trust model.
DateTimeOriginal→highand promotes. Digitisation tags →low, no promotion. ADateTimeOriginalthat fails a plausibility check — in the future, before 1826, or within a year of now — is alsolow, because some scanners write that tag too and a "capture" date from last month on a box of prints is a scan.highrather thanhardper §10.1, which reserveshardfor format rules.One decision beyond what the issue asked for, which I'd like you to sanity-check. A distrusted date is recorded as an upper bound —
date_highset,date_lowleft open — rather than as a low-reliability point estimate. The issue said "record them atlowwith an explanatory note", and I did that too, but recordingdate_low == date_high == 2025-03-01still asserts the photograph was taken on the day it was digitised. That's the false claim this issue exists to stop; lowering the reliability just makes it quieter. "This photograph was not taken after the scan" is true, and it's genuinely useful to #19. If you'd rather have the point estimate, it's a one-line change in_upper_bound.Provenance is what makes this repairable.
date_evidence.source_detail(migration006) stores the tag name. Without it a wrongly-trusted row is indistinguishable from a correct one and the only repair is re-reading every original file; with it, reassessment is a database operation.python -m app.cli.reassess_exif [--dry-run]does that, sharing the exactassess()function ingest uses so the two cannot drift, and corrects by superseding rather than editing — the archive should record that it once believed something wrong, which is the whole point of having a ledger. Rows predatingsource_detailare reported and skipped rather than guessed at. It's idempotent; a second run corrects nothing.Given there's no data yet, the command is insurance rather than a migration you need to run.
The make/model bug was worse than dead code.
next((v for k, v in ExifTags.TAGS.items() if ExifTags.TAGS[k] == "Make"), None)yields the name"Make", which was then used as a key in an integer-keyed dict — so it was alwaysNone, and it did two pointless O(n) scans per photo to get there. Marked "future use", it would have failed silently the day format-rule evidence was built on it. Now reads tags 271/272 directly and strips the null padding EXIF strings routinely carry.CIRCA_EXIF_TRUSTis implemented as suggested (scan_awaredefault /full/off), withCIRCA_EXIF_EARLIEST_PLAUSIBLE_YEARandCIRCA_EXIF_SCAN_WINDOW_DAYSfor the plausibility gate.This unblocks #19 — constraint propagation will no longer be fed poisoned
hardevidence.