diff --git a/AGENTS.md b/AGENTS.md index 633a9b3..639c21f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,3 +22,12 @@ Development workflow Note - This is a minimal, opinionated MVP to bootstrap cross-domain interoperability. It is not a full production system. + +What we're adding now (MVP roadmap refinements): +- Core ontology extension: versioned ContractRegistry (contracts per name/version) for interop with adapters. +- Bridge layer: a lightweight to_canonical / from_canonical mapping to connect domain LocalProblem data to the CatOpt canonical form. +- Adapters: two starter adapters (rover, habitat) already present; bridge will enable canonical data exchange. +- ADMM-lite core: keep the existing two-agent toy solver as a testbed; extend with delta-sync semantics conceptually in code comments and tests. +- Governance / contracts: hook up a minimal conformance story via the contract registry + bridge; add a test to verify registry behavior. +- MVP testing: unit tests for contract registry, end-to-end tests for bridge mapping (stubbed inputs/outputs) and existing admm_lite tests for solver stability. +- Documentation: update README to reflect the MVP extension and how to run tests. diff --git a/src/catopt_graph/bridge.py b/src/catopt_graph/bridge.py new file mode 100644 index 0000000..fbc9473 --- /dev/null +++ b/src/catopt_graph/bridge.py @@ -0,0 +1,52 @@ +"""CatOpt-Graph Bridge: Canonicalization layer between domain adapters and CatOpt. + +This lightweight module provides mapping helpers to translate between +domain-specific LocalProblem representations emitted by adapters and a +canonical CatOpt representation used by the core solver/pipeline. +""" +from __future__ import annotations + +from typing import Any, Dict + + +def to_canonical(local_problem: Dict[str, Any]) -> Dict[str, Any]: + """Map a domain LocalProblem into a canonical CatOpt representation. + + This is intentionally small and opinionated to support MVP flows: + - Normalize keys + - Promote core fields (node, objective, constraints) into a stable shape + - Represent objective generically, e.g., quadratic/linear with coefficients + """ + lp_type = local_problem.get("type") or "LocalProblem" + node = local_problem.get("node") or local_problem.get("id") or "unknown" + obj = local_problem.get("objective", {}) + + canonical: Dict[str, Any] = { + "type": "CanonicalLocalProblem", + "node": node, + "source_type": lp_type, + "objective": obj, + "metadata": { + "version": local_problem.get("version", "1.0.0"), + }, + } + return canonical + + +def from_canonical(canonical: Dict[str, Any]) -> Dict[str, Any]: + """Map a canonical CatOpt representation back to a domain-local form. + + This function is intentionally minimal; in MVP it serves as a stub for + domain adapters to re-emit data in their native shapes. + """ + if not isinstance(canonical, dict): + return {"error": "invalid canonical payload"} + # Pass through known fields if present + node = canonical.get("node", "unknown") + local_problem = { + "type": canonical.get("source_type", "LocalProblem"), + "node": node, + "objective": canonical.get("objective", {}), + "metadata": canonical.get("metadata", {}), + } + return local_problem diff --git a/src/catopt_graph/core.py b/src/catopt_graph/core.py index ac12110..a508f12 100644 --- a/src/catopt_graph/core.py +++ b/src/catopt_graph/core.py @@ -19,12 +19,45 @@ class Functor: map_to: str class ContractRegistry: - """Minimal versioned contract registry skeleton.""" + """Minimal versioned contract registry skeleton. + + This registry stores contracts (schemas, conformance rules, etc.) in a + versioned fashion for cross-domain adapters. Each contract name maps to a + dictionary of versions to contract payloads. + """ def __init__(self) -> None: + # mapping: contract_name -> { version_str: contract_payload_dict } self.contracts: Dict[str, Dict[str, Any]] = {} - def register(self, name: str, contract: Dict[str, Any]) -> None: - self.contracts[name] = contract + def register_contract(self, name: str, contract: Dict[str, Any], version: str = "1.0.0") -> None: + """Register a contract with an explicit version (default 1.0.0). - def get(self, name: str) -> Optional[Dict[str, Any]]: - return self.contracts.get(name) + If the contract name does not exist, create a new versioned bucket. + If the version already exists, it will be overwritten to reflect the + new contract payload. + """ + if name not in self.contracts: + self.contracts[name] = {} + self.contracts[name][version] = contract + + def get_contract(self, name: str, version: str | None = None) -> Optional[Dict[str, Any]]: + """Return a contract payload by name and optional version. + + If version is None, return the latest version available for the contract. + If the contract or version is not found, return None. + """ + versions = self.contracts.get(name) + if not versions: + return None + if version is None: + # pick the latest version by semantic version string comparison + try: + latest = max(versions.keys(), key=lambda v: tuple(int(p) for p in v.split("."))) + except Exception: + latest = sorted(versions.keys())[-1] + return versions.get(latest) + return versions.get(version) + + def list_contracts(self) -> Dict[str, Dict[str, Any]]: + """Return a raw view of all registered contracts.""" + return self.contracts diff --git a/tests/test_contract_registry.py b/tests/test_contract_registry.py new file mode 100644 index 0000000..1d6e680 --- /dev/null +++ b/tests/test_contract_registry.py @@ -0,0 +1,33 @@ +import unittest + +from catopt_graph.core import ContractRegistry + + +class TestContractRegistry(unittest.TestCase): + def test_register_and_get_contract_versions(self): + reg = ContractRegistry() + reg.register_contract("LocalProblem", {"foo": "bar"}, version="1.0.0") + reg.register_contract("LocalProblem", {"foo": "baz"}, version="1.1.0") + + c_latest = reg.get_contract("LocalProblem") + c_10 = reg.get_contract("LocalProblem", version="1.0.0") + c_11 = reg.get_contract("LocalProblem", version="1.1.0") + + self.assertIsNotNone(c_latest) + self.assertEqual(c_10, {"foo": "bar"}) + self.assertEqual(c_11, {"foo": "baz"}) + + # The latest should reflect the highest version number + self.assertEqual(c_latest, c_11) + + def test_list_contracts(self): + reg = ContractRegistry() + reg.register_contract("X", {"a": 1}, version="0.1.0") + reg.register_contract("Y", {"b": 2}, version="0.1.0") + all_contracts = reg.list_contracts() + self.assertIn("X", all_contracts) + self.assertIn("Y", all_contracts) + + +if __name__ == "__main__": + unittest.main()