93 lines
2.7 KiB
Python
93 lines
2.7 KiB
Python
|
|
"""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,
|
||
|
|
)
|