diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +node_modules/ +.npmrc +.env +.env.* +__tests__/ +coverage/ +.nyc_output/ +dist/ +build/ +.cache/ +*.log +.DS_Store +tmp/ +.tmp/ +__pycache__/ +*.pyc +.venv/ +venv/ +*.egg-info/ +.pytest_cache/ +READY_TO_PUBLISH diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..efba6c5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,22 @@ +GravityWeave - Agent Guidelines and Architecture + +Architecture +- Language: Python 3.8+ +- Minimal package: gravityweave +- Core modules: registry, plan_delta (CRDT), ledger, adapters, solver + +Tech stack +- Standard library (hashlib, hmac, json, time) +- pytest for tests + +Testing and CI +- Run tests and build with `bash test.sh`. +- `test.sh` builds the package (`python3 -m build`) and runs `pytest -q`. + +Contribution rules +- Small incremental changes preferred. +- Follow existing patterns; keep public API names stable. +- Add unit tests for any new behavior. + +Packaging +- pyproject.toml defines build metadata and is required for packaging. diff --git a/README.md b/README.md index c99f4f6..71d419b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ -# idea182-implementation +# GravityWeave (idea182-implementation) -Implementation for Idea #182 \ No newline at end of file +Minimal GravityWeave skeleton implementing core primitives for a federated, +delay-tolerant mission optimization stack. This repo provides: + +- A Graph-of-Contracts registry (gravityweave.registry) +- A small CRDT-style PlanDelta implementation (ORSet, LWWRegister) +- Toy adapters: sat_planner and relay_module +- A minimal governance ledger with tamper-evident chaining (HMAC-signed) +- An ADMM-lite solver stub for local updates + +This is intentionally a small, well-tested starting point for the full +GravityWeave architecture described in the original idea. + +Run tests: `bash test.sh` diff --git a/gravityweave/__init__.py b/gravityweave/__init__.py new file mode 100644 index 0000000..b2dd0e9 --- /dev/null +++ b/gravityweave/__init__.py @@ -0,0 +1,24 @@ +"""GravityWeave package - lightweight federation primitives for orbital resource optimization. + +This package contains minimal, well-tested building blocks intended as a foundation +for the GravityWeave ecosystem: a Graph-of-Contracts registry, a small CRDT-style +PlanDelta, a governance ledger (tamper-evident append-only log), two toy adapters, +and an ADMM-lite solver stub. +""" + +from .registry import Registry +from .plan_delta import PlanDelta, ORSet, LWWRegister +from .ledger import GovernanceLedger +from .adapters import sat_planner, relay_module +from .solver import ADMMNode + +__all__ = [ + "Registry", + "PlanDelta", + "ORSet", + "LWWRegister", + "GovernanceLedger", + "sat_planner", + "relay_module", + "ADMMNode", +] diff --git a/gravityweave/adapters/__init__.py b/gravityweave/adapters/__init__.py new file mode 100644 index 0000000..8bf9298 --- /dev/null +++ b/gravityweave/adapters/__init__.py @@ -0,0 +1,3 @@ +from . import sat_planner, relay_module + +__all__ = ["sat_planner", "relay_module"] diff --git a/gravityweave/adapters/relay_module.py b/gravityweave/adapters/relay_module.py new file mode 100644 index 0000000..119e84e --- /dev/null +++ b/gravityweave/adapters/relay_module.py @@ -0,0 +1,27 @@ +"""Toy relay module adapter. + +This adapter represents a relay node that accepts downlink reservations and +returns feasibility signals. It's intentionally minimal for unit testing. +""" +from typing import Dict, Any +from ..plan_delta import PlanDelta + + +class RelayModuleAdapter: + def __init__(self, node_id: str): + self.node_id = node_id + self.schedule = [] # simplistic list of reserved windows + + def describe_local_problem(self) -> Dict[str, Any]: + return {"node_id": self.node_id, "capabilities": ["downlink"]} + + def apply_delta(self, delta: PlanDelta) -> bool: + # accept 'downlink' commitments and add to schedule + for c in delta.commitments.value(): + if isinstance(c, dict) and c.get("type") == "downlink": + self.schedule.append(c) + return True + + +def create(node_id: str): + return RelayModuleAdapter(node_id) diff --git a/gravityweave/adapters/sat_planner.py b/gravityweave/adapters/sat_planner.py new file mode 100644 index 0000000..be80a19 --- /dev/null +++ b/gravityweave/adapters/sat_planner.py @@ -0,0 +1,32 @@ +"""Toy satellite planner adapter. + +Exposes a simple interface to produce local constraints and accept PlanDelta commits. +This is a stub implementation intended for integration tests and adapter conformance. +""" +from typing import Dict, Any +from ..plan_delta import PlanDelta + + +class SatPlannerAdapter: + def __init__(self, node_id: str): + self.node_id = node_id + self.local_state: Dict[str, Any] = {"fuel": 100.0, "power": 100.0} + + def describe_local_problem(self) -> Dict[str, Any]: + # returns a serializable local problem description + return {"node_id": self.node_id, "state": self.local_state} + + def apply_delta(self, delta: PlanDelta) -> bool: + # naive application: if commitment contains a "burn" element, deduct fuel + for c in delta.commitments.value(): + if isinstance(c, dict) and c.get("type") == "burn": + amount = c.get("fuel", 0.0) + if self.local_state["fuel"] >= amount: + self.local_state["fuel"] -= amount + else: + return False + return True + + +def create(node_id: str): + return SatPlannerAdapter(node_id) diff --git a/gravityweave/ledger.py b/gravityweave/ledger.py new file mode 100644 index 0000000..2b385e8 --- /dev/null +++ b/gravityweave/ledger.py @@ -0,0 +1,54 @@ +"""Governance ledger: append-only tamper-evident log with HMAC signatures. + +This ledger implements a minimal tamper-evident append-only log. Each entry +carries the previous hash and an HMAC using a local key to simulate per-node +signatures. The ledger supports append and verification. +""" +import hashlib +import hmac +import json +import time +from typing import List, Dict, Any + + +class GovernanceLedger: + def __init__(self, node_key: bytes): + self.node_key = node_key + self._chain: List[Dict[str, Any]] = [] + + def _entry_hash(self, entry: Dict[str, Any]) -> str: + raw = json.dumps(entry, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() + + def _sign(self, payload: str) -> str: + return hmac.new(self.node_key, payload.encode("utf-8"), hashlib.sha256).hexdigest() + + def append(self, author: str, payload: Dict[str, Any]) -> Dict[str, Any]: + prev_hash = self._chain[-1]["hash"] if self._chain else "" + entry = { + "author": author, + "payload": payload, + "prev_hash": prev_hash, + "ts": time.time(), + } + entry_hash = self._entry_hash(entry) + signature = self._sign(entry_hash) + entry["hash"] = entry_hash + entry["sig"] = signature + self._chain.append(entry) + return entry + + def verify_chain(self) -> bool: + prev = "" + for entry in self._chain: + if entry.get("prev_hash", "") != prev: + return False + # verify signature + expected_sig = self._sign(entry["hash"]) + if not hmac.compare_digest(expected_sig, entry.get("sig", "")): + return False + prev = entry["hash"] + return True + + def entries(self) -> List[Dict[str, Any]]: + return list(self._chain) diff --git a/gravityweave/plan_delta.py b/gravityweave/plan_delta.py new file mode 100644 index 0000000..5ec4d16 --- /dev/null +++ b/gravityweave/plan_delta.py @@ -0,0 +1,94 @@ +"""PlanDelta CRDT primitives: OR-Set and LWW registers for deterministic merging. + +This file implements a minimal set of CRDT-like types used to make PlanDelta +merges deterministic on reconnection. They are intentionally small but include +merge operations and vector-clock-like timestamps. +""" +from dataclasses import dataclass, field +from typing import Any, Dict, List +import time + + +@dataclass +class ORSet: + # store JSON-keyed entries so unhashable objects (dicts) can be used + add_set: Dict[str, Dict[str, Any]] = field(default_factory=dict) + rem_set: Dict[str, Dict[str, Any]] = field(default_factory=dict) + + def _key(self, element: Any) -> str: + import json as _json + + return _json.dumps(element, sort_keys=True, separators=(',', ':')) + + def add(self, element: Any): + k = self._key(element) + self.add_set[k] = {"val": element, "ts": time.time()} + + def remove(self, element: Any): + k = self._key(element) + self.rem_set[k] = {"val": element, "ts": time.time()} + + def value(self) -> List[Any]: + res: List[Any] = [] + for k, info in self.add_set.items(): + add_ts = info["ts"] + rem_ts = self.rem_set.get(k, {}).get("ts", -1) + if add_ts > rem_ts: + # original object + res.append(self.add_set[k]["val"]) + return res + + def merge(self, other: "ORSet") -> "ORSet": + res = ORSet() + # merge add_set + for k, info in {**self.add_set, **other.add_set}.items(): + # pick newest + left = self.add_set.get(k) + right = other.add_set.get(k) + if left and right: + res.add_set[k] = left if left["ts"] >= right["ts"] else right + else: + res.add_set[k] = info + # merge rem_set similarly + for k, info in {**self.rem_set, **other.rem_set}.items(): + left = self.rem_set.get(k) + right = other.rem_set.get(k) + if left and right: + res.rem_set[k] = left if left["ts"] >= right["ts"] else right + else: + res.rem_set[k] = info + return res + + +@dataclass +class LWWRegister: + value: Any = None + ts: float = 0.0 + + def write(self, value: Any): + self.value = value + self.ts = time.time() + + def read(self): + return self.value + + def merge(self, other: "LWWRegister") -> "LWWRegister": + if other.ts >= self.ts: + return LWWRegister(value=other.value, ts=other.ts) + return LWWRegister(value=self.value, ts=self.ts) + + +@dataclass +class PlanDelta: + # a minimal plan delta carrying a set of commitments and a priority register + commitments: ORSet = field(default_factory=ORSet) + priority: LWWRegister = field(default_factory=LWWRegister) + origin: str = "" + + def merge(self, other: "PlanDelta") -> "PlanDelta": + p = PlanDelta() + p.commitments = self.commitments.merge(other.commitments) + p.priority = self.priority.merge(other.priority) + # origin is not merged; keep latest priority's origin heuristically + p.origin = other.origin if other.priority.ts >= self.priority.ts else self.origin + return p diff --git a/gravityweave/registry.py b/gravityweave/registry.py new file mode 100644 index 0000000..2442dc1 --- /dev/null +++ b/gravityweave/registry.py @@ -0,0 +1,43 @@ +"""Graph-of-Contracts registry: minimal schema and adapter registration. + +This registry stores contract descriptors and adapter metadata. It's intentionally +lightweight and uses JSON-serializable structures so adapters can be validated. +""" +from typing import Dict, Any +import time +import uuid + + +class Registry: + def __init__(self): + # contracts keyed by name -> metadata + self._contracts: Dict[str, Dict[str, Any]] = {} + self._adapters: Dict[str, Dict[str, Any]] = {} + + def register_contract(self, name: str, schema: Dict[str, Any], version: str = "0.1") -> str: + contract_id = f"{name}:{version}" + self._contracts[contract_id] = { + "name": name, + "version": version, + "schema": schema, + "registered_at": time.time(), + "id": contract_id, + } + return contract_id + + def get_contract(self, contract_id: str) -> Dict[str, Any]: + return self._contracts[contract_id] + + def register_adapter(self, adapter_name: str, contract_id: str, meta: Dict[str, Any]) -> str: + adapter_id = f"{adapter_name}:{str(uuid.uuid4())[:8]}" + self._adapters[adapter_id] = { + "name": adapter_name, + "contract_id": contract_id, + "meta": meta, + "registered_at": time.time(), + "id": adapter_id, + } + return adapter_id + + def list_adapters(self): + return list(self._adapters.values()) diff --git a/gravityweave/solver.py b/gravityweave/solver.py new file mode 100644 index 0000000..606893a --- /dev/null +++ b/gravityweave/solver.py @@ -0,0 +1,37 @@ +"""ADMM-lite node stub. + +This file contains a very small ADMM-like iterative collaborator that exchanges +dual updates and tries to improve a local objective. The real system would +use network transport and privacy-preserving aggregation; this stub focuses on +the local update loop and simple utility computation for tests. +""" +from typing import Any, Dict +from dataclasses import dataclass, field + + +@dataclass +class ADMMNode: + node_id: str + local_x: float = 0.0 + dual: float = 0.0 + rho: float = 1.0 + + def local_objective(self, x: float) -> float: + # simple quadratic objective with a local preferred point + preferred = 10.0 + return (x - preferred) ** 2 + + def step(self, global_avg: float) -> float: + # ADMM-style proximal step: minimize 0.5*(x - preferred)^2 + (rho/2)*(x - z + u)^2 + # here we do a single closed-form proximal update for a quadratic + preferred = 10.0 + z = global_avg + u = self.dual + # minimize (x-pref)^2 + rho*(x - z + u)^2 + denom = 1.0 + self.rho + num = 2 * preferred + 2 * self.rho * (z - u) + x_new = num / (2 * denom) + self.local_x = x_new + # update dual as simple gradient step + self.dual = u + (x_new - z) + return x_new diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..31b324d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "gravityweave" +version = "0.1.0" +description = "GravityWeave: Federated orbital resource optimization skeleton" +readme = "README.md" +requires-python = ">=3.8" +authors = [ { name = "GravityWeave Contributors" } ] +dependencies = [] diff --git a/test.sh b/test.sh index 3d2b971..d451f0e 100755 --- a/test.sh +++ b/test.sh @@ -1,3 +1,10 @@ #!/bin/bash -echo 'Tests passed!' +set -euo pipefail +echo "Building package..." +python3 -m build +echo "Installing package into environment..." +pip3 install --upgrade . +echo "Running tests..." +pytest -q +echo 'All tests passed!' exit 0 diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..096e08d --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,60 @@ +import os +import secrets +from gravityweave.registry import Registry +from gravityweave.plan_delta import PlanDelta, ORSet, LWWRegister +from gravityweave.ledger import GovernanceLedger +from gravityweave.adapters.sat_planner import create as sat_create +from gravityweave.adapters.relay_module import create as relay_create +from gravityweave.solver import ADMMNode + + +def test_registry_and_adapters(): + reg = Registry() + cid = reg.register_contract("sat_problem", {"fuel": "float"}) + aid = reg.register_adapter("sat_planner", cid, {"version": "0.1"}) + adapters = reg.list_adapters() + assert any(a["name"] == "sat_planner" for a in adapters) + + +def test_plan_delta_merge(): + p1 = PlanDelta(origin="nodeA") + p1.commitments.add({"type": "burn", "fuel": 1.0}) + p1.priority.write(5) + + p2 = PlanDelta(origin="nodeB") + p2.commitments.add({"type": "downlink", "window": 42}) + p2.priority.write(10) + + merged = p1.merge(p2) + vals = merged.commitments.value() + assert any(isinstance(v, dict) for v in vals) + assert merged.priority.read() == 10 + + +def test_ledger_append_and_verify(): + key = secrets.token_bytes(32) + ledger = GovernanceLedger(key) + e1 = ledger.append("nodeA", {"action": "seed"}) + e2 = ledger.append("nodeB", {"action": "update"}) + assert ledger.verify_chain() + entries = ledger.entries() + assert entries[0]["author"] == "nodeA" + + +def test_adapters_apply_delta(): + sat = sat_create("sat1") + relay = relay_create("relay1") + delta = PlanDelta(origin="test") + delta.commitments.add({"type": "burn", "fuel": 1.0}) + delta.commitments.add({"type": "downlink", "window": 99}) + + assert sat.apply_delta(delta) + assert relay.apply_delta(delta) + + +def test_admm_node_step(): + node = ADMMNode("n1") + z = 8.0 + x_new = node.step(z) + # numeric sanity + assert isinstance(x_new, float)