generated from coulomb/repo-seed
feat(adapters): GitShardAdapter history adopt + cross-substrate integration (WP-0012 T3)
Adopt git-native history (TSD §A.5): a VERSION-gated history(key) surfaces the commit list for a path (newest-first sha + subject) — declared by every git-IS-store shard, read-only or not. Integration proves the union/overlay/edit machinery works unchanged across folder + git substrates: resolve/chorus span both, edit through a git shard fast-forwards as a commit, apply-under-drift refuses on an external commit (sha drift) without clobbering, and a read-only git target keeps the overlay as a draft. SCOPE updated; WP-0012 done. 196 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2
SCOPE.md
2
SCOPE.md
@@ -17,7 +17,7 @@ Learnings update both SCOPE and INTENT where necessary.
|
||||
|
||||
| Layer | State |
|
||||
|-------|-------|
|
||||
| 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, overlay-aware), `InformationSpace` orchestrator. Write path added (SHARD-WP-0008): writable adapter, overlay engine (draft→patch→apply-under-drift), edit() unifies write-through + overlay-before-mutation. Native engine implemented (SHARD-WP-0014): `engine` (kernel + typed-extension runtime + per-shard activation [ADR-0001] + capability-profile-from-extensions + EngineShardAdapter + the `ext.struct` built-in) — an engine shard attaches to an InformationSpace as a canonical-mode shard. Git-backed coordination log (SHARD-WP-0009): `DecisionLog` storage factored behind an `EventStore`; `GitEventStore` makes the log git-addressable (each space a ref, append = immutable CAS-guarded commit), a per-space `AppendAuthority` (lease) gives a single-writer total order with re-grantable HA hand-off, cross-process read-your-writes verified, and a verbatim one-time importer (`migrate_space`/JSONL) replays in-memory logs into git; `InformationSpace.git_backed(...)` wires it. Derived views (SHARD-WP-0010): `views` (wikilink + red-link model, BackLinks, RecentChanges, AllPages/SiteMap) — recomputable, provenance-carrying, presentation-free, exposed via `InformationSpace.backlinks/recent_changes/all_pages/site_map`. Incremental-first derived tier (SHARD-WP-0011): `incremental` (indexed equivalence via MinHash/LSH blocking + verify, change-driven delta maintenance with retraction/propagation, Merkle-style digest + self-healing I-2 consistency-checker, `UnionIndex` routed behind `InformationSpace.all_pages` with rebuild as explicit fallback). 173 tests green, ~97% coverage |
|
||||
| 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, overlay-aware), `InformationSpace` orchestrator. Write path added (SHARD-WP-0008): writable adapter, overlay engine (draft→patch→apply-under-drift), edit() unifies write-through + overlay-before-mutation. Native engine implemented (SHARD-WP-0014): `engine` (kernel + typed-extension runtime + per-shard activation [ADR-0001] + capability-profile-from-extensions + EngineShardAdapter + the `ext.struct` built-in) — an engine shard attaches to an InformationSpace as a canonical-mode shard. Git-backed coordination log (SHARD-WP-0009): `DecisionLog` storage factored behind an `EventStore`; `GitEventStore` makes the log git-addressable (each space a ref, append = immutable CAS-guarded commit), a per-space `AppendAuthority` (lease) gives a single-writer total order with re-grantable HA hand-off, cross-process read-your-writes verified, and a verbatim one-time importer (`migrate_space`/JSONL) replays in-memory logs into git; `InformationSpace.git_backed(...)` wires it. Derived views (SHARD-WP-0010): `views` (wikilink + red-link model, BackLinks, RecentChanges, AllPages/SiteMap) — recomputable, provenance-carrying, presentation-free, exposed via `InformationSpace.backlinks/recent_changes/all_pages/site_map`. Incremental-first derived tier (SHARD-WP-0011): `incremental` (indexed equivalence via MinHash/LSH blocking + verify, change-driven delta maintenance with retraction/propagation, Merkle-style digest + self-healing I-2 consistency-checker, `UnionIndex` routed behind `InformationSpace.all_pages` with rebuild as explicit fallback). Second adapter (SHARD-WP-0012): `GitShardAdapter` — git-IS-store substrate (read=tracked *.md, write=commit, current_rev=per-path sha for drift, adopted git-native history), passes conformance, works across folder+git shards in union/overlay/edit with no core change (capability-as-data proven on a second substrate). 196 tests green, ~97% coverage |
|
||||
| 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 |
|
||||
|
||||
@@ -9,12 +9,13 @@ from shard_wiki.adapters.conformance import (
|
||||
)
|
||||
from shard_wiki.adapters.contract import CONTRACT_VERSION, ShardAdapter
|
||||
from shard_wiki.adapters.folder import FolderAdapter
|
||||
from shard_wiki.adapters.git import GitShardAdapter
|
||||
from shard_wiki.adapters.git import GitShardAdapter, PageRevision
|
||||
|
||||
__all__ = [
|
||||
"ShardAdapter",
|
||||
"FolderAdapter",
|
||||
"GitShardAdapter",
|
||||
"PageRevision",
|
||||
"CONTRACT_VERSION",
|
||||
"Check",
|
||||
"ConformanceReport",
|
||||
|
||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import subprocess
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from shard_wiki.adapters.contract import ShardAdapter
|
||||
@@ -40,7 +41,15 @@ from shard_wiki.model import (
|
||||
)
|
||||
from shard_wiki.provenance import Liveness, ProvenanceEnvelope, Staleness
|
||||
|
||||
__all__ = ["GitShardAdapter"]
|
||||
__all__ = ["GitShardAdapter", "PageRevision"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PageRevision:
|
||||
"""One adopted git-native revision of a page: the commit sha and its subject line."""
|
||||
|
||||
sha: str
|
||||
message: str
|
||||
|
||||
_GIT_IDENTITY = {
|
||||
"GIT_AUTHOR_NAME": "shard-wiki",
|
||||
@@ -66,11 +75,12 @@ class GitShardAdapter(ShardAdapter):
|
||||
return self._shard_id
|
||||
|
||||
def profile(self) -> CapabilityProfile:
|
||||
# Write is a commit; VERSION is git-native history (adopt, §A.5). Read-only drops both.
|
||||
verbs = {Verb.READ}
|
||||
# VERSION is always available — a git-IS-store has git-native history to adopt (§A.5),
|
||||
# read-only or not. WRITE (= commit, PER_PAGE) is added only in writable mode.
|
||||
verbs = {Verb.READ, Verb.VERSION}
|
||||
granularity = WriteGranularity.NONE
|
||||
if self._writable:
|
||||
verbs |= {Verb.WRITE, Verb.VERSION}
|
||||
verbs |= {Verb.WRITE}
|
||||
granularity = WriteGranularity.PER_PAGE
|
||||
return CapabilityProfile(
|
||||
substrate=Substrate.GIT,
|
||||
@@ -133,6 +143,23 @@ class GitShardAdapter(ShardAdapter):
|
||||
sha = self._git("log", "-1", "--format=%H", "--", rel).decode().strip()
|
||||
return sha or None
|
||||
|
||||
def history(self, key: str) -> tuple[PageRevision, ...]:
|
||||
"""Adopt git-native history (§A.5): the commit list for ``key``'s path, newest-first.
|
||||
|
||||
VERSION-gated; raises ``KeyError`` for an unknown page. Each revision is a commit sha +
|
||||
subject — the native log surfaced through the contract, not re-implemented.
|
||||
"""
|
||||
if not self.profile().supports(Verb.VERSION):
|
||||
raise NotSupported(f"{type(self).__name__} does not support version")
|
||||
if not self._path_for(key).is_file():
|
||||
raise KeyError(key)
|
||||
out = self._git("log", "--format=%H%x00%s", "--", f"{key}.md").decode()
|
||||
revisions = []
|
||||
for line in out.splitlines():
|
||||
sha, _, message = line.partition("\x00")
|
||||
revisions.append(PageRevision(sha=sha, message=message))
|
||||
return tuple(revisions)
|
||||
|
||||
# -- git plumbing --------------------------------------------------------
|
||||
|
||||
def _path_for(self, key: str) -> Path:
|
||||
|
||||
116
tests/test_git_adapter_integration.py
Normal file
116
tests/test_git_adapter_integration.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""GitShardAdapter history adopt + cross-substrate integration (SHARD-WP-0012 T3)."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from shard_wiki.adapters import FolderAdapter, GitShardAdapter
|
||||
from shard_wiki.coordination import ApplyStatus
|
||||
from shard_wiki.space import InformationSpace
|
||||
|
||||
_ENV = {
|
||||
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
|
||||
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t",
|
||||
"PATH": os.environ.get("PATH", ""),
|
||||
}
|
||||
|
||||
|
||||
def _git(repo, *args):
|
||||
return subprocess.run(
|
||||
["git", "-C", str(repo), *args], check=True, capture_output=True, text=True, env=_ENV
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def _git_repo(tmp_path, files, name="git"):
|
||||
repo = tmp_path / name
|
||||
repo.mkdir()
|
||||
_git(repo, "init", "--quiet")
|
||||
for rel, text in files.items():
|
||||
(repo / rel).parent.mkdir(parents=True, exist_ok=True)
|
||||
(repo / rel).write_text(text, encoding="utf-8")
|
||||
_git(repo, "add", rel)
|
||||
_git(repo, "commit", "-m", "seed")
|
||||
return repo
|
||||
|
||||
|
||||
def _folder(tmp_path, name, files, writable=False):
|
||||
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, writable=writable)
|
||||
|
||||
|
||||
# -- history adopt -------------------------------------------------------------
|
||||
|
||||
|
||||
def test_history_lists_commits_newest_first(tmp_path):
|
||||
repo = _git_repo(tmp_path, {"Home.md": "v1"})
|
||||
adapter = GitShardAdapter("git", repo, writable=True)
|
||||
adapter.write("Home", "v2")
|
||||
history = adapter.history("Home")
|
||||
assert len(history) == 2
|
||||
assert history[0].message == "write Home.md" # newest first
|
||||
assert history[-1].message == "seed"
|
||||
assert all(rev.sha for rev in history)
|
||||
|
||||
|
||||
def test_history_unknown_key_raises(tmp_path):
|
||||
adapter = GitShardAdapter("git", _git_repo(tmp_path, {"Home.md": "h"}))
|
||||
with pytest.raises(KeyError):
|
||||
adapter.history("Nope")
|
||||
|
||||
|
||||
# -- cross-substrate integration ----------------------------------------------
|
||||
|
||||
|
||||
def test_resolve_across_git_and_folder(tmp_path):
|
||||
space = InformationSpace("space")
|
||||
space.attach(GitShardAdapter("git", _git_repo(tmp_path, {"Home.md": "git home"})))
|
||||
space.attach(_folder(tmp_path, "notes", {"Daily.md": "folder daily"}))
|
||||
assert space.read("Home").body == "git home" # resolved from the git shard
|
||||
assert space.read("Daily").body == "folder daily" # resolved from the folder shard
|
||||
|
||||
|
||||
def test_chorus_spans_substrates_with_divergence(tmp_path):
|
||||
space = InformationSpace("space")
|
||||
space.attach(GitShardAdapter("git", _git_repo(tmp_path, {"Shared.md": "from git"})))
|
||||
space.attach(_folder(tmp_path, "notes", {"Shared.md": "from folder"}))
|
||||
res = space.resolve("Shared")
|
||||
assert {p.body for p in res.pages} == {"from git", "from folder"} # chorus across substrates
|
||||
git_page = next(p for p in res.pages if p.identity.shard == "git")
|
||||
assert git_page.envelope.divergence # divergence recorded, not erased
|
||||
|
||||
|
||||
def test_edit_through_git_shard_commits(tmp_path):
|
||||
repo = _git_repo(tmp_path, {"Home.md": "original"})
|
||||
space = InformationSpace("space")
|
||||
space.attach(GitShardAdapter("git", repo, writable=True))
|
||||
result = space.edit("Home", "edited via overlay")
|
||||
assert result.status is ApplyStatus.APPLIED # write-through fast-forward on a git shard
|
||||
assert space.read("Home").body == "edited via overlay"
|
||||
assert int(_git(repo, "rev-list", "--count", "HEAD")) == 2 # the edit became a commit
|
||||
|
||||
|
||||
def test_apply_under_drift_refuses_on_external_commit(tmp_path):
|
||||
repo = _git_repo(tmp_path, {"Home.md": "original"})
|
||||
space = InformationSpace("space")
|
||||
space.attach(GitShardAdapter("git", repo, writable=True))
|
||||
overlay = space.overlay("Home", "my draft") # base_rev = current git sha
|
||||
# Another writer commits to the same path → the sha moves underneath the draft.
|
||||
(repo / "Home.md").write_text("someone else", encoding="utf-8")
|
||||
_git(repo, "add", "Home.md")
|
||||
_git(repo, "commit", "-m", "external")
|
||||
result = space.apply_overlay(overlay.overlay_id)
|
||||
assert result.status is ApplyStatus.REFUSED_DRIFT # never clobber (sha drift detected)
|
||||
# The shard itself is untouched — the external commit stands; the draft remains a draft.
|
||||
assert space.union.shard("git").read("Home").body == "someone else"
|
||||
|
||||
|
||||
def test_overlay_on_read_only_git_shard_kept_as_draft(tmp_path):
|
||||
space = InformationSpace("space")
|
||||
space.attach(GitShardAdapter("git", _git_repo(tmp_path, {"Home.md": "ro"}), writable=False))
|
||||
result = space.edit("Home", "wanted change")
|
||||
assert result.status is ApplyStatus.KEPT_DRAFT # read-only target → overlay retained
|
||||
@@ -4,7 +4,7 @@ type: workplan
|
||||
title: "second adapter — git-IS-store shard (contract validation on a new substrate)"
|
||||
domain: whynot
|
||||
repo: shard-wiki
|
||||
status: active
|
||||
status: done
|
||||
owner: tegwick
|
||||
topic_slug: whynot
|
||||
created: "2026-06-15"
|
||||
@@ -40,7 +40,7 @@ merge beyond fast-forward (apply-under-drift refuse is enough, as in SHARD-WP-00
|
||||
|
||||
```task
|
||||
id: SHARD-WP-0012-T1
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8a1c7c80-a0cc-4e02-a611-1f1fd7dec57b"
|
||||
```
|
||||
@@ -54,7 +54,7 @@ implication rules. Tests: read tracked files; profile validates; conformance rea
|
||||
|
||||
```task
|
||||
id: SHARD-WP-0012-T2
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b47dfb86-46c1-4e97-a62f-377719499ff2"
|
||||
```
|
||||
@@ -68,7 +68,7 @@ changes after an external commit.
|
||||
|
||||
```task
|
||||
id: SHARD-WP-0012-T3
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "4c895f42-671d-4948-8bdf-941fd85644bb"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user