build(agent): semicolon#54de0b iteration

This commit is contained in:
agent-54de0bcc6a17828b 2026-04-24 20:43:42 +02:00
parent 73cf40b345
commit 4be4d536a8
8 changed files with 259 additions and 34 deletions

View File

@ -3,6 +3,7 @@
Architecture overview Architecture overview
- Core: crisisops/core.py defines the Graph-of-Contracts primitives: Object, Morphism, PlanDelta, DualVariables. - Core: crisisops/core.py defines the Graph-of-Contracts primitives: Object, Morphism, PlanDelta, DualVariables.
- Planner: crisisops/planner.py implements a lightweight ADMM-like planner for MVP orchestration between domains. - Planner: crisisops/planner.py implements a lightweight ADMM-like planner for MVP orchestration between domains.
- Replay: crisisops/replay.py provides deterministic snapshots, canonical JSON, and restore/replay helpers.
- Adapters: crisisops/adapters_registry.py provides a registry; toy adapters live in crisisops/adapters/. - Adapters: crisisops/adapters_registry.py provides a registry; toy adapters live in crisisops/adapters/.
- Governance: crisisops/governance.py offers a simple SQLite-backed governance ledger for auditability. - Governance: crisisops/governance.py offers a simple SQLite-backed governance ledger for auditability.
- CLI: crisisops/cli.py exposes a tiny CLI for quick experiments. - CLI: crisisops/cli.py exposes a tiny CLI for quick experiments.
@ -16,6 +17,7 @@ Tech stack
Testing commands Testing commands
- Run tests: ./test.sh - Run tests: ./test.sh
- Run planner demo: python -m crisisops.cli plan (or pytest tests/test_planner.py) - Run planner demo: python -m crisisops.cli plan (or pytest tests/test_planner.py)
- Inspect a snapshot: python -m crisisops.cli snapshot
Contribution rules Contribution rules
- Implement small, composable features with clear interfaces. - Implement small, composable features with clear interfaces.

View File

@ -2,23 +2,26 @@
Overview Overview
- CrisisOps provides a Graph-of-Contracts (GoC) primitive model to coordinate crisis-response operations across domains (e.g., field logistics, shelters) with offline-first delta-sync and auditability. - CrisisOps provides a Graph-of-Contracts (GoC) primitive model to coordinate crisis-response operations across domains (e.g., field logistics, shelters) with offline-first delta-sync and auditability.
- MVP demonstrates two domains with toy adapters and a minimal ADMM-like planner. It is designed to be production-ready in architecture, not just a proof-of-concept. - The current implementation adds deterministic snapshots, replay helpers, and audit-ready plan hashes so field decisions can be replayed after an event.
- The planner now emits ordered dispatch actions with a lightweight utility score to make plan comparisons easier.
Whats inside Whats inside
- Python package crisisops with core primitives, planner, adapters registry, and governance ledger. - Python package crisisops with core primitives, planner, adapters registry, and governance ledger.
- Toy adapters: inventory_portal and gis_dispatch (under crisisops/adapters). - Toy adapters: inventory_portal and gis_dispatch (under crisisops/adapters).
- CLI scaffold for quick experiments. - CLI scaffold for quick experiments and snapshot inspection.
- Tests validating planner behavior. - Tests validating planner behavior and deterministic replay.
- Packaging metadata and test script for CI compliance. - Packaging metadata and test script for CI compliance.
Getting started Getting started
- Install: python3 -m build (from pyproject.toml) - Install: python3 -m build (from pyproject.toml)
- Run tests: ./test.sh - Run tests: ./test.sh
- Explore planner: python3 -m crisisops.cli plan - Explore planner: python3 -m crisisops.cli plan
- Inspect a snapshot: python3 -m crisisops.cli snapshot
Architecture links Architecture links
- Core primitives: crisisops.core - Core primitives: crisisops.core
- Planner: crisisops.planner - Planner: crisisops.planner
- Replay and snapshot utilities: crisisops.replay
- Adapters registry and toy adapters: crisisops.adapters_registry, crisisops.adapters.* - Adapters registry and toy adapters: crisisops.adapters_registry, crisisops.adapters.*
- Governance ledger: crisisops.governance - Governance ledger: crisisops.governance

View File

@ -4,6 +4,7 @@ from .core import Object, Morphism, PlanDelta, DualVariables
from .planner import ADMMPlanner from .planner import ADMMPlanner
from .adapters_registry import AdaptersRegistry from .adapters_registry import AdaptersRegistry
from .governance import GovernanceLedger from .governance import GovernanceLedger
from .replay import ReplayJournal, graph_snapshot, restore_graph, canonical_json
__all__ = [ __all__ = [
"Object", "Object",
@ -13,4 +14,8 @@ __all__ = [
"ADMMPlanner", "ADMMPlanner",
"AdaptersRegistry", "AdaptersRegistry",
"GovernanceLedger", "GovernanceLedger",
"ReplayJournal",
"graph_snapshot",
"restore_graph",
"canonical_json",
] ]

View File

@ -1,13 +1,13 @@
"""Minimal CLI to exercise CrisisOps MVP.""" """Minimal CLI to exercise CrisisOps MVP."""
import argparse import argparse
import json import json
import time
from crisisops.planner import ADMMPlanner from crisisops.planner import ADMMPlanner
from crisisops.core import GraphOfContracts, Object, Morphism from crisisops.core import GraphOfContracts, Object, Morphism
from crisisops.replay import graph_snapshot
def build_sample_goC(): def build_sample_goc():
# Simple two-object domains: field resources and shelters # Simple two-object domains: field resources and shelters
o1 = Object(id="fuel-truck-1", type="vehicle", attributes={"capacity": 1000}) o1 = Object(id="fuel-truck-1", type="vehicle", attributes={"capacity": 1000})
o2 = Object(id="shelter-a", type="facility", attributes={"capacity": 500}) o2 = Object(id="shelter-a", type="facility", attributes={"capacity": 500})
@ -21,13 +21,20 @@ def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="cmd") sub = parser.add_subparsers(dest="cmd")
sub.add_parser("plan", help="Run a toy planning pass on a sample GoC") sub.add_parser("plan", help="Run a toy planning pass on a sample GoC")
sub.add_parser("snapshot", help="Print a deterministic snapshot of the sample GoC")
args = parser.parse_args() args = parser.parse_args()
if args.cmd == "plan": if args.cmd == "plan":
gof = build_sample_goC() gof = build_sample_goc()
planner = ADMMPlanner() planner = ADMMPlanner()
plan = planner.optimize(gof) plan = planner.optimize(gof)
print(json.dumps({"plan": plan.actions}, default=str, indent=2)) snapshot = graph_snapshot(gof, plan=plan)
print(json.dumps({"plan": plan.actions, "snapshot_hash": snapshot["snapshot_hash"]}, default=str, indent=2))
return 0
if args.cmd == "snapshot":
gof = build_sample_goc()
snapshot = graph_snapshot(gof)
print(json.dumps(snapshot, default=str, indent=2))
return 0 return 0
print("No command provided. Use 'plan'.") print("No command provided. Use 'plan'.")
return 1 return 1

View File

@ -1,7 +1,9 @@
"""A simple governance ledger using SQLite for auditability.""" """A simple governance ledger using SQLite for auditability."""
import sqlite3 import sqlite3
import time import time
from typing import List, Dict, Any from typing import List, Dict, Any, cast
from .replay import canonical_json
class GovernanceLedger: class GovernanceLedger:
@ -25,15 +27,24 @@ class GovernanceLedger:
) )
self.conn.commit() self.conn.commit()
def log(self, action: str, actor: str, status: str, details: str | None = None) -> int: def log(self, action: str, actor: str, status: str, details: Any | None = None) -> int:
ts = time.time() ts = time.time()
normalized_details = details
if isinstance(details, (dict, list)):
normalized_details = canonical_json(details)
cur = self.conn.cursor() cur = self.conn.cursor()
cur.execute( cur.execute(
"INSERT INTO governance_logs (action, actor, timestamp, status, details) VALUES (?, ?, ?, ?, ?)", "INSERT INTO governance_logs (action, actor, timestamp, status, details) VALUES (?, ?, ?, ?, ?)",
(action, actor, ts, status, details), (action, actor, ts, status, normalized_details),
) )
self.conn.commit() self.conn.commit()
return cur.lastrowid return cast(int, cur.lastrowid)
def log_plan(self, actor: str, status: str, plan_hash: str, details: Dict[str, Any] | None = None) -> int:
payload = {"plan_hash": plan_hash}
if details:
payload.update(details)
return self.log(action="plan", actor=actor, status=status, details=payload)
def list_logs(self) -> List[Dict[str, Any]]: def list_logs(self) -> List[Dict[str, Any]]:
cur = self.conn.cursor() cur = self.conn.cursor()

View File

@ -7,7 +7,7 @@ reliability of critical deliveries while minimizing idle trips in a toy setting.
from __future__ import annotations from __future__ import annotations
import time import time
from typing import Dict, List from typing import Any, Dict, List
from .core import GraphOfContracts, PlanDelta from .core import GraphOfContracts, PlanDelta
@ -24,35 +24,66 @@ class ADMMPlanner:
"""Run a toy optimization pass over the GoC to produce a PlanDelta. """Run a toy optimization pass over the GoC to produce a PlanDelta.
We allocate available objects to matching morphisms (requests) by a We allocate available objects to matching morphisms (requests) by a
simplistic rule: if a source object and target object share a compatible deterministic rule: dispatch-style morphisms create actions when the
type and the requested quantity is available, we create an action. source and target exist, and the action payload includes a utility score
that favors balanced capacity and higher-priority signals.
""" """
actions: List[Dict[str, object]] = [] actions: List[Dict[str, object]] = []
now = time.time() now = time.time()
# Example heuristic: pair objects by type and create dispatch actions for mid in sorted(gof.morphisms):
# Primitive: for each morphism, try to assign a matching object as resource. morph = gof.morphisms[mid]
for mid, morph in gof.morphisms.items():
src = gof.objects.get(morph.source_id) src = gof.objects.get(morph.source_id)
dst = gof.objects.get(morph.target_id) dst = gof.objects.get(morph.target_id)
if not src or not dst: if not src or not dst:
continue continue
# Simple compatibility rule: if both share a category/role (type)
if src.type == dst.type: if morph.type not in {"dispatch", "transfer"} and src.type != dst.type:
action = { continue
"morphism_id": mid,
"action": "dispatch", action = {
"resource_id": src.id, "morphism_id": mid,
"destination_id": dst.id, "action": "dispatch",
"timestamp": now, "resource_id": src.id,
"details": { "destination_id": dst.id,
"src_type": src.type, "timestamp": now,
"dst_type": dst.type, "details": {
}, "src_type": src.type,
} "dst_type": dst.type,
actions.append(action) "priority": morph.signals.get("priority", "normal"),
"utility_score": self._utility_score(src.attributes, dst.attributes, morph.signals),
},
}
actions.append(action)
actions.sort(key=lambda action: (action["morphism_id"], action["resource_id"], action["destination_id"]))
# Record plan delta # Record plan delta
plan = PlanDelta(timestamp=now, actions=actions) plan = PlanDelta(timestamp=now, actions=actions)
gof.plan_delta = plan gof.plan_delta = plan
return plan return plan
@staticmethod
def _numeric_attribute(attributes: Dict[str, Any], keys: List[str], default: float = 0.0) -> float:
for key in keys:
value = attributes.get(key)
if value is None:
continue
try:
return float(value)
except (TypeError, ValueError):
continue
return default
def _utility_score(
self,
src_attributes: Dict[str, object],
dst_attributes: Dict[str, object],
signals: Dict[str, object],
) -> float:
source_capacity = self._numeric_attribute(src_attributes, ["capacity", "stock", "qty", "available"], default=1.0)
target_need = self._numeric_attribute(dst_attributes, ["demand", "capacity", "need", "qty"], default=1.0)
balance = min(source_capacity, target_need) / max(source_capacity, target_need, 1.0)
priority = str(signals.get("priority", "normal"))
priority_boost = {"low": 0.9, "normal": 1.0, "high": 1.1, "critical": 1.25}.get(priority, 1.0)
return round(min(1.0, balance * priority_boost), 3)

129
crisisops/replay.py Normal file
View File

@ -0,0 +1,129 @@
"""Deterministic snapshots and replay helpers for CrisisOps."""
from __future__ import annotations
import hashlib
import json
from typing import Any, Dict, Iterable, List
from .core import DualVariables, GraphOfContracts, Morphism, Object, PlanDelta
SCHEMA_VERSION = "1.0"
def canonical_json(payload: Any) -> str:
return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
def _numeric(value: Any, default: float = 0.0) -> float:
try:
return float(value)
except (TypeError, ValueError):
return default
def _action_sort_key(action: Dict[str, Any]) -> tuple:
return (
str(action.get("morphism_id", "")),
str(action.get("action", "")),
str(action.get("resource_id", "")),
str(action.get("destination_id", "")),
)
def _object_to_dict(item: Object) -> Dict[str, Any]:
return {"id": item.id, "type": item.type, "attributes": dict(item.attributes)}
def _morphism_to_dict(item: Morphism) -> Dict[str, Any]:
return {
"id": item.id,
"source_id": item.source_id,
"target_id": item.target_id,
"type": item.type,
"signals": dict(item.signals),
}
def _dual_to_dict(item: DualVariables) -> Dict[str, Any]:
return {"name": item.name, "values": dict(item.values)}
def _plan_to_dict(plan: PlanDelta | None) -> Dict[str, Any] | None:
if plan is None:
return None
actions = [dict(action) for action in plan.actions]
actions.sort(key=_action_sort_key)
return {"timestamp": plan.timestamp, "actions": actions}
def graph_snapshot(graph: GraphOfContracts, plan: PlanDelta | None = None) -> Dict[str, Any]:
payload = {
"schema_version": SCHEMA_VERSION,
"objects": [_object_to_dict(graph.objects[key]) for key in sorted(graph.objects)],
"morphisms": [_morphism_to_dict(graph.morphisms[key]) for key in sorted(graph.morphisms)],
"duals": [_dual_to_dict(graph.duals[key]) for key in sorted(graph.duals)],
"plan_delta": _plan_to_dict(plan or graph.plan_delta),
}
payload["snapshot_hash"] = snapshot_hash(payload)
return payload
def snapshot_hash(snapshot: Dict[str, Any]) -> str:
normalized = dict(snapshot)
normalized.pop("snapshot_hash", None)
return hashlib.sha256(canonical_json(normalized).encode("utf-8")).hexdigest()
def restore_graph(snapshot: Dict[str, Any]) -> GraphOfContracts:
objects = {
item["id"]: Object(id=item["id"], type=item["type"], attributes=dict(item.get("attributes", {})))
for item in snapshot.get("objects", [])
}
morphisms = {
item["id"]: Morphism(
id=item["id"],
source_id=item["source_id"],
target_id=item["target_id"],
type=item["type"],
signals=dict(item.get("signals", {})),
)
for item in snapshot.get("morphisms", [])
}
duals = {
item["name"]: DualVariables(name=item["name"], values=dict(item.get("values", {})))
for item in snapshot.get("duals", [])
}
plan_data = snapshot.get("plan_delta")
plan_delta = None
if plan_data is not None:
plan_delta = PlanDelta(
timestamp=_numeric(plan_data.get("timestamp")),
actions=[dict(action) for action in plan_data.get("actions", [])],
)
graph = GraphOfContracts(objects=objects, morphisms=morphisms, plan_delta=plan_delta, duals=duals)
return graph
def replay_graphs(snapshots: Iterable[Dict[str, Any]]) -> List[GraphOfContracts]:
return [restore_graph(snapshot) for snapshot in snapshots]
class ReplayJournal:
"""A minimal append-only journal of canonical graph snapshots."""
def __init__(self) -> None:
self._snapshots: List[Dict[str, Any]] = []
def record(self, graph: GraphOfContracts, plan: PlanDelta | None = None) -> Dict[str, Any]:
snapshot = graph_snapshot(graph, plan=plan)
self._snapshots.append(snapshot)
return snapshot
def list_snapshots(self) -> List[Dict[str, Any]]:
return [dict(snapshot) for snapshot in self._snapshots]
def replay(self) -> List[GraphOfContracts]:
return replay_graphs(self._snapshots)

View File

@ -1,6 +1,9 @@
import json import json
from crisisops.planner import ADMMPlanner from crisisops.planner import ADMMPlanner
from crisisops.core import GraphOfContracts, Object, Morphism from crisisops.core import GraphOfContracts, Object, Morphism
from crisisops.governance import GovernanceLedger
from crisisops.replay import graph_snapshot, restore_graph
def build_simple_goC(): def build_simple_goC():
@ -14,7 +17,41 @@ def test_planner_allocates_dispatch_actions():
gof = build_simple_goC() gof = build_simple_goC()
planner = ADMMPlanner() planner = ADMMPlanner()
plan = planner.optimize(gof) plan = planner.optimize(gof)
# Expect at least one action and that it's a dispatch action
assert isinstance(plan.actions, list) assert isinstance(plan.actions, list)
if plan.actions: assert plan.actions[0]["action"] == "dispatch"
assert plan.actions[0]["action"] == "dispatch" assert plan.actions[0]["details"]["utility_score"] > 0
def test_graph_snapshot_is_deterministic_and_restorable():
gof_a = build_simple_goC()
gof_b = GraphOfContracts(
objects={"b1": gof_a.objects["b1"], "a1": gof_a.objects["a1"]},
morphisms={"m1": gof_a.morphisms["m1"]},
)
snapshot_a = graph_snapshot(gof_a)
snapshot_b = graph_snapshot(gof_b)
assert snapshot_a["snapshot_hash"] == snapshot_b["snapshot_hash"]
restored = restore_graph(snapshot_a)
assert restored.objects["a1"].attributes["qty"] == 10
assert restored.morphisms["m1"].target_id == "b1"
def test_snapshot_hash_tracks_plan_changes():
gof = build_simple_goC()
planner = ADMMPlanner()
plan = planner.optimize(gof)
snapshot = graph_snapshot(gof, plan=plan)
assert snapshot["plan_delta"]["actions"][0]["details"]["utility_score"] > 0
assert snapshot["snapshot_hash"]
def test_governance_ledger_canonicalizes_structured_details():
ledger = GovernanceLedger()
ledger.log_plan(actor="planner-1", status="approved", plan_hash="abc123", details={"agency": "ngo-a"})
record = ledger.list_logs()[0]
assert json.loads(record["details"]) == {"agency": "ngo-a", "plan_hash": "abc123"}