Two Phase-2 followups in one commit since they're coupled at the
write path:
* Agent populates valid_from from the source photo's date_taken
when calling store_fact. Loose semantics — date_taken is *evidence
at that date*, not strictly when the fact started being true — but
gives the curator a calendar anchor and pairs with supersession to
close intervals cleanly. valid_until stays NULL (a single photo
can't tell us when something stopped). Honours the existing
upsert_fact dedup (corroborated facts keep their first-recorded
valid_from).
* Supersession: new column entity_facts.superseded_by INTEGER
(migration 2026-05-10-000200), new status value 'superseded',
new DAO method supersede_fact, new HTTP endpoint
POST /knowledge/facts/{id}/supersede.
Marking an old fact as replaced by a new one atomically: flips
status to 'superseded', sets superseded_by, and stamps
valid_until from the new fact's valid_from (when not already
set). delete_fact clears dangling supersession pointers in the
same transaction so the column never points at a missing row —
no FK because SQLite can't ALTER ADD with REFERENCES, but the
DAO maintains the invariant.
Pairs with conflict detection from the previous slice: once the
old fact's valid_until is closed, its interval no longer overlaps
the new fact's, so they stop flagging — the supersede action
resolves the conflict.
Two tests pin the contract: supersede stamps valid_until from
new.valid_from while respecting an existing valid_until, and
deleting the supersedeR clears the dangling pointer while leaving
the old fact's 'superseded' status in place for history.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
32 lines
1.5 KiB
SQL
32 lines
1.5 KiB
SQL
-- Add a supersession pointer to entity_facts.
|
|
--
|
|
-- Status alone is a one-way trapdoor: 'rejected' loses the link
|
|
-- between the rejected fact and the one that replaced it. For
|
|
-- evolving facts (Cameron's relationship, employer, address) the
|
|
-- curator wants to *replace* a stale fact with a new one and keep
|
|
-- the history readable: "from 2018 until 2022 this was true, then
|
|
-- it became this other thing".
|
|
--
|
|
-- A nullable INTEGER column pointing at another entity_facts.id —
|
|
-- no FK constraint because SQLite can't ALTER ADD COLUMN with REFs;
|
|
-- the DAO's delete_fact clears dangling pointers in the same
|
|
-- transaction as the parent delete to keep the column honest.
|
|
--
|
|
-- A status of 'superseded' on the old fact (alongside the existing
|
|
-- active / reviewed / rejected) signals "replaced by a newer
|
|
-- claim". Read paths already filter 'rejected' out of the active
|
|
-- view; the curation UI will treat 'superseded' the same way for
|
|
-- conflict detection so they don't keep flagging.
|
|
--
|
|
-- Pairs with the valid-time columns from 2026-05-10-000100: the
|
|
-- supersede action auto-stamps the old fact's `valid_until` from
|
|
-- the new fact's `valid_from`, closing the interval cleanly.
|
|
|
|
ALTER TABLE entity_facts ADD COLUMN superseded_by INTEGER;
|
|
|
|
-- Helpful index for "show me what superseded this fact" walks
|
|
-- (rare today; cheap to add now while the table is small).
|
|
CREATE INDEX idx_entity_facts_superseded_by
|
|
ON entity_facts(superseded_by)
|
|
WHERE superseded_by IS NOT NULL;
|