"""UnionIndex — the maintained derived tier wired behind resolution + views (SHARD-WP-0011 T4). Wraps a :class:`UnionGraph` + decision log with an incrementally maintained :class:`EquivalenceIndex`. Content equivalence is kept fresh by deltas (``note_change`` / ``note_removed``); curator bindings are re-synced live from the log fold. A full :meth:`rebuild` is the bounded fallback. :meth:`verify` runs the I-2 consistency-checker over the live source. Consumer-visible results are unchanged — equivalence groups are exposed in the same string form the decision-log fold uses, a *superset* that additionally collapses genuine content duplicates — only freshness and cost differ (recompute-on-read becomes change-driven). """ from __future__ import annotations from shard_wiki.coordination import DecisionLog from shard_wiki.incremental.equivalence import EquivalenceIndex from shard_wiki.incremental.verification import ( ConsistencyChecker, ConsistencyReport, derived_digest, ) from shard_wiki.model import Identity, Page from shard_wiki.union import UnionGraph __all__ = ["UnionIndex"] def _identity(token: str) -> Identity: shard, _, key = token.partition(":") return Identity(shard, key) class UnionIndex: """An incrementally maintained equivalence index over a union, with a rebuild fallback.""" def __init__(self, union: UnionGraph, log: DecisionLog, space: str) -> None: self._union = union self._log = log self._space = space self._eq = EquivalenceIndex() self.rebuild() def rebuild(self) -> None: """The bounded fallback: re-derive the whole index from current union pages + bindings.""" self._eq.build(self._union.iter_pages()) self._sync_curator() def note_change(self, page: Page) -> None: """Change-driven update for one added/edited page (the operational path).""" self._eq.update(page) def note_removed(self, identity: Identity) -> None: self._eq.remove(identity) def _sync_curator(self) -> None: """Re-sync curator equivalence from the live decision-log fold (cheap, always correct).""" groups = self._log.fold(self._space).equivalence_groups edges: list[tuple[Identity, Identity]] = [] for group in groups: members = [_identity(m) for m in group] edges.extend((members[0], other) for other in members[1:]) self._eq.set_curator_edges(edges) def equivalence_groups(self) -> tuple[frozenset[str], ...]: """Equivalence groups in decision-log string form (curator ∪ content), for the views.""" self._sync_curator() return tuple( frozenset(str(identity) for identity in group) for group in self._eq.groups() ) def digest(self) -> str: """The Merkle-style digest of the maintained derived tier (I-2).""" self._sync_curator() return derived_digest(self._eq) def verify(self) -> ConsistencyReport: """Check the maintained index against a from-scratch fold of the live source; self-heal.""" self._sync_curator() checker = ConsistencyChecker( self._eq, pages=lambda: list(self._union.iter_pages()), curator_edges=self._curator_pairs, ) return checker.check_and_repair() def _curator_pairs(self) -> list[tuple[Identity, Identity]]: pairs: list[tuple[Identity, Identity]] = [] for group in self._log.fold(self._space).equivalence_groups: members = [_identity(m) for m in group] pairs.extend((members[0], other) for other in members[1:]) return pairs