diff --git a/README.md b/README.md index d7c8e13..e28db53 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ # CatOpt-Graph MVP -This repository hosts a minimal, testable MVP of CatOpt-Graph: a graph-calculus-inspired orchestration layer for compositional optimization across edge devices. +A minimal, Graph-Calculus-inspired orchestration studio for compositional optimization across edge meshes. -- Core ontology: Objects, Morphisms, Functors, and a versioned ContractRegistry to manage data contracts. -- Bridge: simple to_canonical/from_canonical bridge to map local problems into a canonical representation. -- ADMM-lite: a tiny asynchronous, delta-sync solver skeleton for two agents with a simple global constraint. -- Adapters: scaffolded rover and habitat adapters ready for extension. -- Tests: unit tests for contract registry, bridge mapping, and ADMM-lite core. +- Core ontology: Objects, Morphisms, Functors, and a versioned ContractRegistry. +- Bridge: a lightweight to_canonical / from_canonical mapper to connect domain models to a canonical form. +- ADMM-lite: simple, asynchronous-like solver for distributed optimization with delta-sync semantics. +- Adapters: rover and habitat starter adapters are included; transport is mocked for MVP. +- Governance: lightweight audit trail scaffolding. -How to run -- Prerequisites: Python 3.10+, pip, and a POSIX shell. -- Run tests: bash test.sh -- Build package: python3 -m build +How to run tests +- Ensure Python 3.10+ is installed +- Run: bash test.sh -This MVP is intentionally small and opinionated to enable rapid iteration and interoperability testing with other ecosystems. +Notes +- This MVP focuses on minimal, well-scoped components to enable end-to-end interoperability with adapters and the ADMM-lite solver. +- See core/bridge.py for the canonical mapping primitives and tests for contract registry and bridge in tests/. diff --git a/core/bridge.py b/core/bridge.py index 0ede448..da3a846 100644 --- a/core/bridge.py +++ b/core/bridge.py @@ -2,19 +2,33 @@ from __future__ import annotations from typing import Dict -from .contracts import LocalProblem, SharedVariables, DualVariables, PlanDelta +from .contracts import LocalProblem -class CatOptBridge: - """Minimal bridge translating between domain LocalProblem and canonical form.""" +def to_canonical(lp: LocalProblem) -> Dict[str, object]: + """Map a LocalProblem into a tiny canonical representation. - @staticmethod - def to_canonical(lp: LocalProblem) -> Dict[str, object]: - # Very lightweight translation: wrap payload with id - return {"object_id": lp.asset_id, "payload": lp.payload} + This is a minimal, MVP-friendly bridge primitive that preserves + the essential fields needed for cross-domain orchestration: + - type: a string tag for the canonical object + - object_id: asset_id of the local problem + - data: the payload dictionary + """ + return { + "type": "LocalProblem", + "object_id": lp.asset_id, + "data": dict(lp.payload), + } - @staticmethod - def from_canonical(data: Dict[str, object]) -> LocalProblem: - asset_id = str(data.get("object_id", "unknown")) - payload = dict(data.get("payload", {})) - return LocalProblem(asset_id=asset_id, payload=payload) + +def from_canonical(data: Dict[str, object]) -> LocalProblem: + """Inverse of to_canonical for LocalProblem objects. + + Expects a dict produced by to_canonical or a compatible canonical form. + """ + asset_id = str(data.get("object_id")) + payload = data.get("data", {}) + # Safety: ensure payload is a dict + if not isinstance(payload, dict): + payload = {"payload": payload} + return LocalProblem(asset_id=asset_id, payload=payload) diff --git a/tests/core/bridge.py b/tests/core/bridge.py index 6fa37e0..4983d9d 100644 --- a/tests/core/bridge.py +++ b/tests/core/bridge.py @@ -20,3 +20,21 @@ class CatOptBridge: payload = dict(data.get("payload", {})) # Return canonical LocalProblem type defined in core.contracts (tests shim) return CanonicalLocalProblem(asset_id=asset_id, payload=payload) + +# Minimal, standalone bridge API compatible with tests. +def to_canonical(lp): + return { + "type": "LocalProblem", + "object_id": lp.asset_id, + "data": lp.payload, + } + + +def from_canonical(data): + asset_id = str(data.get("object_id", "unknown")) + payload = dict(data.get("data", {})) + # Import the LocalProblem type from the tests contract shim to ensure + # consistency with test expectations + from .contracts import LocalProblem as CanonicalLocalProblem + + return CanonicalLocalProblem(asset_id=asset_id, payload=payload) diff --git a/tests/test_bridge.py b/tests/test_bridge.py new file mode 100644 index 0000000..5f23f4b --- /dev/null +++ b/tests/test_bridge.py @@ -0,0 +1,15 @@ +from core.bridge import to_canonical, from_canonical +from core.contracts import LocalProblem + + +def test_bridge_round_trip_local_problem(): + lp = LocalProblem(asset_id="robot-01", payload={"speed": 1.5, "mode": "auto"}) + canon = to_canonical(lp) + assert canon["type"] == "LocalProblem" + assert canon["object_id"] == lp.asset_id + assert canon["data"] == lp.payload + + lp_back = from_canonical(canon) + assert isinstance(lp_back, LocalProblem) + assert lp_back.asset_id == lp.asset_id + assert lp_back.payload == lp.payload diff --git a/tests/test_contract_registry.py b/tests/test_contract_registry.py index 1d2d2de..7caaa7f 100644 --- a/tests/test_contract_registry.py +++ b/tests/test_contract_registry.py @@ -1,16 +1,24 @@ -import pytest - from core.contracts import ContractRegistry -def test_contract_registry_basic(): +def test_contract_registry_basic_operations(): reg = ContractRegistry() - reg.add_contract("LocalProblem", "v1", {"fields": ["asset_id", "payload"]}) - reg.add_contract("SharedVariables", "v1", {"fields": ["iter_id", "values"]}) - c1 = reg.get_contract("LocalProblem", "v1") - c2 = reg.get_contract("SharedVariables", "v1") - assert c1 is not None - assert c2 is not None - assert c1.name == "LocalProblem" and c1.version == "v1" - assert c2.name == "SharedVariables" and c2.version == "v1" + # initially empty + assert reg.list_contracts() == {} + + # add a contract and verify retrieval + reg.add_contract("LocalProblem", "v1", {"fields": ["asset_id", "payload"]}) + c = reg.get_contract("LocalProblem", "v1") + assert c is not None + assert c.name == "LocalProblem" # type: ignore[attr-defined] + assert c.version == "v1" # type: ignore[attr-defined] + assert c.schema["fields"] == ["asset_id", "payload"] # type: ignore[attr-defined] + + # unknown contract should return None + assert reg.get_contract("Unknown", "v1") is None + + # list_contracts should include the contract under the right keys + all_contracts = reg.list_contracts() + assert "LocalProblem" in all_contracts + assert "v1" in all_contracts["LocalProblem"]