From 517cf1d2826594fb7a09799388e7aacc5f0de2e8 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 15 Jun 2026 10:12:38 +0200 Subject: [PATCH] feat(space): InformationSpace orchestrator + integration; foundation slice complete (WP-0007 T7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InformationSpace ties the slice together: conformance-gated attach → resolve → read, with alias() recording coordination decisions in the log. Exposed from the package root. End-to-end integration test (two folder shards → union read with layered provenance + chorus + alias redirect + red-link + nonconformant-rejected). pyflakes clean, 39 tests green. Flips WP-0007 done. Co-Authored-By: Claude Opus 4.8 --- SCOPE.md | 2 +- src/shard_wiki/__init__.py | 14 +++- src/shard_wiki/provenance/__init__.py | 2 +- src/shard_wiki/space.py | 42 ++++++++++ tests/test_integration.py | 80 +++++++++++++++++++ ...SHARD-WP-0007-foundation-implementation.md | 4 +- 6 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 src/shard_wiki/space.py create mode 100644 tests/test_integration.py diff --git a/SCOPE.md b/SCOPE.md index 82035f0..8d2621d 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -17,7 +17,7 @@ Learnings update both SCOPE and INTENT where necessary. | Layer | State | |-------|-------| -| Code | Python package scaffold (`src/shard_wiki/`, smoke tests only) | +| Code | Foundation slice implemented (SHARD-WP-0007): `provenance` + `policy` leaves, `model` (Identity/Placement/Span/Page/CapabilityProfile), `adapters` (contract + FolderAdapter + conformance suite), `coordination` (event-sourced DecisionLog), `union` (resolution + chorus), `InformationSpace` orchestrator — attach→resolve→read works; 39 tests green | | Intent | `INTENT.md` established; authorization-in-core amendments drafted | | Research | yawex prior art; c2 origins; federation concepts; wikiengines overview (`research/260608-*/`); XWiki/TWiki/Foswiki deep dives (`research/260613-*/`); Xanadu + ZigZag + Roam + Obsidian + Notion + Joplin + Logseq + local-first workspaces (Anytype/AFFiNE/AppFlowy) + Trilium + Wiki.js + Federated Wiki + Wikibase + git-forge wikis + TiddlyWiki + ikiwiki + Quip + MojoMojo + Oddmuse + UseModWiki deep dives & shard-spectrum synthesis (`research/260614-*/`) | | Demand | NetKingdom integration asks captured, not yet negotiated | diff --git a/src/shard_wiki/__init__.py b/src/shard_wiki/__init__.py index 2f89c86..fb4ac04 100644 --- a/src/shard_wiki/__init__.py +++ b/src/shard_wiki/__init__.py @@ -1,10 +1,16 @@ """shard-wiki — Git-based Markdown wiki orchestrator and federation layer. -See INTENT.md for the authoritative specification of scope and boundaries. -This package orchestrates wiki-shaped content across heterogeneous *shards*; -it is not itself a wiki engine. +See INTENT.md for the authoritative specification of scope and boundaries, and +spec/CoreArchitectureBlueprint.md for the architecture. This package orchestrates +wiki-shaped content across heterogeneous *shards*; it is not itself a wiki engine. + +Foundation slice (SHARD-WP-0007): attach folder shard(s) to an +:class:`~shard_wiki.space.InformationSpace`, resolve a name through the union, and +read a page with layered provenance (chorus on ambiguity). """ +from shard_wiki.space import InformationSpace + __version__ = "0.0.0" -__all__ = ["__version__"] +__all__ = ["__version__", "InformationSpace"] diff --git a/src/shard_wiki/provenance/__init__.py b/src/shard_wiki/provenance/__init__.py index af0354f..d658c9b 100644 --- a/src/shard_wiki/provenance/__init__.py +++ b/src/shard_wiki/provenance/__init__.py @@ -13,7 +13,7 @@ only stable data types plus the pure ``effective`` composition. Mechanism never from __future__ import annotations import dataclasses -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import datetime from enum import Enum diff --git a/src/shard_wiki/space.py b/src/shard_wiki/space.py new file mode 100644 index 0000000..b8547f4 --- /dev/null +++ b/src/shard_wiki/space.py @@ -0,0 +1,42 @@ +"""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 shard_wiki.adapters import ShardAdapter, assert_conformant +from shard_wiki.coordination import DecisionLog, EventType +from shard_wiki.model import Page +from shard_wiki.policy import DEFAULT_POLICY, Policy +from shard_wiki.union import Resolution, UnionGraph + +__all__ = ["InformationSpace"] + + +class InformationSpace: + def __init__(self, space_id: str, policy: Policy = DEFAULT_POLICY) -> None: + self.space_id = space_id + self.log = DecisionLog() + self.union = UnionGraph(space_id, log=self.log, policy=policy) + + 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) + + 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() diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..5709392 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,80 @@ +"""End-to-end slice test (SHARD-WP-0007 T7): attach folder shards → resolve → read union.""" + +import shard_wiki +from shard_wiki import InformationSpace +from shard_wiki.adapters import ConformanceError, FolderAdapter, ShardAdapter +from shard_wiki.model import CapabilityProfile, Identity, Page +from shard_wiki.provenance import ProvenanceEnvelope, Staleness +from shard_wiki.union import ResolutionKind + + +def _folder(tmp_path, name, files): + root = tmp_path / name + for rel, text in files.items(): + p = root / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(text, encoding="utf-8") + return FolderAdapter(name, root) + + +def test_public_api_surface(): + assert isinstance(shard_wiki.__version__, str) and shard_wiki.__version__ + assert shard_wiki.InformationSpace is InformationSpace + + +def test_attach_resolve_read_union_with_provenance_and_chorus(tmp_path): + space = InformationSpace("team") + space.attach(_folder(tmp_path, "wikiA", {"Home.md": "A home", "OnlyA.md": "a"})) + space.attach(_folder(tmp_path, "wikiB", {"Home.md": "B home", "OnlyB.md": "b"})) + + # unique page → single, with provenance attributing the source shard + only_a = space.read("OnlyA") + assert only_a.body == "a" + assert only_a.envelope.source_shard == "wikiA" + assert only_a.envelope.staleness is Staleness.FRESH + assert only_a.envelope.observed_at is not None + + # same name in both shards → chorus, divergence recorded both ways (union without erasure) + res = space.resolve("Home") + assert res.kind is ResolutionKind.CHORUS + assert {p.envelope.source_shard for p in res.pages} == {"wikiA", "wikiB"} + for p in res.pages: + assert len(p.envelope.divergence) == 1 + + # missing → red-link + assert space.resolve("Ghost").is_red_link + + +def test_alias_redirects_across_the_union(tmp_path): + space = InformationSpace("team") + space.attach(_folder(tmp_path, "wikiA", {"Index.md": "the index"})) + space.alias("Start", "wikiA:Index") + assert space.read("Start").body == "the index" + + +class _LyingShard(ShardAdapter): + def __init__(self, profile: CapabilityProfile) -> None: + self._p = profile + + @property + def shard_id(self) -> str: + return "liar" + + def profile(self) -> CapabilityProfile: + return self._p + + def keys(self): + return ["X"] + + def read(self, key: str) -> Page: + return Page(Identity("not-liar", key), "x", ProvenanceEnvelope(source_shard="x")) + + +def test_attach_rejects_nonconformant_shard(tmp_path): + good_profile = _folder(tmp_path, "wikiA", {"Home.md": "x"}).profile() + space = InformationSpace("team") + try: + space.attach(_LyingShard(good_profile)) + raise AssertionError("expected ConformanceError") + except ConformanceError: + pass diff --git a/workplans/SHARD-WP-0007-foundation-implementation.md b/workplans/SHARD-WP-0007-foundation-implementation.md index 14f9e0e..e8cd0fa 100644 --- a/workplans/SHARD-WP-0007-foundation-implementation.md +++ b/workplans/SHARD-WP-0007-foundation-implementation.md @@ -4,7 +4,7 @@ type: workplan title: "foundation implementation — model, contract, decision log, union read" domain: whynot repo: shard-wiki -status: active +status: done owner: tegwick topic_slug: whynot created: "2026-06-15" @@ -131,7 +131,7 @@ the same name → chorus; alias from the log redirects resolution. ```task id: SHARD-WP-0007-T7 -status: todo +status: done priority: medium state_hub_task_id: "86c0b255-e373-460d-859d-a39bf6ea4ffb" ```