generated from coulomb/repo-seed
feat(union): overlay-aware resolution (WP-0008 T5)
resolve() layers open overlays onto canonical pages (overlay_state=DRAFT always surfaced; overlaid body projected when policy.show_drafts); draft-only edits make a not-yet-existing page resolvable. Never hides an unapplied overlay (I-4). Policy gains show_drafts. 4 tests green, pyflakes clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,9 @@ class Policy:
|
||||
|
||||
canonical_source: CanonicalSource = CanonicalSource.CHORUS
|
||||
designated_shard: str | None = None
|
||||
# Whether an unapplied overlay's body is projected over its canonical page on read. Either
|
||||
# way the overlay is never *hidden* — overlay_state is always surfaced in provenance.
|
||||
show_drafts: bool = True
|
||||
|
||||
|
||||
DEFAULT_POLICY = Policy()
|
||||
|
||||
@@ -17,9 +17,10 @@ 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.coordination import DecisionLog, Overlay
|
||||
from shard_wiki.model import Identity, Page
|
||||
from shard_wiki.policy import DEFAULT_POLICY, CanonicalSource, Policy
|
||||
from shard_wiki.provenance import OverlayState, ProvenanceEnvelope, Staleness
|
||||
|
||||
__all__ = ["ResolutionKind", "Resolution", "UnionGraph"]
|
||||
|
||||
@@ -77,21 +78,28 @@ class UnionGraph:
|
||||
return pages
|
||||
|
||||
def resolve(self, name: str) -> Resolution:
|
||||
# 1. alias redirect (coordination-canonical, via the log fold)
|
||||
state = self.log.fold(self.space)
|
||||
overlays = {
|
||||
Overlay.from_payload(p).target: Overlay.from_payload(p)
|
||||
for p in state.open_overlays.values()
|
||||
}
|
||||
|
||||
# 1. alias redirect (coordination-canonical, via the log fold)
|
||||
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)
|
||||
page = self._with_overlay(shard.read(key), overlays)
|
||||
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)
|
||||
# 2/3. union lookup by key across shards, then layer open overlays
|
||||
pages = [self._with_overlay(p, overlays) for p in self._read_all(name)]
|
||||
pages.extend(self._draft_only_pages(name, pages, overlays))
|
||||
|
||||
if not pages:
|
||||
return Resolution(name, ResolutionKind.RED_LINK)
|
||||
if len(pages) == 1:
|
||||
@@ -102,6 +110,34 @@ class UnionGraph:
|
||||
marked = tuple(_with_divergence(p, ordered) for p in ordered)
|
||||
return Resolution(name, ResolutionKind.CHORUS, marked)
|
||||
|
||||
def _with_overlay(self, page: Page, overlays: dict[Identity, Overlay]) -> Page:
|
||||
"""Mark a canonical page that has an open overlay (overlay_state DRAFT; project the
|
||||
overlaid body where policy shows drafts) — never hides the overlay (I-4)."""
|
||||
overlay = overlays.get(page.identity)
|
||||
if overlay is None:
|
||||
return page
|
||||
env = dataclasses.replace(page.envelope, overlay_state=OverlayState.DRAFT)
|
||||
body = overlay.body if self.policy.show_drafts else page.body
|
||||
return dataclasses.replace(page, body=body, envelope=env)
|
||||
|
||||
def _draft_only_pages(
|
||||
self, name: str, existing: list[Page], overlays: dict[Identity, Overlay]
|
||||
) -> list[Page]:
|
||||
"""Drafts that create a not-yet-existing page on an attached shard become resolvable."""
|
||||
have = {p.identity for p in existing}
|
||||
out: list[Page] = []
|
||||
for identity, overlay in overlays.items():
|
||||
if identity.key != name or identity in have or self._shard(identity.shard) is None:
|
||||
continue
|
||||
env = ProvenanceEnvelope(
|
||||
source_shard=identity.shard,
|
||||
staleness=Staleness.FRESH,
|
||||
overlay_state=OverlayState.DRAFT,
|
||||
)
|
||||
body = overlay.body if self.policy.show_drafts else ""
|
||||
out.append(Page(identity=identity, body=body, envelope=env))
|
||||
return out
|
||||
|
||||
def _order_for_policy(self, pages: list[Page]) -> list[Page]:
|
||||
if (
|
||||
self.policy.canonical_source is CanonicalSource.DESIGNATED_CANONICAL
|
||||
|
||||
50
tests/test_overlay_aware_read.py
Normal file
50
tests/test_overlay_aware_read.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Tests for overlay-aware union read (SHARD-WP-0008 T5)."""
|
||||
|
||||
from shard_wiki.adapters import FolderAdapter
|
||||
from shard_wiki.coordination import DecisionLog, OverlayEngine
|
||||
from shard_wiki.model import Identity
|
||||
from shard_wiki.policy import Policy
|
||||
from shard_wiki.provenance import OverlayState
|
||||
from shard_wiki.union import ResolutionKind, UnionGraph
|
||||
|
||||
|
||||
def _union(tmp_path, files, policy=None):
|
||||
for rel, text in files.items():
|
||||
(tmp_path / rel).write_text(text, encoding="utf-8")
|
||||
log = DecisionLog()
|
||||
u = UnionGraph("space", log=log, policy=policy) if policy else UnionGraph("space", log=log)
|
||||
u.attach(FolderAdapter("wikiA", tmp_path))
|
||||
return u, OverlayEngine("space", log)
|
||||
|
||||
|
||||
def test_no_overlay_reads_clean(tmp_path):
|
||||
u, _ = _union(tmp_path, {"Home.md": "canon"})
|
||||
page = u.resolve("Home").single()
|
||||
assert page.body == "canon"
|
||||
assert page.envelope.overlay_state is OverlayState.NONE
|
||||
|
||||
|
||||
def test_open_overlay_surfaces_draft_and_projects_body(tmp_path):
|
||||
u, eng = _union(tmp_path, {"Home.md": "canon"})
|
||||
eng.draft(Identity("wikiA", "Home"), "my draft", base_rev=None)
|
||||
page = u.resolve("Home").single()
|
||||
assert page.envelope.overlay_state is OverlayState.DRAFT # never hidden
|
||||
assert page.body == "my draft" # projected (show_drafts default True)
|
||||
|
||||
|
||||
def test_show_drafts_false_keeps_canonical_body_but_still_flags(tmp_path):
|
||||
u, eng = _union(tmp_path, {"Home.md": "canon"}, policy=Policy(show_drafts=False))
|
||||
eng.draft(Identity("wikiA", "Home"), "my draft", base_rev=None)
|
||||
page = u.resolve("Home").single()
|
||||
assert page.body == "canon" # not projected
|
||||
assert page.envelope.overlay_state is OverlayState.DRAFT # but still surfaced (I-4)
|
||||
|
||||
|
||||
def test_draft_only_new_page_becomes_resolvable(tmp_path):
|
||||
u, eng = _union(tmp_path, {"Home.md": "x"})
|
||||
assert u.resolve("Brand").is_red_link # nothing yet
|
||||
eng.draft(Identity("wikiA", "Brand"), "drafted into being", base_rev=None)
|
||||
res = u.resolve("Brand")
|
||||
assert res.kind is ResolutionKind.SINGLE
|
||||
assert res.single().body == "drafted into being"
|
||||
assert res.single().envelope.overlay_state is OverlayState.DRAFT
|
||||
@@ -100,7 +100,7 @@ Tests: ff apply mutates the shard; drift refuses; read-only keeps draft.
|
||||
|
||||
```task
|
||||
id: SHARD-WP-0008-T5
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "4536d74f-3860-4b4c-82d2-e8d20e6e2125"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user