feat(coordination): git backend wiring + verbatim log migration (WP-0009 T4)
InformationSpace.git_backed(space_id, repo_path) wires the git coordination log; the default constructor stays in-memory for tests (new keyword-only store=). A one-time importer (migrate_space / import_log / JSONL export+import) replays an existing in-memory or JSON log into git verbatim — preserving seq, timestamp and actor (union-without-erasure) and refusing out-of-order import. Same fold after migration; no behavioural change to overlay/union. SCOPE updated; WP-0009 done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f0fee65cc0
commit
c731c96634
7 changed files with 186 additions and 8 deletions
|
|
@ -17,6 +17,12 @@ from shard_wiki.coordination.append_authority import (
|
|||
LeaseRegistry,
|
||||
)
|
||||
from shard_wiki.coordination.git_event_store import GitEventStore
|
||||
from shard_wiki.coordination.migration import (
|
||||
export_jsonl,
|
||||
import_jsonl,
|
||||
import_log,
|
||||
migrate_space,
|
||||
)
|
||||
from shard_wiki.coordination.overlay import (
|
||||
ApplyResult,
|
||||
ApplyStatus,
|
||||
|
|
@ -37,6 +43,10 @@ __all__ = [
|
|||
"LeaseHeld",
|
||||
"LeaseRegistry",
|
||||
"AppendAuthority",
|
||||
"import_log",
|
||||
"migrate_space",
|
||||
"export_jsonl",
|
||||
"import_jsonl",
|
||||
"serialize_event",
|
||||
"deserialize_event",
|
||||
"Overlay",
|
||||
|
|
|
|||
|
|
@ -75,6 +75,24 @@ class GitEventStore:
|
|||
return event
|
||||
raise RuntimeError(f"append contention on {space!r}: exhausted {_MAX_CAS_RETRIES} retries")
|
||||
|
||||
def import_event(self, event: DecisionEvent) -> None:
|
||||
"""Replay one pre-existing event *verbatim* (preserving seq / timestamp / actor) onto its
|
||||
space ref — the one-time migration path (SHARD-WP-0009 T4), not a live append.
|
||||
|
||||
Refuses out-of-order import so the imported chain stays a contiguous total order; preserving
|
||||
the original fields keeps provenance intact (union-without-erasure) rather than restamping.
|
||||
"""
|
||||
ref = self._ref(event.space)
|
||||
head = self._head(ref)
|
||||
expected = self._count(ref, head)
|
||||
if event.seq != expected:
|
||||
raise ValueError(
|
||||
f"out-of-order import on {event.space!r}: expected seq {expected}, got {event.seq}"
|
||||
)
|
||||
commit = self._commit_event(event, parent=head)
|
||||
if not self._cas_update(ref, new=commit, old=head):
|
||||
raise RuntimeError(f"import race on {ref}")
|
||||
|
||||
def events(self, space: str) -> tuple[DecisionEvent, ...]:
|
||||
"""The space's events oldest→newest (append/total order)."""
|
||||
ref = self._ref(space)
|
||||
|
|
|
|||
53
src/shard_wiki/coordination/migration.py
Normal file
53
src/shard_wiki/coordination/migration.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""One-time migration of a coordination log into git (SHARD-WP-0009 T4).
|
||||
|
||||
Replays an existing decision log — an in-memory store, or a JSON-lines export — into a
|
||||
:class:`GitEventStore`, preserving each event verbatim (seq / timestamp / actor) so provenance
|
||||
survives the move (union-without-erasure). After migration the same :meth:`DecisionLog.fold`
|
||||
reproduces identical coordination state; only durability changes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from shard_wiki.coordination.decision_log import (
|
||||
DecisionEvent,
|
||||
EventStore,
|
||||
deserialize_event,
|
||||
serialize_event,
|
||||
)
|
||||
from shard_wiki.coordination.git_event_store import GitEventStore
|
||||
|
||||
__all__ = ["import_log", "migrate_space", "export_jsonl", "import_jsonl"]
|
||||
|
||||
|
||||
def import_log(events: Iterable[DecisionEvent], dest: GitEventStore) -> int:
|
||||
"""Replay ``events`` (in space/seq order) into ``dest``. Returns the count imported."""
|
||||
count = 0
|
||||
for event in events:
|
||||
dest.import_event(event)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def migrate_space(source: EventStore, space: str, dest: GitEventStore) -> int:
|
||||
"""Migrate one space's log from any :class:`EventStore` into the git backend verbatim."""
|
||||
return import_log(source.events(space), dest)
|
||||
|
||||
|
||||
def export_jsonl(events: Iterable[DecisionEvent], path: str | Path) -> int:
|
||||
"""Write events as newline-delimited canonical JSON (a portable, diffable log export)."""
|
||||
count = 0
|
||||
with open(path, "wb") as handle:
|
||||
for event in events:
|
||||
handle.write(serialize_event(event) + b"\n")
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def import_jsonl(path: str | Path, dest: GitEventStore) -> int:
|
||||
"""Replay a JSON-lines export (see :func:`export_jsonl`) into the git backend."""
|
||||
with open(path, "rb") as handle:
|
||||
events = [deserialize_event(line) for line in handle if line.strip()]
|
||||
return import_log(events, dest)
|
||||
|
|
@ -8,11 +8,15 @@ a network API is a later workplan.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from shard_wiki.adapters import ShardAdapter, assert_conformant
|
||||
from shard_wiki.coordination import (
|
||||
ApplyResult,
|
||||
DecisionLog,
|
||||
EventStore,
|
||||
EventType,
|
||||
GitEventStore,
|
||||
Overlay,
|
||||
OverlayEngine,
|
||||
)
|
||||
|
|
@ -24,12 +28,31 @@ __all__ = ["InformationSpace"]
|
|||
|
||||
|
||||
class InformationSpace:
|
||||
def __init__(self, space_id: str, policy: Policy = DEFAULT_POLICY) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
space_id: str,
|
||||
policy: Policy = DEFAULT_POLICY,
|
||||
*,
|
||||
store: EventStore | None = None,
|
||||
) -> None:
|
||||
"""Tie the slice together. ``store`` selects the coordination-log backend: the default
|
||||
in-memory store (tests) or a git-addressable one. Use :meth:`git_backed` for the latter."""
|
||||
self.space_id = space_id
|
||||
self.log = DecisionLog()
|
||||
self.log = DecisionLog(store)
|
||||
self.union = UnionGraph(space_id, log=self.log, policy=policy)
|
||||
self.overlays = OverlayEngine(space_id, self.log)
|
||||
|
||||
@classmethod
|
||||
def git_backed(
|
||||
cls,
|
||||
space_id: str,
|
||||
repo_path: str | Path,
|
||||
policy: Policy = DEFAULT_POLICY,
|
||||
) -> InformationSpace:
|
||||
"""An information space whose coordination log is git-addressable (history/patch/review/
|
||||
backup — I-6). The decision log lives in the git repo at ``repo_path``."""
|
||||
return cls(space_id, policy, store=GitEventStore(repo_path))
|
||||
|
||||
def attach(self, adapter: ShardAdapter) -> None:
|
||||
"""Attach a shard — only if it passes conformance (verified profile, I-3/§6.6)."""
|
||||
assert_conformant(adapter)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue