generated from coulomb/repo-seed
feat(adapters): writable FolderAdapter + positive write conformance (WP-0008 T1)
FolderAdapter(writable=True) declares WRITE+PER_PAGE, implements write() and current_rev() (mtime token for drift detection). Conformance gains a content-preserving positive write probe for WRITE-claiming adapters. 5 tests green, full suite green, pyflakes clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,22 @@ def run_conformance(adapter: ShardAdapter) -> ConformanceReport:
|
||||
if profile.supports(Verb.READ):
|
||||
checks.append(_safe(_read_round_trips, "read-round-trips"))
|
||||
|
||||
# WRITE positive probe: a claimed-writable shard must actually round-trip a write. The probe
|
||||
# is content-preserving (rewrite an existing page with its own body) so it is non-destructive.
|
||||
def _write_round_trips() -> Check:
|
||||
keys = list(adapter.keys())
|
||||
if not keys:
|
||||
return Check("write-round-trips", True, "empty shard")
|
||||
k = keys[0]
|
||||
original = adapter.read(k).body
|
||||
adapter.write(k, original)
|
||||
if adapter.read(k).body != original:
|
||||
return Check("write-round-trips", False, "rewrite did not preserve body")
|
||||
return Check("write-round-trips", True)
|
||||
|
||||
if profile.supports(Verb.WRITE):
|
||||
checks.append(_safe(_write_round_trips, "write-round-trips"))
|
||||
|
||||
# Honest absence: an *unclaimed* optional verb must raise NotSupported when invoked.
|
||||
for verb in _HONEST_ABSENCE_VERBS:
|
||||
if profile.supports(verb):
|
||||
|
||||
@@ -23,6 +23,7 @@ from shard_wiki.model import (
|
||||
Identity,
|
||||
MergeModel,
|
||||
NativeQuery,
|
||||
NotSupported,
|
||||
OperationalEnvelope,
|
||||
Page,
|
||||
Placement,
|
||||
@@ -37,19 +38,22 @@ __all__ = ["FolderAdapter"]
|
||||
|
||||
|
||||
class FolderAdapter(ShardAdapter):
|
||||
def __init__(self, shard_id: str, root: str | Path) -> None:
|
||||
def __init__(self, shard_id: str, root: str | Path, writable: bool = False) -> None:
|
||||
self._shard_id = shard_id
|
||||
self._root = Path(root)
|
||||
self._writable = writable
|
||||
|
||||
@property
|
||||
def shard_id(self) -> str:
|
||||
return self._shard_id
|
||||
|
||||
def profile(self) -> CapabilityProfile:
|
||||
verbs = {Verb.READ, Verb.WRITE} if self._writable else {Verb.READ}
|
||||
granularity = WriteGranularity.PER_PAGE if self._writable else WriteGranularity.NONE
|
||||
return CapabilityProfile(
|
||||
substrate=Substrate.FILES,
|
||||
attachment_mode=AttachmentMode.FILE_STORE,
|
||||
write_granularity=WriteGranularity.NONE,
|
||||
write_granularity=granularity,
|
||||
content_opacity=ContentOpacity.TRANSPARENT,
|
||||
operational_envelope=OperationalEnvelope.LOCAL_UNBOUNDED,
|
||||
access_grant=AccessGrant.OPEN,
|
||||
@@ -59,12 +63,28 @@ class FolderAdapter(ShardAdapter):
|
||||
addressing=Addressing.PATH,
|
||||
native_query=NativeQuery.NONE,
|
||||
translation=Translation.NATIVE,
|
||||
supported_verbs=frozenset({Verb.READ}),
|
||||
supported_verbs=frozenset(verbs),
|
||||
).validate()
|
||||
|
||||
def _path_for(self, key: str) -> Path:
|
||||
return self._root / f"{key}.md"
|
||||
|
||||
def current_rev(self, key: str) -> str | None:
|
||||
"""The shard's current revision token for ``key`` (mtime iso), or ``None`` if absent.
|
||||
Used for apply-under-drift comparison (blueprint §8.6)."""
|
||||
path = self._path_for(key)
|
||||
if not path.is_file():
|
||||
return None
|
||||
return datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc).isoformat()
|
||||
|
||||
def write(self, key: str, body: str) -> Page:
|
||||
if not self._writable:
|
||||
raise NotSupported(f"{type(self).__name__} is read-only")
|
||||
path = self._path_for(key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(body, encoding="utf-8")
|
||||
return self.read(key)
|
||||
|
||||
def keys(self) -> Iterable[str]:
|
||||
for p in sorted(self._root.rglob("*.md")):
|
||||
yield p.relative_to(self._root).with_suffix("").as_posix()
|
||||
|
||||
Reference in New Issue
Block a user