Implement the group constraint propagation engine #19
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?
Context
This is the central idea of the product. Most photos in a family collection carry no
date of their own, but they sit between photos that do. If photo 12 is from June 1987
and photo 20 is from September 1987, photos 13-19 are bounded by those dates. The
schema is already built for it:
EvidenceSource.constraint,EvidenceReliability.derived,DateEvidence.derived_from, andJobType.constraint_rebuildall exist and are unused.Scope
The engine that derives date constraints from album ordering and known anchors, and
writes them as derived evidence.
Implementation notes
format rules (a print format that only existed in a given period).
a range. Write it as
DateEvidencewithsource = constraint,reliability = derived,and
derived_frompointing at the evidence it was inferred from — the provenance chainis what makes the explanation UI possible, so it must be populated correctly.
approval always wins. Derived constraints are a suggestion, never an auto-approval.
constraint_rebuildjob when an ordering changes, a decision is made,or an anchor moves. Recomputation must be incremental where possible — rebuilding the
whole collection on every single decision will not scale.
superseded like any other evidence. History stays intact.
inconsistent (a later photo dated earlier than an earlier one), surface the conflict
for human resolution rather than silently picking one. Guard against propagating a
contradiction outward through the chain.
input to its own recomputation.
Done when
derived_fromReferences
backend/app/models/models.py(EvidenceSource.constraint,EvidenceReliability.derived,derived_from,JobType.constraint_rebuild)docs/circa-spec.md(constraint propagation)Depends on: album ordering, #2 (worker runtime).
Amended by the audit of 2026-07-28.
Three prerequisites this issue depends on that are not currently represented:
The schema cannot express what this engine produces (#108).
DateEvidence.derived_fromis asingle FK, but a constraint derives a range from two anchors ("between photo 12 and photo
20"). #20's explanation UI depends on that chain being traversable. Fix before the evidence table
holds tens of thousands of rows — on SQLite this is a table-rebuild migration.
Album context has no ingestion path (#105).
filename_parser.pyextractsalbum_slug,sequence, andis_back, and ingest discards all three.Photo.album_idandsequence_in_albumare never populated, and nothing in the codebase creates anAlbum. Thisengine would start from empty data.
The evidence it will trust is currently poisoned (#74). Scanner EXIF dates are recorded as
hard/dayprecision, andhardis exactly what this engine amplifies and propagates toneighbours.
Also relevant:
PhotoGroup(spec §8) does not exist. This issue is titled "group constraintpropagation" but its body only uses albums and anchors — #109 creates the missing entity.
Context from #144, which deliberately left this alone.
#144 wired ingest up to enqueue OCR and stopped there.
docs/circa-spec.md§8.3 step 9 also asks ingest to enqueue constraint propagation — this issue — and that half is still unwired:JobType.constraint_rebuildremains unused, as the body says. Recording it so the gap is not rediscovered as a surprise rather than found in the spec.The "depends on album ordering" line is now partly satisfied, and the distinction matters.
This issue's whole premise is that photo 12 is June 1987, photo 20 is September 1987, and 13–19 are bounded by them — which requires knowing that those photographs are in the same album, in that order. When this was written none of that existed: nothing in the codebase had ever created an
Albumrow, andphoto.album_idwas NULL in every deployment.Since then:
sequence_in_albumfrom the filename.Photo.album_sort— a generated column carrying a(album_id, sequence)keyset, with the NULL-sentinel handling already worked out.So implicit ordering — album membership plus a filename-derived sequence — exists and is populated. That is enough to build a first constraint pass against.
#18 is still open, and it is the one that adds explicit ordering — a reviewer reordering an album by hand when the filenames are wrong or absent. So the dependency is satisfied for the common case and not for the general one. Worth deciding deliberately whether this waits for #18 or is built against the filename-derived sequence and refined later; both are defensible, and building first would surface what #18 actually needs to expose.
Two things to reuse rather than reinvent:
app/services/jobs.py::enqueue_photo_jobis the single enqueue — attempt numbering, idempotency key, in-flight fast path, race recovery — called by both the rerun route and ingest. A third copy is what #105 paid for. Note its collision recovery is a savepoint rather thandb.rollback(), so a queue collision cannot discard an in-flight caller's work.One caution the body already implies but is worth stating against the new ingest:
sequence_in_albumis parsed from a filename and is only as trustworthy as the naming. A gap in the sequence may be a missing page, or may be a scan whose name did not parse. Deriving a date range across a gap assumes the former, so this engine should not treat sequence adjacency as physical adjacency without saying so.