Skip to content

Commit 913c9b1

Browse files
Derive verifier navigation from checker metadata
1 parent 89a7ca4 commit 913c9b1

8 files changed

Lines changed: 375 additions & 141 deletions

File tree

src/jacobian/adapters/mcp/projections.py

Lines changed: 37 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -62,66 +62,6 @@ def _mcp_text_json_bytes(value: object) -> bytes:
6262
"materialize named Boolean CNF for finite colorings and forbidden patterns",
6363
),
6464
),
65-
"graph.invariant.maximum_matching.compute": (
66-
(
67-
"graph.invariant.maximum_matching.verify",
68-
"independently replay the stored Tutte-Berge certificate",
69-
),
70-
),
71-
"graph.invariant.maximum_matching.verify": (
72-
(
73-
"graph.invariant.maximum_matching.compute",
74-
"produce a matching witness and Tutte-Berge certificate",
75-
),
76-
),
77-
"probability.graph_reliability.connection_probability.compute": (
78-
(
79-
"probability.graph_reliability.connection_probability.verify",
80-
"independently replay every edge state and terminal connection",
81-
),
82-
),
83-
"probability.graph_reliability.connection_probability.verify": (
84-
(
85-
"probability.graph_reliability.connection_probability.compute",
86-
"compute the complete exact edge-state ledger",
87-
),
88-
),
89-
"graph.hamiltonian_path.decide": (
90-
(
91-
"graph.hamiltonian_path.verify",
92-
"independently verify the stored positive or negative decision",
93-
),
94-
),
95-
"graph.hamiltonian_path.verify": (
96-
(
97-
"graph.hamiltonian_path.decide",
98-
"produce a complete bounded decision and optional path witness",
99-
),
100-
),
101-
"polynomial.jacobian_syzygy.minimum_degree.compute": (
102-
(
103-
"polynomial.jacobian_syzygy.minimum_degree.verify",
104-
"independently rebuild the graded maps, ranks, minors, and first kernel",
105-
),
106-
),
107-
"polynomial.jacobian_syzygy.minimum_degree.verify": (
108-
(
109-
"polynomial.jacobian_syzygy.minimum_degree.compute",
110-
"produce the provenance-bound graded rank ledger and kernel witness",
111-
),
112-
),
113-
"geometry.projective_line_arrangement.flats.materialize": (
114-
(
115-
"geometry.projective_line_arrangement.flats.verify",
116-
"independently rebuild all projective flats and pair accounting",
117-
),
118-
),
119-
"geometry.projective_line_arrangement.flats.verify": (
120-
(
121-
"geometry.projective_line_arrangement.flats.materialize",
122-
"materialize normalized lines, exact flats, incidences and multiplicities",
123-
),
124-
),
12565
}
12666

12767

@@ -140,16 +80,27 @@ def _capability_inspection_extensions(
14080
descriptors: dict[str, CapabilityDescriptor],
14181
) -> dict[str, Any]:
14282
extensions: dict[str, Any] = {}
143-
related = [
83+
related = {
84+
item.capability_id: item.model_dump(mode="json")
85+
for item in descriptors[capability_id].related_capabilities
86+
if item.capability_id in descriptors and item.capability_id != capability_id
87+
}
88+
related.update(
14489
{
145-
"capability_id": related_id,
146-
"relationship": relationship,
90+
related_id: {
91+
"capability_id": related_id,
92+
"relationship": relationship,
93+
}
94+
for related_id, relationship in _RELATED_CAPABILITIES.get(capability_id, ())
95+
if related_id in descriptors
96+
and related_id != capability_id
97+
and related_id not in related
14798
}
148-
for related_id, relationship in _RELATED_CAPABILITIES.get(capability_id, ())
149-
if related_id in descriptors
150-
]
99+
)
151100
if related:
152-
extensions["related_capabilities"] = related
101+
extensions["related_capabilities"] = [
102+
related[related_id] for related_id in sorted(related)
103+
]
153104
if capability_id.startswith(("sat.", "smt.")):
154105
extensions["synchronous_execution"] = {
155106
"remote_safe_wall_seconds_max": 150,
@@ -222,16 +173,26 @@ def _discovery_operation_card(
222173
"""Add compact decision facts without recommending a research action."""
223174

224175
runtime = descriptor.provider_runtime
225-
related = [
176+
related = {
177+
item.capability_id: item.model_dump(mode="json")
178+
for item in descriptor.related_capabilities
179+
if item.capability_id in descriptors
180+
and item.capability_id != descriptor.capability_id
181+
}
182+
related.update(
226183
{
227-
"capability_id": related_id,
228-
"relationship": relationship,
184+
related_id: {
185+
"capability_id": related_id,
186+
"relationship": relationship,
187+
}
188+
for related_id, relationship in _RELATED_CAPABILITIES.get(
189+
descriptor.capability_id, ()
190+
)
191+
if related_id in descriptors
192+
and related_id != descriptor.capability_id
193+
and related_id not in related
229194
}
230-
for related_id, relationship in _RELATED_CAPABILITIES.get(
231-
descriptor.capability_id, ()
232-
)
233-
if related_id in descriptors
234-
]
195+
)
235196
invocation_example = None
236197
if descriptor.invocation_examples:
237198
example = descriptor.invocation_examples[0]
@@ -259,7 +220,7 @@ def _discovery_operation_card(
259220
"provider_availability": (
260221
runtime.availability.value if runtime is not None else "UNKNOWN"
261222
),
262-
"related_capabilities": related,
223+
"related_capabilities": [related[item] for item in sorted(related)],
263224
**(
264225
{"invocation_example": invocation_example}
265226
if invocation_example is not None

src/jacobian/capability_registry.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,44 @@ def register(self: Any, adapter: AdapterLike) -> None:
5858
self._adapters[descriptor.capability_id] = adapter
5959

6060
def catalog(self: Any) -> CapabilityCatalog:
61-
visible = tuple(
61+
projected = tuple(
6262
projected
6363
for name in sorted(self._adapters)
6464
if (projected := self.policy.project(self._adapters[name].descriptor))
6565
is not None
6666
)
67+
visible_ids = {descriptor.capability_id for descriptor in projected}
68+
visible = []
69+
for descriptor in projected:
70+
relationships = {
71+
item.capability_id: item for item in descriptor.related_capabilities
72+
}
73+
for related_id, relationship in self._catalog_relationships.get(
74+
descriptor.capability_id, {}
75+
).items():
76+
previous = relationships.get(related_id)
77+
if previous is not None and previous != relationship:
78+
raise CapabilityError(
79+
"conflicting projected catalog relationship: "
80+
f"{descriptor.capability_id} -> {related_id}"
81+
)
82+
relationships[related_id] = relationship
83+
visible.append(
84+
descriptor.model_copy(
85+
update={
86+
"related_capabilities": tuple(
87+
relationships[related_id]
88+
for related_id in sorted(relationships)
89+
if related_id in visible_ids
90+
and related_id != descriptor.capability_id
91+
)
92+
}
93+
)
94+
)
6795
return CapabilityCatalog(
6896
policy_profile=self.policy.profile,
6997
policy_digest=self.policy.digest,
70-
capabilities=visible,
98+
capabilities=tuple(visible),
7199
)
72100

73101

src/jacobian/capability_service.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from jacobian.capability_registry import CapabilityRegistryMixin
3333
from jacobian.capability_verification import CapabilityVerificationMixin
3434
from jacobian.contracts.capabilities import (
35+
CapabilityCatalogRelationship,
3536
CapabilityDescriptor,
3637
CapabilityMode,
3738
CapabilityRequest,
@@ -195,6 +196,27 @@ def __init__(
195196
self.store = store
196197
self.policy = policy or CapabilityPolicy()
197198
self._adapters: dict[str, CapabilityAdapter] = {}
199+
self._catalog_relationships: dict[
200+
str, dict[str, CapabilityCatalogRelationship]
201+
] = {}
202+
203+
def register_catalog_relationship(
204+
self,
205+
source_capability_id: str,
206+
relationship: CapabilityCatalogRelationship,
207+
) -> None:
208+
"""Register one authoritative directed catalog relationship."""
209+
210+
if source_capability_id == relationship.capability_id:
211+
raise CapabilityError("a capability cannot relate to itself")
212+
related = self._catalog_relationships.setdefault(source_capability_id, {})
213+
previous = related.get(relationship.capability_id)
214+
if previous is not None and previous != relationship:
215+
raise CapabilityError(
216+
"conflicting catalog relationship: "
217+
f"{source_capability_id} -> {relationship.capability_id}"
218+
)
219+
related[relationship.capability_id] = relationship
198220

199221

200222
def load_capability_adapter(

src/jacobian/contracts/capabilities.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,34 @@ class CapabilityCompletenessStatus(StrEnum):
412412
COMPLETE = "COMPLETE"
413413

414414

415+
class CapabilityCatalogRelationshipKind(StrEnum):
416+
"""Factual installed-capability relationship exposed by the catalog."""
417+
418+
INDEPENDENT_VERIFIER = "INDEPENDENT_VERIFIER"
419+
VERIFIABLE_RESULT_PRODUCER = "VERIFIABLE_RESULT_PRODUCER"
420+
421+
422+
class CapabilityCatalogRelationship(ContractModel):
423+
"""One typed navigation edge to another installed capability."""
424+
425+
capability_id: CapabilityId
426+
kind: CapabilityCatalogRelationshipKind
427+
relationship: str = Field(min_length=1, max_length=256)
428+
429+
430+
class CapabilityCatalogRelationshipRegistration(ContractModel):
431+
"""One authoritative directed relationship before catalog projection."""
432+
433+
source_capability_id: CapabilityId
434+
related_capability: CapabilityCatalogRelationship
435+
436+
@model_validator(mode="after")
437+
def reject_self_relationship(self) -> Self:
438+
if self.source_capability_id == self.related_capability.capability_id:
439+
raise ValueError("a capability cannot relate to itself")
440+
return self
441+
442+
415443
class CapabilityDescriptor(ContractModel):
416444
"""One installed operation advertised by an operator-installed adapter."""
417445

@@ -432,6 +460,7 @@ class CapabilityDescriptor(ContractModel):
432460
)
433461
accepted_artifact_types: tuple[ArtifactUri, ...] = ()
434462
produced_artifact_types: tuple[ArtifactUri, ...] = ()
463+
related_capabilities: tuple[CapabilityCatalogRelationship, ...] = ()
435464
discovery_visible: bool = True
436465
invocation_examples: tuple[CapabilityInvocationExample, ...] = ()
437466

@@ -447,6 +476,11 @@ def require_modes_and_canonical_schemas(self) -> Self:
447476
)
448477
if len(set(self.produced_artifact_types)) != len(self.produced_artifact_types):
449478
raise ValueError("produced artifact types must be unique")
479+
related_ids = [item.capability_id for item in self.related_capabilities]
480+
if self.capability_id in related_ids:
481+
raise ValueError("a capability cannot relate to itself")
482+
if len(set(related_ids)) != len(related_ids):
483+
raise ValueError("related capability IDs must be unique")
450484
if len({example.name for example in self.invocation_examples}) != len(
451485
self.invocation_examples
452486
):

src/jacobian/exact_domain_checkers.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import logging
66
from collections.abc import Mapping
7-
from dataclasses import dataclass
7+
from dataclasses import dataclass, replace
88
from typing import Any, Literal
99

1010
from pydantic import ValidationError
@@ -17,6 +17,9 @@
1717
from jacobian.contracts.capabilities import (
1818
CapabilityAssurance,
1919
CapabilityAssuranceLevel,
20+
CapabilityCatalogRelationship,
21+
CapabilityCatalogRelationshipKind,
22+
CapabilityCatalogRelationshipRegistration,
2023
CapabilityCompleteness,
2124
CapabilityCompletenessStatus,
2225
CapabilityDescriptor,
@@ -97,6 +100,7 @@ class ExactDomainCheckerInstallation:
97100
provider_runtimes: dict[str, CapabilityProviderRuntime]
98101
witness_schema_uri: str | None = None
99102
diagnostics: tuple[CapabilityDiagnostic, ...] = ()
103+
catalog_relationships: tuple[CapabilityCatalogRelationshipRegistration, ...] = ()
100104

101105

102106
@dataclass(frozen=True, slots=True)
@@ -368,6 +372,7 @@ def install_exact_domain_verification(
368372
):
369373
return (), installation
370374
adapters: list[CapabilityAdapter] = []
375+
catalog_relationships: list[CapabilityCatalogRelationshipRegistration] = []
371376
result_models = {
372377
operation.capability_id: operation.result_model
373378
for bundle, _installed_bundle in bundles.values()
@@ -404,6 +409,38 @@ def install_exact_domain_verification(
404409
stored_result_input=declaration.capability_id in stored_producers,
405410
)
406411
)
412+
verifier_id = declaration.verification_capability_id
413+
if verifier_id is None:
414+
raise ValueError("exact replay declaration has no verifier capability ID")
415+
catalog_relationships.extend(
416+
(
417+
CapabilityCatalogRelationshipRegistration(
418+
source_capability_id=declaration.capability_id,
419+
related_capability=CapabilityCatalogRelationship(
420+
capability_id=verifier_id,
421+
kind=(CapabilityCatalogRelationshipKind.INDEPENDENT_VERIFIER),
422+
relationship=(
423+
"independently verify this exact producer result"
424+
),
425+
),
426+
),
427+
CapabilityCatalogRelationshipRegistration(
428+
source_capability_id=verifier_id,
429+
related_capability=CapabilityCatalogRelationship(
430+
capability_id=declaration.capability_id,
431+
kind=(
432+
CapabilityCatalogRelationshipKind.VERIFIABLE_RESULT_PRODUCER
433+
),
434+
relationship=(
435+
"produce the exact result accepted by this verifier"
436+
),
437+
),
438+
),
439+
)
440+
)
441+
installation = replace(
442+
installation, catalog_relationships=tuple(catalog_relationships)
443+
)
407444
return tuple(adapters), installation
408445

409446

src/jacobian/portfolio/core_installation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@ def install_domain_verification(
206206
)
207207
for adapter in adapters:
208208
self.context.register_capability(adapter)
209+
for relationship in result.exact_domain_checkers.catalog_relationships:
210+
ctx.capabilities.register_catalog_relationship(
211+
relationship.source_capability_id,
212+
relationship.related_capability,
213+
)
209214

210215

211216
def _conjecture_ingestion_installation(

0 commit comments

Comments
 (0)