Files
shard-wiki/src/shard_wiki/space.py
tegwick 37681d89b6 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>
2026-06-16 02:21:39 +02:00

149 lines
6.1 KiB
Python

"""InformationSpace — the thin orchestrator entry tying the slice together (blueprint §3 L6).
A root entity / information space: shards attach to it (conformance-gated, TSD §A.2), a
coordination decision log records its canonical decisions, and a derived union resolves names to
pages. This is the L6 consumer surface for the foundation slice (attach → resolve → read);
a network API is a later workplan.
"""
from __future__ import annotations
from pathlib import Path
from shard_wiki.adapters import ShardAdapter, assert_conformant
from shard_wiki.coordination import (
ApplyResult,
DecisionLog,
EventStore,
EventType,
GitEventStore,
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
from shard_wiki.views import (
AllPagesEntry,
BackLink,
ChangeEntry,
SiteMapNode,
all_pages,
build_backlinks,
recent_changes,
site_map,
)
__all__ = ["InformationSpace"]
class InformationSpace:
def __init__(
self,
space_id: str,
policy: Policy = DEFAULT_POLICY,
*,
store: EventStore | None = None,
) -> None:
"""Tie the slice together. ``store`` selects the coordination-log backend: the default
in-memory store (tests) or a git-addressable one. Use :meth:`git_backed` for the latter."""
self.space_id = space_id
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(
cls,
space_id: str,
repo_path: str | Path,
policy: Policy = DEFAULT_POLICY,
) -> InformationSpace:
"""An information space whose coordination log is git-addressable (history/patch/review/
backup — I-6). The decision log lives in the git repo at ``repo_path``."""
return cls(space_id, policy, store=GitEventStore(repo_path))
def attach(self, adapter: ShardAdapter) -> None:
"""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."""
self.log.append(
self.space_id, EventType.ALIAS_SET, {"alias": name, "target": target}, actor=actor
)
def resolve(self, name: str) -> Resolution:
return self.union.resolve(name)
def read(self, name: str) -> Page:
"""Resolve and return the page (or the canonical pick of a chorus). KeyError if red-link."""
return self.union.resolve(name).single()
def overlay(self, name: str, body: str, actor: str | None = None) -> Overlay:
"""Draft a non-destructive overlay against the resolved page (overlay-before-mutation)."""
page = self.read(name)
return self.overlays.draft(page.identity, body, page.envelope.source_rev, actor=actor)
def apply_overlay(self, overlay_id: str) -> ApplyResult:
"""Apply a draft overlay to its target shard (apply-under-drift, §8.6)."""
overlay = self.overlays.get(overlay_id)
if overlay is None:
raise KeyError(overlay_id)
adapter = self.union.shard(overlay.target.shard)
if adapter is None:
raise KeyError(overlay.target.shard)
return self.overlays.apply(overlay_id, adapter)
def edit(self, name: str, body: str, actor: str | None = None) -> ApplyResult:
"""Edit a page through the one principled path: draft an overlay, then apply it. A
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)
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 ---
def backlinks(self, name: str, *, camelcase: bool = False) -> tuple[BackLink, ...]:
"""Pages across the union that link to ``name`` (UC-18)."""
return build_backlinks(self.union, camelcase=camelcase).to(name)
def recent_changes(self, *, limit: int | None = None) -> tuple[ChangeEntry, ...]:
"""The merged newest-first change feed: coordination journal + shard signals (UC-17)."""
return recent_changes(self.union, self.log, self.space_id, limit=limit)
def all_pages(self) -> tuple[AllPagesEntry, ...]:
"""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."""
return site_map(self.union)