Build the T08 domain core with the engine behind a seam

approval-engine APPROVAL-WP-0002-T01 is still progress and its namespace has no
pods, so the live end-to-end proof cannot run. Built everything that does not
depend on it, with the engine behind a Protocol plus a fake carrying its real
refusal semantics, so its arrival is a wiring change rather than a build.

- memo.py: the Decision Memo, versions, binding document. Principal, Scope,
  Awareness and Hat are dataclasses rather than dicts because the canonicalizer
  requires a shape and a missing key should fail at construction rather than
  deep inside hashing — which is exactly how it failed twice while building
  this. Field names follow the governed canonicalizer (item_id, severity,
  locator): the published vectors are the contract, so the object was aligned to
  them rather than the reverse.
- presentation.py: the sole writer of view_hash. One writer, one canonicalizer,
  one place to audit. Acknowledgment is an explicit method call and nothing
  infers it from scroll, dwell or focus.
- disposition.py: verbs and guards G_NOAGENT, G_STEP, G_PRES, G_ACK, G_REASONS,
  G_SEALED. accept is ABSENT from weak steps rather than present-and-disabled,
  because a greyed-out accept still teaches the wrong model. Only accept reaches
  the engine; a memo return is not represented there at all.
- provenance.py: claim routes per A-16. assert_human_control_dischargeable
  refuses a registration-supplied human, so PR-11's limitation fires at the
  point of use instead of sitting in a document.
- evidence.py: local outbox, commitment-only records carrying the
  GH-DEC-2026-014 §4 existence assertion, per-class reconciliation counts, and a
  custody-locator guard that rejects credentialed URLs (PR-12).
- approval_client.py: 409 duplicate_approver is success, 409 conflict terminal,
  503 fail-closed, approval:consume refused before a token is requested.

87 tests pass, including every negative case in the Use Case Catalog and that a
fail-closed outcome is recorded as a stance application with no verb field —
never as a decline, because the human did not make one.

T08 stays progress: the live proof is the remainder.

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:
tegwick 2026-09-10 20:38:20 +02:00
parent 4379576abd
commit 9e1f77e32b
18 changed files with 1503 additions and 10 deletions

View file

@ -6,15 +6,28 @@
## Status — 2026-09-09
**Specification and declaration are complete; no service is deployed.**
**Specification, declaration and the domain core are complete. No service is
deployed.**
What exists and is tested: the layer and stance declarations (`layer.yaml`,
`pep-stance.yaml`, `informed_decision/stance.py`), the governed canonicalizer
and schema, and the four specs under `docs/specs/`. 46 tests pass.
What exists and is tested (87 tests):
What does not exist: any HTTP surface, any storage, any UI, any deployment. The
walking skeleton is `INFD-WP-0001-T08` and is gated on an external decision
(§"Open" below).
- layer and stance declarations — `layer.yaml`, `pep-stance.yaml`,
`informed_decision/stance.py`, with published-equals-shipped asserted;
- the governed canonicalizer and schema, with the three published vectors
reproducing byte for byte and all four isolation properties pinned;
- the **domain core**: `memo.py` (the Decision Memo, its versions and the
binding document), `presentation.py` (the sole writer of `view_hash`),
`disposition.py` (the verb vocabulary and guards `G_NOAGENT`, `G_STEP`,
`G_PRES`, `G_ACK`, `G_REASONS`, `G_SEALED`), `provenance.py` (claim routes,
A-16), `evidence.py` (the local outbox and commitment records);
- `approval_client.py` — the seam to `approval-engine` plus a fake carrying its
actual refusal semantics.
What does not exist: any HTTP surface, any persistence, any UI, any deployment.
The origin `decisions.coulomb.social` is live but serves an nginx placeholder.
`INFD-WP-0001-T08` remains open for the live end-to-end proof, which is gated on
`APPROVAL-WP-0002-T01` and a deployed `approval-engine`.
## One-liner

View file

@ -1,9 +1,27 @@
"""informed-decision — presentation and binding surface for decisions.
This package must never contain an authorization decision. See ``INTENT.md``
and ``AGENTS.md``: ``access-engine`` is the only policy decision point.
and ``AGENTS.md``: ``access-engine`` is the only policy decision point. What is
recorded here is evidence that a human performed an act, never a verdict on
whether the act was permitted.
"""
from .canonicalize import awareness_hash, view_hash
from .disposition import Actor, ActorKind, Disposition, DispositionRefused, Verb, legal_verbs, record
from .evidence import Commitment, EventClass, Outbox, heartbeat
from .memo import Awareness, BindingLevel, BindingSlice, Highlight, Memo, PacketItem, Principal, Scope, StepKind
from .presentation import Phase, Presentation, render
from .provenance import Claim, Route
from .stance import STANCE, resolve
__all__ = ["view_hash", "awareness_hash"]
__all__ = [
"view_hash", "awareness_hash",
"Memo", "BindingSlice", "Principal", "Scope", "PacketItem", "Highlight",
"Awareness", "BindingLevel", "StepKind",
"render", "Presentation", "Phase",
"record", "Disposition", "Verb", "Actor", "ActorKind", "DispositionRefused",
"legal_verbs",
"Outbox", "Commitment", "EventClass", "heartbeat",
"Claim", "Route",
"STANCE", "resolve",
]

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,114 @@
"""The seam to `approval-engine`.
A Protocol plus a fake, because the engine is not deployed
(`APPROVAL-WP-0002-T01` is `progress`, no pods). When it deploys this becomes a
wiring change rather than a build.
Hard rules, from `approval-engine/docs/approver-surface-requirements.md`:
- `GET /v1/approvals/{id}` and `/claim` need ``approval:read``;
`POST /entries` needs ``approval:approve``.
- **Never** ``/consume``. Human principals are refused there in code, and
consumption belongs to the PEP causing the side effect (`GH-DEC-2026-003`).
- No path containing ``check`` or ending ``/authorize`` exists to call.
- ``POST /entries`` **discards its request body** identity, assurance and
``evidence_ref`` come only from the verified token. So ``view_hash`` does not
ride into the entry; correlation is ``(approval_id, subject, approved_at)``.
- **No inbox.** Get-by-id only. Never poll for work.
- `approved` is a state of an object, never permission to act.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
FORBIDDEN_SCOPE = "approval:consume"
REQUIRED_SCOPES = ("openid", "approval:read", "approval:approve")
class ApprovalEngineError(Exception):
def __init__(self, status: int, reason: str) -> None:
super().__init__(f"{status} {reason}")
self.status = status
self.reason = reason
@dataclass(frozen=True)
class EntryResult:
"""What comes back after a successful entry.
These three fields are the correlation triple that ties the entry back to
our presentation record `GOAL.md` DoD-3.
"""
approval_id: str
subject: str
approved_at: str
status: str
duplicate: bool = False
@property
def correlation(self) -> tuple[str, str, str]:
return (self.approval_id, self.subject, self.approved_at)
class ApprovalEngine(Protocol):
def get_approval(self, approval_id: str) -> dict: ...
def add_entry(self, approval_id: str) -> EntryResult: ...
def assert_scopes_permissible(scopes: tuple[str, ...]) -> None:
"""Refuse ``approval:consume`` before a token is ever requested."""
if FORBIDDEN_SCOPE in scopes:
raise ValueError(
f"{FORBIDDEN_SCOPE} must never be requested: approval-engine refuses it "
"for human principals and consumption belongs to the PEP causing the "
"side effect (GH-DEC-2026-003)"
)
def is_success(err: ApprovalEngineError) -> bool:
"""`409 duplicate_approver` is SUCCESS, not failure.
A browser double-submit is routine and the first entry stands. Rendering it
as an error is a lie to the approver.
"""
return err.status == 409 and err.reason == "duplicate_approver"
class FakeApprovalEngine:
"""In-process stand-in with the engine's actual refusal semantics."""
def __init__(self, approvals: dict[str, dict] | None = None) -> None:
self._approvals = approvals or {}
self._entries: dict[str, set[str]] = {}
self.available = True
def get_approval(self, approval_id: str) -> dict:
if not self.available:
raise ApprovalEngineError(503, "store_unavailable")
if approval_id not in self._approvals:
raise ApprovalEngineError(404, "not_found")
return dict(self._approvals[approval_id])
def add_entry(self, approval_id: str, subject: str = "approver") -> EntryResult:
if not self.available:
raise ApprovalEngineError(503, "store_unavailable")
approval = self._approvals.get(approval_id)
if approval is None:
raise ApprovalEngineError(404, "not_found")
if approval.get("status") in ("revoked", "superseded", "consumed", "expired"):
raise ApprovalEngineError(409, "conflict")
seen = self._entries.setdefault(approval_id, set())
duplicate = subject in seen
seen.add(subject)
if not duplicate and len(seen) >= approval.get("required_count", 1):
approval["status"] = "approved"
return EntryResult(
approval_id=approval_id,
subject=subject,
approved_at="2026-09-10T15:00:00Z",
status=approval.get("status", "requested"),
duplicate=duplicate,
)

View file

@ -0,0 +1,174 @@
"""The disposition vocabulary and its guards.
Approve/reject is the wrong vocabulary for judgment. `return` is success, not
failure, and `accept` on a Kenntnisnahme step is illegal on purpose.
Nothing here is an authorization decision. A disposition is evidence that a
human performed an act; it never answers whether the act was permitted
(`access-engine`, statute §6).
"""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from .memo import Memo, StepKind
from .presentation import Presentation
class Verb(str, Enum):
COMMENT = "comment"
DISCUSS = "discuss"
RETURN = "return"
FORWARD = "forward"
ESCALATE = "escalate"
ACKNOWLEDGE = "acknowledge"
ACCEPT = "accept"
DECLINE = "decline"
WITHDRAW = "withdraw"
CONFIGURE = "configure"
#: Verbs that bind an identity to an act. Guarded hardest.
BINDING_VERBS = frozenset({Verb.ACCEPT, Verb.DECLINE, Verb.ACKNOWLEDGE})
#: Always available while the memo is circulating, whatever the step kind.
OVERLAY_VERBS = frozenset({Verb.COMMENT, Verb.DISCUSS})
class ActorKind(str, Enum):
PERSON = "person"
AGENT = "agent"
SERVICE = "service"
SYSTEM = "system"
@dataclass(frozen=True)
class Actor:
sub: str
kind: ActorKind
class DispositionRefused(Exception):
"""A verb was refused. Never an authorization verdict — see module docstring."""
def __init__(self, guard: str, message: str) -> None:
super().__init__(f"{guard}: {message}")
self.guard = guard
def legal_verbs(step_kind: StepKind) -> frozenset[Verb]:
"""Which verbs the UI may offer for a step kind.
`accept` is ABSENT for weak steps, not present-and-disabled: Kenntnisnahme
is not approval, and a greyed-out accept still teaches the wrong model.
"""
base = set(OVERLAY_VERBS) | {Verb.RETURN, Verb.FORWARD, Verb.ESCALATE}
if step_kind.is_weak:
return frozenset(base | {Verb.ACKNOWLEDGE})
return frozenset(base | {Verb.ACCEPT, Verb.DECLINE, Verb.ACKNOWLEDGE})
@dataclass(frozen=True)
class Disposition:
id: str
memo_id: str
memo_version: int
presentation_id: str
verb: Verb
actor: Actor
at: str
reasons: tuple[str, ...] = ()
note: str | None = None
#: Co-reference to the act (GH-DEC-2026-012 R3), identifier only.
approval_id: str | None = None
@property
def reaches_approval_engine(self) -> bool:
"""Only `accept` becomes a POST to /entries.
`return`, `discuss`, `escalate` and the rest are dispositions of a MEMO;
approval-engine models entries against an approval object and knows
nothing of them. A memo `return` must not be represented there at all.
"""
return self.verb is Verb.ACCEPT
def _now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
def record(
memo: Memo,
presentation: Presentation,
verb: Verb,
actor: Actor,
*,
reasons: tuple[str, ...] = (),
note: str | None = None,
) -> Disposition:
"""Record a disposition, or refuse it.
Guards, in the order a defect is most likely to be caught:
- ``G_NOAGENT`` humans bind, agents draft. No upstream backstop exists.
- ``G_STEP`` the verb must be legal for this step kind.
- ``G_PRES`` the presentation must be of this memo AND this version.
- ``G_ACK`` required highlights acked before any binding verb.
- ``G_REASONS`` a return carries at least one coded reason.
- ``G_SEALED`` binding verbs on a sealed version are illegal.
"""
if verb in BINDING_VERBS and actor.kind is not ActorKind.PERSON:
raise DispositionRefused(
"G_NOAGENT",
f"{actor.kind.value} principals may draft but never bind "
"(INTENT principle 10; approval-engine provides no upstream backstop)",
)
if verb not in legal_verbs(memo.step_kind):
detail = ""
if verb is Verb.ACCEPT and memo.step_kind.is_weak:
detail = " — Kenntnisnahme is not approval"
raise DispositionRefused(
"G_STEP", f"{verb.value} is not legal on a {memo.step_kind.value} step{detail}"
)
if presentation.memo_id != memo.id:
raise DispositionRefused("G_PRES", "presentation belongs to a different memo")
if presentation.memo_version != memo.version:
raise DispositionRefused(
"G_PRES",
f"presentation is of version {presentation.memo_version}, memo is at "
f"{memo.version} — no silent upgrade",
)
if verb in BINDING_VERBS:
if memo.sealed:
raise DispositionRefused("G_SEALED", "binding verbs on a sealed version are illegal")
outstanding = memo.required_ack_ids - presentation.acked_highlight_ids
if outstanding:
raise DispositionRefused(
"G_ACK",
"required highlights not acknowledged: " + ", ".join(sorted(outstanding)),
)
if verb is Verb.RETURN and not reasons:
raise DispositionRefused(
"G_REASONS", "a return carries at least one coded reason; free text is not a return"
)
return Disposition(
id=f"disp-{uuid.uuid4()}",
memo_id=memo.id,
memo_version=memo.version,
presentation_id=presentation.id,
verb=verb,
actor=actor,
at=_now(),
reasons=reasons,
note=note,
approval_id=memo.approval_id,
)

View file

@ -0,0 +1,239 @@
"""The local transactional outbox and the commitment records it queues.
Payload is **commitment-only**, granted for Stage 1 by `GH-DEC-2026-014`:
hashes, principal, timestamps, acks, the co-referenced approval id. Never the
brief, never the packet.
What that does and does not establish is not a detail see
`docs/specs/EvidenceModel.md` §8d:
- It satisfies **non-alteration**. It does **not** satisfy
**reconstructability**, and must never be described as doing so.
- It 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.
Which is why every record carries the §4 existence assertion.
"""
from __future__ import annotations
import json
import re
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from .disposition import Disposition
from .presentation import Presentation
class EventClass(str, Enum):
"""One source, distinct type values per class (`audit-core`, `AUDIT-IN-0003`)."""
PRESENTATION = "informed-decision.presentation"
DISPOSITION = "informed-decision.disposition"
STANCE_APPLICATION = "informed-decision.stance_application"
HEARTBEAT = "audit-core.heartbeat"
#: Heartbeat gap per class, seconds. Declared per class rather than per source:
#: a per-source heartbeat from a mixed-volume emitter is satisfied by its
#: chattiest class and says nothing about the quiet, security-relevant one.
HEARTBEAT_CLASSES: dict[str, int] = {
EventClass.PRESENTATION.value: 86400,
EventClass.DISPOSITION.value: 86400,
EventClass.STANCE_APPLICATION.value: 86400,
}
class CustodyLocatorRejected(Exception):
"""The custody locator looked secret-shaped (PR-12).
`audit-core` applies ``secret_policy: redact``, which scans ``data``. A
credentialed URL is redacted out and the existence declaration arrives
without its pointer visibly (``details.redaction.paths`` records it), but
the declaration is then useless while *looking* complete. Build-breaking,
not a warning.
"""
#: Anything carrying userinfo, a query string, or a token-shaped segment.
_SECRET_SHAPED = re.compile(
r"(://[^/@\s]*@)|([?&](token|key|secret|sig|password|access[_-]?token)=)|(\bBearer\b)",
re.IGNORECASE,
)
def assert_custody_locator_safe(locator: str) -> None:
if not locator:
raise CustodyLocatorRejected("custody locator must be non-empty")
if _SECRET_SHAPED.search(locator):
raise CustodyLocatorRejected(
f"custody locator {locator!r} is secret-shaped; use a stable "
"identifier the custodian resolves, never a credentialed URL (PR-12)"
)
def _now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
@dataclass(frozen=True)
class Commitment:
"""One emitted record. `data` is stored verbatim by `audit-core` and chained."""
id: str
event_class: EventClass
at: str
data: dict
def as_envelope(self) -> dict:
return {"type": self.event_class.value, "data": self.data}
def _existence(custody: str) -> dict:
"""The `GH-DEC-2026-014` §4 assertion.
A commitment with no assertion that something is being committed to is
indistinguishable from a commitment to nothing. Failure to produce at
retrieval is then a conformance failure attributable to the custodian
which is us.
"""
assert_custody_locator_safe(custody)
return {"content_exists": True, "custody": custody}
def commit_presentation(p: Presentation, *, custody: str) -> Commitment:
data = {
"memo_id": p.memo_id,
"memo_version": p.memo_version,
"presentation_id": p.id,
"principal_sub": p.principal_sub,
"locale": p.locale,
"ui_release": p.ui_release,
"rendered_at": p.rendered_at,
"view_hash": p.view_hash,
"awareness_hash": p.awareness_hash,
"phase": p.phase.value,
"acked_highlight_ids": sorted(p.acked_highlight_ids),
"approval_id": p.approval_id,
}
# Claims travel with their route (PR-09/PR-11), never as bare strings.
if p.tenant is not None:
data["tenant"] = {"value": p.tenant.value, "route": p.tenant.route.value}
if p.principal_type is not None:
data["principal_type"] = {
"value": p.principal_type.value,
"route": p.principal_type.route.value,
}
data.update(_existence(custody))
return Commitment(f"ev-{uuid.uuid4()}", EventClass.PRESENTATION, _now(), data)
def commit_disposition(d: Disposition, *, custody: str) -> Commitment:
data = {
"memo_id": d.memo_id,
"memo_version": d.memo_version,
"presentation_id": d.presentation_id,
"disposition_id": d.id,
"verb": d.verb.value,
"actor_sub": d.actor.sub,
"actor_kind": d.actor.kind.value,
"at": d.at,
"reasons": list(d.reasons),
"approval_id": d.approval_id,
}
data.update(_existence(custody))
return Commitment(f"ev-{uuid.uuid4()}", EventClass.DISPOSITION, _now(), data)
def commit_stance_application(
*, memo_id: str, memo_version: int, binding_level: str | None,
binding_level_state: str, stance: str, dependency: str, custody: str,
) -> Commitment:
"""A fail-closed outcome is recorded as a STANCE APPLICATION.
Never as a decline: no disposition exists, because the human did not make
one. Conflating them would put a refusal in the record that no person
authored.
"""
data = {
"memo_id": memo_id,
"memo_version": memo_version,
"binding_level": binding_level,
"binding_level_state": binding_level_state,
"stance_applied": stance,
"unreachable_dependency": dependency,
# GH-DEC-2026-010: we can show a decision was obtained and what it said;
# we cannot show access-engine said it. Pinned false until FLEX-WP-0024.
"decision_attributable": False,
"at": _now(),
}
data.update(_existence(custody))
return Commitment(f"ev-{uuid.uuid4()}", EventClass.STANCE_APPLICATION, _now(), data)
def heartbeat(event_class: EventClass) -> Commitment:
"""An ORDINARY event — same envelope, same chain.
Deliberately so: a heartbeat stored outside the chain would be the one
record that could be back-dated.
"""
return Commitment(
f"ev-{uuid.uuid4()}",
EventClass.HEARTBEAT,
_now(),
{"class": event_class.value, "assertion": "nothing-to-report"},
)
class Outbox:
"""Local, transactional. Written in the same transaction as the state change.
Emit-after-commit is a defect. The queue is local so an `audit-core` outage
never blocks a binding act the same reasoning that keeps it from blocking
a revocation upstream.
This in-memory implementation stands in for the transactional store; the
property it must preserve is that :meth:`append` cannot succeed while the
state change fails, or vice versa.
"""
def __init__(self) -> None:
self._pending: list[Commitment] = []
self._drained: list[Commitment] = []
def append(self, commitment: Commitment) -> None:
self._pending.append(commitment)
@property
def pending(self) -> tuple[Commitment, ...]:
return tuple(self._pending)
@property
def drained(self) -> tuple[Commitment, ...]:
return tuple(self._drained)
def drain(self, sink) -> int:
"""Deliver to `audit-core`. Failure leaves the record pending, never lost."""
sent = 0
while self._pending:
c = self._pending[0]
sink(c.as_envelope())
self._drained.append(self._pending.pop(0))
sent += 1
return sent
def counts_by_class(self) -> dict[str, int]:
"""Our side of reconciliation, keyed by class.
Bounded: where the emitter is compromised this count is suppressed
alongside the event, and reconciliation agrees with it. Covers loss,
outage, drain failure and accident not adversarial omission by us.
"""
counts: dict[str, int] = {}
for c in [*self._drained, *self._pending]:
counts[c.event_class.value] = counts.get(c.event_class.value, 0) + 1
return counts

263
informed_decision/memo.py Normal file
View file

@ -0,0 +1,263 @@
"""The Decision Memo object.
One question per memo. A memo carries the question, the requested act, the
binding level, the brief, a hashed packet, highlights pointing into it, and the
binding slice committing *which scope this act is being entered into*.
`binding.target` is the **act-scope**. It is never derived from the token's
`tenant` claim, which is a membership fact about the principal
(`GH-DEC-2026-013` §5, PR-08).
"""
from __future__ import annotations
from dataclasses import dataclass, field, replace
from enum import Enum
class BindingLevel(str, Enum):
ACKNOWLEDGMENT = "acknowledgment"
ORGANIZATIONAL = "organizational"
AES = "aes"
QES = "qes"
class StepKind(str, Enum):
# Weak — acknowledgment only. `accept` is illegal here on purpose.
INFORM = "inform"
COMMENT = "comment"
REVIEW = "review"
ACKNOWLEDGE = "acknowledge"
# Co-sign
MITZEICHNUNG = "mitzeichnung"
APPROVE = "approve"
# Bind
SCHLUSSZEICHNUNG = "schlusszeichnung"
SIGN = "sign"
SEAL = "seal"
@property
def is_weak(self) -> bool:
return self in (StepKind.INFORM, StepKind.COMMENT, StepKind.REVIEW, StepKind.ACKNOWLEDGE)
@property
def is_bind(self) -> bool:
return self in (StepKind.SCHLUSSZEICHNUNG, StepKind.SIGN, StepKind.SEAL)
@dataclass(frozen=True)
class PacketItem:
"""A hashed document in the packet.
Field names follow ``canonicalize.normalize_packet``, which is the governed
contract the published vectors depend on ``item_id``, not ``id``. ``label``
is UI-only and is deliberately outside the hash: renaming a document does
not change what was shown.
"""
item_id: str
label: str
hash: str
@dataclass(frozen=True)
class Highlight:
"""A pointer into the packet.
Highlights are an aid to attention, never a filter. Acknowledging them does
not narrow what is bound the whole instrument is bound (PR-12/W-highlights).
"""
id: str
item_id: str
note: str
required_ack: bool = False
severity: str = "informational"
locator: dict = field(default_factory=dict)
@dataclass(frozen=True)
class Identifier:
scheme: str
value: str
@dataclass(frozen=True)
class Principal:
"""A party in the binding slice.
A dataclass rather than a dict because the canonicalizer requires
``display_name``, ``id`` and ``kind``, and a missing one is a runtime
KeyError deep inside hashing rather than a visible defect at construction.
"""
id: str
kind: str
display_name: str
role: str | None = None
identifiers: tuple[Identifier, ...] = ()
def as_document(self) -> dict:
out: dict = {"display_name": self.display_name, "id": self.id, "kind": self.kind}
if self.role:
out["role"] = self.role
if self.identifiers:
out["identifiers"] = [
{"scheme": i.scheme, "value": i.value} for i in self.identifiers
]
return out
@dataclass(frozen=True)
class Scope:
"""The act-scope. What the person is entering, not who they are."""
kind: str
id: str
label: str
environment: str | None = None
requires_new_bind: bool = False
@dataclass(frozen=True)
class BindingSlice:
"""Committed and signed. Inside ``view_hash``."""
principal: Principal
target: Scope
terms: str | None = None
justification: str | None = None
@dataclass(frozen=True)
class Hat:
"""A role. Not a scope — `hats are not scopes` (INTENT invariant)."""
id: str
label: str
elevates: bool = False
def as_document(self) -> dict:
return {"id": self.id, "label": self.label, "elevates": self.elevates}
@dataclass(frozen=True)
class Awareness:
"""Shown on the same surface, hashed separately, never signed.
Typed for the same reason ``Principal`` is: the canonicalizer requires a
shape, and a malformed one should fail at construction rather than deep
inside hashing.
Nothing here enters ``view_hash``. Defaulting a hat to last-used is required
for situational awareness and forbidden from silently entering the signed
payload (INTENT principle 11).
"""
proposed_hat: Hat | None = None
proposed_hat_source: str | None = None
available_hats: tuple[Hat, ...] = ()
situation_note: str | None = None
def as_document(self) -> dict:
out: dict = {}
if self.proposed_hat is not None:
out["proposed_hat"] = self.proposed_hat.as_document()
if self.proposed_hat_source is not None:
out["proposed_hat_source"] = self.proposed_hat_source
if self.available_hats:
out["available_hats"] = [h.as_document() for h in self.available_hats]
if self.situation_note is not None:
out["situation_note"] = self.situation_note
return out
@dataclass(frozen=True)
class Memo:
id: str
version: int
question: str
requested_act: str
binding_level: BindingLevel
brief: str
binding: BindingSlice
step_kind: StepKind
packet: tuple[PacketItem, ...] = ()
highlights: tuple[Highlight, ...] = ()
locale: str = "en"
ui_release: str = "informed-decision@0.1.0"
#: Co-reference to the act this memo presents (GH-DEC-2026-012 R3).
#: The identifier only — never approval-engine's binding digest, which we
#: do not recompute or restate. Nesting is permitted by GH-DEC-2026-015 but
#: NOT ACTIVE; see layer.yaml nesting_permission_active.
approval_id: str | None = None
sealed: bool = False
def __post_init__(self) -> None:
if not self.question:
raise ValueError("a memo without a question does not render")
packet_ids = {p.item_id for p in self.packet}
for h in self.highlights:
if h.item_id not in packet_ids:
raise ValueError(f"highlight {h.id} points outside the packet")
@property
def required_ack_ids(self) -> frozenset[str]:
return frozenset(h.id for h in self.highlights if h.required_ack)
def next_version(self, **changes) -> "Memo":
"""A change creates version n+1. Outstanding presentations die with it."""
return replace(self, version=self.version + 1, **changes)
def binding_document(self) -> dict:
"""The document ``view_hash`` is computed over.
Note what is here and what is not: the packet and highlights are here
because they were shown; the token's ``tenant`` claim is not, because it
is not the act-scope.
"""
return {
"memo_id": self.id,
"memo_version": self.version,
"question": self.question,
"requested_act": self.requested_act,
"binding_level": self.binding_level.value,
"brief": self.brief,
"locale": self.locale,
"ui_release": self.ui_release,
"packet": [{"item_id": p.item_id, "hash": p.hash} for p in self.packet],
"highlights": [
{
"id": h.id,
"item_id": h.item_id,
"required_ack": h.required_ack,
"severity": h.severity,
"locator": h.locator,
}
for h in self.highlights
],
"binding": self._binding_document(),
}
def _binding_document(self) -> dict:
target: dict = {
"kind": self.binding.target.kind,
"id": self.binding.target.id,
"label": self.binding.target.label,
"requires_new_bind": self.binding.target.requires_new_bind,
}
if self.binding.target.environment is not None:
target["environment"] = self.binding.target.environment
out: dict = {"principal": self.binding.principal.as_document(), "target": target}
if self.binding.terms is not None:
out["terms"] = self.binding.terms
if self.binding.justification is not None:
out["justification"] = self.binding.justification
return out
def awareness_document(self, awareness: "Awareness | None" = None) -> dict:
"""Shown on the same surface, hashed separately, never signed."""
doc = {"memo_id": self.id, "memo_version": self.version, "locale": self.locale}
if awareness is not None:
doc.update(awareness.as_document())
return doc

View file

@ -0,0 +1,92 @@
"""Presentation records — the sole writer of ``view_hash``.
One writer, one canonicalizer, one place to audit. A second path that computes
a hash is a defect, not an optimisation (`ArchitectureBlueprint` §3).
A presentation is a record of an event that happened. It is append-only:
editing one is falsifying evidence.
"""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from .canonicalize import awareness_hash, view_hash
from .memo import Awareness, Memo
from .provenance import Claim
class Phase(str, Enum):
PRE_BIND = "pre_bind"
BIND = "bind"
POST_BIND = "post_bind"
def _now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
@dataclass(frozen=True)
class Presentation:
id: str
memo_id: str
memo_version: int
principal_sub: str
locale: str
ui_release: str
rendered_at: str
view_hash: str
awareness_hash: str
phase: Phase
#: Co-reference to the act (GH-DEC-2026-012 R3). The identifier only.
approval_id: str | None = None
#: Claims stored WITH their route, never as bare strings (PR-09, PR-11).
tenant: Claim | None = None
principal_type: Claim | None = None
acked_highlight_ids: frozenset[str] = field(default_factory=frozenset)
def with_ack(self, highlight_id: str) -> "Presentation":
"""Acknowledgment is an explicit act.
Never inferred from scroll position, dwell time, focus or viewport
intersection (PR-21). Only this method records one, and only a
deliberate control activation calls it.
"""
from dataclasses import replace
return replace(self, acked_highlight_ids=self.acked_highlight_ids | {highlight_id})
def render(
memo: Memo,
*,
principal_sub: str,
tenant: Claim | None = None,
principal_type: Claim | None = None,
awareness: Awareness | None = None,
phase: Phase = Phase.PRE_BIND,
) -> Presentation:
"""Render a memo, producing exactly one presentation record.
This is the only function in the package that computes ``view_hash``.
"""
binding_doc = memo.binding_document()
awareness_doc = memo.awareness_document(awareness)
return Presentation(
id=f"pres-{uuid.uuid4()}",
memo_id=memo.id,
memo_version=memo.version,
principal_sub=principal_sub,
locale=memo.locale,
ui_release=memo.ui_release,
rendered_at=_now(),
view_hash=view_hash(binding_doc)["hex"],
awareness_hash=awareness_hash(awareness_doc)["hex"],
phase=phase,
approval_id=memo.approval_id,
tenant=tenant,
principal_type=principal_type,
)

View file

@ -0,0 +1,91 @@
"""Claim provenance — A-16 applied to claims this surface consumes.
`key-cape` emits `tenant` and `principal_type` as bare strings. A consumer
cannot tell a value 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 (PR-09), and never discharges a human-in-the-loop
control on a registration-supplied assertion of humanity (PR-11,
`GH-DEC-2026-016` §5).
A-16's rider applies to this module: where the route marker is written by the
party whose conduct the route describes, it constrains a defect but not an
adversary. These markers are written by us about claims we received, which is
the semi-independent case we gain nothing by mislabelling them.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class Route(str, Enum):
"""How a claim value reached us. Never collapsed, never defaulted."""
#: Asserted by the directory about the person. Strong.
DIRECTORY = "directory-asserted"
#: Supplied by the client registration. The GH-DEC-2026-013 bounded gap.
REGISTRATION = "registration-supplied"
#: Derived from the authentication event itself.
AUTHENTICATION = "authentication-derived"
#: Present, but its route is not determinable. Never treated as any of the above.
INDETERMINATE = "indeterminate"
@dataclass(frozen=True)
class Claim:
"""A claim value with the route it arrived by.
There is no constructor that takes a value without a route. A claim whose
provenance is unknown is ``INDETERMINATE``, explicitly not defaulted to
the strongest reading.
"""
value: str
route: Route
def __post_init__(self) -> None:
if not self.value:
raise ValueError("claim value must be non-empty")
@property
def is_verified_about_the_person(self) -> bool:
"""True only where the claim was asserted about the *person*.
A registration-supplied claim says something about the client the person
came through, not about the person. `GH-DEC-2026-016` §5: refusing a
service principal while accepting an unverified assertion of humanity
moves the defect rather than closing it.
"""
return self.route in (Route.DIRECTORY, Route.AUTHENTICATION)
class HumanControlNotDischargeable(Exception):
"""Raised when a human-in-the-loop control cannot be discharged.
Not an authorization decision. This surface is not saying the actor may not
act; it is saying *this claim cannot carry that weight*.
"""
def assert_human_control_dischargeable(principal_type: Claim) -> None:
"""Guard for `GH-DEC-2026-016` §5 / PR-11.
Today `principal_type: human` is a property of the client registration, so
this raises. That is correct and deliberate: the guard exists so the
limitation is visible at the point of use rather than buried in a document.
"""
if principal_type.value != "human":
raise HumanControlNotDischargeable(
f"principal_type is {principal_type.value!r}, not 'human'"
)
if not principal_type.is_verified_about_the_person:
raise HumanControlNotDischargeable(
"principal_type 'human' arrived by "
f"{principal_type.route.value}; a human-in-the-loop control must not "
"be discharged on a claim that describes the client rather than the "
"person (GH-DEC-2026-016 §5, A-16)"
)

456
tests/test_skeleton.py Normal file
View file

@ -0,0 +1,456 @@
"""Walking skeleton — the negative cases are as load-bearing as the happy path.
Each negative case here is one from `docs/specs/UseCaseCatalog.md` and protects
a named invariant. If one of these starts passing by doing the forbidden thing,
the surface has become something this repository says it must not be.
"""
from __future__ import annotations
import pytest
from informed_decision.approval_client import (
ApprovalEngineError,
FakeApprovalEngine,
assert_scopes_permissible,
is_success,
)
from informed_decision.disposition import (
Actor,
ActorKind,
DispositionRefused,
Verb,
legal_verbs,
record,
)
from informed_decision.evidence import (
CustodyLocatorRejected,
EventClass,
Outbox,
assert_custody_locator_safe,
commit_disposition,
commit_presentation,
commit_stance_application,
heartbeat,
)
from informed_decision.memo import (
Awareness,
BindingLevel,
BindingSlice,
Hat,
Highlight,
Memo,
PacketItem,
Principal,
Scope,
StepKind,
)
from informed_decision.presentation import render
from informed_decision.provenance import (
Claim,
HumanControlNotDischargeable,
Route,
assert_human_control_dischargeable,
)
CUSTODY = "informed-decision:presentations"
HUMAN = Actor("bernd", ActorKind.PERSON)
AGENT = Actor("drafter-bot", ActorKind.AGENT)
def make_memo(**over) -> Memo:
kw = dict(
id="memo-1",
version=1,
question="Approve rotation of the production database credential for T-1183?",
requested_act="approve",
binding_level=BindingLevel.ORGANIZATIONAL,
brief="The credential is 400 days old.",
binding=BindingSlice(
principal=Principal(id="p-1", kind="person", display_name="Bernd Worsch"),
target=Scope(kind="tenant", id="tenant:acme", label="ACME", environment="prod"),
),
step_kind=StepKind.APPROVE,
packet=(PacketItem("doc-1", "Change request", "sha256:" + "a" * 64),),
highlights=(
Highlight("h-1", "doc-1", "Target is production", required_ack=True, severity="critical"),
),
approval_id="appr-1",
)
kw.update(over)
return Memo(**kw)
# -------------------------------------------------------------------------
# Happy path
# -------------------------------------------------------------------------
def test_one_approval_end_to_end_in_process():
memo = make_memo()
engine = FakeApprovalEngine({"appr-1": {"status": "requested", "required_count": 1}})
outbox = Outbox()
pres = render(
memo,
principal_sub="bernd",
tenant=Claim("tenant:platform", Route.REGISTRATION),
principal_type=Claim("human", Route.REGISTRATION),
)
outbox.append(commit_presentation(pres, custody=CUSTODY))
pres = pres.with_ack("h-1")
disp = record(memo, pres, Verb.ACCEPT, HUMAN)
outbox.append(commit_disposition(disp, custody=CUSTODY))
assert disp.reaches_approval_engine
result = engine.add_entry(memo.approval_id, "bernd")
assert result.correlation == ("appr-1", "bernd", "2026-09-10T15:00:00Z")
assert result.status == "approved"
assert len(outbox.pending) == 2
def test_the_presentation_is_reachable_from_the_correlation_triple():
"""DoD-3 — satisfied by the triple, not by a hash on the entry."""
memo = make_memo()
pres = render(memo, principal_sub="bernd").with_ack("h-1")
engine = FakeApprovalEngine({"appr-1": {"status": "requested", "required_count": 1}})
result = engine.add_entry("appr-1", "bernd")
assert pres.approval_id == result.approval_id
assert pres.principal_sub == result.subject
assert pres.view_hash
# -------------------------------------------------------------------------
# NC-01 — accept on a Kenntnisnahme step
# -------------------------------------------------------------------------
@pytest.mark.parametrize(
"kind", [StepKind.INFORM, StepKind.COMMENT, StepKind.REVIEW, StepKind.ACKNOWLEDGE]
)
def test_accept_is_absent_from_weak_steps_not_merely_disabled(kind):
assert Verb.ACCEPT not in legal_verbs(kind)
assert Verb.ACKNOWLEDGE in legal_verbs(kind)
def test_accept_on_a_weak_step_is_refused_at_the_api():
memo = make_memo(step_kind=StepKind.REVIEW, highlights=())
pres = render(memo, principal_sub="bernd")
with pytest.raises(DispositionRefused) as e:
record(memo, pres, Verb.ACCEPT, HUMAN)
assert e.value.guard == "G_STEP"
assert "Kenntnisnahme is not approval" in str(e.value)
# -------------------------------------------------------------------------
# NC-02 — bind with unacknowledged required highlights
# -------------------------------------------------------------------------
def test_bind_without_required_ack_fails_closed_and_creates_nothing():
memo = make_memo()
pres = render(memo, principal_sub="bernd")
with pytest.raises(DispositionRefused) as e:
record(memo, pres, Verb.ACCEPT, HUMAN)
assert e.value.guard == "G_ACK"
assert "h-1" in str(e.value)
def test_acknowledgment_is_explicit_never_inferred():
memo = make_memo()
pres = render(memo, principal_sub="bernd")
assert pres.acked_highlight_ids == frozenset()
assert pres.with_ack("h-1").acked_highlight_ids == {"h-1"}
# -------------------------------------------------------------------------
# NC-03 — agents never bind. There is no upstream backstop.
# -------------------------------------------------------------------------
@pytest.mark.parametrize("verb", [Verb.ACCEPT, Verb.DECLINE, Verb.ACKNOWLEDGE])
def test_agent_cannot_perform_a_binding_verb(verb):
memo = make_memo()
pres = render(memo, principal_sub="bot").with_ack("h-1")
with pytest.raises(DispositionRefused) as e:
record(memo, pres, verb, AGENT)
assert e.value.guard == "G_NOAGENT"
def test_agent_may_still_comment():
memo = make_memo()
pres = render(memo, principal_sub="bot")
assert record(memo, pres, Verb.COMMENT, AGENT).verb is Verb.COMMENT
# -------------------------------------------------------------------------
# NC-05 / NC-06 — the binding/awareness split, on live objects
# -------------------------------------------------------------------------
def test_awareness_never_enters_view_hash():
memo = make_memo()
plain = render(memo, principal_sub="bernd")
oriented = render(
memo,
principal_sub="bernd",
awareness=Awareness(
proposed_hat=Hat("hat:fc", "Finance Controller"),
proposed_hat_source="last_used",
situation_note="changed after the presentation was taken",
),
)
assert plain.view_hash == oriented.view_hash
assert plain.awareness_hash != oriented.awareness_hash
def test_changing_the_act_scope_changes_view_hash():
memo = make_memo()
before = render(memo, principal_sub="bernd").view_hash
moved = memo.next_version(
binding=BindingSlice(
principal=memo.binding.principal,
target=Scope(kind="tenant", id="tenant:beta", label="Beta GmbH"),
)
)
assert render(moved, principal_sub="bernd").view_hash != before
def test_the_tenant_claim_is_not_the_act_scope():
"""PR-08 — two facts, one field upstream; never conflated here."""
memo = make_memo()
a = render(memo, principal_sub="bernd", tenant=Claim("tenant:platform", Route.REGISTRATION))
b = render(memo, principal_sub="bernd", tenant=Claim("tenant:coulomb", Route.DIRECTORY))
assert a.view_hash == b.view_hash, "the tenant claim must not reach view_hash"
assert memo.binding.target.id == "tenant:acme"
# -------------------------------------------------------------------------
# NC-07 — no silent version upgrade
# -------------------------------------------------------------------------
def test_bind_against_a_stale_presentation_is_refused():
memo = make_memo()
stale = render(memo, principal_sub="bernd").with_ack("h-1")
advanced = memo.next_version(brief="revised")
with pytest.raises(DispositionRefused) as e:
record(advanced, stale, Verb.ACCEPT, HUMAN)
assert e.value.guard == "G_PRES"
# -------------------------------------------------------------------------
# NC-08 — return is structured, and is not decline
# -------------------------------------------------------------------------
def test_return_without_coded_reasons_is_refused():
memo = make_memo()
pres = render(memo, principal_sub="bernd")
with pytest.raises(DispositionRefused) as e:
record(memo, pres, Verb.RETURN, HUMAN, note="please clarify")
assert e.value.guard == "G_REASONS"
def test_return_is_distinguishable_from_decline_and_never_reaches_the_engine():
memo = make_memo()
pres = render(memo, principal_sub="bernd").with_ack("h-1")
returned = record(memo, pres, Verb.RETURN, HUMAN, reasons=("insufficient-context",))
declined = record(memo, pres, Verb.DECLINE, HUMAN)
assert returned.verb is not declined.verb
assert not returned.reaches_approval_engine
assert not declined.reaches_approval_engine
def test_only_accept_reaches_the_engine():
memo = make_memo()
pres = render(memo, principal_sub="bernd").with_ack("h-1")
assert record(memo, pres, Verb.ACCEPT, HUMAN).reaches_approval_engine
for verb in (Verb.COMMENT, Verb.DISCUSS, Verb.FORWARD, Verb.ESCALATE):
assert not record(memo, pres, verb, HUMAN).reaches_approval_engine
# -------------------------------------------------------------------------
# Engine semantics
# -------------------------------------------------------------------------
def test_duplicate_approver_is_success_not_failure():
assert is_success(ApprovalEngineError(409, "duplicate_approver"))
assert not is_success(ApprovalEngineError(409, "conflict"))
assert not is_success(ApprovalEngineError(503, "store_unavailable"))
def test_consume_scope_is_refused_before_a_token_is_requested():
with pytest.raises(ValueError, match="approval:consume"):
assert_scopes_permissible(("openid", "approval:read", "approval:consume"))
def test_store_unavailable_fails_closed():
engine = FakeApprovalEngine({"appr-1": {"status": "requested"}})
engine.available = False
with pytest.raises(ApprovalEngineError) as e:
engine.add_entry("appr-1", "bernd")
assert e.value.status == 503
def test_terminal_approval_conflicts():
engine = FakeApprovalEngine({"appr-1": {"status": "revoked"}})
with pytest.raises(ApprovalEngineError) as e:
engine.add_entry("appr-1", "bernd")
assert (e.value.status, e.value.reason) == (409, "conflict")
# -------------------------------------------------------------------------
# GH-DEC-2026-016 §5 / PR-11 — humanity provenance
# -------------------------------------------------------------------------
def test_human_control_not_dischargeable_on_a_registration_supplied_claim():
with pytest.raises(HumanControlNotDischargeable, match="registration-supplied"):
assert_human_control_dischargeable(Claim("human", Route.REGISTRATION))
@pytest.mark.parametrize("route", [Route.DIRECTORY, Route.AUTHENTICATION])
def test_human_control_dischargeable_when_asserted_about_the_person(route):
assert_human_control_dischargeable(Claim("human", route))
def test_service_principal_is_refused_outright():
with pytest.raises(HumanControlNotDischargeable, match="not 'human'"):
assert_human_control_dischargeable(Claim("service", Route.DIRECTORY))
def test_claims_are_stored_with_their_route_never_bare():
memo = make_memo()
pres = render(
memo,
principal_sub="bernd",
tenant=Claim("tenant:platform", Route.REGISTRATION),
principal_type=Claim("human", Route.REGISTRATION),
)
data = commit_presentation(pres, custody=CUSTODY).data
assert data["tenant"] == {"value": "tenant:platform", "route": "registration-supplied"}
assert data["principal_type"]["route"] == "registration-supplied"
# -------------------------------------------------------------------------
# GH-DEC-2026-014 §4 — the existence assertion, and PR-12
# -------------------------------------------------------------------------
def test_every_commitment_carries_the_existence_assertion():
memo = make_memo()
pres = render(memo, principal_sub="bernd").with_ack("h-1")
disp = record(memo, pres, Verb.ACCEPT, HUMAN)
for c in (
commit_presentation(pres, custody=CUSTODY),
commit_disposition(disp, custody=CUSTODY),
commit_stance_application(
memo_id=memo.id, memo_version=1, binding_level="organizational",
binding_level_state="present", stance="fail_closed",
dependency="access-engine", custody=CUSTODY,
),
):
assert c.data["content_exists"] is True
assert c.data["custody"] == CUSTODY
def test_commitment_carries_no_content():
"""Commitment-only: never the brief, never the packet."""
memo = make_memo()
data = commit_presentation(render(memo, principal_sub="bernd"), custody=CUSTODY).data
flat = str(data)
assert memo.brief not in flat
assert "Change request" not in flat
assert data["view_hash"]
@pytest.mark.parametrize(
"locator",
[
"https://user:pw@store.example/obj",
"https://store.example/obj?token=abc123",
"https://store.example/o?access_token=x",
"",
],
)
def test_secret_shaped_custody_locators_are_rejected(locator):
with pytest.raises(CustodyLocatorRejected):
assert_custody_locator_safe(locator)
def test_stable_identifier_custody_locator_is_accepted():
assert_custody_locator_safe("informed-decision:memo/memo-1/presentation/pres-9")
# -------------------------------------------------------------------------
# Stance application is not a decline
# -------------------------------------------------------------------------
def test_fail_closed_is_recorded_as_a_stance_not_as_a_decline():
c = commit_stance_application(
memo_id="memo-1", memo_version=1, binding_level=None,
binding_level_state="absent", stance="fail_closed",
dependency="access-engine", custody=CUSTODY,
)
assert c.event_class is EventClass.STANCE_APPLICATION
assert "verb" not in c.data
assert c.data["stance_applied"] == "fail_closed"
def test_stance_application_pins_decision_attributable_false():
"""GH-DEC-2026-010 inherited: we cannot show access-engine said it."""
c = commit_stance_application(
memo_id="m", memo_version=1, binding_level="organizational",
binding_level_state="present", stance="fail_closed",
dependency="access-engine", custody=CUSTODY,
)
assert c.data["decision_attributable"] is False
# -------------------------------------------------------------------------
# Outbox
# -------------------------------------------------------------------------
def test_drain_failure_leaves_the_record_pending_never_lost():
outbox = Outbox()
memo = make_memo()
outbox.append(commit_presentation(render(memo, principal_sub="b"), custody=CUSTODY))
def failing_sink(_):
raise RuntimeError("audit-core unreachable")
with pytest.raises(RuntimeError):
outbox.drain(failing_sink)
assert len(outbox.pending) == 1
assert outbox.drained == ()
def test_drain_delivers_and_counts_reconcile_per_class():
outbox = Outbox()
memo = make_memo()
pres = render(memo, principal_sub="b").with_ack("h-1")
outbox.append(commit_presentation(pres, custody=CUSTODY))
outbox.append(commit_disposition(record(memo, pres, Verb.ACCEPT, HUMAN), custody=CUSTODY))
sent: list[dict] = []
assert outbox.drain(sent.append) == 2
assert outbox.counts_by_class() == {
EventClass.PRESENTATION.value: 1,
EventClass.DISPOSITION.value: 1,
}
assert all("type" in e and "data" in e for e in sent)
def test_heartbeat_is_an_ordinary_event_with_the_same_envelope():
hb = heartbeat(EventClass.DISPOSITION)
env = hb.as_envelope()
assert env["type"] == "audit-core.heartbeat"
assert env["data"] == {
"class": "informed-decision.disposition",
"assertion": "nothing-to-report",
}

View file

@ -429,7 +429,7 @@ issuer, so it is not this repository's to decide alone.
```task
id: INFD-WP-0001-T08
status: todo
status: progress
priority: medium
state_hub_task_id: "b5c1d329-9580-5672-9640-2930cbbb729a"
@ -454,6 +454,39 @@ act is permitted.
Gated externally on `approval-engine` `APPROVAL-WP-0002-T01` reaching `done` and
on the service being deployed with an origin this surface can reach.
2026-09-10 — **domain core built and tested; the live proof remains gated.**
`approval-engine` `APPROVAL-WP-0002-T01` is still `progress` and the namespace
has no pods, so the end-to-end proof against a deployed engine cannot run. Built
everything that does not depend on it, with the engine behind a seam so its
arrival is a wiring change rather than a build:
- `memo.py` — the Decision Memo, versions, and the binding document.
`Principal`, `Scope`, `Awareness` and `Hat` are dataclasses rather than dicts
because the canonicalizer requires a shape, and a missing key should fail at
construction rather than deep inside hashing. Field names follow the governed
canonicalizer (`item_id`, `severity`, `locator`) — the vectors are the
contract, so the object was aligned to them rather than the reverse.
- `presentation.py` — the **sole writer** of `view_hash`. Acknowledgment is an
explicit method call; nothing infers it.
- `disposition.py` — the verb vocabulary and guards. `accept` is **absent** from
weak steps rather than present-and-disabled, because a greyed-out accept still
teaches the wrong model.
- `provenance.py` — claim routes (A-16). `assert_human_control_dischargeable`
refuses a registration-supplied `human`, so PR-11's limitation is visible at
the point of use rather than buried in a document.
- `evidence.py` — the local outbox, commitment-only records carrying the
`GH-DEC-2026-014` §4 existence assertion, and a custody-locator guard that
rejects secret-shaped values (PR-12).
- `approval_client.py` — Protocol plus a fake with the engine's real semantics:
`409 duplicate_approver` is success, `409 conflict` terminal, `503` fail-closed,
and `approval:consume` refused before a token is ever requested.
87 tests pass, including every negative case in the Use Case Catalog: NC-01
through NC-08, the humanity-provenance guard, the existence assertion, and that
a fail-closed outcome is recorded as a stance application with no `verb` field —
never as a decline, because the human did not make one.
Remaining for `done`: the live proof. Superseded context —
2026-09-09: additionally gated on `INFD-IN-0003``GH-DEC-2026-012` limit 3
requires the evidence copy to reach `audit-core` independently of this
component, and the payload question is open. Design and decision request in