feat(incremental): wire maintained tier behind views; rebuild fallback (WP-0011 T4)
Route InformationSpace.all_pages through a maintained UnionIndex: equivalence is served from the incrementally maintained index (curator bindings re-synced live from the log fold + detected content edges), exposed in decision-log string form so results are a behaviour-preserving superset. The index is built lazily and rebuilt (bounded fallback) when the union mutates (attach/edit invalidate it); reindex() forces a rebuild and verify_index() runs the I-2 self-healing checker. all_pages() gains an optional equivalence_groups source (default = fold) so direct callers are unaffected. SCOPE updated; WP-0011 done. 173 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a8e65235a8
commit
37681d89b6
8 changed files with 219 additions and 13 deletions
|
|
@ -22,6 +22,7 @@ from shard_wiki.incremental.minhash import (
|
|||
jaccard,
|
||||
shingles,
|
||||
)
|
||||
from shard_wiki.incremental.union_index import UnionIndex
|
||||
from shard_wiki.incremental.verification import (
|
||||
ConsistencyChecker,
|
||||
ConsistencyReport,
|
||||
|
|
@ -41,4 +42,5 @@ __all__ = [
|
|||
"region_digest",
|
||||
"ConsistencyReport",
|
||||
"ConsistencyChecker",
|
||||
"UnionIndex",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -134,6 +134,10 @@ class EquivalenceIndex:
|
|||
def unbind(self, a: Identity, b: Identity) -> None:
|
||||
self._curator_edges.discard(_pair(a, b))
|
||||
|
||||
def set_curator_edges(self, edges: Iterable[tuple[Identity, Identity]]) -> None:
|
||||
"""Replace all curator edges at once (re-syncing from the decision-log fold)."""
|
||||
self._curator_edges = {_pair(a, b) for a, b in edges if a != b}
|
||||
|
||||
# -- queries -------------------------------------------------------------
|
||||
|
||||
def identities(self) -> frozenset[Identity]:
|
||||
|
|
|
|||
91
src/shard_wiki/incremental/union_index.py
Normal file
91
src/shard_wiki/incremental/union_index.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"""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
|
||||
|
|
@ -20,6 +20,7 @@ from shard_wiki.coordination import (
|
|||
Overlay,
|
||||
OverlayEngine,
|
||||
)
|
||||
from shard_wiki.incremental import ConsistencyReport, UnionIndex
|
||||
from shard_wiki.model import Page
|
||||
from shard_wiki.policy import DEFAULT_POLICY, Policy
|
||||
from shard_wiki.union import Resolution, UnionGraph
|
||||
|
|
@ -51,6 +52,8 @@ class InformationSpace:
|
|||
self.log = DecisionLog(store)
|
||||
self.union = UnionGraph(space_id, log=self.log, policy=policy)
|
||||
self.overlays = OverlayEngine(space_id, self.log)
|
||||
self._index: UnionIndex | None = None # maintained derived tier, built lazily
|
||||
self._index_stale = True
|
||||
|
||||
@classmethod
|
||||
def git_backed(
|
||||
|
|
@ -67,6 +70,7 @@ class InformationSpace:
|
|||
"""Attach a shard — only if it passes conformance (verified profile, I-3/§6.6)."""
|
||||
assert_conformant(adapter)
|
||||
self.union.attach(adapter)
|
||||
self._index_stale = True
|
||||
|
||||
def alias(self, name: str, target: str, actor: str | None = None) -> None:
|
||||
"""Record a coordination-canonical alias (``name`` → ``"shard:key"``) in the log."""
|
||||
|
|
@ -101,7 +105,29 @@ class InformationSpace:
|
|||
write-through-capable target fast-forwards (write-through); a read-only target keeps the
|
||||
draft as local truth (I-5: overlay before mutation, always)."""
|
||||
overlay = self.overlay(name, body, actor=actor)
|
||||
return self.apply_overlay(overlay.overlay_id)
|
||||
result = self.apply_overlay(overlay.overlay_id)
|
||||
self._index_stale = True # the applied edit changes the derived tier
|
||||
return result
|
||||
|
||||
# --- maintained derived tier (SHARD-WP-0011): incremental-first, rebuild as fallback ---
|
||||
|
||||
@property
|
||||
def index(self) -> UnionIndex:
|
||||
"""The maintained equivalence index (built lazily; rebuilt when the union has changed)."""
|
||||
if self._index is None:
|
||||
self._index = UnionIndex(self.union, self.log, self.space_id)
|
||||
elif self._index_stale:
|
||||
self._index.rebuild() # bounded fallback after a mutation
|
||||
self._index_stale = False
|
||||
return self._index
|
||||
|
||||
def reindex(self) -> None:
|
||||
"""Force a full rebuild of the maintained derived tier (the explicit fallback path)."""
|
||||
self.index.rebuild()
|
||||
|
||||
def verify_index(self) -> ConsistencyReport:
|
||||
"""Run the I-2 consistency-checker over the maintained tier; self-heal any drift."""
|
||||
return self.index.verify()
|
||||
|
||||
# --- derived views (SHARD-WP-0010): recomputable, provenance-carrying, presentation-free ---
|
||||
|
||||
|
|
@ -114,8 +140,8 @@ class InformationSpace:
|
|||
return recent_changes(self.union, self.log, self.space_id, limit=limit)
|
||||
|
||||
def all_pages(self) -> tuple[AllPagesEntry, ...]:
|
||||
"""The union's distinct pages, chorus/equivalence-collapsed with divergence noted."""
|
||||
return all_pages(self.union)
|
||||
"""The union's distinct pages, collapsed via the maintained equivalence index."""
|
||||
return all_pages(self.union, equivalence_groups=self.index.equivalence_groups())
|
||||
|
||||
def site_map(self) -> SiteMapNode:
|
||||
"""The union namespace tree built from page placements."""
|
||||
|
|
|
|||
|
|
@ -62,8 +62,16 @@ class _UnionFind:
|
|||
self._parent[max(ra, rb)] = min(ra, rb)
|
||||
|
||||
|
||||
def all_pages(union: UnionGraph) -> tuple[AllPagesEntry, ...]:
|
||||
"""Enumerate the union's distinct pages, collapsing chorus + equivalence-bound members."""
|
||||
def all_pages(
|
||||
union: UnionGraph,
|
||||
equivalence_groups: tuple[frozenset[str], ...] | None = None,
|
||||
) -> tuple[AllPagesEntry, ...]:
|
||||
"""Enumerate the union's distinct pages, collapsing chorus + equivalence-bound members.
|
||||
|
||||
``equivalence_groups`` (string identities, decision-log form) overrides the source of
|
||||
equivalence — the orchestrator passes the maintained index's groups (SHARD-WP-0011 T4); the
|
||||
default falls back to the decision-log fold, so direct callers are unaffected.
|
||||
"""
|
||||
pages: dict[str, Page] = {}
|
||||
by_key: dict[str, list[str]] = {}
|
||||
for page in union.iter_pages():
|
||||
|
|
@ -77,8 +85,9 @@ def all_pages(union: UnionGraph) -> tuple[AllPagesEntry, ...]:
|
|||
for idents in by_key.values(): # same key across shards → chorus
|
||||
for other in idents[1:]:
|
||||
uf.union(idents[0], other)
|
||||
fold = union.log.fold(union.space)
|
||||
for group in fold.equivalence_groups: # decision-log bindings
|
||||
if equivalence_groups is None:
|
||||
equivalence_groups = union.log.fold(union.space).equivalence_groups
|
||||
for group in equivalence_groups: # curator bindings (+ maintained content edges)
|
||||
present = [m for m in group if m in pages]
|
||||
for other in present[1:]:
|
||||
uf.union(present[0], other)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue