supersede_evidence never sets superseded_by #80
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: MEDIUM
The bug
POST /api/evidence/{id}/supersede(backend/app/api/routes/photos.py:333-355) setsis_active = Falseand stops:It never sets
superseded_by. That column is written nowhere in the codebase — the evidencechain the schema was explicitly designed around is never linked.
EvidenceRepository.supersede, which does set it correctly, is dead code with no call sites.The route also emits no projection update and no audit context, and performs no
review_versioncheck.Why it matters
DateEvidence.superseded_byexists so that retiring evidence preserves why and by what. Theconstraint explanation UI (#20) and the provenance chain depend on this being populated. Right
now superseding silently breaks the chain, and there is no rebuild path to detect it.
Fix
EvidenceRepository.supersedesosuperseded_byis set.review_versionlike every sibling write.Collection scoping for this route is covered separately in the security milestone.
Done when
superseded_byis populated on every supersedeReferences
backend/app/api/routes/photos.py:333-355backend/app/repositories/evidence.py(supersede, unused)backend/app/models/models.py(DateEvidence.superseded_by)Done in
a239b8e. All four "done when" items covered.Supersede now goes through
EvidenceRepository.supersede— the method that was written with the schema, is correct, and had no call sites at all. The route setis_active = Falseby hand, sosuperseded_bywas written nowhere in the codebase and the chain was empty everywhere.One design call worth your eye: the successor is optional. The issue says "
superseded_byis populated on every supersede", which only holds if every supersede has a replacement. I don't think it does. Retiring and replacing are different acts — a reviewer who can see an OCR reading is nonsense has nothing to put in its place, and requiring one would put a fiction in the chain.NULLmeans "retired, nothing replaced it", which is a fact worth recording rather than a broken link. Push back if you'd rather force a successor.The shape that actually produces chains is the other half:
POST /photos/{id}/evidence/manualnow acceptssupersedes, so "that reading was wrong, here is the correct one" is a single transaction. Splitting it into two requests leaves a window where the photo has neither piece of evidence or both. That's also the shape the worker will use when a rerun produces better evidence than the run before it.Guards, all tested: a successor on another photo (400 — linking across photos corrupts the chain rather than recording it), a self-reference (400), a double-supersede (409), and a bad
supersedesid leaving no orphan replacement behind.Audit context carries
old/newsummaries and the successor id.On "update the photo projection if the superseded evidence affected it": there is nothing to do there today. The
current_effective_date_*fields are projected from decisions, not evidence — evidence feeds a reviewer's judgement, not the projection directly. What supersede does now is bumpreview_version(added in #70), which is the mechanism that actually tells a reviewer mid-decision that the ground moved. If constraint propagation (#19) later derives projections from evidence, this becomes real and should be revisited.Note that #79 lists this route as one of the two places that bypass the projection path entirely; that half is addressed there, where the fold is defined in one place.
The conflict check and collection scoping landed in #70, as you noted they would.