ADR-0004 — policy_hash covers default_verdict and ignores file layout¶
Status: accepted · week 1 Refines: ADR-002.7, SPEC §4.6.
Context¶
ADR-002.7 defines policy_hash = sha256(canonical_json(sorted rules)). The hash
exists so that a verdict in the chain pins exactly which policy produced it: if
the policy changes, the hash changes, and an auditor can tell the two eras apart.
Two things fall outside "sorted rules".
default_verdict. It lives in qed.yaml, not in a rule file, and it
decides the outcome of every proposal that matches nothing. Flipping it from
ALLOW to REQUIRE_APPROVAL changes the behaviour of the whole system while
leaving a rules-only hash identical. That is precisely the situation the hash is
meant to make impossible.
Which file a rule came from. A rule's source is useful in lint output and
in policy_loaded. If it were hashed, moving a rule from rules.yaml to
prod.yaml would look, in the chain, exactly like changing the policy.
Decision¶
policy_hash = sha256(JCS({
"default_verdict": <value>,
"rules": [ <hashed fields>, ... sorted by rule_id ]
}))
Hashed per rule: rule_id, name, message, verdict, severity, mode,
applies_to, when, approval, fix, compliance, disabled, reason.
Not hashed: source, disabled_by, references.
disabled is hashed — switching a rule off changes decisions. reason is
hashed because it is the auditor-facing justification and should not be editable
without a trace. disabled_by is not, because it is an attribution recorded
separately in policy_loaded.
Consequences¶
- Changing
default_verdictproduces a newpolicy_hashand a newpolicy_loadedrecord. Replay stays honest. - Reorganising policy files is a no-op for the chain, so teams can split rules by environment or ownership without generating noise.
- Rule order within a file, and across files, is irrelevant: rules are sorted by
rule_idbefore hashing, and a golden test shuffles the bundle and compares verdict bytes. - This is a deliberate, documented divergence from the literal text of
ADR-002.7. The
policy_bundlestable stores the hash, so an instance that predates this ADR would compute a different hash for the same rules; nothing predates it.