generated from coulomb/repo-seed
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>
149 lines
6.1 KiB
Python
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)
|