# Matcher contract tests — including THE negative check (a mismatched
# pairing must say no, legibly).
_surface = {
"methods": [
{"name": "add_nodes", "signature": "(self, nodes)", "params": ["nodes"]},
{"name": "query_nodes", "signature": "(self, query)", "params": ["query"]},
{"name": "get_context", "signature": "(self, node_id, depth=1, filter_labels=None)",
"params": ["node_id", "depth", "filter_labels"]},
],
"properties": ["name", "version"],
"attributes": [],
}
# positive: exact + prefix (extra trailing surface params fine) + property
_proto_ok = {
"methods": [
{"name": "add_nodes", "params": ["nodes"]},
{"name": "get_context", "params": ["node_id", "depth"]},
],
"properties": ["name"],
}
v = match_protocol_against_surface(_proto_ok, _surface)
assert v["compatible"], v
# negative: missing method (the CR-17 negative check)
_proto_missing = {"methods": [{"name": "transcribe", "params": ["audio"]}], "properties": []}
v = match_protocol_against_surface(_proto_missing, _surface)
assert not v["compatible"] and v["missing_methods"] == ["transcribe"], v
# negative: reordered / wrong params
_proto_params = {"methods": [{"name": "get_context", "params": ["depth", "node_id"]}],
"properties": []}
v = match_protocol_against_surface(_proto_params, _surface)
assert not v["compatible"] and v["param_mismatches"][0]["method"] == "get_context", v
# negative: missing property
v = match_protocol_against_surface({"methods": [], "properties": ["task_count"]}, _surface)
assert not v["compatible"] and v["missing_properties"] == ["task_count"], v
# pre-fracture surface (None) -> not compatible, reason spelled out
v = match_protocol_against_surface(_proto_ok, None)
assert not v["compatible"] and "structural_surface" in v["reason"], v
# param-less old-format surface entries fall back to name-only matching
_old_surface = {"methods": [{"name": "add_nodes", "signature": "(...)"}], "properties": []}
v = match_protocol_against_surface({"methods": [{"name": "add_nodes", "params": ["nodes"]}],
"properties": []}, _old_surface)
assert v["compatible"], v
# manifest round-trip (the on-disk "class" key)
am = AdapterManifest(
name="cjm_graph_storage_adapter_interface.generic.GenericGraphStorageAdapter",
version="0.0.1", task_name="graph-storage",
module="cjm_graph_storage_adapter_interface.generic",
class_name="GenericGraphStorageAdapter",
required_tool_protocol="cjm_graph_storage_adapter_interface.adapter.GraphStorageToolProtocol",
protocol_members=_proto_ok,
)
d = am.to_dict()
assert is_adapter_manifest(d) and d["class"] == "GenericGraphStorageAdapter"
assert adapter_manifest_from_dict(d) == am
assert not is_adapter_manifest({"name": "x"}) # capability manifests lack "unit"core.adapter_manifest
discover_manifests(); compatibility is DERIVED, not declared — the capability records only its structural surface, the adapter declares its protocol (recorded here as member names + parameter lists), and the substrate matches manifest-vs-manifest. Works against UNLOADED capabilities with zero protocol imports host-side.
Design notes
Matching semantics (v1, evidence-honest): a protocol method matches when the surface has a method of the same name AND — when both sides record parameter names — the surface method’s params START WITH the protocol’s (prefix rule: extra trailing, defaulted, surface params are fine; missing or reordered ones are not). Annotations are recorded in both manifests for diagnostics but NOT compared: cross-env rendering of type annotations differs (List[GraphNode] vs fully-qualified forms), so signature-string equality would false-negative on compatible pairs. Known edge (documented, accepted): a surface method absorbing protocol params via **kwargs registers a param mismatch — no current protocol needs it.
A capability whose manifest has NO recorded surface (pre-fracture) is NOT compatible — regenerate the manifest. Staleness stays visible instead of silently mis-answering compatibility queries (the manager’s structural_surface_drift flag covers the stale-recording case).
AdapterManifest
def AdapterManifest(
name:str, version:str, task_name:str, module:str, class_name:str, required_tool_protocol:str,
protocol_members:Dict=<factory>, conda_env:str='', generated_at:str='', unit:str='adapter'
)->None:
A discovered ADAPTER unit (CR-17 pt 2) — the registration record for one task-adapter implementation installed in some tool’s worker env.
Generated in-env by cjm-ctl generate-adapter-manifest (the protocol members are introspected where the protocol is importable); discovered host-side beside capability manifests via the unit discriminator.
adapter_manifest_from_dict
def adapter_manifest_from_dict(
d:Dict, # On-disk JSON dict (the "class" key maps to class_name)
)->AdapterManifest: # Typed adapter manifest
Reconstruct an AdapterManifest from its on-disk JSON shape.
is_adapter_manifest
def is_adapter_manifest(
data:Any, # Raw JSON-decoded manifest content
)->bool: # True when the payload declares the adapter unit kind
Route a manifest file by the unit discriminator (capability manifests carry no unit key; checked BEFORE load_manifest parsing).
match_protocol_against_surface
def match_protocol_against_surface(
protocol_members:Dict, # {"methods": [...], "properties": [...]} from the adapter manifest
structural_surface:Optional, # Capability manifest code.structural_surface (None = pre-fracture)
)->Dict: # {"compatible", "missing_methods", "missing_properties", "param_mismatches", "reason"}
Surface-based compatibility (pass-2 Thread 3) — host-side, manifest-vs- manifest, safe against UNLOADED capabilities.
Method rule: same name present + (when both sides record params) the surface params must START WITH the protocol params (prefix rule). Property rule: name present in the surface’s properties. No surface recorded -> NOT compatible, with the reason spelled out.