Extraction Run Row Mapper
Translates extraction runs to and from the property maps the Neo4j graph store reads and writes.
Neo4j properties are scalars and flat arrays, and a run header carries lists of value objects, so the structured parts — source revisions, the requested model configuration, the failure list — are serialized to JSON strings. JSON also survives the pipes, tabs, newlines and quotes that turn up in strings coming out of LLM extraction, which a delimiter-joined encoding does not.
Invocation records are not here. They are child rows with their own key and their own mapper, ExtractionInvocationRowMapper, and a header write never touches one. That is what makes a save unable to delete a recorded attempt, and it is why the store gets the contract's invocation-preservation rule for free rather than having to implement it.
Instants are written three ways. The ISO-8601 string is what round-trips and what a person reading a node wants; Instant.parse inverts Instant.toString exactly. The epoch second and the nanosecond let the database sort and range-filter at full precision. Epoch milliseconds would truncate — two runs started 500 microseconds apart would compare equal, leaving "newest first" arbitrary between them, and a since bound falling inside a millisecond would sweep in runs started just before it. Sorting on the ISO string has its own failure: Instant.toString writes no fraction on a whole second and 'Z' outranks '.', so 12:00:00Z sorts after 12:00:00.500Z.
Reads are strict. A property this mapper wrote must be there when it is read again. A node missing one is corrupt, so the accessor throws and the store's surrounding guard logs the row and skips it. Optional fields are the exception, and their absence means the run declared none: a SET of null in Cypher leaves no property behind, so "no profile" and "no experiment label" are stored as the absence of a property rather than as a sentinel.
The terminal fingerprint is never derived here. It is the string ExtractionRunTransition.fingerprint computed, stored verbatim on its own node and compared verbatim. Re-deriving it from a stored run would make a correct retry that happened after another attempt was recorded look like an incompatible rewrite.
Functions
Rebuilds a run from a header node's properties and the child rows read alongside it.
Everything a header write sets, minus the two key properties, which the MERGE pattern owns and nothing may move.
A digest of everything a header write owns, computed from the same map headerBindMap builds and compared verbatim against what an earlier write stored — never re-derived from a row read back afterward.
The one derived property on a run header: an injective encoding of everything about a run's lineage that a later save could disagree about.
The properties a terminal write sets, and only those.