Promote schema and canonicalizer out of history; add EvidenceModel (T06)
Verified the three published hashes reproduce byte for byte before promoting anything, then moved the schema, canonicalizer and vectors into governed assets. history/20260909-initial-exploration/ is untouched and stays the provenance record. - schemas/, informed_decision/, tests/vectors/ populated; the reference canonicalizer's ad-hoc __main__ block replaced by a real `python -m informed_decision` entry point. - tests/test_canonicalize.py — 20 tests, all green. Published vectors, all four isolation properties, canonical-form round-trip, key sorting, and a provenance test asserting the governed fixtures have not drifted from history/. - docs/specs/EvidenceModel.md — the two hashes, the split and why it exists, the four isolation properties, the presentation record, the bundle, and the relationship to audit-core. - pyproject.toml, Makefile. One test of mine was wrong on first run: it scanned for ", " to assert no insignificant whitespace, which fires on prose inside a brief. Replaced with a canonical round-trip comparison, which is the property actually meant. The canonicalizer was correct. EvidenceModel leads with what the model does NOT claim — no proof of comprehension, no proof of reading (deliberately, since the alternative is surveillance), no survival of a compromised surface, and audit-core's inherited bound that a hash chain cannot prove a record was never sent. T06 stays progress: the SCOPE.md rewrite is gated on the T02 ruling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR Assistant: claude-code Assistant-Model: opus Assistant-Process: 1565372@bnt-lap001 Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
This commit is contained in:
parent
7ae67b2f4e
commit
a8e227851e
19 changed files with 2207 additions and 2 deletions
197
docs/specs/EvidenceModel.md
Normal file
197
docs/specs/EvidenceModel.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# 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.
|
||||
|
||||
## 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue