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:
parent
4379576abd
commit
9e1f77e32b
18 changed files with 1503 additions and 10 deletions
92
informed_decision/presentation.py
Normal file
92
informed_decision/presentation.py
Normal 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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue