DocumentCard with its span-carrying dates, and
CE-9 typed some of those into assertions. But a card date sitting in one document
and a hearing date in another are still just unordered strings: "5 May
2019" here, "1 January 2020" there, with nothing saying which came first or that
they belong to one timeline. This story turns those dates — across every document
in the matter — into first-class (:Event) nodes on the Neptune graph
of record, ordered by deterministic [:PRECEDES] edges. Now
get_chronology can return an ordered, span-carrying timeline the
Proof-mode surface renders, instead of a bag of dates a human has to sort by hand.
The redline (item 2) is emphatic: chronology is a first-class edge type,
not a card-date aggregation.
The whole story is src/shared/context/chronology_extractor.py, wired
into the registration consumer right after CE-9 normalizes a card. It reads the
matter's relational truth (card dates + event-kind assertions), builds
(:Event) nodes chained by [:PRECEDES], and upserts them
idempotently through the CE-8 tenancy helper — in the one order the RFD fixes
(relational is the source, the graph is the projection).
| Step | What it does | RFD |
|---|---|---|
| Extract | Every card date becomes an event (its kind is the description); every event_assertion row with a parseable date becomes one too. | §5.3 chronology extraction |
| Gate | An ExtractedEvent cannot be built without an event_date AND ≥1 span — undated / unspanned candidates are dropped at extraction, never projected. | §5.3 "an undated or unspanned event does not exist" |
| Order | Sort by (event_date, event_id) and chain consecutive events with [:PRECEDES {basis: date_order}] — the deterministic ordering, materialized as edges. | §5.3 deterministic PRECEDES |
| Project (graph follows) | Idempotent openCypher MERGEs through the CE-8 tenancy helper — one (:Event) per event, keyed by a stable event_id, plus the PRECEDES chain. | §5.3 write path (2) |
context.document_cards card dates and the
context.assertions event_assertion rows, both RLS-guarded.
So this story ships no migration: the substrate (the
Event label and PRECEDES edge) already exists in the CE-8
closed graph registry.
| Gate | How it is guaranteed (not just tested) |
|---|---|
| Ordered, span-carrying timeline on the fixture matter | Every (:Event) node carries the span ids of the card date /
assertion it came from (the reader hydrates them to byte-exact
SourceSpans). Ordering is not a stored opinion: events are sorted by
(event_date, event_id) and chained — n events yield
n−1 PRECEDES edges, the same order get_chronology re-derives.
The integration test seeds two documents in one matter and asserts the four
events come back in strict date order, each spanned. |
| Undated / unspanned events excluded by construction | Card dates are dated + spanned by their contract
(CardDate.date required, spans min_length=1),
so they cannot be malformed. The exclusion bites on the assertion source:
assertion_event_from_row returns None — no node ever
exists — when value_json has no parseable event_date or
the row carries no span. It is a property of the ExtractedEvent type,
not a filter someone can forget to apply. |
"Deterministic" means the same inputs always produce the same graph — so a re-projection MERGEs in place and never duplicates, and the reconciler can rebuild an identical timeline from relational truth. Two ingredients make it so:
event_id: a hash of the event's source
identity — ev1:sha256(document_id, "card_date", kind, date) for a
card date, or ev1:sha256("assertion", assertion_id) for an assertion.
The same source fact always maps to the same node key.sort by (event_date, event_id). The
date orders the timeline; the id is the tiebreaker so same-day events have one
stable order, not a coin flip.
card dates + event assertions (across the whole matter)
│ extract (drop undated/unspanned)
▼
[ 2019-05-05 execution_date ] ev1:…a
[ 2019-12-01 notice served ] ev1:…b sort by (date, id)
[ 2020-01-01 hearing_date ] ev1:…c ────────────────────►
[ 2020-06-01 deadline ] ev1:…d
│ chain consecutive
▼
(a)─[:PRECEDES {date_order}]→(b)─[:PRECEDES]→(c)─[:PRECEDES]→(d)
│ upsert via ScopedGraph (CE-8 tenancy helper)
▼
get_chronology → ordered, span-carrying ChronologyEvent list
_project_chronology runs in the registration consumer after a card is
ingested and normalized, in its own isolated read transaction — a fault is logged,
never raised (the document is already registered; the CE-8 reconciler over
load_chronology_projection_source re-drives any missed matter). It
rebuilds the whole matter's chronology because a new date shifts the timeline
order; that per-document rebuild is bounded per invocation (two matter-scoped reads
+ event-count-bounded upserts) and is superseded by the §9 debounced rebuild
consumer (CE-13) that reacts to the coalesced matter_assertions.updated
— the same in-consumer projection pattern CE-9 uses today.
(:Event) nodes ordered by
deterministic [:PRECEDES] edges — an undated or unspanned event never
exists — so get_chronology can return a timeline a lawyer can walk with
the proof for every step.