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