Skip to content

ADR-0050: A spill branch may state no condition, and the store records that absence as null

Status: Accepted Date: 2026-09-11

Decisions

D-116 SpillResponseBranch.conditionSourceSpanId is nullable, and null is the only way a branch states no condition

Where a document states no branch condition, its spill steps are stored under a real branch whose condition is null. A blank condition is refused, not stored — it is an invented threshold in disguise, a gate that renders empty — so null and an empty string are never interchangeable. Unbranched steps are never parked in the Spill Response field's CuratedFieldEntry instead.

Context

D-97 rules that where an SDS states no branch condition there is one procedure and no invented condition of any kind. Half the curation spike's sample states none. The schema contradicted it: conditionSourceSpanId was a non-null Int, so a branch could not exist without a stated condition. The snapshot serialiser already handled a null condition (ticket 16), but the store could never produce one — that null path had been unreachable since it shipped, and Spill Response, one of five named record fields, was uncurable for roughly half the Corpus.

The owner ruled on ticket 27, 2026-09-11, while the spec was frozen: migrate the column nullable and let selection accept a null condition. This records it.

What this ADR does not do. The handoff listed a second item beside this one: ADR-0026's D-55 says the branch condition "doubles as D-08's escalation gate", which D-97 contradicts. That was framed as a live contradiction needing a superseding document. It was not. ADR-0041 already amends ADR-0026 and removes exactly that clause — its What each amendment touches section says so, dated 2026-09-10, the day before the item was raised. The ruling was on disk; what was missing was any sign of it where a reader looks. ADR-0026's title still states the removed clause, and the decision index listed it as plain Accepted.

Writing that amendment again here would make a third document saying the same thing. The index now derives "amended by" from every amending ADR's Status line, so ADR-0026's row names ADR-0041 — and so do fifteen other amended ADRs' rows, which were silent the same way.

Decision

A branch stating no condition carries null. Selection accepts it, refuses a blank, and never moves the steps out of a branch to avoid it.

Rejected options

  • Keep the column required — rejected. It makes D-97 unrepresentable, and a curator facing a document that states no condition could either leave Spill Response uncurated or invent one.
  • Park unbranched steps in the field's CuratedFieldEntry — rejected, and should stay rejected. corpus_check.py's D-54 step-order assertion walks spillResponseBranches[].steps only, so steps parked there would escape the ship gate: the write path disagreeing with the check on what it produced.
  • A sentinel "no condition stated" span — rejected. A Source Span is text from a document, and this one would be text the document does not contain.
  • Supersede ADR-0026 — rejected. D-54 and D-55's never-auto-select rule stand, and are cited across both application repositories; restating them under new ids to remove a clause already removed would leave every one of those citations pointing at a dead ruling.

This ruling may not be re-decided

If a change contradicts this ADR: stop and raise it. Do not implement over it.

Specifically: do not store an empty string as a condition, do not give the column a default, and do not route steps around a branch because their document states no condition.

Consequences

What becomes reachable. A document that states no branch condition is curable end to end, and the serialiser's null path is exercised by a null the write path produced rather than one a fixture assigned.

What was accepted by name. The re-created foreign key carries ON DELETE SET NULL, Prisma's default for an optional relation, kept so migrate diff does not re-emit it forever. If a SourceSpan were ever hard-deleted, a branch that stated a condition would silently become one stating none — the exact direction the write path refuses. Inert while soft delete is mandatory and nothing calls .delete(); an acceptance, not a non-event.

What is still unproven. The migration was produced by prisma migrate diff against no live database and has not been applied, like every migration after the template's.

corpus.spec.ts names the exception: conditionSourceSpanId is asserted Int? and asserted to be the only optional span-bearing field, so making any other one optional still fails.

Coverage

No upstream finding lands here. The ruling came from reading the schema against D-97 while building ticket 27 (D-104).