core.adapter_manifest

The ADAPTER unit’s registration manifest + the surface-based compatibility matcher (CR-17 pt 2, stage 4). Pass-2 Thread 3: registration/discovery = per-unit manifests generated in-env and found by 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.

# 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"