"""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, )