File size: 2,039 Bytes
795f737 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | """Canonical semantic state; hashes are identifiers, never embeddings."""
from dataclasses import dataclass, asdict
import hashlib
import json
def canonical(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value):
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def normalize(text):
return " ".join(text.split())
@dataclass(frozen=True)
class Element:
ref: str
role: str
name: str
path: str
visible: bool = True
enabled: bool = True
sensitive: bool = False
def semantic(self):
return dict(role=self.role, name="[REDACTED]" if self.sensitive else normalize(self.name),
path=self.path, visible=self.visible, enabled=self.enabled,
sensitive=self.sensitive)
@property
def signature(self):
return digest(self.semantic())
@dataclass(frozen=True)
class State:
document_id: str
revision: int
origin: str
elements: tuple[Element, ...]
@property
def state_hash(self):
# Order is meaningful for ordinal tasks. Ephemeral refs are excluded.
return digest(dict(origin=self.origin, elements=[e.semantic() for e in self.elements]))
def compact(self):
return dict(document=self.document_id, revision=self.revision, hash=self.state_hash,
elements=[dict(ref=e.ref, **e.semantic()) for e in self.elements])
def delta(self, previous):
if self.document_id != previous.document_id:
return dict(reset=self.compact())
before = {e.ref: e for e in previous.elements}
after = {e.ref: e for e in self.elements}
return dict(base=previous.state_hash, hash=self.state_hash, revision=self.revision,
removed=sorted(before.keys() - after.keys()),
changed=[dict(ref=r, **e.semantic()) for r, e in after.items()
if r not in before or e != before[r]],
order=list(after))
|