# Mirror — protocol stability policy

Version 1.0 · published 2026-09-28 · applies to protocol `mirror/1` (the spec pin carried on `/health` and in every record).

Banks sign agreements against what stays frozen. This page says what is frozen within a major version, what can only grow, what is experimental, and how anything is ever removed. Each promise names the test that holds the codebase to it.

## Frozen within a major version

These can change only in a new major version (`mirror/2`), announced at least **180 days** ahead:

- **The three verdict states**: `reconciled`, `divergent`, `undetermined`. There is no fourth state, and `cleared` is true only for `reconciled`.
- **The commit primitive.** The per-shipment salt derivation, the band commitment preimage (`salt ":" band`), the corridor commitment, and exact-decimal banding. Pinned by the conformance vectors (`conformance/vectors.json`, `conformance/reference-impl.test.js`).
- **Canonical JSON**: recursively sorted keys, UTF-8, `undefined` encoded as `null`, and no NaN or Infinity. Signatures are ES256 over the canonical text.
- **Record identity.** A record's `v` and `type` never change meaning.
- **Refusals.** No refusal in [`/cannot.json`](/cannot.json) is removed, reused or weakened. See change control below.
- **Codes.** No code in [`/signals.json`](/signals.json) is removed, reused, or moved to another HTTP status class (`server/signals.test.js`).
- **The raw-data wall** only grows. A key refused today is refused in every later version.

## Additive only

These may grow in any minor version; nothing that exists is taken away:

- **New record blocks.** Each appears only when its feature is used, so a record that does not use one signs exactly the bytes it signed before. Examples: `anchor`, `resolution`, `physical`, `origin`, `price_basis`, `implementations`, `asserts`, `cargo`, `route`.
- **New leg blocks.** They are bound into the leg signature only when present (`fine`, `physical`, `dimensions`, `origin`, `basis_commitment`, `bander`). A leg without them has byte-identical envelope bytes.
- **New routes, codes, refusals and witness classes.**

## Experimental, for now

These are labelled experimental until **2027-03-31**. Their field sets may change with 90 days' notice:

- path certificates and ghost-coverage certificates;
- citation corridors;
- services legs;
- the terminal commitment bulletin format;
- the corridor graph. It is also *not publishable* until an external review of its mechanism is named (see `MATH.md`);
- webhook subscriptions and the delivery envelope (`mirror.webhook-delivery`). The subscription and delivery **ids** are frozen: SPEC §8.2;
- key delegation certificates (`mirror.key-delegation`) and their scope names;
- the signed time (`mirror.time`), the audit export line schema (`mirror.siem-event/v1`) and the signed roadmap (`mirror.roadmap`);
- the key-rotation plan and rehearsal records, and the incident-support pack.

## Leg roles: an open namespace with defined unknown-role behaviour

Roles are lowercase strings, not a closed enum, because the roadmap needs `insurer`, `broker`, `logistics`, `beneficiary_bank` and more:

- An unknown role is compared like any other role.
- It is **required** only when a policy's `required_roles` names it.
- It never gains independence from its name. A leg's class comes from the ratified key that signed it (§7.2 of the spec), never from what the role says.

So adding a role can degrade only to defined behaviour, never to a parse failure or a silent clear.

## Idempotency and replay

- **A retry** (identical legs) produces a byte-identical record and is **counted once**. The ledger keeps every submission, and every count reads distinct canonical records (`kernel/mirror-idempotency.test.js`).
- **An amendment** (any leg changed) produces a new verdict. The earlier one is never erased.
- **A table version changing mid-flight** gives legs banded under different tables different corridor commitments. The result is `undetermined`, never a silent clear.
- **A failover resubmission** (the same commitments re-signed by another bander host) is the same **judgement**. Counts and webhook delivery ids leave signature bytes out, so it is counted once and pushed once (`server/bander-ha.test.js`).
- **A verdict the store did not append** still comes back signed, with `ledger: "NOT appended ..."`. Resubmitting the same legs appends it as the same verdict.
- **A re-requested verdict** is the same record under a fresh low-S signature. Signatures are canonical: a high-S copy is a legacy record or a malleated copy, never a second verdict (`kernel/mirror-malleability.test.js`).
- **503 responses** (`no_key`, `ledger_unreadable`, `archive_failed`, `store_unwritable`) are safe to retry with backoff. `501 webhooks_unavailable` is a capability the operator lacks; retrying will not help.

## Change control over the refusal list

[`/cannot.json`](/cannot.json) is versioned:

- Adding a refusal is a minor version.
- Removing or weakening one needs a major version, a dated changelog entry, and at least 90 days' published notice. Weakening means a structural refusal becoming merely enforced, or a wire code changing.
- `server/cannot.test.js` holds the registry against the previously published one on every build. A silent weakening fails before it ships.

## Deprecation

- **Additive features:** at least 90 days' notice, dated here and on the changelog.
- **Frozen items:** a new major version, announced 180 days ahead.
- **Plaintext join keys** are the live example. They are accepted, and flagged on every record, until the published cutover of **2026-12-01**. After that they are refused with `plaintext_join_key`.

## What this policy does not do

It binds this codebase, and each promise is pinned by a test. It does not prove that a particular deployment runs this code. For that, compare the spec pin on `/health` with the published conformance-suite hash (`/conformance-suite.json`).
