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 | | 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 | | 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-*/`) | | 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 | | Demand | NetKingdom integration asks captured, not yet negotiated |

View File

@@ -1,10 +1,16 @@
"""shard-wiki — Git-based Markdown wiki orchestrator and federation layer. """shard-wiki — Git-based Markdown wiki orchestrator and federation layer.
See INTENT.md for the authoritative specification of scope and boundaries. See INTENT.md for the authoritative specification of scope and boundaries, and
This package orchestrates wiki-shaped content across heterogeneous *shards*; spec/CoreArchitectureBlueprint.md for the architecture. This package orchestrates
it is not itself a wiki engine. 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" __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" title: "foundation implementation — model, contract, decision log, union read"
domain: whynot domain: whynot
repo: shard-wiki repo: shard-wiki
status: active status: done
owner: tegwick owner: tegwick
topic_slug: whynot topic_slug: whynot
created: "2026-06-15" created: "2026-06-15"
updated: "2026-06-15" updated: "2026-06-15"
depends_on: depends_on:
- SHARD-WP-0002 - SHARD-WP-0002
state_hub_workstream_id: "d551fba1-9c48-4841-a9a5-a9f190f73a60"
--- ---
# SHARD-WP-0007 — Foundation implementation # SHARD-WP-0007 — Foundation implementation
@@ -41,8 +42,9 @@ projection, authz beyond the null provider, a network API. Those are later workp
```task ```task
id: SHARD-WP-0007-T1 id: SHARD-WP-0007-T1
status: todo status: done
priority: high priority: high
state_hub_task_id: "be8f9efe-cfba-46d0-be91-f9b6e87bc0d2"
``` ```
`src/shard_wiki/provenance/`: the `ProvenanceEnvelope` value type (source_shard, source_rev?, `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 ```task
id: SHARD-WP-0007-T2 id: SHARD-WP-0007-T2
status: todo status: done
priority: high priority: high
state_hub_task_id: "780ad01f-c3e1-4b49-9ae9-60e0324178a7"
``` ```
`src/shard_wiki/model/`: `Identity` (shard-scoped stable handle, value-equal, survives edits), `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 ```task
id: SHARD-WP-0007-T3 id: SHARD-WP-0007-T3
status: todo status: done
priority: high priority: high
state_hub_task_id: "f6e35ddb-ab1e-406a-82f8-563244455f6b"
``` ```
`src/shard_wiki/adapters/`: `AdapterContract` (a `Protocol`/ABC with the operation verbs; `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 ```task
id: SHARD-WP-0007-T4 id: SHARD-WP-0007-T4
status: todo status: done
priority: medium 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 `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 ```task
id: SHARD-WP-0007-T5 id: SHARD-WP-0007-T5
status: todo status: done
priority: high priority: high
state_hub_task_id: "c87b1896-59a5-4cde-a292-1086caebd085"
``` ```
`src/shard_wiki/coordination/`: `DecisionLog` — append-only, **totally ordered per space** `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 ```task
id: SHARD-WP-0007-T6 id: SHARD-WP-0007-T6
status: todo status: done
priority: high priority: high
state_hub_task_id: "fed38b60-dc0b-40cf-93e9-ab0260aa3ff9"
``` ```
`src/shard_wiki/policy/` (leaf: presets + `resolve()` for canonical-source = chorus default); `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 ```task
id: SHARD-WP-0007-T7 id: SHARD-WP-0007-T7
status: todo status: done
priority: medium 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 A thin `shard_wiki` entry (attach shard, resolve, read) tying the slice together; an