# Evidence Model **Repo:** informed-decision **Workplan task:** `INFD-WP-0001-T06` **Status:** draft, pending review **Companions:** `ProductRequirementsDocument.md`, `UseCaseCatalog.md`, `ArchitectureBlueprint.md` (gated on `INFD-WP-0001-T02`) **Normative algorithm:** `history/20260909-initial-exploration/canonicalize.md` **Governed implementation:** `informed_decision/canonicalize.py` **Tests:** `tests/test_canonicalize.py` --- ## 1. What this model claims > **This person, in this role, was shown this view, and bound this act.** Not *"the file was signed"*. The difference is the whole product. A signature over a document proves a key was used on some bytes; it does not establish what the signer was looking at when they decided, which is the question an auditor actually asks and the question no e-signature product answers. The mechanism is **dynamic linking**: the human-readable content actually presented is canonicalized and its digest is bound into the act. ## 2. What this model does not claim Stated first, because an evidence model that oversells is worse than none. **E-01 — It does not prove the person understood.** It proves what was presented and what was explicitly acknowledged. Comprehension is not observable and this model does not pretend to observe it. **E-02 — It does not prove the person read anything.** Deliberately. Evidence of informedness is the hash of the presented view plus explicit acknowledgment — never dwell time, scroll depth, or attention telemetry. That choice trades a weaker claim for the absence of surveillance, and the trade is intentional (`INTENT.md` principle 12, `PR-70`/`PR-71`). **E-03 — It does not survive a compromised surface.** `view_hash` is computed by the same component that renders. A compromised surface can present X and attest Y. This is structurally the same residual `approval-engine` names for adversarial omission at a compromised source, and it is not closed here, not closable at this layer, and never to be described as closed. **E-04 — A hash chain proves records were not altered or truncated after arrival. It cannot prove a record was never sent.** Carried over from `approval-engine`'s reasoning rather than rediscovered: a suppressed event leaves the chain perfectly intact and verification reports intact. Completeness is a separate obligation met by cadence, not by chaining. **E-05 — Stage 1 binding is organizational only.** No AES, no QES. The object carries `binding_level` so higher levels are expressible without a schema change; nothing above organizational is implemented or claimed. **E-06 — A Stage 1 escalation is an assertion, not a verified claim.** Without the mandate graph, guard `G_ESC` cannot be fully evaluated. The bundle records what the approver asserted and marks it unverified. ## 3. The two hashes | | `view_hash` | `awareness_hash` | | --- | --- | --- | | Over | the binding document | the awareness document | | Signed | yes, from organizational level up | **no** | | Purpose | what the person committed to | what they were oriented with | | In AES/QES signed attributes | yes | no, unless promotion ran | **Binding allow-list:** `memo_id`, `memo_version`, `question`, `requested_act`, `binding_level`, `brief`, `locale`, `ui_release`, normalized `packet`, normalized `highlights`, `binding`, and optionally `awareness_promoted`. **Awareness allow-list:** proposed hat and its source, available hats and scopes, last session, situation note. Profile: JCS-inspired restricted JSON — UTF-8, keys sorted by code unit, no insignificant whitespace, integers only, unknown keys stripped, nulls dropped. ``` canonical = dumps(document) hash = SHA-256(UTF-8(canonical)) ``` ### Why the split exists The login case forced it and it now applies everywhere. Defaulting a role to last-used is **required** for situational awareness — an operator who cannot see which hat they are about to wear is less safe, not more — and is **forbidden** from silently entering the signed payload. A person must not be bound to orientation material they did not commit to. Promotion is the only bridge: moving a field from awareness into binding creates a new memo version, invalidates outstanding presentations, and copies the named fields into `awareness_promoted` where `view_hash` covers them (guard `G_PROMOTE`). ## 4. The four isolation properties These are the model. Each is a test in `tests/test_canonicalize.py` and each must stay green; a failure means `view_hash` no longer means what §1 claims. | # | Property | Protects | Test | | --- | --- | --- | --- | | 1 | Shuffling object keys does not change either hash | Any verifier, in any language, agrees with us | `test_key_order_does_not_change_*` | | 2 | Editing an awareness field does not change `view_hash` | The person is bound only to what they committed to | `test_editing_awareness_does_not_change_view_hash` | | 3 | Changing `binding.target` **does** change `view_hash` | Positive control — without it, 2 could pass vacuously | `test_changing_binding_target_changes_view_hash` | | 4 | Selecting a hat after login does not rewrite `view_hash` | Post-bind session state is a different object | `test_post_bind_hat_selection_does_not_change_view_hash` | Property 3 is not decorative. Properties 1, 2 and 4 are all *negative* — they assert that things do **not** change the hash. A canonicalizer that returned a constant would pass all three. Property 3, plus the per-field variants over `question`, `requested_act`, `binding_level` and `packet`, is what makes the suite meaningful. **Published vectors** reproduce byte for byte and are asserted in CI: ``` login view_hash 492d9d31…1b06b8 login awareness_hash 2be77429…4fe05d1 ADR-0042 view_hash 1c89ec07…e91f202 ``` **Provenance is tested.** `history/20260909-initial-exploration/` is never edited. `test_governed_vectors_match_the_preserved_history_copy` asserts the governed fixtures have not drifted from the founding copies, so a silent edit to a vector to make a failing test pass is itself a test failure. ## 5. The presentation record Every render creates one. It carries: - `memo_id`, `memo_version` - `principal` — who it was shown to - `locale`, `ui_release` — both inside `view_hash` - `rendered_at` - `acked_highlight_ids` - `view_hash`, `awareness_hash` - `phase` — `pre_bind` | `bind` | `post_bind` A disposition references exactly one presentation, of the same memo version (guard `G_PRES`). A presentation taken at version *n* cannot bind after the memo advances to *n+1* — no silent upgrade (`NC-07`). Because `locale` is inside `view_hash`, the same memo read in German and in English produces two different presentations of one binding. Intended, and documented rather than smoothed over (`PR-61`). ## 6. The evidence bundle An **export**, not a screenshot (`INTENT.md` principle 7). Contains the memo and all its versions, every presentation with both hashes, every disposition, the thread, the route as executed, and the append-only hash chain. Requirements: - **Verifies offline.** No network, no access to this service. A verifier recomputes `view_hash` from the bundle's own binding document and compares. - **Detects mutation.** Altering any covered field breaks verification. - **States its own bound.** The residuals in §2 travel with the bundle as metadata. An auditor must not have to read this document to learn that E-03 exists. - **Marks unverified assertions as such** — see E-06. ## 7. Relationship to `audit-core` `audit-core` holds the archive; this repository holds the operative record and produces the export. This surface emits its transitions there and does not duplicate the archive's role. Its guarantee is bounded and the bound is inherited, not re-litigated: `audit-core` is not WORM or object-locked, its tamper evidence is conditional on live preconditions, and stronger custody was **decided against** in statute §16. This repository must not plan around a stronger archive, and must not imply the bundle is more durable than the archive it reconciles against. ## 8. Open — `view_hash` versus the approval binding digest **Unresolved.** Filed as `INFD-IN-0001` R3 with `gate-house`. `approval-engine`'s claim carries *"a digest over the same canonical binding the decision point already computes."* `view_hash` is also a digest over "the binding". They cover different material: the approval digest exists with no human in the loop; `view_hash` additionally covers brief, packet, highlights, locale and UI release. Until ruled, this repository computes `view_hash` and **asserts no relationship** between the two. Three candidate outcomes and their costs are set out in `docs/gate-house-decision-request-layer-placement.md` §2 R3. The outcome to be avoided is both digests shipping with no stated authority rule, leaving the estate with two canonicalizations of one act. ## 8b. Where `view_hash` actually lives (Stage 1) **It is not in the approval entry, by the engine's deliberate design.** `POST /v1/approvals/{id}/entries` reads the request body and **discards it**. Approver identity, `assurance` and `evidence_ref` come only from the verified token (`evidence_ref` is `jwt-sha256:`). The reasoning is sound and we adopt it rather than argue it: a caller-supplied approver field is provenance, not evidence, and that entry is currently the one record containing no caller-supplied data. Stage 1 arrangement, requiring no change to `approval-engine`: 1. this repository holds the presentation record and computes both hashes; 2. it emits that record to `audit-core` as its own evidence; 3. correlation to the approval entry is by **`(approval_id, subject, approved_at)`** — all three returned by the engine after a successful entry. **Consequence for an auditor, stated plainly:** someone holding only the approval object cannot reach the presentation. They need this surface's record or `audit-core` as well. `GOAL.md` DoD-3 — "the approval entry is reconstructable from a `view_hash`" — is satisfied by the correlation triple, not by a hash stored on the entry. Putting the binding *inside* the approval object would be a real change request against `approval-engine` (an optional, verified-alongside-token `view_hash` on the entry) and it is doctrine before it is a change request, so it goes to `gate-house` first. Not raised at Stage 1; recorded here so the option is not lost. ### `assurance` is the only carrier of authentication facts `approval-engine` persists the token's `assurance` claim verbatim and interprets nothing — an empty `{}` is *accepted* and evidences nothing. It is therefore the single place where "this was bound under MFA" survives into the approval record. The shape must carry authentication method, `acr`/`amr` and `auth_time` at minimum, and `informed-decision` and `key-cape` own it between them. Nothing downstream reconstructs it. ### Principal type is verified evidence; `subject_id` is not Schema v4 records `entries[].principal_type` from the verified token. Design principle 10 (humans bind, agents draft) is enforceable from the evidence chain by reading that field. Never infer it from the shape of `subject_id` — a naming convention is not a verified claim. Entries written before v4 are `null` and must not be read as `human`. ## 9. Signed attributes (L4+, horizon) When AES/QES arrives, the signed attributes carry `memo_id`, `memo_version`, `disposition.verb` and `presentation.view_hash`. They do **not** carry `awareness_hash` unless promotion ran. Recorded here so nothing built at Stage 1 forecloses it.