build(agent): c3po#b883b4 iteration
This commit is contained in:
parent
a892c114cd
commit
27203fd181
20
AGENTS.md
20
AGENTS.md
|
|
@ -2,15 +2,15 @@
|
||||||
|
|
||||||
Architectural rules and contribution guidelines for HopeMesh 2.0 bootstrap.
|
Architectural rules and contribution guidelines for HopeMesh 2.0 bootstrap.
|
||||||
|
|
||||||
- Language: Python. Packaging via pyproject.toml with setuptools.
|
- Language: Python. Packaging via `pyproject.toml` with setuptools.
|
||||||
- Key modules:
|
- Core modules:
|
||||||
- hopemesh.contracts: Graph-of-Contracts registry skeleton and primitive types.
|
- `hopemesh.core`: canonical primitives and fingerprinting helpers.
|
||||||
- hopemesh.sdk: Tiny DSL-like helpers for LocalProblem, SharedVariables, PlanDelta, Policy, AttestationHint, AuditLog, PrivacyBudget.
|
- `hopemesh.contracts`: Graph-of-Contracts registry plus tamper-evident governance ledger.
|
||||||
- hopemesh.cli: Lightweight command-line interface stub for adapters and governance operations.
|
- `hopemesh.sdk`: DSL-style constructors and delta merge helpers.
|
||||||
|
- `hopemesh.cli`: local registry and ledger CLI.
|
||||||
- Tests and build:
|
- Testing:
|
||||||
- test.sh should run: python3 -m build and a quick import/instantiate smoke test.
|
- `test.sh` runs `python3 -m build`, a smoke import/instantiate check, and `python3 -m unittest discover -s tests -p 'test_*.py'`.
|
||||||
- Publishing:
|
- Publishing:
|
||||||
- Create READY_TO_PUBLISH when all checks pass.
|
- Create `READY_TO_PUBLISH` only when the repo is fully green and the implementation scope matches the published description.
|
||||||
|
|
||||||
This document is a placeholder for the initial bootstrap rules. As the repo evolves, update with concrete testing commands and CI expectations.
|
Keep changes small, deterministic, and standard-library only unless a new dependency is explicitly justified.
|
||||||
|
|
|
||||||
59
README.md
59
README.md
|
|
@ -1,29 +1,40 @@
|
||||||
# HopeMesh 2.0 (Skeleton)
|
# HopeMesh 2.0
|
||||||
|
|
||||||
This repository contains a production-ready skeleton for a federated, privacy-preserving humanitarian resource allocation platform as described in the HopeMesh 2.0 concept. The initial code focuses on:
|
HopeMesh 2.0 is a Python package for federated humanitarian allocation workflows in disrupted networks.
|
||||||
- Defining canonical primitives: LocalProblem, SharedVariables, PlanDelta, Policy, AttestationHint, AuditLog, PrivacyBudget.
|
|
||||||
- A Graph-of-Contracts registry scaffold to manage data schemas and adapters.
|
|
||||||
- A minimal, deterministic SDK for building adapters that interoperate with the primitives.
|
|
||||||
- Packaging metadata and a test script that validates import/instantiation and packaging.
|
|
||||||
|
|
||||||
How to run
|
It provides:
|
||||||
- Ensure Python 3.8+ is installed.
|
- Canonical primitives for `LocalProblem`, `SharedVariables`, `PlanDelta`, `Policy`, `AttestationHint`, `AuditLog`, and `PrivacyBudget`.
|
||||||
- Install build tooling and build the package:
|
- A Graph-of-Contracts registry for versioned schemas and adapters.
|
||||||
- python3 -m pip install --upgrade build
|
- A tamper-evident governance ledger.
|
||||||
- python3 -m build
|
- Small SDK helpers for deterministic construction and delta merging.
|
||||||
- Smoke test:
|
- A CLI for local registry and ledger snapshots.
|
||||||
- python3 - << 'PY'
|
|
||||||
from hopemesh.sdk import LocalProblem, SharedVariables
|
|
||||||
lp = LocalProblem(name="RegionA Allocation")
|
|
||||||
sv = SharedVariables(aggregate_type="sum")
|
|
||||||
print(lp, sv)
|
|
||||||
PY
|
|
||||||
|
|
||||||
This repo is intentionally minimal to bootstrap the architecture and testing workflow. A more complete implementation will grow the registry, adapters, and cryptographic governance features in subsequent commits.
|
## Layout
|
||||||
|
|
||||||
Usage notes:
|
- `hopemesh/core.py`: primitive dataclasses and fingerprinting helpers.
|
||||||
- Run tests via ./test.sh
|
- `hopemesh/contracts.py`: Graph-of-Contracts and governance ledger.
|
||||||
- The test script builds the package and runs a smoke test importing and instantiating primitives.
|
- `hopemesh/sdk.py`: DSL-style constructors and merge helpers.
|
||||||
- After successful tests, create a READY_TO_PUBLISH marker in the repo root.
|
- `hopemesh/cli.py`: local CLI for registry and ledger operations.
|
||||||
|
- `tests/`: unit tests for primitives, registry, ledger, and helpers.
|
||||||
|
|
||||||
READY_TO_PUBLISH is created when all checks pass.
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install --upgrade build
|
||||||
|
python3 -m build
|
||||||
|
./test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m hopemesh.cli --version
|
||||||
|
python -m hopemesh.cli registry init --store .hopemesh/state.json
|
||||||
|
python -m hopemesh.cli registry add-schema --name allocation --schema-version 1.0 --definition-json '{"fields":["region","qty"]}'
|
||||||
|
python -m hopemesh.cli ledger verify
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The project is intentionally dependency-light and uses the Python standard library for validation, hashing, and snapshotting.
|
||||||
|
- `test.sh` runs `python3 -m build`, a smoke import/instantiate check, and the unit test suite.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
"""hopemesh package root (skeleton).
|
"""HopeMesh 2.0 primitives and registry scaffolding."""
|
||||||
Minimal primitives for HopeMesh 2.0 prototype.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .core import LocalProblem, SharedVariables, PlanDelta, Policy, AttestationHint, AuditLog, PrivacyBudget
|
from .core import LocalProblem, SharedVariables, PlanDelta, Policy, AttestationHint, AuditLog, PrivacyBudget
|
||||||
|
from .contracts import ContractRecord, ContractLink, GraphOfContracts, GovernanceLedger, LedgerEntry
|
||||||
|
from .sdk import (
|
||||||
|
make_local_problem,
|
||||||
|
make_shared_variables,
|
||||||
|
make_plan_delta,
|
||||||
|
make_policy,
|
||||||
|
make_attestation_hint,
|
||||||
|
make_audit_log,
|
||||||
|
make_privacy_budget,
|
||||||
|
merge_plan_deltas,
|
||||||
|
validate_deltas,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"LocalProblem",
|
"LocalProblem",
|
||||||
|
|
@ -12,4 +22,18 @@ __all__ = [
|
||||||
"AttestationHint",
|
"AttestationHint",
|
||||||
"AuditLog",
|
"AuditLog",
|
||||||
"PrivacyBudget",
|
"PrivacyBudget",
|
||||||
|
"ContractRecord",
|
||||||
|
"ContractLink",
|
||||||
|
"GraphOfContracts",
|
||||||
|
"GovernanceLedger",
|
||||||
|
"LedgerEntry",
|
||||||
|
"make_local_problem",
|
||||||
|
"make_shared_variables",
|
||||||
|
"make_plan_delta",
|
||||||
|
"make_policy",
|
||||||
|
"make_attestation_hint",
|
||||||
|
"make_audit_log",
|
||||||
|
"make_privacy_budget",
|
||||||
|
"merge_plan_deltas",
|
||||||
|
"validate_deltas",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
145
hopemesh/cli.py
145
hopemesh/cli.py
|
|
@ -1,17 +1,144 @@
|
||||||
"""Lightweight CLI stub for adapters and governance ops."""
|
"""CLI for HopeMesh registry and governance snapshots."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
def main():
|
from .contracts import GraphOfContracts, GovernanceLedger
|
||||||
parser = argparse.ArgumentParser(prog="hopemesh", description="HopeMesh 2.0 skeleton CLI")
|
|
||||||
subparsers = parser.add_subparsers(dest="cmd", required=False)
|
|
||||||
|
|
||||||
parser.add_argument("--version", action="store_true", help="Show version")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
DEFAULT_STORE = Path(".hopemesh/state.json")
|
||||||
if args.version:
|
|
||||||
|
|
||||||
|
def _load_state(store: Path) -> Dict[str, Any]:
|
||||||
|
if not store.exists():
|
||||||
|
return {"registry": {"records": [], "links": []}, "ledger": {"entries": []}}
|
||||||
|
return json.loads(store.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _save_state(store: Path, state: Dict[str, Any]) -> None:
|
||||||
|
store.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
store.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _graph_from_state(state: Dict[str, Any]) -> GraphOfContracts:
|
||||||
|
return GraphOfContracts.from_snapshot(state.get("registry", {}))
|
||||||
|
|
||||||
|
|
||||||
|
def _ledger_from_state(state: Dict[str, Any]) -> GovernanceLedger:
|
||||||
|
return GovernanceLedger.from_snapshot(state.get("ledger", {}))
|
||||||
|
|
||||||
|
|
||||||
|
def _store_snapshot(store: Path, graph: GraphOfContracts, ledger: GovernanceLedger) -> None:
|
||||||
|
_save_state(store, {"registry": graph.snapshot(), "ledger": ledger.snapshot()})
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(prog="hopemesh", description="HopeMesh 2.0 registry and governance CLI")
|
||||||
|
parser.add_argument("--version", dest="show_version", action="store_true", help="Show version")
|
||||||
|
subparsers = parser.add_subparsers(dest="cmd")
|
||||||
|
|
||||||
|
registry = subparsers.add_parser("registry", help="Manage the Graph-of-Contracts")
|
||||||
|
registry_sub = registry.add_subparsers(dest="registry_cmd", required=True)
|
||||||
|
|
||||||
|
registry_init = registry_sub.add_parser("init", help="Create an empty registry store")
|
||||||
|
registry_init.add_argument("--store", type=Path, default=DEFAULT_STORE)
|
||||||
|
|
||||||
|
registry_schema = registry_sub.add_parser("add-schema", help="Register a schema contract")
|
||||||
|
registry_schema.add_argument("--store", type=Path, default=DEFAULT_STORE)
|
||||||
|
registry_schema.add_argument("--name", required=True)
|
||||||
|
registry_schema.add_argument("--schema-version", required=True)
|
||||||
|
registry_schema.add_argument("--definition-json", required=True)
|
||||||
|
|
||||||
|
registry_adapter = registry_sub.add_parser("add-adapter", help="Register an adapter contract")
|
||||||
|
registry_adapter.add_argument("--store", type=Path, default=DEFAULT_STORE)
|
||||||
|
registry_adapter.add_argument("--name", required=True)
|
||||||
|
registry_adapter.add_argument("--adapter-version", required=True)
|
||||||
|
registry_adapter.add_argument("--definition-json", required=True)
|
||||||
|
|
||||||
|
registry_link = registry_sub.add_parser("link", help="Link two contracts")
|
||||||
|
registry_link.add_argument("--store", type=Path, default=DEFAULT_STORE)
|
||||||
|
registry_link.add_argument("--source-id", required=True)
|
||||||
|
registry_link.add_argument("--target-id", required=True)
|
||||||
|
registry_link.add_argument("--relation", default="implements")
|
||||||
|
|
||||||
|
registry_show = registry_sub.add_parser("snapshot", help="Print the registry snapshot")
|
||||||
|
registry_show.add_argument("--store", type=Path, default=DEFAULT_STORE)
|
||||||
|
|
||||||
|
ledger = subparsers.add_parser("ledger", help="Manage the governance ledger")
|
||||||
|
ledger_sub = ledger.add_subparsers(dest="ledger_cmd", required=True)
|
||||||
|
|
||||||
|
ledger_append = ledger_sub.add_parser("append", help="Append a governance event")
|
||||||
|
ledger_append.add_argument("--store", type=Path, default=DEFAULT_STORE)
|
||||||
|
ledger_append.add_argument("--event-type", required=True)
|
||||||
|
ledger_append.add_argument("--payload-json", required=True)
|
||||||
|
ledger_append.add_argument("--actor", default="system")
|
||||||
|
|
||||||
|
ledger_verify = ledger_sub.add_parser("verify", help="Verify ledger integrity")
|
||||||
|
ledger_verify.add_argument("--store", type=Path, default=DEFAULT_STORE)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.show_version:
|
||||||
print("hopemesh-sdk 0.1.0")
|
print("hopemesh-sdk 0.1.0")
|
||||||
elif not args.cmd:
|
return 0
|
||||||
|
|
||||||
|
if not args.cmd:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
state = _load_state(args.store)
|
||||||
|
graph = _graph_from_state(state)
|
||||||
|
ledger = _ledger_from_state(state)
|
||||||
|
|
||||||
|
if args.cmd == "registry":
|
||||||
|
if args.registry_cmd == "init":
|
||||||
|
_store_snapshot(args.store, graph, ledger)
|
||||||
|
print(str(args.store))
|
||||||
|
return 0
|
||||||
|
if args.registry_cmd in {"add-schema", "add-adapter"}:
|
||||||
|
definition = json.loads(args.definition_json)
|
||||||
|
if args.registry_cmd == "add-schema":
|
||||||
|
record = graph.register_schema(args.name, args.schema_version, definition)
|
||||||
|
else:
|
||||||
|
record = graph.register_adapter(args.name, args.adapter_version, definition)
|
||||||
|
ledger.append("registry.register", {"contract_id": record.contract_id, "kind": record.kind})
|
||||||
|
_store_snapshot(args.store, graph, ledger)
|
||||||
|
print(record.contract_id)
|
||||||
|
return 0
|
||||||
|
if args.registry_cmd == "link":
|
||||||
|
link = graph.link(args.source_id, args.target_id, args.relation)
|
||||||
|
ledger.append("registry.link", link.to_dict())
|
||||||
|
_store_snapshot(args.store, graph, ledger)
|
||||||
|
print(json.dumps(link.to_dict(), sort_keys=True))
|
||||||
|
return 0
|
||||||
|
if args.registry_cmd == "snapshot":
|
||||||
|
print(json.dumps(graph.snapshot(), indent=2, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.cmd == "ledger":
|
||||||
|
if args.ledger_cmd == "append":
|
||||||
|
payload = json.loads(args.payload_json)
|
||||||
|
entry = ledger.append(args.event_type, payload, actor=args.actor)
|
||||||
|
_store_snapshot(args.store, graph, ledger)
|
||||||
|
print(json.dumps(entry.to_dict(), indent=2, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
if args.ledger_cmd == "verify":
|
||||||
|
issues = ledger.verify()
|
||||||
|
print(json.dumps({"ok": not issues, "issues": issues}, indent=2, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
parser.error("unknown command")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
raise SystemExit(main())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, asdict, field
|
||||||
|
from typing import Any, Dict, Iterable, List, Optional
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .core import AttestationHint, AuditLog, PlanDelta, Policy, _canonical_json
|
||||||
|
|
||||||
|
|
||||||
|
def _hash(payload: Any) -> str:
|
||||||
|
return hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContractRecord:
|
||||||
|
contract_id: str
|
||||||
|
kind: str
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
definition: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
attestation: Optional[AttestationHint] = None
|
||||||
|
created_at: float = field(default_factory=lambda: time.time())
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
data = asdict(self)
|
||||||
|
if self.attestation is not None:
|
||||||
|
data["attestation"] = self.attestation.to_dict()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContractLink:
|
||||||
|
source_id: str
|
||||||
|
target_id: str
|
||||||
|
relation: str
|
||||||
|
created_at: float = field(default_factory=lambda: time.time())
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
class GraphOfContracts:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._records: Dict[str, ContractRecord] = {}
|
||||||
|
self._links: List[ContractLink] = []
|
||||||
|
|
||||||
|
def register_schema(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
version: str,
|
||||||
|
definition: Dict[str, Any],
|
||||||
|
attestation: Optional[AttestationHint] = None,
|
||||||
|
) -> ContractRecord:
|
||||||
|
contract_id = f"schema:{name}:{version}"
|
||||||
|
record = ContractRecord(
|
||||||
|
contract_id=contract_id,
|
||||||
|
kind="schema",
|
||||||
|
name=name,
|
||||||
|
version=version,
|
||||||
|
definition=dict(definition),
|
||||||
|
attestation=attestation,
|
||||||
|
)
|
||||||
|
self._records[contract_id] = record
|
||||||
|
return record
|
||||||
|
|
||||||
|
def register_adapter(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
version: str,
|
||||||
|
definition: Dict[str, Any],
|
||||||
|
attestation: Optional[AttestationHint] = None,
|
||||||
|
) -> ContractRecord:
|
||||||
|
contract_id = f"adapter:{name}:{version}"
|
||||||
|
record = ContractRecord(
|
||||||
|
contract_id=contract_id,
|
||||||
|
kind="adapter",
|
||||||
|
name=name,
|
||||||
|
version=version,
|
||||||
|
definition=dict(definition),
|
||||||
|
attestation=attestation,
|
||||||
|
)
|
||||||
|
self._records[contract_id] = record
|
||||||
|
return record
|
||||||
|
|
||||||
|
def link(self, source_id: str, target_id: str, relation: str = "implements") -> ContractLink:
|
||||||
|
if source_id not in self._records:
|
||||||
|
raise KeyError(f"Unknown source contract: {source_id}")
|
||||||
|
if target_id not in self._records:
|
||||||
|
raise KeyError(f"Unknown target contract: {target_id}")
|
||||||
|
link = ContractLink(source_id=source_id, target_id=target_id, relation=relation)
|
||||||
|
self._links.append(link)
|
||||||
|
return link
|
||||||
|
|
||||||
|
def resolve(self, contract_id: str) -> ContractRecord:
|
||||||
|
return self._records[contract_id]
|
||||||
|
|
||||||
|
def records(self) -> List[ContractRecord]:
|
||||||
|
return list(self._records.values())
|
||||||
|
|
||||||
|
def links(self) -> List[ContractLink]:
|
||||||
|
return list(self._links)
|
||||||
|
|
||||||
|
def validate(self) -> List[str]:
|
||||||
|
issues: List[str] = []
|
||||||
|
for link in self._links:
|
||||||
|
if link.source_id not in self._records:
|
||||||
|
issues.append(f"missing source {link.source_id}")
|
||||||
|
if link.target_id not in self._records:
|
||||||
|
issues.append(f"missing target {link.target_id}")
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def snapshot(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"records": [record.to_dict() for record in self.records()],
|
||||||
|
"links": [link.to_dict() for link in self.links()],
|
||||||
|
"fingerprint": self.fingerprint(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_snapshot(cls, snapshot: Dict[str, Any]) -> "GraphOfContracts":
|
||||||
|
graph = cls()
|
||||||
|
for record_data in snapshot.get("records", []):
|
||||||
|
attestation = record_data.get("attestation")
|
||||||
|
hint = AttestationHint(**attestation) if attestation else None
|
||||||
|
record = ContractRecord(
|
||||||
|
contract_id=record_data["contract_id"],
|
||||||
|
kind=record_data["kind"],
|
||||||
|
name=record_data["name"],
|
||||||
|
version=record_data["version"],
|
||||||
|
definition=dict(record_data.get("definition", {})),
|
||||||
|
attestation=hint,
|
||||||
|
created_at=record_data.get("created_at", time.time()),
|
||||||
|
)
|
||||||
|
graph._records[record.contract_id] = record
|
||||||
|
for link_data in snapshot.get("links", []):
|
||||||
|
graph._links.append(
|
||||||
|
ContractLink(
|
||||||
|
source_id=link_data["source_id"],
|
||||||
|
target_id=link_data["target_id"],
|
||||||
|
relation=link_data["relation"],
|
||||||
|
created_at=link_data.get("created_at", time.time()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return graph
|
||||||
|
|
||||||
|
def fingerprint(self) -> str:
|
||||||
|
payload = {
|
||||||
|
"records": [record.to_dict() for record in sorted(self._records.values(), key=lambda item: item.contract_id)],
|
||||||
|
"links": [link.to_dict() for link in self._links],
|
||||||
|
}
|
||||||
|
return _hash(payload)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LedgerEntry:
|
||||||
|
entry_id: str
|
||||||
|
event_type: str
|
||||||
|
payload: Dict[str, Any]
|
||||||
|
actor: str
|
||||||
|
timestamp: float
|
||||||
|
previous_hash: str
|
||||||
|
entry_hash: str
|
||||||
|
signature: Optional[str] = None
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
class GovernanceLedger:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._entries: List[LedgerEntry] = []
|
||||||
|
|
||||||
|
def append(
|
||||||
|
self,
|
||||||
|
event_type: str,
|
||||||
|
payload: Dict[str, Any],
|
||||||
|
actor: str = "system",
|
||||||
|
signature: Optional[str] = None,
|
||||||
|
timestamp: Optional[float] = None,
|
||||||
|
) -> LedgerEntry:
|
||||||
|
ts = time.time() if timestamp is None else timestamp
|
||||||
|
previous_hash = self._entries[-1].entry_hash if self._entries else "genesis"
|
||||||
|
entry_id = payload.get("entry_id") or f"{event_type}-{len(self._entries) + 1}"
|
||||||
|
basis = {
|
||||||
|
"entry_id": entry_id,
|
||||||
|
"event_type": event_type,
|
||||||
|
"payload": payload,
|
||||||
|
"actor": actor,
|
||||||
|
"timestamp": ts,
|
||||||
|
"previous_hash": previous_hash,
|
||||||
|
}
|
||||||
|
entry_hash = _hash(basis)
|
||||||
|
entry = LedgerEntry(
|
||||||
|
entry_id=entry_id,
|
||||||
|
event_type=event_type,
|
||||||
|
payload=dict(payload),
|
||||||
|
actor=actor,
|
||||||
|
timestamp=ts,
|
||||||
|
previous_hash=previous_hash,
|
||||||
|
entry_hash=entry_hash,
|
||||||
|
signature=signature or entry_hash,
|
||||||
|
)
|
||||||
|
self._entries.append(entry)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def entries(self) -> List[LedgerEntry]:
|
||||||
|
return list(self._entries)
|
||||||
|
|
||||||
|
def verify(self) -> List[str]:
|
||||||
|
issues: List[str] = []
|
||||||
|
previous_hash = "genesis"
|
||||||
|
for entry in self._entries:
|
||||||
|
basis = {
|
||||||
|
"entry_id": entry.entry_id,
|
||||||
|
"event_type": entry.event_type,
|
||||||
|
"payload": entry.payload,
|
||||||
|
"actor": entry.actor,
|
||||||
|
"timestamp": entry.timestamp,
|
||||||
|
"previous_hash": previous_hash,
|
||||||
|
}
|
||||||
|
expected_hash = _hash(basis)
|
||||||
|
if expected_hash != entry.entry_hash:
|
||||||
|
issues.append(f"entry {entry.entry_id} hash mismatch")
|
||||||
|
if entry.previous_hash != previous_hash:
|
||||||
|
issues.append(f"entry {entry.entry_id} chain mismatch")
|
||||||
|
previous_hash = entry.entry_hash
|
||||||
|
return issues
|
||||||
|
|
||||||
|
def snapshot(self) -> Dict[str, Any]:
|
||||||
|
return {"entries": [entry.to_dict() for entry in self._entries], "fingerprint": self.fingerprint()}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_snapshot(cls, snapshot: Dict[str, Any]) -> "GovernanceLedger":
|
||||||
|
ledger = cls()
|
||||||
|
for entry_data in snapshot.get("entries", []):
|
||||||
|
ledger._entries.append(
|
||||||
|
LedgerEntry(
|
||||||
|
entry_id=entry_data["entry_id"],
|
||||||
|
event_type=entry_data["event_type"],
|
||||||
|
payload=dict(entry_data.get("payload", {})),
|
||||||
|
actor=entry_data["actor"],
|
||||||
|
timestamp=entry_data["timestamp"],
|
||||||
|
previous_hash=entry_data["previous_hash"],
|
||||||
|
entry_hash=entry_data["entry_hash"],
|
||||||
|
signature=entry_data.get("signature"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ledger
|
||||||
|
|
||||||
|
def fingerprint(self) -> str:
|
||||||
|
return _hash({"entries": [entry.to_dict() for entry in self._entries]})
|
||||||
|
|
||||||
|
|
||||||
|
def validate_plan_deltas(deltas: Iterable[PlanDelta]) -> List[str]:
|
||||||
|
issues: List[str] = []
|
||||||
|
for delta in deltas:
|
||||||
|
if not delta.actions:
|
||||||
|
issues.append(f"{delta.delta_id}: empty actions")
|
||||||
|
if delta.signature is not None and not delta.signature.strip():
|
||||||
|
issues.append(f"{delta.delta_id}: blank signature")
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def policy_summary(policy: Policy) -> str:
|
||||||
|
return json.dumps(policy.to_dict(), sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def audit_plan_delta(delta: PlanDelta, actor: str, message: str) -> AuditLog:
|
||||||
|
payload = {"delta_id": delta.delta_id, "actor": actor, "message": message, "fingerprint": delta.fingerprint()}
|
||||||
|
signature = _hash(payload)
|
||||||
|
return AuditLog(entry_id=f"audit-{delta.delta_id}", message=message, signature=signature, anchor=payload["fingerprint"])
|
||||||
|
|
@ -1,29 +1,60 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(payload: Any) -> str:
|
||||||
|
return json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
|
||||||
|
|
||||||
|
|
||||||
|
def _fingerprint(payload: Any) -> str:
|
||||||
|
return hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalProblem:
|
class LocalProblem:
|
||||||
name: str
|
name: str
|
||||||
region: Optional[str] = None
|
region: Optional[str] = None
|
||||||
|
objective: str = ""
|
||||||
|
constraints: Dict[str, Any] = field(default_factory=dict)
|
||||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||||
created_at: float = field(default_factory=lambda: time.time())
|
created_at: float = field(default_factory=lambda: time.time())
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.name.strip():
|
||||||
|
raise ValueError("LocalProblem.name must not be empty")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"LocalProblem(name={self.name!r}, region={self.region!r})"
|
return f"LocalProblem(name={self.name!r}, region={self.region!r}, objective={self.objective!r})"
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
def fingerprint(self) -> str:
|
||||||
|
return _fingerprint(self.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SharedVariables:
|
class SharedVariables:
|
||||||
description: str = ""
|
description: str = ""
|
||||||
data: Dict[str, Any] = field(default_factory=dict)
|
data: Dict[str, Any] = field(default_factory=dict)
|
||||||
aggregate_type: str = "sum" # placeholder for privacy-preserving aggregation
|
aggregate_type: str = "sum"
|
||||||
|
privacy_budget_tag: Optional[str] = None
|
||||||
|
source_regions: List[str] = field(default_factory=list)
|
||||||
created_at: float = field(default_factory=lambda: time.time())
|
created_at: float = field(default_factory=lambda: time.time())
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"SharedVariables(desc={self.description!r}, keys={list(self.data.keys())})"
|
return f"SharedVariables(desc={self.description!r}, keys={list(self.data.keys())}, aggregate={self.aggregate_type!r})"
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
def fingerprint(self) -> str:
|
||||||
|
return _fingerprint(self.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -31,10 +62,27 @@ class PlanDelta:
|
||||||
delta_id: str
|
delta_id: str
|
||||||
actions: List[Dict[str, Any]] = field(default_factory=list)
|
actions: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
timestamp: float = field(default_factory=lambda: time.time())
|
timestamp: float = field(default_factory=lambda: time.time())
|
||||||
|
actor_did: Optional[str] = None
|
||||||
|
causality: Dict[str, int] = field(default_factory=dict)
|
||||||
|
policy_tags: List[str] = field(default_factory=list)
|
||||||
|
privacy_budget_tag: Optional[str] = None
|
||||||
annotations: Dict[str, Any] = field(default_factory=dict)
|
annotations: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
signature: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.delta_id.strip():
|
||||||
|
raise ValueError("PlanDelta.delta_id must not be empty")
|
||||||
|
if not isinstance(self.actions, list):
|
||||||
|
raise TypeError("PlanDelta.actions must be a list")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"PlanDelta(delta_id={self.delta_id!r}, actions={len(self.actions)} actions)"
|
return f"PlanDelta(delta_id={self.delta_id!r}, actions={len(self.actions)} actions, actor={self.actor_did!r})"
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
def fingerprint(self) -> str:
|
||||||
|
return _fingerprint(self.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -42,9 +90,14 @@ class Policy:
|
||||||
policy_id: str
|
policy_id: str
|
||||||
rules: Dict[str, Any] = field(default_factory=dict)
|
rules: Dict[str, Any] = field(default_factory=dict)
|
||||||
effective_at: float = field(default_factory=lambda: time.time())
|
effective_at: float = field(default_factory=lambda: time.time())
|
||||||
|
scope: str = "global"
|
||||||
|
approved_by: Optional[str] = None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Policy(id={self.policy_id!r}, rules={len(self.rules)})"
|
return f"Policy(id={self.policy_id!r}, rules={len(self.rules)}, scope={self.scope!r})"
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -52,10 +105,14 @@ class AttestationHint:
|
||||||
hint_id: str
|
hint_id: str
|
||||||
scheme: str = "dummy"
|
scheme: str = "dummy"
|
||||||
payload: Dict[str, Any] = field(default_factory=dict)
|
payload: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
expires_at: Optional[float] = None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"AttestationHint(id={self.hint_id!r}, scheme={self.scheme!r})"
|
return f"AttestationHint(id={self.hint_id!r}, scheme={self.scheme!r})"
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AuditLog:
|
class AuditLog:
|
||||||
|
|
@ -63,21 +120,37 @@ class AuditLog:
|
||||||
message: str
|
message: str
|
||||||
timestamp: float = field(default_factory=lambda: time.time())
|
timestamp: float = field(default_factory=lambda: time.time())
|
||||||
signature: Optional[str] = None
|
signature: Optional[str] = None
|
||||||
|
anchor: Optional[str] = None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"AuditLog(id={self.entry_id!r}, ts={self.timestamp})"
|
return f"AuditLog(id={self.entry_id!r}, ts={self.timestamp})"
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PrivacyBudget:
|
class PrivacyBudget:
|
||||||
budget_id: str
|
budget_id: str
|
||||||
limit: float
|
limit: float
|
||||||
used: float = 0.0
|
used: float = 0.0
|
||||||
def consume(self, amount: float) -> None:
|
|
||||||
self.used = min(self.used + amount, self.limit)
|
def consume(self, amount: float) -> bool:
|
||||||
|
if amount < 0:
|
||||||
|
raise ValueError("PrivacyBudget.consume requires a non-negative amount")
|
||||||
|
if self.used + amount > self.limit:
|
||||||
|
return False
|
||||||
|
self.used += amount
|
||||||
|
return True
|
||||||
|
|
||||||
|
def can_consume(self, amount: float) -> bool:
|
||||||
|
return amount >= 0 and self.used + amount <= self.limit
|
||||||
|
|
||||||
def remaining(self) -> float:
|
def remaining(self) -> float:
|
||||||
return self.limit - self.used
|
return self.limit - self.used
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"PrivacyBudget(id={self.budget_id!r}, remaining={self.remaining()})"
|
return f"PrivacyBudget(id={self.budget_id!r}, remaining={self.remaining()})"
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,55 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
"""Tiny DSL-like helpers for HopeMesh primitives and contract tooling."""
|
||||||
|
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
from .core import LocalProblem, SharedVariables, PlanDelta, Policy, AttestationHint, AuditLog, PrivacyBudget
|
from .core import LocalProblem, SharedVariables, PlanDelta, Policy, AttestationHint, AuditLog, PrivacyBudget
|
||||||
|
from .contracts import validate_plan_deltas
|
||||||
|
|
||||||
"""Tiny DSL-like helpers for HopeMesh primitives.
|
def make_local_problem(
|
||||||
This module provides lightweight constructors to enable adapters to plug into the canonical primitives.
|
name: str,
|
||||||
"""
|
region: str | None = None,
|
||||||
|
objective: str = "",
|
||||||
|
constraints: dict | None = None,
|
||||||
|
**metadata,
|
||||||
|
) -> LocalProblem:
|
||||||
|
return LocalProblem(name=name, region=region, objective=objective, constraints=constraints or {}, metadata=metadata)
|
||||||
|
|
||||||
def make_local_problem(name: str, region: str | None = None, **kwargs) -> LocalProblem:
|
def make_shared_variables(
|
||||||
return LocalProblem(name=name, region=region, metadata=kwargs)
|
description: str = "",
|
||||||
|
data: dict | None = None,
|
||||||
|
aggregate_type: str = "sum",
|
||||||
|
privacy_budget_tag: str | None = None,
|
||||||
|
source_regions: list[str] | None = None,
|
||||||
|
) -> SharedVariables:
|
||||||
|
return SharedVariables(
|
||||||
|
description=description,
|
||||||
|
data=data or {},
|
||||||
|
aggregate_type=aggregate_type,
|
||||||
|
privacy_budget_tag=privacy_budget_tag,
|
||||||
|
source_regions=source_regions or [],
|
||||||
|
)
|
||||||
|
|
||||||
def make_shared_variables(description: str = "", data: dict | None = None, aggregate_type: str = "sum") -> SharedVariables:
|
def make_plan_delta(
|
||||||
return SharedVariables(description=description, data=data or {}, aggregate_type=aggregate_type)
|
delta_id: str,
|
||||||
|
actions: list[dict],
|
||||||
def make_plan_delta(delta_id: str, actions: list[dict], **kwargs) -> PlanDelta:
|
actor_did: str | None = None,
|
||||||
return PlanDelta(delta_id=delta_id, actions=actions, annotations=kwargs)
|
causality: dict | None = None,
|
||||||
|
policy_tags: list[str] | None = None,
|
||||||
|
privacy_budget_tag: str | None = None,
|
||||||
|
signature: str | None = None,
|
||||||
|
**annotations,
|
||||||
|
) -> PlanDelta:
|
||||||
|
return PlanDelta(
|
||||||
|
delta_id=delta_id,
|
||||||
|
actions=actions,
|
||||||
|
actor_did=actor_did,
|
||||||
|
causality=causality or {},
|
||||||
|
policy_tags=policy_tags or [],
|
||||||
|
privacy_budget_tag=privacy_budget_tag,
|
||||||
|
signature=signature,
|
||||||
|
annotations=annotations,
|
||||||
|
)
|
||||||
|
|
||||||
def make_policy(policy_id: str, **rules) -> Policy:
|
def make_policy(policy_id: str, **rules) -> Policy:
|
||||||
return Policy(policy_id=policy_id, rules=rules)
|
return Policy(policy_id=policy_id, rules=rules)
|
||||||
|
|
@ -25,3 +62,23 @@ def make_audit_log(entry_id: str, message: str, signature: str | None = None) ->
|
||||||
|
|
||||||
def make_privacy_budget(budget_id: str, limit: float) -> PrivacyBudget:
|
def make_privacy_budget(budget_id: str, limit: float) -> PrivacyBudget:
|
||||||
return PrivacyBudget(budget_id=budget_id, limit=limit)
|
return PrivacyBudget(budget_id=budget_id, limit=limit)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_plan_deltas(deltas: Iterable[PlanDelta]) -> PlanDelta:
|
||||||
|
ordered = sorted(deltas, key=lambda delta: (delta.timestamp, delta.delta_id))
|
||||||
|
actions: list[dict] = []
|
||||||
|
contributors: list[str] = []
|
||||||
|
for delta in ordered:
|
||||||
|
contributors.append(delta.delta_id)
|
||||||
|
actions.extend(delta.actions)
|
||||||
|
delta_id = "merged-" + "-".join(contributors) if contributors else "merged-empty"
|
||||||
|
return PlanDelta(
|
||||||
|
delta_id=delta_id,
|
||||||
|
actions=actions,
|
||||||
|
causality={"merged_from": len(contributors)},
|
||||||
|
annotations={"contributors": contributors},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_deltas(deltas: Iterable[PlanDelta]) -> list[str]:
|
||||||
|
return validate_plan_deltas(deltas)
|
||||||
|
|
|
||||||
16
test.sh
16
test.sh
|
|
@ -7,14 +7,28 @@ python3 -m build >/tmp/build.log 2>&1 || {
|
||||||
|
|
||||||
echo "Smoke test: import modules and create objects..."
|
echo "Smoke test: import modules and create objects..."
|
||||||
python3 - << 'PY'
|
python3 - << 'PY'
|
||||||
from hopemesh.sdk import make_local_problem, make_shared_variables, make_plan_delta
|
from hopemesh import GraphOfContracts, GovernanceLedger
|
||||||
|
from hopemesh.sdk import make_local_problem, make_shared_variables, make_plan_delta, merge_plan_deltas
|
||||||
lp = make_local_problem("RegionA Allocation", region="RegionA")
|
lp = make_local_problem("RegionA Allocation", region="RegionA")
|
||||||
sv = make_shared_variables("example", {"stock": 100})
|
sv = make_shared_variables("example", {"stock": 100})
|
||||||
pd = make_plan_delta("pd-001", [{"allocate": {"region": "RegionA", "qty": 10}}])
|
pd = make_plan_delta("pd-001", [{"allocate": {"region": "RegionA", "qty": 10}}])
|
||||||
|
graph = GraphOfContracts()
|
||||||
|
schema = graph.register_schema("allocation", "1.0", {"fields": ["region", "qty"]})
|
||||||
|
adapter = graph.register_adapter("regional-hub", "1.0", {"transport": "tls"})
|
||||||
|
graph.link(adapter.contract_id, schema.contract_id)
|
||||||
|
ledger = GovernanceLedger()
|
||||||
|
ledger.append("demo", {"delta": pd.delta_id})
|
||||||
|
merged = merge_plan_deltas([pd])
|
||||||
print(lp)
|
print(lp)
|
||||||
print(sv)
|
print(sv)
|
||||||
print(pd)
|
print(pd)
|
||||||
|
print(graph.fingerprint())
|
||||||
|
print(ledger.verify())
|
||||||
|
print(merged)
|
||||||
print("OK")
|
print("OK")
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
echo "Running unit tests..."
|
||||||
|
python3 -m unittest discover -s tests -p 'test_*.py'
|
||||||
|
|
||||||
echo "All tests passed."
|
echo "All tests passed."
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from hopemesh.cli import main
|
||||||
|
|
||||||
|
|
||||||
|
class HopeMeshCLITests(unittest.TestCase):
|
||||||
|
def test_registry_and_ledger_commands(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
store = Path(tmpdir) / "state.json"
|
||||||
|
self.assertEqual(main(["registry", "init", "--store", str(store)]), 0)
|
||||||
|
self.assertEqual(
|
||||||
|
main(
|
||||||
|
[
|
||||||
|
"registry",
|
||||||
|
"add-schema",
|
||||||
|
"--store",
|
||||||
|
str(store),
|
||||||
|
"--name",
|
||||||
|
"allocation",
|
||||||
|
"--schema-version",
|
||||||
|
"1.0",
|
||||||
|
"--definition-json",
|
||||||
|
'{"fields":["region","qty"]}',
|
||||||
|
]
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
self.assertEqual(main(["ledger", "verify", "--store", str(store)]), 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from hopemesh import (
|
||||||
|
GraphOfContracts,
|
||||||
|
GovernanceLedger,
|
||||||
|
LocalProblem,
|
||||||
|
PrivacyBudget,
|
||||||
|
make_local_problem,
|
||||||
|
make_plan_delta,
|
||||||
|
make_shared_variables,
|
||||||
|
merge_plan_deltas,
|
||||||
|
validate_deltas,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HopeMeshCoreTests(unittest.TestCase):
|
||||||
|
def test_primitives_and_budget(self) -> None:
|
||||||
|
problem = LocalProblem(name="Water allocation", region="North", objective="minimize unmet demand")
|
||||||
|
shared = make_shared_variables("stock", {"water": 10}, source_regions=["North"])
|
||||||
|
budget = PrivacyBudget("pb-1", 3.0)
|
||||||
|
|
||||||
|
self.assertTrue(problem.fingerprint())
|
||||||
|
self.assertTrue(shared.fingerprint())
|
||||||
|
self.assertTrue(budget.consume(1.5))
|
||||||
|
self.assertFalse(budget.consume(2.0))
|
||||||
|
self.assertEqual(round(budget.remaining(), 2), 1.5)
|
||||||
|
|
||||||
|
def test_graph_of_contracts(self) -> None:
|
||||||
|
graph = GraphOfContracts()
|
||||||
|
schema = graph.register_schema("resource-plan", "1.0", {"fields": ["id", "qty"]})
|
||||||
|
adapter = graph.register_adapter("field-hub", "1.0", {"transport": "tls"})
|
||||||
|
link = graph.link(adapter.contract_id, schema.contract_id)
|
||||||
|
|
||||||
|
self.assertEqual(link.relation, "implements")
|
||||||
|
self.assertEqual(graph.validate(), [])
|
||||||
|
snapshot = graph.snapshot()
|
||||||
|
restored = GraphOfContracts.from_snapshot(snapshot)
|
||||||
|
self.assertEqual(restored.fingerprint(), graph.fingerprint())
|
||||||
|
|
||||||
|
def test_ledger_chain(self) -> None:
|
||||||
|
ledger = GovernanceLedger()
|
||||||
|
first = ledger.append("audit", {"message": "created"})
|
||||||
|
second = ledger.append("audit", {"message": "updated"}, actor="operator")
|
||||||
|
|
||||||
|
self.assertNotEqual(first.entry_hash, second.entry_hash)
|
||||||
|
self.assertEqual(ledger.verify(), [])
|
||||||
|
restored = GovernanceLedger.from_snapshot(ledger.snapshot())
|
||||||
|
self.assertEqual(restored.verify(), [])
|
||||||
|
|
||||||
|
def test_delta_helpers(self) -> None:
|
||||||
|
delta_a = make_plan_delta("a", [{"allocate": {"region": "North", "qty": 2}}])
|
||||||
|
delta_b = make_plan_delta("b", [{"allocate": {"region": "South", "qty": 3}}])
|
||||||
|
merged = merge_plan_deltas([delta_b, delta_a])
|
||||||
|
|
||||||
|
self.assertEqual(len(merged.actions), 2)
|
||||||
|
self.assertEqual(validate_deltas([delta_a, delta_b]), [])
|
||||||
|
|
||||||
|
def test_factory_alias(self) -> None:
|
||||||
|
problem = make_local_problem("Hospital supply", region="East", urgent=True)
|
||||||
|
self.assertEqual(problem.region, "East")
|
||||||
|
self.assertEqual(problem.metadata["urgent"], True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue