From def699c1eb43f5af59305570ee74802318efd810 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 16 Jun 2026 02:41:19 +0200 Subject: [PATCH] feat(adapters): GitShardAdapter history adopt + cross-substrate integration (WP-0012 T3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- SCOPE.md | 2 +- src/shard_wiki/adapters/__init__.py | 3 +- src/shard_wiki/adapters/git.py | 35 +++++- tests/test_git_adapter_integration.py | 116 +++++++++++++++++++ workplans/SHARD-WP-0012-git-shard-adapter.md | 8 +- 5 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 tests/test_git_adapter_integration.py diff --git a/SCOPE.md b/SCOPE.md index 8e422f7..5d79b31 100644 --- a/SCOPE.md +++ b/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 | diff --git a/src/shard_wiki/adapters/__init__.py b/src/shard_wiki/adapters/__init__.py index 167da46..d469670 100644 --- a/src/shard_wiki/adapters/__init__.py +++ b/src/shard_wiki/adapters/__init__.py @@ -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", diff --git a/src/shard_wiki/adapters/git.py b/src/shard_wiki/adapters/git.py index ddeb0d7..a9d5ada 100644 --- a/src/shard_wiki/adapters/git.py +++ b/src/shard_wiki/adapters/git.py @@ -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: diff --git a/tests/test_git_adapter_integration.py b/tests/test_git_adapter_integration.py new file mode 100644 index 0000000..9f28a2e --- /dev/null +++ b/tests/test_git_adapter_integration.py @@ -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 diff --git a/workplans/SHARD-WP-0012-git-shard-adapter.md b/workplans/SHARD-WP-0012-git-shard-adapter.md index 9049eb8..fb5d568 100644 --- a/workplans/SHARD-WP-0012-git-shard-adapter.md +++ b/workplans/SHARD-WP-0012-git-shard-adapter.md @@ -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" ```