# Policy Binding Spec

Additive, optional mechanism for pinning a Decision Receipt to the *exact
historical version* of a policy, protocol, or rule set that was in force
at the time of the decision.

Status: **DOCUMENTED**. Wire format is defined; server-side accept path is
additive and non-breaking. Verifier support is planned but does not gate
receipt-level cryptographic verification.

## Problem statement

Today a receipt may carry:

```json
"decision": { "policies": ["internal-credit-v3"] }
```

Two problems for anyone re-evaluating that receipt years later:

1. The string `"internal-credit-v3"` is meaningful only in the issuer's
   private context.
2. Even if the evaluator can locate the issuer's policy repository, they
   have no way to know **which content** was labelled `v3` at the moment
   of the decision. The policy may have been amended after issuance and
   the label reused.

For safety-adjacent, medical, financial, and regulated workflows the
"which content was in force at time T" question is the whole point of an
audit.

## Design

An optional array field on `ReceiptBody`, signed as part of the body:

```jsonc
"policy_bindings": [
  {
    "policy_id": "hospital-triage-protocol",
    "policy_version": "2026.04",
    "policy_hash": "sha256:af…",
    "effective_from": "2026-04-01T00:00:00Z",
    "effective_to": null,
    "reference_uri": "https://example.hospital/policies/triage/2026-04.pdf"
  }
]
```

### Field semantics

| Field | Required | Meaning |
|-------|----------|---------|
| `policy_id` | yes | Stable identifier meaningful to the issuer. |
| `policy_version` | yes | Human-readable version label. |
| `policy_hash` | yes | `sha256:<hex>` of the policy document as the issuer sealed it. This is the anchor for later verification. |
| `effective_from` | yes | ISO-8601 timestamp of the earliest moment the pinned version was in force. |
| `effective_to` | no | ISO-8601 timestamp of the last moment the pinned version was in force. `null` when the version was still in force at seal time. |
| `reference_uri` | no | Optional pointer for humans. **Not** a trust anchor. |

### Signed body

The array is added to `ReceiptBody` before hashing. Old bodies do not
carry the field, so the field is omitted from their canonical form
(dropped by canonicaliser rules), and their `receipt_hash` is unchanged.
New bodies with `policy_bindings` produce a different `receipt_hash`, as
expected — the binding is part of what is being attested.

### Verification model

At verification time, a caller may supply the original policy document(s)
that were in force. The verifier:

1. Confirms `sha256(canonical(policy_document)) == policy_hash` for each
   `policy_bindings[i]`.
2. If a document is supplied but its hash does not match the recorded
   `policy_hash`, verification fails with a specific error code
   `policy_binding_mismatch` — the receipt is cryptographically valid
   but the supplied document is **not** the one that was in force.
3. If no document is supplied, the verifier reports
   `policy_binding_unresolved: <policy_id>@<policy_version>` and treats
   this as UNKNOWN (not FAIL). Cryptographic integrity of the receipt is
   independent of whether the caller happens to hold the policy document.

### Storage responsibility

Signatrust **does not store** the policy documents. It stores only their
hashes inside signed receipts. The archive of policy documents is the
customer's responsibility. Recommended patterns:

- Version-controlled repository (Git tag matching `policy_version`),
  with a build step that recomputes `policy_hash` from the tagged tree
  and emits a manifest.
- Content-addressed storage (S3 with object-lock, or a content-addressed
  store) keyed by `policy_hash`.
- Optional Offline Verification Bundle export (see
  `OFFLINE_VERIFICATION_SPEC.md`) that ships the policy document alongside
  the receipt for hand-off to an auditor.

### What this does NOT do

- It does **not** turn Signatrust into a judge of policy correctness.
- It does **not** guarantee the policy document was itself lawful,
  ethical, or fit for purpose.
- It does **not** rely on the `reference_uri` remaining reachable. A
  broken URL does not affect verification.
- It does **not** attest that the decision **followed** the pinned
  policy — only that the issuer, when sealing, committed the receipt to
  a specific policy version.

### Backward compatibility

- Receipts sealed before this field existed remain unchanged.
- Their `receipt_hash` values are not recomputed.
- A verifier reads `policy_bindings` if present; ignores it if absent.
- The wire-level schema hash used by the ADR spec bumps only when a
  breaking field is added; adding an optional field does not.

### UI language rules

The verify page must render, per binding:

```
Policy binding present
  policy_id       hospital-triage-protocol
  policy_version  2026.04
  policy_hash     sha256:af...
  policy_document supplied by caller
  match           PASS | FAIL | UNKNOWN (no document supplied)
```

Never render `policy_bindings` as "policy compliant". Compliance is a
statement about the world, not about the receipt.

### Mandatory tests

See `scripts/regression-matrix.ts` §policy-binding:

- `policy_hash_matches_supplied_document`
- `policy_hash_mismatch_reported_as_fail`
- `policy_binding_absent_treated_as_unknown_not_failure`
- `changing_policy_version_after_signing_breaks_integrity`
- `old_receipt_without_policy_bindings_still_verifies`
- `multiple_policy_bindings_all_reported`
