Two rulings landed and both corrected something. GH-DEC-2026-013 accepted our binding-versus-awareness argument, wrote it into the record as its §6, and did not change the outcome — it sharpened the defect. Two different facts share one field named tenant: the act-scope, a property of the act that our binding slice commits, and the principal's membership, a property of the person that approval-engine exact-matches. Gate House's correction stands: a binding slice that must commit the scope being entered should commit that scope, not borrow a membership claim to stand in for it. Our schema already does — binding.target IS the act-scope and is inside view_hash — so no field was added, only a statement (PR-08) and a provenance record (PR-09), since key-cape emits tenant as a bare string. key-cape had already implemented registration-bound tenancy on 2026-09-09, correct under both candidate rulings, so the fail-closed-at-first-use risk that made us withhold the client strings was already retired. IN-0002 closed. The one remaining input to T07 is the deployed origin. GH-DEC-2026-014 granted commitment-only evidence and bounded it. It satisfies non-alteration and NOT reconstructability, and must not be described otherwise anywhere. It also corrected our wording of the gap: we wrote that it leaves us able to erase the content, which understates it. Commitment-only moves integrity out of our control and leaves availability entirely inside it — the party that can withhold the content is the party the evidence is about. Limit 3's condition reduced, not removed. The grant carries a condition we did not propose and would not have thought of: the path must assert that committed content exists and where custody sits, so non-production is a finding attributable to the custodian rather than an unremarkable blank. A commitment with no assertion that something is being committed to is indistinguishable from a commitment to nothing. Booked as PR-53, and marked not-a-reversal-candidate. Recorded the meta-rule Gate House named, now in its third setting here: unknown versus absent in the stance map, directory-asserted versus registration-supplied in the tenant claim, erased versus never held in the evidence path. Wherever a system reaches one appearance by two routes, the record must say which route. 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
16 KiB
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_versionprincipal— who it was shown tolocale,ui_release— both insideview_hashrendered_atacked_highlight_idsview_hash,awareness_hashphase—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_hashfrom 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:<digest of the presented token>). 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:
- this repository holds the presentation record and computes both hashes;
- it emits that record to
audit-coreas its own evidence; - 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.
8c. Act-scope is not membership — GH-DEC-2026-013 §5
Gate House made the sharpest observation anyone has made about this object model, and it is worth carrying in full because it changes what we record even though it changes no field.
Two different facts are being asked to travel in one claim named tenant:
| Fact | Property of | Who needs it |
|---|---|---|
| Which scope is this act being entered into | the act | this repository's binding slice |
| Which tenant is this person a member of | the principal | approval-engine's exact-match admission |
"A binding slice that must commit the scope being entered should commit THAT SCOPE, not borrow a membership claim to stand in for it."
Our schema already does the right thing. binding.target is the act-scope:
the tenant, environment and system being entered, committed in the binding
document and therefore inside view_hash. It is not derived from, and must
never be replaced by, the token's tenant claim.
What was missing is not a field but a statement and a record:
binding.targetis the authoritative act-scope commitment. Thetenantclaim is a membership fact about the principal and is not the act-scope.- The two must never be collapsed. Where they agree, that agreement is itself a
fact worth recording; where they disagree, issuance is refused upstream
(
key-cape329e48f) and this surface records the refusal rather than choosing a winner. - The
tenantclaim's provenance is recorded.key-capeemitstenantas a bare string, so a consumer cannot tell a tenant the directory asserted about the person from one a registration supplied about the client they came through.GH-DEC-2026-013§5 requires the claim to carry its provenance; until it does, this surface records which route the value arrived by rather than storing an undifferentiated string.
This is the same rule as unknown versus absent in the stance map and
erased versus never held in §8d: wherever a system reaches one appearance
by two routes, the record must say which route, or the safer reading becomes
unavailable to everyone.
8d. What commitment-only does and does not establish — GH-DEC-2026-014
Commitment-only emission is granted for Stage 1. It is granted on the test
GH-DEC-2026-013 set: its distinguishing case fails closed. A reviewer who
cannot obtain the content gets no reconstruction rather than a wrong one.
It satisfies non-alteration. It does not satisfy reconstructability, and must
not be described as doing so — not here, not in audit-core's documents, and
not in any conformance claim.
A commitment-only record is weaker than an archive in a specific way. An archive proves records were not altered or truncated after arrival, never that one was never sent (§2, E-04). A commitment additionally does not carry what it commits to. It establishes that the commitment was made when it says and has not changed. About what was committed to it establishes nothing — except conditionally: if a document is later produced, whether it is the one.
The gap, in the right words
An earlier draft of ours said commitment-only "leaves us able to erase the content". That understates where the problem sits, and Gate House corrected it:
Commitment-only moves integrity out of our control and leaves availability entirely inside it. The party that can withhold the content is the party the evidence is about.
That is the condition limit 3 exists to prevent, reduced and not removed. The reduction is real. It is not sufficient alone — which is why the grant carries the condition below.
The condition that makes the grant safe
Non-production must be detectable as a finding, not present as an absence.
If the independent path holds only a commitment, a reviewer who asks for content and gets nothing cannot distinguish erased, withheld, lost, and never held. The absence reads as an unremarkable blank.
So the evidence path must carry, alongside the commitment:
- an assertion that committed content exists, and
- where custody sits,
such that failure to produce at retrieval is a conformance failure attributable to the custodian.
A commitment with no assertion that something is being committed to is indistinguishable from a commitment to nothing.
This is not a reversal candidate. If it proves expensive, the answer is a cheaper mechanism for the same property, never removal of the property.
Residual, again
Not closed, not credited. The existence assertion narrows the erasure gap — a detectable non-production tells a reviewer something is missing and who owed it. It does not produce the missing thing.
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.