Extraction Run Lineage
Where a run sits among the runs around it: its own reference, its parent, its root, what it supersedes, and which pass it is.
Parent and supersession are two different axes and stay separate fields. A parent is the run this one continues from — a later pass reading the entities its parent resolved. A superseded run is one this one replaces — a re-extraction after the prompt changed. A run can have one of each, both, or neither.
The root reference is denormalized on purpose. It is set once when the lineage is minted: a run with no parent is its own root, and a run with a parent takes its parent's root. That makes "everything in this lineage" a single indexed read on one property, instead of walking the parent chain a hop at a time. OpenLineage's ParentRunFacet does the same thing — it carries an optional root alongside the immediate parent, so consumers do not have to walk — and deep pass-and-retry chains are exactly where walking hurts.
root and childOf are the only public way to mint one. The constructor is private, and copy() follows its visibility too (@ConsistentCopyVisibility on this class), so neither can hand a caller an independently-set root. root sets rootRunRef to the run's own ref, since it takes no parent at all; childOf derives it from the actual parent, the full ExtractionRunLineage it is handed, so the parent's own record supplies the root. Either way, a root that disagrees with the parent chain has no public constructor parameter to arrive through. A future store slice reconstructing a lineage from stored fields has to go through childOf with the parent's own lineage in hand, the same way; re-assembling rootRunRef and parentRunRef from separate columns is the shortcut this closes off.
This closes the public API, and nothing wider. Kotlin reflection can still call the private constructor directly and hand it a root that contradicts the parent it names — ExtractionRunLineageTest demonstrates the call. Jackson's Kotlin module resolves a data class's primary constructor the same reflective way, so a deserializer reading this type from JSON would reach the same gap. Nothing serializes an ExtractionRunLineage today; this is a residual for whoever builds that wiring, unrelated to anything that exists yet.
What is checked here, and what is not. The constructor rejects self-reference on both axes. It cannot see a cycle of length two or more, because a value type holds one run and cycle detection needs the other runs. The store that walks these chains is where bounded, cycle-safe traversal lives.
EXPERIMENTAL. The shape may still change while extraction runs (DICE #67) land.
Properties
The run this one continues from, or null
The oldest run in this lineage, which is runRef itself when there is no parent
This run
The run this one replaces, or null