generated from coulomb/repo-seed
feat(adapters): conformance suite — profiles verified not asserted (WP-0007 T4)
run_conformance/assert_conformant verify declared profile == observed behaviour (profile validates, READ round-trips to the right shard, unclaimed verbs raise NotSupported); ConformanceReport gives a precise capability diff. FolderAdapter passes; lying-read and dishonest-write stubs fail. 3 tests green. (TSD §A.2) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,22 @@
|
|||||||
"""adapters/ — the shard adapter contract (bottom waist) and concrete adapters."""
|
"""adapters/ — the shard adapter contract (bottom waist) and concrete adapters."""
|
||||||
|
|
||||||
|
from shard_wiki.adapters.conformance import (
|
||||||
|
Check,
|
||||||
|
ConformanceError,
|
||||||
|
ConformanceReport,
|
||||||
|
assert_conformant,
|
||||||
|
run_conformance,
|
||||||
|
)
|
||||||
from shard_wiki.adapters.contract import CONTRACT_VERSION, ShardAdapter
|
from shard_wiki.adapters.contract import CONTRACT_VERSION, ShardAdapter
|
||||||
from shard_wiki.adapters.folder import FolderAdapter
|
from shard_wiki.adapters.folder import FolderAdapter
|
||||||
|
|
||||||
__all__ = ["ShardAdapter", "FolderAdapter", "CONTRACT_VERSION"]
|
__all__ = [
|
||||||
|
"ShardAdapter",
|
||||||
|
"FolderAdapter",
|
||||||
|
"CONTRACT_VERSION",
|
||||||
|
"Check",
|
||||||
|
"ConformanceReport",
|
||||||
|
"ConformanceError",
|
||||||
|
"run_conformance",
|
||||||
|
"assert_conformant",
|
||||||
|
]
|
||||||
|
|||||||
126
src/shard_wiki/adapters/conformance.py
Normal file
126
src/shard_wiki/adapters/conformance.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
"""Adapter conformance — profiles are verified, not self-asserted (TSD §A.2, blueprint §6.6).
|
||||||
|
|
||||||
|
Capability-as-data (I-3) is only sound if a binding's *declared* profile matches its *observed*
|
||||||
|
behaviour. This battery exercises a binding and reports, check by check, whether claim == reality;
|
||||||
|
``assert_conformant`` gates registration. A lying/buggy profile fails here instead of silently
|
||||||
|
poisoning degradation decisions downstream.
|
||||||
|
|
||||||
|
This slice verifies the read path + honest absence of unclaimed verbs. Positive probes for
|
||||||
|
claimed write/diff/merge are deferred (they mutate) to a later workplan.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from shard_wiki.adapters.contract import ShardAdapter
|
||||||
|
from shard_wiki.model import NotSupported, Verb
|
||||||
|
|
||||||
|
__all__ = ["Check", "ConformanceReport", "ConformanceError", "run_conformance", "assert_conformant"]
|
||||||
|
|
||||||
|
# Optional verbs whose *absence* must be honest (calling an unclaimed one raises NotSupported).
|
||||||
|
_HONEST_ABSENCE_VERBS = (Verb.WRITE, Verb.DIFF, Verb.NOTIFY)
|
||||||
|
_PROBE = {
|
||||||
|
Verb.WRITE: lambda a: a.write("__probe__", ""),
|
||||||
|
Verb.DIFF: lambda a: a.diff("__probe__", "__probe2__"),
|
||||||
|
Verb.NOTIFY: lambda a: a.notify(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Check:
|
||||||
|
name: str
|
||||||
|
ok: bool
|
||||||
|
detail: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ConformanceReport:
|
||||||
|
adapter: str
|
||||||
|
checks: tuple[Check, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ok(self) -> bool:
|
||||||
|
return all(c.ok for c in self.checks)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failures(self) -> tuple[Check, ...]:
|
||||||
|
return tuple(c for c in self.checks if not c.ok)
|
||||||
|
|
||||||
|
def diff(self) -> str:
|
||||||
|
return "; ".join(f"{c.name}: {c.detail}" for c in self.failures) or "conformant"
|
||||||
|
|
||||||
|
|
||||||
|
class ConformanceError(Exception):
|
||||||
|
def __init__(self, report: ConformanceReport) -> None:
|
||||||
|
super().__init__(f"{report.adapter} not conformant — {report.diff()}")
|
||||||
|
self.report = report
|
||||||
|
|
||||||
|
|
||||||
|
def _safe(fn, name: str, ok_detail: str = "") -> Check:
|
||||||
|
try:
|
||||||
|
return fn()
|
||||||
|
except Exception as exc: # noqa: BLE001 — a check must never crash the battery
|
||||||
|
return Check(name, False, f"unexpected error: {exc!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_conformance(adapter: ShardAdapter) -> ConformanceReport:
|
||||||
|
checks: list[Check] = []
|
||||||
|
profile = None
|
||||||
|
|
||||||
|
def _profile_validates() -> Check:
|
||||||
|
nonlocal profile
|
||||||
|
profile = adapter.profile()
|
||||||
|
profile.validate()
|
||||||
|
return Check("profile-validates", True)
|
||||||
|
|
||||||
|
checks.append(_safe(_profile_validates, "profile-validates"))
|
||||||
|
if profile is None: # profile() or validate() failed; can't probe further meaningfully
|
||||||
|
return ConformanceReport(type(adapter).__name__, tuple(checks))
|
||||||
|
|
||||||
|
# READ is the capability floor.
|
||||||
|
checks.append(Check("supports-read", profile.supports(Verb.READ),
|
||||||
|
"" if profile.supports(Verb.READ) else "READ not declared"))
|
||||||
|
|
||||||
|
# READ round-trips: a declared-readable shard must actually read its own keys.
|
||||||
|
def _read_round_trips() -> Check:
|
||||||
|
keys = list(adapter.keys())
|
||||||
|
if not keys:
|
||||||
|
return Check("read-round-trips", True, "empty shard")
|
||||||
|
page = adapter.read(keys[0])
|
||||||
|
if page.identity.shard != adapter.shard_id:
|
||||||
|
return Check("read-round-trips", False,
|
||||||
|
f"page shard {page.identity.shard!r} != {adapter.shard_id!r}")
|
||||||
|
if not isinstance(page.body, str):
|
||||||
|
return Check("read-round-trips", False, "body is not text")
|
||||||
|
return Check("read-round-trips", True)
|
||||||
|
|
||||||
|
if profile.supports(Verb.READ):
|
||||||
|
checks.append(_safe(_read_round_trips, "read-round-trips"))
|
||||||
|
|
||||||
|
# Honest absence: an *unclaimed* optional verb must raise NotSupported when invoked.
|
||||||
|
for verb in _HONEST_ABSENCE_VERBS:
|
||||||
|
if profile.supports(verb):
|
||||||
|
continue # claimed → positive probe deferred (would mutate)
|
||||||
|
name = f"honest-absence:{verb.value}"
|
||||||
|
|
||||||
|
def _probe(v=verb, n=name) -> Check:
|
||||||
|
try:
|
||||||
|
_PROBE[v](adapter)
|
||||||
|
except NotSupported:
|
||||||
|
return Check(n, True)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return Check(n, False, f"raised {type(exc).__name__}, expected NotSupported")
|
||||||
|
return Check(n, False, "did not raise NotSupported though verb is unclaimed")
|
||||||
|
|
||||||
|
checks.append(_probe())
|
||||||
|
|
||||||
|
return ConformanceReport(type(adapter).__name__, tuple(checks))
|
||||||
|
|
||||||
|
|
||||||
|
def assert_conformant(adapter: ShardAdapter) -> ConformanceReport:
|
||||||
|
"""Run the battery; raise :class:`ConformanceError` if any check fails. Returns the report."""
|
||||||
|
report = run_conformance(adapter)
|
||||||
|
if not report.ok:
|
||||||
|
raise ConformanceError(report)
|
||||||
|
return report
|
||||||
67
tests/test_conformance.py
Normal file
67
tests/test_conformance.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"""Tests for the adapter conformance suite (SHARD-WP-0007 T4)."""
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from shard_wiki.adapters import (
|
||||||
|
ConformanceError,
|
||||||
|
FolderAdapter,
|
||||||
|
ShardAdapter,
|
||||||
|
assert_conformant,
|
||||||
|
run_conformance,
|
||||||
|
)
|
||||||
|
from shard_wiki.model import CapabilityProfile, Identity, Page
|
||||||
|
from shard_wiki.provenance import ProvenanceEnvelope
|
||||||
|
|
||||||
|
|
||||||
|
def test_folder_adapter_is_conformant(tmp_path):
|
||||||
|
(tmp_path / "Home.md").write_text("# Home", encoding="utf-8")
|
||||||
|
report = assert_conformant(FolderAdapter("shardA", tmp_path))
|
||||||
|
assert report.ok
|
||||||
|
assert report.diff() == "conformant"
|
||||||
|
|
||||||
|
|
||||||
|
class _LyingReadAdapter(ShardAdapter):
|
||||||
|
"""Claims READ but read() is broken — must fail conformance."""
|
||||||
|
|
||||||
|
def __init__(self, profile: CapabilityProfile) -> None:
|
||||||
|
self._profile = profile
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shard_id(self) -> str:
|
||||||
|
return "liar"
|
||||||
|
|
||||||
|
def profile(self) -> CapabilityProfile:
|
||||||
|
return self._profile
|
||||||
|
|
||||||
|
def keys(self) -> Iterable[str]:
|
||||||
|
return ["Home"]
|
||||||
|
|
||||||
|
def read(self, key: str) -> Page:
|
||||||
|
# Returns a page attributed to the WRONG shard — a lie about its own content.
|
||||||
|
return Page(Identity("someone-else", key), "x", ProvenanceEnvelope(source_shard="x"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_lying_read_adapter_fails_with_precise_diff(tmp_path):
|
||||||
|
good = FolderAdapter("shardA", tmp_path).profile() # a valid read-only profile
|
||||||
|
liar = _LyingReadAdapter(good)
|
||||||
|
report = run_conformance(liar)
|
||||||
|
assert not report.ok
|
||||||
|
assert "read-round-trips" in report.diff()
|
||||||
|
with pytest.raises(ConformanceError, match="read-round-trips"):
|
||||||
|
assert_conformant(liar)
|
||||||
|
|
||||||
|
|
||||||
|
class _DishonestWriteAdapter(FolderAdapter):
|
||||||
|
"""Declares no WRITE (inherits read-only folder profile) but write() silently succeeds."""
|
||||||
|
|
||||||
|
def write(self, key: str, body: str) -> Page: # noqa: ARG002
|
||||||
|
return self.read(next(iter(self.keys())))
|
||||||
|
|
||||||
|
|
||||||
|
def test_dishonest_absence_fails(tmp_path):
|
||||||
|
(tmp_path / "Home.md").write_text("x", encoding="utf-8")
|
||||||
|
report = run_conformance(_DishonestWriteAdapter("shardA", tmp_path))
|
||||||
|
assert not report.ok
|
||||||
|
assert "honest-absence:write" in report.diff()
|
||||||
@@ -86,7 +86,7 @@ git-or-none history). Imports `model/`, `provenance/`. Tests: read a temp folder
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: SHARD-WP-0007-T4
|
id: SHARD-WP-0007-T4
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "12d7fb8d-3842-4142-a462-9d1e6efe58bd"
|
state_hub_task_id: "12d7fb8d-3842-4142-a462-9d1e6efe58bd"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user