informed-decision/docs/specs/EvidenceModel.md

440 lines
21 KiB
Markdown
Raw Normal View History

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
2026-09-09 14:16:28 +02:00
# 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.
Raise INFD-IN-0004: approval-engine's R3 answer conflicts with the ruling approval-engine answered R3 and recommends exactly the option GH-DEC-2026-012 refused: that our binding document carry their binding.digest as a field rather than re-canonicalize action/actor/principal/purpose/target ourselves. We cannot comply with both, so this is raised as a finding rather than resolved. It is not a wording difference. Our binding slice canonicalizes principal and target, two of the five fields in their digest, so co-reference by identifier alone leaves two independent canonicalizations of one act linked by a shared id. GH-DEC-2026-012 manages that with an authority rule; approval-engine's proposal removes it. Both are coherent and they are not the same design. New information the refusal may not have had: their digest covers exactly five act fields, and they state that widening it to cover presentation would be a defect since a new UI release would invalidate every prior approval. The GH-DEC-2026-008 cycle condition is mutual containment, so if their digest structurally cannot contain view_hash the containment is one-directional. Not asserted as settling it — that cycle was found by two engines independently within hours, and "the cycle cannot arise here" is the belief such failures punish. Deliberately not adopted, despite coming from the layer that owns the digest and despite our having offered to let them settle R3. Gate House was right that a bilateral agreement produces agreement rather than an authority rule, and that applies to this one too. layer.yaml is unchanged and carries a linkage_under_review marker rather than a silent edit. Also carries the smaller doctrine question approval-engine handed on: whether approver evidence should be human-only at the engine, since entries[].principal_type is auditable after the fact and stops nothing, and there is no upstream backstop for humans-bind-agents-draft. 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
2026-09-10 13:59:52 +02:00
**Ruled `GH-DEC-2026-012` R3, and now re-opened as `INFD-IN-0004`.** The ruling:
distinct attestations with an authority rule — the binding digest is
authoritative for what the request *is*, `view_hash` for what was *shown*,
neither substitutable, and a disagreement between them is a finding against the
presenting surface rather than a fact about the request. Linkage by
co-reference, not nesting.
`approval-engine` has since published what its digests actually cover
(`docs/approval-claim.md`) and recommends the opposite linkage — that our
binding document carry their `binding.digest` as a field, which is the option
`GH-DEC-2026-012` refused. Their reason is the stronger version of the risk we
raised: our binding slice canonicalizes `principal` and `target`, two of their
five digest fields, so co-reference by identifier alone leaves **two
independent canonicalizations of one act** rather than removing the duplication.
Raised as a finding rather than resolved bilaterally
Apply GH-DEC-2026-015/016 and the audit-core registration; both blockers cleared Origin and evidence path both landed today. T07 origin: railiance-apps deployed decisions.coulomb.social and corrected the hostname in this repo — not the decide.coulomb.social this workplan proposed. Verified here rather than taken on report: both paths 200, TLS verify 0, Let's Encrypt cert valid to 2026-12-09. T08: audit-core registered the source with every field as proposed and landed the detection half. INFD-IN-0003 closed. Their refinements booked — reconciliation on the high-volume class too, since rate detects a stream stopping but never a stream missing the particular renders that mattered, which is exactly our threat model; and PR-12, the custody locator must be a stable non-secret identifier because redact scans data and an existence declaration arriving without its pointer looks complete while being useless. INFD-IN-0004 ruled as GH-DEC-2026-015: gate-house reversed itself and nesting is permitted for this pair. The decisive ground was not the cycle argument we led with — our binding slice canonicalizes principal and target, two of the five digest fields, so co-reference left us performing a partial recomputation of one act in a second vocabulary, closer to the translation R3 forbade than nesting is. Our ordering objection was withdrawn as mistaken. The permission is conditioned and NOT ACTIVE until approval-engine states its presentation exclusion as normative and tested. layer.yaml is deliberately unchanged and carries nesting_permission_active false — we do not activate on our own initiative. GH-DEC-2026-016 ruled NC-03. Its §5 is live rather than hypothetical and is booked as PR-11: principal_type: human is a property of the client registration, structurally the same shape as the gap-route tenant, so a human-in-the-loop control must not be discharged on it as verified humanity. T07 stays progress: the submission to key-cape is written but unsent, blocked by the local permission classifier rather than by any repository. 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
2026-09-10 19:19:10 +02:00
(`docs/finding-r3-linkage-conflict.md`). **Re-ruled by `GH-DEC-2026-015`:
gate-house reversed itself — nesting is permitted for this pair, so `view_hash`
may carry `binding.digest` and our binding slice stops independently
canonicalizing act material.**
The decisive ground was not the cycle argument we led with. It was that our
binding slice canonicalizes `principal` and `target`, two of the five fields in
their digest, so co-reference by identifier left us performing *a partial
recomputation of one act in a second vocabulary* — **closer to the translation
R3 forbade than nesting is**. Nesting removes the duplication; co-reference
manages it.
**The permission is conditioned and not yet active.** It activates when
`approval-engine` states its presentation exclusion as **normative and tested**
rather than as design intent — our own A-17 correction applied to gate-house's
permission, since the distinguishing case is someone widening the digest and
that case is unobservable until approvals start failing. Co-reference remains in
force until then, and this repository does not activate on its own initiative.
Our ordering-dependency objection to option (c) was **withdrawn as mistaken**
and recorded as withdrawn: `binding.digest` is over act material, determined
before anyone is presented anything. We stated that cost, gate-house took it
from us, and neither of us checked it.
Raise INFD-IN-0004: approval-engine's R3 answer conflicts with the ruling approval-engine answered R3 and recommends exactly the option GH-DEC-2026-012 refused: that our binding document carry their binding.digest as a field rather than re-canonicalize action/actor/principal/purpose/target ourselves. We cannot comply with both, so this is raised as a finding rather than resolved. It is not a wording difference. Our binding slice canonicalizes principal and target, two of the five fields in their digest, so co-reference by identifier alone leaves two independent canonicalizations of one act linked by a shared id. GH-DEC-2026-012 manages that with an authority rule; approval-engine's proposal removes it. Both are coherent and they are not the same design. New information the refusal may not have had: their digest covers exactly five act fields, and they state that widening it to cover presentation would be a defect since a new UI release would invalidate every prior approval. The GH-DEC-2026-008 cycle condition is mutual containment, so if their digest structurally cannot contain view_hash the containment is one-directional. Not asserted as settling it — that cycle was found by two engines independently within hours, and "the cycle cannot arise here" is the belief such failures punish. Deliberately not adopted, despite coming from the layer that owns the digest and despite our having offered to let them settle R3. Gate House was right that a bilateral agreement produces agreement rather than an authority rule, and that applies to this one too. layer.yaml is unchanged and carries a linkage_under_review marker rather than a silent edit. Also carries the smaller doctrine question approval-engine handed on: whether approver evidence should be human-only at the engine, since entries[].principal_type is auditable after the fact and stops nothing, and there is no upstream backstop for humans-bind-agents-draft. 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
2026-09-10 13:59:52 +02:00
For reference, three hashes answering three questions:
| Hash | Owner | Covers | Answers |
| --- | --- | --- | --- |
| `binding.digest` | `approval-engine` | `action`, `actor`, `principal`, `purpose`, `target` | which **act** is approved |
| `binding.pdp_digest` | `approval-engine` | the PDP's decision-request binding | which **decision request** |
| `view_hash` | here | brief, packet, highlights, locale, UI release, binding slice | what a **person was shown** | Three candidate outcomes and their costs are set out in
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
2026-09-09 14:16:28 +02:00
`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.
Revise specs against approval-engine's approver-surface requirements approval-engine replied to INFD-IN-0001 with docs/approver-surface-requirements.md (31da1af, 5203f46) and two corrections. Several of my requirements were wrong or incomplete; revised rather than appended to. Corrected: - PR-02 listed only approval:approve. Wrong — the surface also needs approval:read to fetch what it renders. As drafted it would have shipped a client able to submit an entry it could never display. That changes a registration key-cape has already implemented, so it is their call (open question A). - NC-03 implied approval-engine refuses non-human approver entries. It does not; only /consume is principal-restricted, and the operator service client holds approval:approve. Enforcement of "humans bind, agents draft" is therefore ours alone, and is auditable via schema v4's entries[].principal_type — never from the shape of subject_id. Added: - PR-04 assurance shape. approval-engine persists it verbatim and accepts an empty object, so it is the only place MFA survives into the approval record. Needs auth method, acr/amr, auth_time, agreed with key-cape. - PR-05 entitlement. A 200 from the engine is not permission to view; we owe access-engine a check before rendering. Consuming a decision, not making one. - PR-06 response mapping, including 409 duplicate_approver rendered as SUCCESS (a browser double-submit is routine and the first entry stands) and 503 as fail-closed. - PR-07 and a matching INTENT wrongness condition: never render `approved` as permission to act. That is a PDP in the browser wearing UI copy. - L-05, L-06 and EvidenceModel 8b: view_hash cannot ride into the entry — the POST discards its body by design — so Stage 1 correlates by (approval_id, subject, approved_at). DoD-3 is satisfied by the triple, not by a stored hash. PRD open question 1 is answered by construction: there is no inbox endpoint and there will not be one, so the approvals-inbox shape is foreclosed upstream. 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
2026-09-09 14:20:13 +02:00
## 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`:
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`.
Apply GH-DEC-2026-013 and GH-DEC-2026-014; close INFD-IN-0002 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
2026-09-10 07:57:57 +02:00
## 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.target` is the authoritative act-scope commitment. The `tenant` claim
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-cape` `329e48f`) and this surface records the refusal rather than
choosing a winner.
- **The `tenant` claim's provenance is recorded.** `key-cape` emits `tenant` as
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:
1. an assertion that committed content **exists**, and
2. **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.
Apply GH-DEC-2026-015/016 and the audit-core registration; both blockers cleared Origin and evidence path both landed today. T07 origin: railiance-apps deployed decisions.coulomb.social and corrected the hostname in this repo — not the decide.coulomb.social this workplan proposed. Verified here rather than taken on report: both paths 200, TLS verify 0, Let's Encrypt cert valid to 2026-12-09. T08: audit-core registered the source with every field as proposed and landed the detection half. INFD-IN-0003 closed. Their refinements booked — reconciliation on the high-volume class too, since rate detects a stream stopping but never a stream missing the particular renders that mattered, which is exactly our threat model; and PR-12, the custody locator must be a stable non-secret identifier because redact scans data and an existence declaration arriving without its pointer looks complete while being useless. INFD-IN-0004 ruled as GH-DEC-2026-015: gate-house reversed itself and nesting is permitted for this pair. The decisive ground was not the cycle argument we led with — our binding slice canonicalizes principal and target, two of the five digest fields, so co-reference left us performing a partial recomputation of one act in a second vocabulary, closer to the translation R3 forbade than nesting is. Our ordering objection was withdrawn as mistaken. The permission is conditioned and NOT ACTIVE until approval-engine states its presentation exclusion as normative and tested. layer.yaml is deliberately unchanged and carries nesting_permission_active false — we do not activate on our own initiative. GH-DEC-2026-016 ruled NC-03. Its §5 is live rather than hypothetical and is booked as PR-11: principal_type: human is a property of the client registration, structurally the same shape as the gap-route tenant, so a human-in-the-loop control must not be discharged on it as verified humanity. T07 stays progress: the submission to key-cape is written but unsent, blocked by the local permission classifier rather than by any repository. 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
2026-09-10 19:19:10 +02:00
## 8e. Cadence — declared, and now supportable
`audit-core` registered this source as proposed (`AUDIT-IN-0003`,
`docs/informed-decision-source-registration.md`) and landed the detection half
(`AUDIT-WP-0009` T04/T06/T07). The cadence can stop being described as
declared-but-not-operating once heartbeats are flowing.
**Per class, not per source** — the shape this repository argued for and
`audit-core` adopted. A per-source heartbeat from a mixed-volume emitter is
satisfied by its chattiest class and says nothing about the quiet,
security-relevant one, which is the only reason heartbeats exist.
| Class | Volume | Heartbeat | Reconciliation |
| --- | --- | --- | --- |
| `presentation` | one per render | yes — legitimately silent for days at Stage 1 | yes |
| `disposition` | low, security-relevant | yes | yes |
| `stance_application` | low, security-relevant | yes | yes |
Reconciliation applies to **every** class including the high-volume one: rate
detects a stream stopping, never a stream missing the particular renders that
mattered.
A heartbeat is an **ordinary event** — same envelope, same append-only custody,
same chain. Deliberately so: a heartbeat stored outside the chain would be the
one record that could be back-dated.
Note `no_heartbeat_since_registration`: declaring a heartbeat and never sending
one is **its own finding**, not a skip.
### The bound on both controls
Reconciliation compares `audit-core`'s counts against counts we compute from our
own state. **Where the emitter is compromised, both controls agree with it** — a
compromised surface suppresses the event and its own count together, and emits a
truthful-looking `nothing-to-report`.
Both cover loss, outage, drain failure and accident, which is most of what
actually goes wrong. **Neither covers the residual we already declare**, and
neither may be described as covering it. Closing it needs an observer
independent of the emitter, which §16 put outside `audit-core`'s scope.
### Tenant provenance is not in the envelope
`audit-core`'s tenant is not an identity claim they resolve; it is a value our
credential is permitted to write, checked by exact string equality. Recording a
route in the audit event would be them restating something they did not observe
— the same error as claiming an event occurred. It lands in the registration
document instead, whose authority is `GH-DEC-2026-013`'s bounded gap rather than
a populated directory record. If that gap closes, the entry is revisited rather
than assumed still correct.
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
2026-09-09 14:16:28 +02:00
## 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.