Part of #364 (D107 temporal clocks). Gates WP-T.1. Lands as a design amendment + decision, reviewed by Codex, before any WP-T.1 code.
Problem (design §12.1; raised by Codex review round 5 on #362)
D107 seeds a fact's verdict window once from its seed claim — "the claim whose processing created the row" — and records it in seed_claim_id and the add adjudication's triggering_claim_id. For observations this is deterministic: D90 drains staging per entity in the total order (asserted_at NULLS LAST, claim_id, statement), so the seed is the first claim in that order. For relations it is not: D88 lets concurrent claim jobs run upsert relations for this claim directly (FactCatalog.upsert_relation, fact_catalog.py:39-105), and D88's decision text says "relation evidence attach is commutative under concurrency". A per-key advisory lock serialises whichever worker arrives first, not the first claim in the specified total order. Recording seed_claim_id atomically therefore records a race: two schedules can pick different seed windows (and, for occurrences, different occurrence identities), which breaks D7 rebuild determinism and D88's own order-independence goal.
The same path is why D107 §4.2 requires the relation write to become staged: today upsert_relation finds a live triple and attaches evidence before the relation ladder runs (supersession.py:169-178 treats the same redirected object as an exact no-op), which would collapse two same-triple occurrences before any verdict.
What the spike must decide
- A durable, claim-idempotent relation staging relation (the analogue of
normalize_observation_staging), with the fields the verdict needs (claim id, triple, canonical D41 window, kind, said-on).
- A post-barrier per-block drain in the chosen total order — per
(subject, predicate, object) block, or per subject entity as D90 does — including cross-version single-flight behaviour so two versions of one document cannot interleave (mirror D90 §5.5).
- The atomic
new transaction: fact row + add adjudication (triggering_claim_id populated) + evidence link; the atomic evidence attach; idempotency on (triggering_claim_id, adjudicator generation) so retries and concurrent normalisers replay the recorded verdict.
- Interaction with the D88 barrier/fan-out (
e3_claim_level_normalize_fanout_design.md §§5.3–5.5) and with the D90 obs flush: does relation drain run in the same unit, after it, or as its own stage (adjudicate_supersession today runs after normalize)?
- Amend D88's "commutative relation upsert" contract and
e3_claim_level_normalize_fanout_design.md; roll the normaliser/adjudicator generations and the LoCoMo protocol.
Acceptance
A same-triple occurrence claim is held unattached until its verdict; two normalisers racing on one block produce the same seed and the same rows in any schedule; a retried claim replays its verdict; the D90 reversed-order acceptance case has a relation twin.
🤖 Generated with Claude Code
Part of #364 (D107 temporal clocks). Gates WP-T.1. Lands as a design amendment + decision, reviewed by Codex, before any WP-T.1 code.
Problem (design §12.1; raised by Codex review round 5 on #362)
D107 seeds a fact's verdict window once from its seed claim — "the claim whose processing created the row" — and records it in
seed_claim_idand theaddadjudication'striggering_claim_id. For observations this is deterministic: D90 drains staging per entity in the total order(asserted_at NULLS LAST, claim_id, statement), so the seed is the first claim in that order. For relations it is not: D88 lets concurrent claim jobs runupsert relations for this claimdirectly (FactCatalog.upsert_relation,fact_catalog.py:39-105), and D88's decision text says "relation evidence attach is commutative under concurrency". A per-key advisory lock serialises whichever worker arrives first, not the first claim in the specified total order. Recordingseed_claim_idatomically therefore records a race: two schedules can pick different seed windows (and, for occurrences, different occurrence identities), which breaks D7 rebuild determinism and D88's own order-independence goal.The same path is why D107 §4.2 requires the relation write to become staged: today
upsert_relationfinds a live triple and attaches evidence before the relation ladder runs (supersession.py:169-178treats the same redirected object as an exact no-op), which would collapse two same-triple occurrences before any verdict.What the spike must decide
normalize_observation_staging), with the fields the verdict needs (claim id, triple, canonical D41 window, kind, said-on).(subject, predicate, object)block, or per subject entity as D90 does — including cross-version single-flight behaviour so two versions of one document cannot interleave (mirror D90 §5.5).newtransaction: fact row +addadjudication (triggering_claim_idpopulated) + evidence link; the atomicevidenceattach; idempotency on(triggering_claim_id, adjudicator generation)so retries and concurrent normalisers replay the recorded verdict.e3_claim_level_normalize_fanout_design.md§§5.3–5.5) and with the D90 obs flush: does relation drain run in the same unit, after it, or as its own stage (adjudicate_supersessiontoday runs after normalize)?e3_claim_level_normalize_fanout_design.md; roll the normaliser/adjudicator generations and the LoCoMo protocol.Acceptance
A same-triple occurrence claim is held unattached until its verdict; two normalisers racing on one block produce the same seed and the same rows in any schedule; a retried claim replays its verdict; the D90 reversed-order acceptance case has a relation twin.
🤖 Generated with Claude Code