Compare commits

...

7 Commits

Author SHA1 Message Date
517cf1d282 feat(space): InformationSpace orchestrator + integration; foundation slice complete (WP-0007 T7)
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 <noreply@anthropic.com>
2026-06-15 10:12:38 +02:00
b44b2a74a4 feat(policy,union): policy leaf + UnionGraph resolution with chorus (WP-0007 T6)
policy/ leaf (CanonicalSource presets, default chorus). union/ UnionGraph:
identity-keyed resolve (alias-redirect via log fold → union lookup → chorus →
red-link); chorus records divergent peers in each page's provenance envelope
(union without erasure); designated-canonical orders the pick. Imports down only.
6 tests green. (blueprint §8.4, ADR-01)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:02:09 +02:00
24108b65aa feat(coordination): event-sourced DecisionLog keystone (WP-0007 T5)
Append-only, totally-ordered-per-space decision log (in-process append authority;
git+lease later) with event types overlay/binding/alias/merge/fork, and a derived
fold to CoordinationState (aliases LWW, transitively-merged equivalence groups,
open overlays). derived = f(log); read-your-writes. 6 tests green. (blueprint §8.1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:45:11 +02:00
6d48a1d3e6 feat(adapters): conformance suite — profiles verified not asserted (WP-0007 T4)
run_conformance/assert_conformant verify declared profile == observed behaviour
(profile validates, READ round-trips to the right shard, unclaimed verbs raise
NotSupported); ConformanceReport gives a precise capability diff. FolderAdapter
passes; lying-read and dishonest-write stubs fail. 3 tests green. (TSD §A.2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:42:59 +02:00
9a4e00a05a feat(adapters): ShardAdapter contract + read-only FolderAdapter (WP-0007 T3)
Versioned ShardAdapter ABC (shard_id/profile/keys/read mandatory; optional verbs
raise NotSupported by default = honest absence). FolderAdapter reads a dir of
Markdown into Pages (relpath=key, mtime=source_rev, provenance envelope),
declaring a validated read-only file-store profile. 4 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:37:02 +02:00
5a77ea879c feat(model): Identity/Placement/Span, Page, CapabilityProfile (WP-0007 T2)
Identity = stable shard-scoped handle (not a fingerprint); Placement separate;
Span carries a layered provenance delta. Page model (PageShape incl. the four
computational shapes). CapabilityProfile with orthogonal-core axes + validate()
applying §6.5 implication rules that reject impossible profiles. Imports only
provenance/. 8 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 09:00:49 +02:00
aca9bf30f9 feat(provenance): layered ProvenanceEnvelope + effective() leaf (WP-0007 T1)
Dependency-free leaf rail: ProvenanceEnvelope + SpanProvenanceDelta + effective()
(page envelope ⊕ span delta; zero-cost inheritance when uniform). Liveness/
Staleness/OverlayState enums. 6 tests green. (blueprint §7.3, §11)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 08:55:35 +02:00
25 changed files with 1633 additions and 13 deletions

View File

@@ -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 |

View File

@@ -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"]

View File

@@ -0,0 +1,22 @@
"""adapters/ — the shard adapter contract (bottom waist) and concrete adapters."""
from shard_wiki.adapters.conformance import (
Check,
ConformanceError,
ConformanceReport,
assert_conformant,
run_conformance,
)
from shard_wiki.adapters.contract import CONTRACT_VERSION, ShardAdapter
from shard_wiki.adapters.folder import FolderAdapter
__all__ = [
"ShardAdapter",
"FolderAdapter",
"CONTRACT_VERSION",
"Check",
"ConformanceReport",
"ConformanceError",
"run_conformance",
"assert_conformant",
]

View File

@@ -0,0 +1,126 @@
"""Adapter conformance — profiles are verified, not self-asserted (TSD §A.2, blueprint §6.6).
Capability-as-data (I-3) is only sound if a binding's *declared* profile matches its *observed*
behaviour. This battery exercises a binding and reports, check by check, whether claim == reality;
``assert_conformant`` gates registration. A lying/buggy profile fails here instead of silently
poisoning degradation decisions downstream.
This slice verifies the read path + honest absence of unclaimed verbs. Positive probes for
claimed write/diff/merge are deferred (they mutate) to a later workplan.
"""
from __future__ import annotations
from dataclasses import dataclass
from shard_wiki.adapters.contract import ShardAdapter
from shard_wiki.model import NotSupported, Verb
__all__ = ["Check", "ConformanceReport", "ConformanceError", "run_conformance", "assert_conformant"]
# Optional verbs whose *absence* must be honest (calling an unclaimed one raises NotSupported).
_HONEST_ABSENCE_VERBS = (Verb.WRITE, Verb.DIFF, Verb.NOTIFY)
_PROBE = {
Verb.WRITE: lambda a: a.write("__probe__", ""),
Verb.DIFF: lambda a: a.diff("__probe__", "__probe2__"),
Verb.NOTIFY: lambda a: a.notify(),
}
@dataclass(frozen=True, slots=True)
class Check:
name: str
ok: bool
detail: str = ""
@dataclass(frozen=True, slots=True)
class ConformanceReport:
adapter: str
checks: tuple[Check, ...]
@property
def ok(self) -> bool:
return all(c.ok for c in self.checks)
@property
def failures(self) -> tuple[Check, ...]:
return tuple(c for c in self.checks if not c.ok)
def diff(self) -> str:
return "; ".join(f"{c.name}: {c.detail}" for c in self.failures) or "conformant"
class ConformanceError(Exception):
def __init__(self, report: ConformanceReport) -> None:
super().__init__(f"{report.adapter} not conformant — {report.diff()}")
self.report = report
def _safe(fn, name: str, ok_detail: str = "") -> Check:
try:
return fn()
except Exception as exc: # noqa: BLE001 — a check must never crash the battery
return Check(name, False, f"unexpected error: {exc!r}")
def run_conformance(adapter: ShardAdapter) -> ConformanceReport:
checks: list[Check] = []
profile = None
def _profile_validates() -> Check:
nonlocal profile
profile = adapter.profile()
profile.validate()
return Check("profile-validates", True)
checks.append(_safe(_profile_validates, "profile-validates"))
if profile is None: # profile() or validate() failed; can't probe further meaningfully
return ConformanceReport(type(adapter).__name__, tuple(checks))
# READ is the capability floor.
checks.append(Check("supports-read", profile.supports(Verb.READ),
"" if profile.supports(Verb.READ) else "READ not declared"))
# READ round-trips: a declared-readable shard must actually read its own keys.
def _read_round_trips() -> Check:
keys = list(adapter.keys())
if not keys:
return Check("read-round-trips", True, "empty shard")
page = adapter.read(keys[0])
if page.identity.shard != adapter.shard_id:
return Check("read-round-trips", False,
f"page shard {page.identity.shard!r} != {adapter.shard_id!r}")
if not isinstance(page.body, str):
return Check("read-round-trips", False, "body is not text")
return Check("read-round-trips", True)
if profile.supports(Verb.READ):
checks.append(_safe(_read_round_trips, "read-round-trips"))
# Honest absence: an *unclaimed* optional verb must raise NotSupported when invoked.
for verb in _HONEST_ABSENCE_VERBS:
if profile.supports(verb):
continue # claimed → positive probe deferred (would mutate)
name = f"honest-absence:{verb.value}"
def _probe(v=verb, n=name) -> Check:
try:
_PROBE[v](adapter)
except NotSupported:
return Check(n, True)
except Exception as exc: # noqa: BLE001
return Check(n, False, f"raised {type(exc).__name__}, expected NotSupported")
return Check(n, False, "did not raise NotSupported though verb is unclaimed")
checks.append(_probe())
return ConformanceReport(type(adapter).__name__, tuple(checks))
def assert_conformant(adapter: ShardAdapter) -> ConformanceReport:
"""Run the battery; raise :class:`ConformanceError` if any check fails. Returns the report."""
report = run_conformance(adapter)
if not report.ok:
raise ConformanceError(report)
return report

View File

@@ -0,0 +1,52 @@
"""The shard adapter contract — the bottom narrow waist (CoreArchitectureBlueprint §6, TSD §A).
A backend participates by implementing :class:`ShardAdapter`. ``shard_id``, ``profile`` and
``read`` are mandatory; everything else is an optional capability that defaults to raising
:class:`~shard_wiki.model.NotSupported` — so a limited backend is honest about what it can't do
(graceful degradation, I-8) and core never assumes a capability it wasn't given (capability-as-
data, I-3). Declared profiles are verified by the conformance suite (T4), never taken on trust.
"""
from __future__ import annotations
import abc
from collections.abc import Iterable
from shard_wiki.model import CapabilityProfile, NotSupported, Page
__all__ = ["ShardAdapter", "CONTRACT_VERSION"]
CONTRACT_VERSION = "0.1"
class ShardAdapter(abc.ABC):
"""Versioned interface a backend implements to attach as a shard."""
contract_version: str = CONTRACT_VERSION
@property
@abc.abstractmethod
def shard_id(self) -> str:
"""Stable id scoping every Identity this shard mints."""
@abc.abstractmethod
def profile(self) -> CapabilityProfile:
"""The (to-be-verified) capability profile of this binding."""
@abc.abstractmethod
def keys(self) -> Iterable[str]:
"""The stable page keys this shard offers (the handle half of Identity)."""
@abc.abstractmethod
def read(self, key: str) -> Page:
"""Read one page by its stable key. Raises ``KeyError`` if absent."""
# --- optional capability verbs: honest NotSupported by default ---
def write(self, key: str, body: str) -> Page: # noqa: ARG002
raise NotSupported(f"{type(self).__name__} does not support write")
def diff(self, key: str, other: str) -> str: # noqa: ARG002
raise NotSupported(f"{type(self).__name__} does not support diff")
def notify(self):
raise NotSupported(f"{type(self).__name__} does not support notify")

View File

@@ -0,0 +1,91 @@
"""FolderAdapter — a read-only file-store shard over a directory of Markdown.
The home-case substrate: a plain folder of ``.md`` files. The relative path (sans extension,
``/``-separated) is the stable page **key**; the file is the page **body**; mtime gives a
freshness stamp. Read-only in this slice (overlay/write-through come later); declared profile
reflects exactly that (read-only, file-store, path addressing, no native history/query).
"""
from __future__ import annotations
from collections.abc import Iterable
from datetime import datetime, timezone
from pathlib import Path
from shard_wiki.adapters.contract import ShardAdapter
from shard_wiki.model import (
AccessGrant,
Addressing,
AttachmentMode,
CapabilityProfile,
ContentOpacity,
History,
Identity,
MergeModel,
NativeQuery,
OperationalEnvelope,
Page,
Placement,
Substrate,
Translation,
Verb,
WriteGranularity,
)
from shard_wiki.provenance import Liveness, ProvenanceEnvelope, Staleness
__all__ = ["FolderAdapter"]
class FolderAdapter(ShardAdapter):
def __init__(self, shard_id: str, root: str | Path) -> None:
self._shard_id = shard_id
self._root = Path(root)
@property
def shard_id(self) -> str:
return self._shard_id
def profile(self) -> CapabilityProfile:
return CapabilityProfile(
substrate=Substrate.FILES,
attachment_mode=AttachmentMode.FILE_STORE,
write_granularity=WriteGranularity.NONE,
content_opacity=ContentOpacity.TRANSPARENT,
operational_envelope=OperationalEnvelope.LOCAL_UNBOUNDED,
access_grant=AccessGrant.OPEN,
liveness=Liveness.STATIC,
history=History.NONE,
merge_model=MergeModel.NONE,
addressing=Addressing.PATH,
native_query=NativeQuery.NONE,
translation=Translation.NATIVE,
supported_verbs=frozenset({Verb.READ}),
).validate()
def _path_for(self, key: str) -> Path:
return self._root / f"{key}.md"
def keys(self) -> Iterable[str]:
for p in sorted(self._root.rglob("*.md")):
yield p.relative_to(self._root).with_suffix("").as_posix()
def read(self, key: str) -> Page:
path = self._path_for(key)
if not path.is_file():
raise KeyError(key)
body = path.read_text(encoding="utf-8")
mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
envelope = ProvenanceEnvelope(
source_shard=self._shard_id,
liveness=Liveness.STATIC,
staleness=Staleness.FRESH,
source_rev=mtime.isoformat(),
observed_at=datetime.now(tz=timezone.utc),
)
rel = path.relative_to(self._root).as_posix()
return Page(
identity=Identity(self._shard_id, key),
body=body,
envelope=envelope,
placements=(Placement(self._shard_id, rel),),
)

View File

@@ -0,0 +1,10 @@
"""coordination/ — the event-sourced decision log (L3, coordination-canonical state)."""
from shard_wiki.coordination.decision_log import (
CoordinationState,
DecisionEvent,
DecisionLog,
EventType,
)
__all__ = ["DecisionLog", "DecisionEvent", "EventType", "CoordinationState"]

View File

@@ -0,0 +1,124 @@
"""The event-sourced coordination decision log — the keystone (CoreArchitectureBlueprint §8.1).
Coordination-canonical state (overlays, equivalence bindings, aliases, merges, forks) is an
**append-only decision log**, not a mutable file; the queryable *current* state is a **derived
fold** of the log (tier-3 disposable). The log is **totally ordered per space** via a single
**append authority** — here an in-process counter; a git-backed, lease-held authority is a later
binding. That total order is what gives read-your-writes across readers (§8.6).
`derived = f(canonical)`: :class:`CoordinationState` is always reproducible by replaying the log.
"""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from types import MappingProxyType
from typing import Any
__all__ = ["EventType", "DecisionEvent", "CoordinationState", "DecisionLog"]
class EventType(Enum):
OVERLAY_CREATED = "overlay-created"
BINDING_MADE = "binding-made"
ALIAS_SET = "alias-set"
MERGE_DECIDED = "merge-decided"
PAGE_FORKED = "page-forked"
@dataclass(frozen=True, slots=True)
class DecisionEvent:
"""One immutable, ordered decision. ``seq`` is the per-space total order."""
seq: int
space: str
type: EventType
payload: Mapping[str, Any]
actor: str | None = None
timestamp: datetime = field(default_factory=lambda: datetime.now(tz=timezone.utc))
@dataclass(frozen=True, slots=True)
class CoordinationState:
"""The derived fold of a space's log: current aliases + equivalence groups + open overlays.
Disposable (tier-3): always recomputable from the log via :meth:`DecisionLog.fold`.
"""
aliases: Mapping[str, str]
equivalence_groups: tuple[frozenset[str], ...]
open_overlays: Mapping[str, Mapping[str, Any]]
def resolve_alias(self, name: str) -> str | None:
return self.aliases.get(name)
def equivalent_to(self, identity: str) -> frozenset[str]:
"""All identities equivalent to ``identity`` (including itself if bound), else just it."""
for group in self.equivalence_groups:
if identity in group:
return group
return frozenset({identity})
class DecisionLog:
"""In-memory append-only log, totally ordered per space (the append authority for a process).
A later binding swaps the storage for git + a per-space lease without changing this API.
"""
def __init__(self) -> None:
self._events: dict[str, list[DecisionEvent]] = {}
def append(
self,
space: str,
type: EventType,
payload: Mapping[str, Any],
actor: str | None = None,
) -> DecisionEvent:
seq = len(self._events.get(space, ()))
event = DecisionEvent(seq=seq, space=space, type=type, payload=dict(payload), actor=actor)
self._events.setdefault(space, []).append(event)
return event
def events(self, space: str) -> tuple[DecisionEvent, ...]:
"""The space's events in append (total) order. Read-your-writes: a just-appended event
is present immediately."""
return tuple(self._events.get(space, ()))
def fold(self, space: str) -> CoordinationState:
"""Replay the log into current coordination state (derived = f(log))."""
aliases: dict[str, str] = {}
overlays: dict[str, dict[str, Any]] = {}
groups: list[set[str]] = []
for event in self.events(space):
if event.type is EventType.ALIAS_SET:
aliases[event.payload["alias"]] = event.payload["target"]
elif event.type is EventType.BINDING_MADE:
_merge_group(groups, {str(m) for m in event.payload["members"]})
elif event.type is EventType.OVERLAY_CREATED:
overlays[event.payload["overlay_id"]] = dict(event.payload)
elif event.type is EventType.MERGE_DECIDED:
# A merge resolution may collapse an overlay; minimal handling for the slice.
overlays.pop(event.payload.get("overlay_id", ""), None)
elif event.type is EventType.PAGE_FORKED:
_merge_group(groups, {str(event.payload["source"]), str(event.payload["fork"])})
return CoordinationState(
aliases=MappingProxyType(dict(aliases)),
equivalence_groups=tuple(frozenset(g) for g in groups),
open_overlays=MappingProxyType({k: MappingProxyType(v) for k, v in overlays.items()}),
)
def _merge_group(groups: list[set[str]], members: set[str]) -> None:
"""Union-merge ``members`` into ``groups`` (any existing group sharing a member absorbs it)."""
touching = [g for g in groups if g & members]
for g in touching:
groups.remove(g)
members |= g
groups.append(members)

View File

@@ -0,0 +1,47 @@
"""model/ — the backend-neutral page model and capability types (the top waist).
Imports only the ``provenance`` leaf; nothing backend-specific lives here.
"""
from shard_wiki.model.capability import (
AccessGrant,
Addressing,
AttachmentMode,
CapabilityProfile,
ContentOpacity,
History,
MergeModel,
NativeQuery,
NotSupported,
OperationalEnvelope,
ProfileError,
Substrate,
Translation,
Verb,
WriteGranularity,
)
from shard_wiki.model.identity import Identity, Placement, Span
from shard_wiki.model.page import Page, PageShape
__all__ = [
"Identity",
"Placement",
"Span",
"Page",
"PageShape",
"CapabilityProfile",
"Verb",
"Substrate",
"AttachmentMode",
"WriteGranularity",
"ContentOpacity",
"OperationalEnvelope",
"AccessGrant",
"History",
"MergeModel",
"Addressing",
"NativeQuery",
"Translation",
"ProfileError",
"NotSupported",
]

View File

@@ -0,0 +1,220 @@
"""Capability profile — capability-as-data (CoreArchitectureBlueprint §6, I-3).
A binding's abilities are a *position on each capability spectrum*, not a boolean checklist.
Descriptively there are fifteen spectra (§6.1); operationally they reduce to a small
**orthogonal core** plus **implied** axes, with **implication rules that forbid impossible
profiles** (§6.5). ``CapabilityProfile.validate()`` enforces those rules; a profile that
violates one is rejected (the structural half of "capability-as-data is sound" — the behavioural
half is the conformance suite, T4).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from shard_wiki.provenance import Liveness
__all__ = [
"Verb",
"Substrate",
"AttachmentMode",
"WriteGranularity",
"ContentOpacity",
"OperationalEnvelope",
"AccessGrant",
"History",
"MergeModel",
"Addressing",
"NativeQuery",
"Translation",
"CapabilityProfile",
"ProfileError",
"NotSupported",
]
CONTRACT_VERSION = "0.1"
class ProfileError(ValueError):
"""Raised when a capability profile is internally impossible (violates §6.5 rules)."""
class NotSupported(Exception):
"""Raised by an adapter when an operation verb is not in its capability profile."""
class Verb(Enum):
READ = "read"
WRITE = "write"
DIFF = "diff"
MERGE = "merge"
LOCK = "lock"
VERSION = "version"
PUBLISH = "publish"
NOTIFY = "notify"
TRANSCLUDE_SOURCE = "transclude-source"
TRANSLATE_SYNTAX = "translate-syntax"
STRUCTURED_PAYLOAD = "structured-payload"
DERIVE_PROJECTION = "derive-projection" # gated, off by default (T18)
EXECUTE = "execute" # gated, off by default (T18)
# --- orthogonal core axes (§6.5a) ---
class Substrate(Enum):
GIT = "git"
FILES = "files"
RELATIONAL_DB = "relational-db"
EXTERNAL_API = "external-api"
CRDT = "crdt"
class WriteGranularity(Enum):
NONE = "none" # read-only
WHOLE_FILE = "whole-file"
SECTION = "section"
PER_PAGE = "per-page"
PER_BLOCK = "per-block"
class ContentOpacity(Enum):
TRANSPARENT = "transparent"
STRUCTURED_REEVALUABLE = "structured-re-evaluable"
ENCRYPTED = "encrypted"
PER_ITEM = "per-item"
PROPRIETARY_LOSSY = "proprietary-lossy"
class OperationalEnvelope(Enum):
LOCAL_UNBOUNDED = "local-unbounded"
REALTIME = "realtime"
RATE_LIMITED = "rate-limited"
class AccessGrant(Enum):
OPEN = "open"
TOKEN = "token"
OAUTH_SCOPED = "oauth-scoped"
P2P_KEY = "p2p-key"
ENTERPRISE_ACL = "enterprise-acl"
# --- implied axes (§6.5b): may be derived from the core, validated for consistency ---
class History(Enum):
NONE = "none"
INTERNAL_ONLY = "internal-only"
CRDT_LOG = "crdt-log"
OPEN_FILE = "open-file"
GIT_NATIVE = "git-native"
class MergeModel(Enum):
NONE = "none"
GIT_TEXT = "git-text"
KEEP_BOTH = "keep-both"
NATIVE_CRDT = "native-crdt"
COEXIST_RANK = "coexist-rank"
class AttachmentMode(Enum):
# NB: there is deliberately no IMAGE mode — an image/live-memory blob is never an attach
# target (I-12). It participates only via export→files.
FILE_STORE = "file-store"
GIT_IS_STORE = "git-is-store"
IN_ENGINE_HOST = "in-engine-host"
LOCAL_REST = "local-rest"
EXTERNAL_API = "external-api"
DIRECT_DB = "direct-db"
CRDT_REPLICA = "crdt-replica"
P2P = "p2p"
class Addressing(Enum):
NONE = "none"
PATH = "path"
PAGE_ID = "page-id"
SPAN = "span"
BLOCK_ID = "block-id"
STORE_UUID = "store-uuid"
TUMBLER = "tumbler"
class NativeQuery(Enum):
NONE = "none"
TEXT = "text"
DERIVED_INDEX = "derived-index"
DATALOG_GRAPH = "datalog-graph"
DB_QUERY = "db-query"
SPARQL = "sparql"
class Translation(Enum):
NATIVE = "native"
LOSSLESS = "lossless"
LOSSY_WITH_REPORT = "lossy-with-report"
@dataclass(frozen=True, slots=True)
class CapabilityProfile:
"""A verified-at-registration position on each capability axis, plus supported verbs."""
# orthogonal core
substrate: Substrate
attachment_mode: AttachmentMode
write_granularity: WriteGranularity
content_opacity: ContentOpacity
operational_envelope: OperationalEnvelope
access_grant: AccessGrant
liveness: Liveness # computational/liveness axis (15th)
# implied
history: History
merge_model: MergeModel
addressing: Addressing
native_query: NativeQuery
translation: Translation
supported_verbs: frozenset[Verb] = field(default_factory=frozenset)
contract_version: str = CONTRACT_VERSION
def supports(self, verb: Verb) -> bool:
return verb in self.supported_verbs
def validate(self) -> CapabilityProfile:
"""Apply the §6.5 implication rules; raise :class:`ProfileError` on an impossible
combination. Returns ``self`` so it can be used fluently at construction."""
rules: list[tuple[bool, str]] = [
(
Verb.READ in self.supported_verbs,
"every shard must support READ (the capability floor)",
),
(
self.attachment_mode is not AttachmentMode.GIT_IS_STORE
or (self.substrate is Substrate.GIT and self.history is History.GIT_NATIVE),
"git-is-store ⟹ substrate=git ∧ history=git-native",
),
(
self.content_opacity is not ContentOpacity.ENCRYPTED
or self.native_query is NativeQuery.NONE,
"encrypted opacity ⟹ native-query=none",
),
(
self.merge_model is not MergeModel.NATIVE_CRDT
or (
self.history is History.CRDT_LOG
and self.operational_envelope is OperationalEnvelope.REALTIME
),
"native-crdt merge ⟹ history=crdt-log ∧ envelope=realtime",
),
(
(self.write_granularity is WriteGranularity.NONE)
== (Verb.WRITE not in self.supported_verbs),
"write-granularity=none ⟺ WRITE not supported (read-only consistency)",
),
(
Verb.MERGE not in self.supported_verbs or self.merge_model is not MergeModel.NONE,
"MERGE verb ⟹ merge-model ≠ none",
),
]
broken = [msg for ok, msg in rules if not ok]
if broken:
raise ProfileError("; ".join(broken))
return self

View File

@@ -0,0 +1,60 @@
"""Identity, Placement, Span — three distinct concepts (CoreArchitectureBlueprint §7.2).
- **Identity** is a *stable handle* assigned/minted by a shard; it survives edits. It is NEVER
derived from content (a content fingerprint identifies a *version*, not a *page*).
- **Placement** is *where* an identity sits (path in a shard); one identity may have many
placements (a DAG). Placement can change without changing identity.
- **Span** is a sub-page address within an identity, optionally carrying a provenance delta.
Equivalence (detecting that two *distinct* identities hold the same content) is a separate
mechanism and lives in ``union/`` — not here.
"""
from __future__ import annotations
from dataclasses import dataclass
from shard_wiki.provenance import SpanProvenanceDelta
__all__ = ["Identity", "Placement", "Span"]
@dataclass(frozen=True, slots=True)
class Identity:
"""A shard-scoped, stable page handle. Value-equal; stable across content edits.
``shard`` scopes the handle so native ids survive projection and never collide across
shards. ``key`` is the backend's stable handle (page name / native uid) — not a fingerprint.
"""
shard: str
key: str
def __str__(self) -> str:
return f"{self.shard}:{self.key}"
@dataclass(frozen=True, slots=True)
class Placement:
"""Where an identity sits: a path within a shard. Mutable over an identity's life."""
shard: str
path: str
def __str__(self) -> str:
return f"{self.shard}/{self.path}"
@dataclass(frozen=True, slots=True)
class Span:
"""A sub-page address within a page identity, with an optional provenance delta.
``ref`` is a native span id where the backend mints one (Roam ``:block/uid``, Logseq
``id::``), else a positional/fingerprint address. ``delta`` overrides the page envelope
only where this span genuinely differs (effective-vs-own, blueprint §7.3).
"""
ref: str
start: int | None = None
end: int | None = None
delta: SpanProvenanceDelta | None = None

View File

@@ -0,0 +1,43 @@
"""The backend-neutral wiki page model — the top narrow waist (CoreArchitectureBlueprint §7).
Markdown-first but stretchable: every shape reduces to
``(content|source, structure, provenance envelope, [derivation rule])``.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from shard_wiki.model.identity import Identity, Placement, Span
from shard_wiki.provenance import ProvenanceEnvelope
__all__ = ["PageShape", "Page"]
class PageShape(Enum):
"""The page-model shapes (blueprint §7.1). The slice handles PROSE; the rest are declared
so adapters can tag content faithfully (union without erasure)."""
PROSE = "prose"
TYPED_RECORD = "typed-record"
TYPED_GRAPH = "typed-graph"
INLINE_EMBEDDED = "inline-embedded"
NON_MARKDOWN_ASSET = "non-markdown-asset"
ONE_SOURCE_MANY_PROJECTIONS = "one-source-many-projections"
NOTEBOOK = "notebook"
PROGRAM_AS_PAGE = "program-as-page"
LIVE_TEMPORAL = "live-temporal"
@dataclass(frozen=True, slots=True)
class Page:
"""A page in the union: a stable identity, content, the page-level provenance envelope,
its placements, and its spans (whose deltas layer over the page envelope, §7.3)."""
identity: Identity
body: str
envelope: ProvenanceEnvelope
shape: PageShape = PageShape.PROSE
placements: tuple[Placement, ...] = ()
spans: tuple[Span, ...] = field(default_factory=tuple)

View File

@@ -0,0 +1,36 @@
"""policy/ — the configurable policy surface, a dependency-free leaf (blueprint §10, §11).
Mechanism over policy (I-7): core mechanisms read policy *choices* from here; they never
hard-code one. This leaf holds only the presets + a pure ``resolve``-style selector. Mechanism
(how a choice is honoured) lives in ``coordination``/``union``/``projection``, never here.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
__all__ = ["CanonicalSource", "Policy", "DEFAULT_POLICY"]
class CanonicalSource(Enum):
"""Resolution policy over a divergent/equivalent set (FederationArchitecture T9). Detection
is core; this is only the *resolution* choice."""
CHORUS = "chorus" # default: present all versions, none privileged
DESIGNATED_CANONICAL = "designated-canonical"
GIT_MERGE = "git-merge"
VOTE = "vote"
OVERLAY_ONLY = "overlay-only"
@dataclass(frozen=True, slots=True)
class Policy:
"""A space's policy choices. Extended as the policy surface grows (freshness, compaction,
execution, tenant mapping — blueprint §10); the slice needs canonical-source + designation."""
canonical_source: CanonicalSource = CanonicalSource.CHORUS
designated_shard: str | None = None
DEFAULT_POLICY = Policy()

View File

@@ -0,0 +1,116 @@
"""Provenance — the cross-cutting leaf rail (CoreArchitectureBlueprint §7.3, §11).
Every artifact in the union carries a :class:`ProvenanceEnvelope`. To keep per-span cost near
zero when provenance is uniform, envelopes are **layered**: a page-level envelope plus optional
per-span deltas. The *effective* provenance of a span is ``page_envelope ⊕ span_delta`` — a span
with no delta inherits the page envelope at zero cost ("effective-vs-own", the same pattern the
page model uses for computed metadata).
This module is a **dependency-free leaf**: it imports nothing else in the package and contains
only stable data types plus the pure ``effective`` composition. Mechanism never lives here.
"""
from __future__ import annotations
import dataclasses
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
__all__ = [
"Liveness",
"Staleness",
"OverlayState",
"ProvenanceEnvelope",
"SpanProvenanceDelta",
"effective",
]
class Liveness(Enum):
"""Where a view sits on the live↔snapshot axis (blueprint §8, T18)."""
STATIC = "static"
CAPTURED_SNAPSHOT = "captured-snapshot"
LIVE_OVER_FILES = "live-over-files"
VIEW_TIME = "view-time"
IRREDUCIBLY_LIVE = "irreducibly-live"
class Staleness(Enum):
"""Freshness state of cached/projected content (blueprint §8.8). ``UNAVAILABLE`` is the
dead-shard state (FederationRequirements ADR-04)."""
LIVE = "live"
FRESH = "fresh"
STALE = "stale"
UNAVAILABLE = "unavailable"
class OverlayState(Enum):
"""Overlay lifecycle state of an artifact (FederationRequirements ADR-05)."""
NONE = "none"
DRAFT = "draft"
PATCH_PENDING = "patch-pending"
APPLIED = "applied"
@dataclass(frozen=True, slots=True)
class ProvenanceEnvelope:
"""The metadata every page/span carries — union without erasure (INTENT I-4).
This is the *page-level* (or fully-resolved *effective*) form. Per-span overrides are
expressed as a :class:`SpanProvenanceDelta` and composed via :func:`effective`.
"""
source_shard: str
liveness: Liveness = Liveness.STATIC
staleness: Staleness = Staleness.FRESH
overlay_state: OverlayState = OverlayState.NONE
source_rev: str | None = None
observed_at: datetime | None = None
authz_context: str | None = None
divergence: tuple[str, ...] = ()
lineage: str | None = None
@dataclass(frozen=True, slots=True)
class SpanProvenanceDelta:
"""A per-span override of a page envelope. Every field is optional; ``None`` (or, for
``divergence``, the sentinel default) means *inherit the page-level value*. A span with an
all-default delta — or no delta at all — costs nothing and resolves to the page envelope.
"""
source_shard: str | None = None
liveness: Liveness | None = None
staleness: Staleness | None = None
overlay_state: OverlayState | None = None
source_rev: str | None = None
observed_at: datetime | None = None
authz_context: str | None = None
# Sentinel: ``None`` means inherit; an explicit (possibly empty) tuple overrides.
divergence: tuple[str, ...] | None = None
lineage: str | None = None
def is_empty(self) -> bool:
"""True when the delta overrides nothing (the common, zero-cost case)."""
return all(getattr(self, f.name) is None for f in dataclasses.fields(self))
def effective(
base: ProvenanceEnvelope, delta: SpanProvenanceDelta | None = None
) -> ProvenanceEnvelope:
"""Compose a page envelope with an optional span delta → the span's effective provenance.
``effective(base, None)`` and ``effective(base, <empty delta>)`` both return ``base``
unchanged (zero-cost inheritance). Any non-``None`` delta field overrides ``base``.
"""
if delta is None or delta.is_empty():
return base
overrides = {
f.name: value
for f in dataclasses.fields(delta)
if (value := getattr(delta, f.name)) is not None
}
return dataclasses.replace(base, **overrides)

42
src/shard_wiki/space.py Normal file
View File

@@ -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()

View File

@@ -0,0 +1,8 @@
"""union/ — the derived-tier union (resolution, equivalence, projection read path).
Disposable/recomputable (I-2); imports down only and is imported by nothing.
"""
from shard_wiki.union.resolver import Resolution, ResolutionKind, UnionGraph
__all__ = ["UnionGraph", "Resolution", "ResolutionKind"]

View File

@@ -0,0 +1,119 @@
"""Union resolution — the derived-tier read path (CoreArchitectureBlueprint §8.4, ADR-01).
A minimal :class:`UnionGraph` over ≥1 attached shard plus the decision-log fold. ``resolve``
keys on **identity** (FederationRequirements ADR-01): alias redirect → exact union lookup →
equivalence chorus → red-link. Ambiguity returns a **chorus set** (union without erasure, I-4):
divergent peers are recorded in each page's provenance envelope rather than silently dropped.
This is derived/disposable: it reads shards + the log fold; it stores nothing canonical. Per the
dependency rule it may import down (model/adapters/coordination/policy/provenance) and is
imported by nothing.
"""
from __future__ import annotations
import dataclasses
from dataclasses import dataclass
from enum import Enum
from shard_wiki.adapters import ShardAdapter
from shard_wiki.coordination import DecisionLog
from shard_wiki.model import Page
from shard_wiki.policy import DEFAULT_POLICY, CanonicalSource, Policy
__all__ = ["ResolutionKind", "Resolution", "UnionGraph"]
class ResolutionKind(Enum):
SINGLE = "single"
CHORUS = "chorus"
RED_LINK = "red-link"
@dataclass(frozen=True, slots=True)
class Resolution:
name: str
kind: ResolutionKind
pages: tuple[Page, ...] = ()
@property
def is_red_link(self) -> bool:
return self.kind is ResolutionKind.RED_LINK
def single(self) -> Page:
"""The one page (SINGLE), or the canonical pick of a CHORUS (first), else KeyError."""
if not self.pages:
raise KeyError(self.name)
return self.pages[0]
class UnionGraph:
"""Composes attached shards + the coordination-log fold into a resolvable union."""
def __init__(
self,
space: str,
log: DecisionLog | None = None,
policy: Policy = DEFAULT_POLICY,
) -> None:
self.space = space
self.log = log or DecisionLog()
self.policy = policy
self._shards: list[ShardAdapter] = []
def attach(self, adapter: ShardAdapter) -> None:
self._shards.append(adapter)
def _shard(self, shard_id: str) -> ShardAdapter | None:
return next((s for s in self._shards if s.shard_id == shard_id), None)
def _read_all(self, key: str) -> list[Page]:
pages: list[Page] = []
for shard in self._shards:
try:
pages.append(shard.read(key))
except KeyError:
continue
return pages
def resolve(self, name: str) -> Resolution:
# 1. alias redirect (coordination-canonical, via the log fold)
state = self.log.fold(self.space)
target = state.resolve_alias(name)
if target is not None and ":" in target:
shard_id, _, key = target.partition(":")
shard = self._shard(shard_id)
if shard is not None:
try:
page = shard.read(key)
return Resolution(name, ResolutionKind.SINGLE, (page,))
except KeyError:
pass # alias dangles → fall through to normal resolution
# 2/3. union lookup by key across shards
pages = self._read_all(name)
if not pages:
return Resolution(name, ResolutionKind.RED_LINK)
if len(pages) == 1:
return Resolution(name, ResolutionKind.SINGLE, (pages[0],))
# ambiguity → chorus, with divergence recorded (never erased, I-4)
ordered = self._order_for_policy(pages)
marked = tuple(_with_divergence(p, ordered) for p in ordered)
return Resolution(name, ResolutionKind.CHORUS, marked)
def _order_for_policy(self, pages: list[Page]) -> list[Page]:
if (
self.policy.canonical_source is CanonicalSource.DESIGNATED_CANONICAL
and self.policy.designated_shard is not None
):
pages = sorted(
pages, key=lambda p: p.identity.shard != self.policy.designated_shard
)
return pages
def _with_divergence(page: Page, group: list[Page]) -> Page:
peers = tuple(str(p.identity) for p in group if p.identity != page.identity)
new_env = dataclasses.replace(page.envelope, divergence=peers)
return dataclasses.replace(page, envelope=new_env)

67
tests/test_conformance.py Normal file
View File

@@ -0,0 +1,67 @@
"""Tests for the adapter conformance suite (SHARD-WP-0007 T4)."""
from collections.abc import Iterable
import pytest
from shard_wiki.adapters import (
ConformanceError,
FolderAdapter,
ShardAdapter,
assert_conformant,
run_conformance,
)
from shard_wiki.model import CapabilityProfile, Identity, Page
from shard_wiki.provenance import ProvenanceEnvelope
def test_folder_adapter_is_conformant(tmp_path):
(tmp_path / "Home.md").write_text("# Home", encoding="utf-8")
report = assert_conformant(FolderAdapter("shardA", tmp_path))
assert report.ok
assert report.diff() == "conformant"
class _LyingReadAdapter(ShardAdapter):
"""Claims READ but read() is broken — must fail conformance."""
def __init__(self, profile: CapabilityProfile) -> None:
self._profile = profile
@property
def shard_id(self) -> str:
return "liar"
def profile(self) -> CapabilityProfile:
return self._profile
def keys(self) -> Iterable[str]:
return ["Home"]
def read(self, key: str) -> Page:
# Returns a page attributed to the WRONG shard — a lie about its own content.
return Page(Identity("someone-else", key), "x", ProvenanceEnvelope(source_shard="x"))
def test_lying_read_adapter_fails_with_precise_diff(tmp_path):
good = FolderAdapter("shardA", tmp_path).profile() # a valid read-only profile
liar = _LyingReadAdapter(good)
report = run_conformance(liar)
assert not report.ok
assert "read-round-trips" in report.diff()
with pytest.raises(ConformanceError, match="read-round-trips"):
assert_conformant(liar)
class _DishonestWriteAdapter(FolderAdapter):
"""Declares no WRITE (inherits read-only folder profile) but write() silently succeeds."""
def write(self, key: str, body: str) -> Page: # noqa: ARG002
return self.read(next(iter(self.keys())))
def test_dishonest_absence_fails(tmp_path):
(tmp_path / "Home.md").write_text("x", encoding="utf-8")
report = run_conformance(_DishonestWriteAdapter("shardA", tmp_path))
assert not report.ok
assert "honest-absence:write" in report.diff()

View File

@@ -0,0 +1,51 @@
"""Tests for the event-sourced DecisionLog (SHARD-WP-0007 T5)."""
from shard_wiki.coordination import DecisionLog, EventType
def test_append_is_totally_ordered_per_space():
log = DecisionLog()
log.append("spaceA", EventType.ALIAS_SET, {"alias": "Home", "target": "shardA:Index"})
log.append("spaceA", EventType.ALIAS_SET, {"alias": "Start", "target": "shardA:Index"})
log.append("spaceB", EventType.ALIAS_SET, {"alias": "X", "target": "shardB:Y"})
seqs = [e.seq for e in log.events("spaceA")]
assert seqs == [0, 1] # per-space monotonic
assert [e.seq for e in log.events("spaceB")] == [0] # independent ordering
def test_read_your_writes():
log = DecisionLog()
ev = log.append("s", EventType.ALIAS_SET, {"alias": "a", "target": "shardA:b"})
assert log.events("s")[-1] is ev
def test_fold_reproduces_current_state():
log = DecisionLog()
log.append("s", EventType.ALIAS_SET, {"alias": "Home", "target": "shardA:Index"})
log.append("s", EventType.ALIAS_SET, {"alias": "Home", "target": "shardB:Main"}) # LWW
state = log.fold("s")
assert state.resolve_alias("Home") == "shardB:Main"
assert state.resolve_alias("missing") is None
def test_fold_is_pure_function_of_log():
log = DecisionLog()
log.append("s", EventType.BINDING_MADE, {"members": ["shardA:Home", "shardB:Home"]})
first = log.fold("s")
second = log.fold("s")
assert first.equivalence_groups == second.equivalence_groups # derived = f(log)
def test_equivalence_groups_merge_transitively():
log = DecisionLog()
log.append("s", EventType.BINDING_MADE, {"members": ["a", "b"]})
log.append("s", EventType.BINDING_MADE, {"members": ["b", "c"]})
state = log.fold("s")
assert state.equivalent_to("a") == frozenset({"a", "b", "c"})
assert state.equivalent_to("lonely") == frozenset({"lonely"})
def test_page_fork_creates_equivalence():
log = DecisionLog()
log.append("s", EventType.PAGE_FORKED, {"source": "shardA:Doc", "fork": "shardB:Doc"})
assert log.fold("s").equivalent_to("shardA:Doc") == frozenset({"shardA:Doc", "shardB:Doc"})

View File

@@ -0,0 +1,44 @@
"""Tests for the FolderAdapter (SHARD-WP-0007 T3)."""
import pytest
from shard_wiki.adapters import FolderAdapter
from shard_wiki.model import Identity, NotSupported, Verb
def _make_shard(tmp_path, files: dict[str, str]) -> FolderAdapter:
for rel, text in files.items():
p = tmp_path / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(text, encoding="utf-8")
return FolderAdapter("shardA", tmp_path)
def test_keys_and_read(tmp_path):
shard = _make_shard(tmp_path, {"Home.md": "# Home", "sub/Topic.md": "topic body"})
assert set(shard.keys()) == {"Home", "sub/Topic"}
page = shard.read("sub/Topic")
assert page.identity == Identity("shardA", "sub/Topic")
assert page.body == "topic body"
assert page.envelope.source_shard == "shardA"
assert page.envelope.source_rev is not None # mtime stamp
assert page.placements[0].path == "sub/Topic.md"
def test_read_missing_raises_keyerror(tmp_path):
shard = _make_shard(tmp_path, {"Home.md": "x"})
with pytest.raises(KeyError):
shard.read("Nope")
def test_profile_is_valid_and_read_only(tmp_path):
shard = _make_shard(tmp_path, {"Home.md": "x"})
prof = shard.profile() # .validate() already called inside
assert prof.supports(Verb.READ)
assert not prof.supports(Verb.WRITE)
def test_unsupported_write_is_honest(tmp_path):
shard = _make_shard(tmp_path, {"Home.md": "x"})
with pytest.raises(NotSupported):
shard.write("Home", "new")

80
tests/test_integration.py Normal file
View File

@@ -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

106
tests/test_model.py Normal file
View File

@@ -0,0 +1,106 @@
"""Tests for the page model + capability profile (SHARD-WP-0007 T2)."""
import pytest
from shard_wiki.model import (
AccessGrant,
Addressing,
AttachmentMode,
CapabilityProfile,
ContentOpacity,
History,
Identity,
MergeModel,
NativeQuery,
OperationalEnvelope,
Page,
PageShape,
ProfileError,
Substrate,
Translation,
Verb,
WriteGranularity,
)
from shard_wiki.provenance import Liveness, ProvenanceEnvelope
def _read_only_folder_profile(**over) -> CapabilityProfile:
base = dict(
substrate=Substrate.FILES,
attachment_mode=AttachmentMode.FILE_STORE,
write_granularity=WriteGranularity.NONE,
content_opacity=ContentOpacity.TRANSPARENT,
operational_envelope=OperationalEnvelope.LOCAL_UNBOUNDED,
access_grant=AccessGrant.OPEN,
liveness=Liveness.STATIC,
history=History.NONE,
merge_model=MergeModel.NONE,
addressing=Addressing.PATH,
native_query=NativeQuery.NONE,
translation=Translation.NATIVE,
supported_verbs=frozenset({Verb.READ}),
)
base.update(over)
return CapabilityProfile(**base)
def test_identity_is_stable_across_content_and_value_equal():
a = Identity("shardA", "Home")
b = Identity("shardA", "Home")
assert a == b and hash(a) == hash(b)
assert str(a) == "shardA:Home"
# Identity does not depend on content: a Page edit reuses the same identity.
env = ProvenanceEnvelope(source_shard="shardA")
p1 = Page(a, "first body", env)
p2 = Page(a, "edited body", env)
assert p1.identity == p2.identity # stable handle, not a fingerprint
def test_read_only_folder_profile_validates():
assert _read_only_folder_profile().validate().supports(Verb.READ)
def test_missing_read_is_rejected():
with pytest.raises(ProfileError, match="READ"):
_read_only_folder_profile(supported_verbs=frozenset()).validate()
def test_git_is_store_requires_git_substrate_and_history():
with pytest.raises(ProfileError, match="git-is-store"):
_read_only_folder_profile(attachment_mode=AttachmentMode.GIT_IS_STORE).validate()
# consistent git-is-store profile validates
_read_only_folder_profile(
attachment_mode=AttachmentMode.GIT_IS_STORE,
substrate=Substrate.GIT,
history=History.GIT_NATIVE,
).validate()
def test_encrypted_forbids_native_query():
with pytest.raises(ProfileError, match="encrypted"):
_read_only_folder_profile(
content_opacity=ContentOpacity.ENCRYPTED, native_query=NativeQuery.DB_QUERY
).validate()
def test_write_verb_requires_write_granularity():
with pytest.raises(ProfileError, match="read-only consistency"):
_read_only_folder_profile(supported_verbs=frozenset({Verb.READ, Verb.WRITE})).validate()
# writable profile is consistent
_read_only_folder_profile(
write_granularity=WriteGranularity.PER_PAGE,
supported_verbs=frozenset({Verb.READ, Verb.WRITE}),
).validate()
def test_native_crdt_merge_requires_crdt_log_and_realtime():
with pytest.raises(ProfileError, match="native-crdt"):
_read_only_folder_profile(
merge_model=MergeModel.NATIVE_CRDT,
supported_verbs=frozenset({Verb.READ, Verb.MERGE}),
).validate()
def test_page_defaults_to_prose():
p = Page(Identity("s", "k"), "x", ProvenanceEnvelope(source_shard="s"))
assert p.shape is PageShape.PROSE

73
tests/test_provenance.py Normal file
View File

@@ -0,0 +1,73 @@
"""Tests for the provenance leaf rail (SHARD-WP-0007 T1)."""
from datetime import datetime, timezone
from shard_wiki.provenance import (
Liveness,
OverlayState,
ProvenanceEnvelope,
SpanProvenanceDelta,
Staleness,
effective,
)
def _base() -> ProvenanceEnvelope:
return ProvenanceEnvelope(
source_shard="shardA",
liveness=Liveness.STATIC,
staleness=Staleness.FRESH,
source_rev="r1",
observed_at=datetime(2026, 6, 15, tzinfo=timezone.utc),
)
def test_no_delta_returns_base_identity():
base = _base()
assert effective(base) is base
assert effective(base, SpanProvenanceDelta()) is base
def test_empty_delta_is_detected():
assert SpanProvenanceDelta().is_empty()
assert not SpanProvenanceDelta(source_shard="other").is_empty()
def test_single_field_override():
base = _base()
eff = effective(base, SpanProvenanceDelta(source_shard="shardB"))
assert eff.source_shard == "shardB"
# Untouched fields inherit from the page envelope.
assert eff.source_rev == "r1"
assert eff.liveness is Liveness.STATIC
# Base is unchanged (frozen, pure composition).
assert base.source_shard == "shardA"
def test_partial_delta_overrides_several_fields():
base = _base()
eff = effective(
base,
SpanProvenanceDelta(
staleness=Staleness.STALE,
overlay_state=OverlayState.DRAFT,
divergence=("shardB:Home",),
),
)
assert eff.staleness is Staleness.STALE
assert eff.overlay_state is OverlayState.DRAFT
assert eff.divergence == ("shardB:Home",)
assert eff.source_shard == "shardA" # inherited
def test_divergence_empty_tuple_overrides_but_none_inherits():
base = ProvenanceEnvelope(source_shard="s", divergence=("x",))
# None → inherit the page's divergence.
assert effective(base, SpanProvenanceDelta(divergence=None)).divergence == ("x",)
# Explicit empty tuple → override (clear it).
assert effective(base, SpanProvenanceDelta(divergence=())).divergence == ()
def test_envelope_is_hashable_and_value_equal():
assert _base() == _base()
assert len({_base(), _base()}) == 1

69
tests/test_union.py Normal file
View File

@@ -0,0 +1,69 @@
"""Tests for union resolution (SHARD-WP-0007 T6)."""
from shard_wiki.adapters import FolderAdapter
from shard_wiki.coordination import DecisionLog, EventType
from shard_wiki.policy import CanonicalSource, Policy
from shard_wiki.union import ResolutionKind, UnionGraph
def _shard(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_single_resolution(tmp_path):
u = UnionGraph("space")
u.attach(_shard(tmp_path, "shardA", {"Home.md": "A home"}))
res = u.resolve("Home")
assert res.kind is ResolutionKind.SINGLE
assert res.single().body == "A home"
def test_red_link_when_absent(tmp_path):
u = UnionGraph("space")
u.attach(_shard(tmp_path, "shardA", {"Home.md": "x"}))
assert u.resolve("Nope").is_red_link
def test_chorus_on_ambiguity_records_divergence(tmp_path):
u = UnionGraph("space")
u.attach(_shard(tmp_path, "shardA", {"Home.md": "A home"}))
u.attach(_shard(tmp_path, "shardB", {"Home.md": "B home"}))
res = u.resolve("Home")
assert res.kind is ResolutionKind.CHORUS
assert {p.body for p in res.pages} == {"A home", "B home"}
# Each page names its divergent peer — union without erasure.
a = next(p for p in res.pages if p.identity.shard == "shardA")
assert a.envelope.divergence == ("shardB:Home",)
def test_designated_canonical_orders_first(tmp_path):
policy = Policy(canonical_source=CanonicalSource.DESIGNATED_CANONICAL, designated_shard="shardB")
u = UnionGraph("space", policy=policy)
u.attach(_shard(tmp_path, "shardA", {"Home.md": "A"}))
u.attach(_shard(tmp_path, "shardB", {"Home.md": "B"}))
res = u.resolve("Home")
assert res.kind is ResolutionKind.CHORUS
assert res.single().identity.shard == "shardB" # designated wins the canonical pick
def test_alias_from_log_redirects(tmp_path):
log = DecisionLog()
log.append("space", EventType.ALIAS_SET, {"alias": "Start", "target": "shardA:Index"})
u = UnionGraph("space", log=log)
u.attach(_shard(tmp_path, "shardA", {"Index.md": "the index"}))
res = u.resolve("Start")
assert res.kind is ResolutionKind.SINGLE
assert res.single().body == "the index"
def test_dangling_alias_falls_through_to_red_link(tmp_path):
log = DecisionLog()
log.append("space", EventType.ALIAS_SET, {"alias": "Start", "target": "shardA:Missing"})
u = UnionGraph("space", log=log)
u.attach(_shard(tmp_path, "shardA", {"Index.md": "x"}))
assert u.resolve("Start").is_red_link

View File

@@ -4,13 +4,14 @@ 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"
updated: "2026-06-15"
depends_on:
- SHARD-WP-0002
state_hub_workstream_id: "d551fba1-9c48-4841-a9a5-a9f190f73a60"
---
# SHARD-WP-0007 — Foundation implementation
@@ -41,8 +42,9 @@ projection, authz beyond the null provider, a network API. Those are later workp
```task
id: SHARD-WP-0007-T1
status: todo
status: done
priority: high
state_hub_task_id: "be8f9efe-cfba-46d0-be91-f9b6e87bc0d2"
```
`src/shard_wiki/provenance/`: the `ProvenanceEnvelope` value type (source_shard, source_rev?,
@@ -54,8 +56,9 @@ delta). Frozen dataclasses, no tree deps. Tests: ⊕ identity (no delta), overri
```task
id: SHARD-WP-0007-T2
status: todo
status: done
priority: high
state_hub_task_id: "780ad01f-c3e1-4b49-9ae9-60e0324178a7"
```
`src/shard_wiki/model/`: `Identity` (shard-scoped stable handle, value-equal, survives edits),
@@ -68,8 +71,9 @@ identity stability vs content change; profile validation accepts/rejects.
```task
id: SHARD-WP-0007-T3
status: todo
status: done
priority: high
state_hub_task_id: "f6e35ddb-ab1e-406a-82f8-563244455f6b"
```
`src/shard_wiki/adapters/`: `AdapterContract` (a `Protocol`/ABC with the operation verbs;
@@ -82,8 +86,9 @@ git-or-none history). Imports `model/`, `provenance/`. Tests: read a temp folder
```task
id: SHARD-WP-0007-T4
status: todo
status: done
priority: medium
state_hub_task_id: "12d7fb8d-3842-4142-a462-9d1e6efe58bd"
```
`src/shard_wiki/adapters/conformance.py`: a battery that, given a binding, **verifies declared
@@ -96,8 +101,9 @@ lying stub fails with a precise diff.
```task
id: SHARD-WP-0007-T5
status: todo
status: done
priority: high
state_hub_task_id: "c87b1896-59a5-4cde-a292-1086caebd085"
```
`src/shard_wiki/coordination/`: `DecisionLog` — append-only, **totally ordered per space**
@@ -110,8 +116,9 @@ append→ordered read; fold reproduces current state; read-your-writes.
```task
id: SHARD-WP-0007-T6
status: todo
status: done
priority: high
state_hub_task_id: "fed38b60-dc0b-40cf-93e9-ab0260aa3ff9"
```
`src/shard_wiki/policy/` (leaf: presets + `resolve()` for canonical-source = chorus default);
@@ -124,8 +131,9 @@ 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"
```
A thin `shard_wiki` entry (attach shard, resolve, read) tying the slice together; an