build(agent): c3po#b883b4 iteration
This commit is contained in:
parent
cc737ceb84
commit
66a3d7a538
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- `idea153_aidmesh_mobile_first` is a small Python package for offline-first disaster-response planning.
|
||||||
|
- Core pieces:
|
||||||
|
- `contracts.py`: parses a tiny ContractScript dialect and compiles it into a canonical portable module.
|
||||||
|
- `solver.py`: deterministic bounded solver for allocating scarce resources to tasks.
|
||||||
|
- `sync.py`: signed plan deltas, merge policy, and append-only plan ledger.
|
||||||
|
- `replay.py`: deterministic replay from signed logs.
|
||||||
|
- `adapters.py`: starter adapters for field inventory and drone routing.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Python 3.11+
|
||||||
|
- `pydantic` for validation
|
||||||
|
- `pytest` for tests
|
||||||
|
- `setuptools` for packaging
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Keep changes deterministic and replayable.
|
||||||
|
- Prefer minimal, explicit data structures over hidden state.
|
||||||
|
- Preserve canonical JSON ordering when hashing or signing.
|
||||||
|
- Add tests for any behavior change in parsing, solving, syncing, or replay.
|
||||||
|
- Keep public APIs small and stable.
|
||||||
|
|
||||||
|
## Test Commands
|
||||||
|
|
||||||
|
- `bash test.sh`
|
||||||
|
- `PYTHONPATH=src python3 -m pytest`
|
||||||
|
- `python3 -m build`
|
||||||
55
README.md
55
README.md
|
|
@ -1,3 +1,54 @@
|
||||||
# idea153-aidmesh-mobile-first
|
# AidMesh
|
||||||
|
|
||||||
Source logic for Idea #153
|
AidMesh is a small offline-first planning engine for disaster response and humanitarian logistics.
|
||||||
|
|
||||||
|
This repository currently ships a production-shaped core slice focused on deterministic field coordination:
|
||||||
|
|
||||||
|
- `ContractScript` parsing into a canonical portable module
|
||||||
|
- bounded resource allocation for scarce field supplies
|
||||||
|
- signed plan deltas with replayable merge semantics
|
||||||
|
- starter adapters for inventory and drone routing
|
||||||
|
- deterministic replay for after-action review
|
||||||
|
|
||||||
|
## What It Does
|
||||||
|
|
||||||
|
- Models a site as capacities plus prioritized tasks.
|
||||||
|
- Compiles a tiny ContractScript into canonical JSON bytes and a stable digest.
|
||||||
|
- Solves tasks greedily under resource limits.
|
||||||
|
- Signs and verifies plan deltas with HMAC-SHA256.
|
||||||
|
- Merges and replays offline updates deterministically.
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
- `src/idea153_aidmesh_mobile_first/contracts.py`
|
||||||
|
- `src/idea153_aidmesh_mobile_first/solver.py`
|
||||||
|
- `src/idea153_aidmesh_mobile_first/sync.py`
|
||||||
|
- `src/idea153_aidmesh_mobile_first/replay.py`
|
||||||
|
- `src/idea153_aidmesh_mobile_first/adapters.py`
|
||||||
|
- `tests/test_aidmesh.py`
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## ContractScript Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
site "field-clinic-7"
|
||||||
|
capacity meds 10
|
||||||
|
capacity water 6
|
||||||
|
task triage priority 10 demand meds=4, water=2
|
||||||
|
task shelter priority 4 demand water=4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Metadata
|
||||||
|
|
||||||
|
The distribution name is `idea153-aidmesh-mobile-first` and the long description is sourced from this `README.md`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "idea153-aidmesh-mobile-first"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "AidMesh is a mobile-first offline planning engine for disaster response."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = ["pydantic>=2.13"]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = ["pytest>=8"]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
package-dir = {"" = "src"}
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
"""AidMesh: offline-first planning and reconciliation primitives."""
|
||||||
|
|
||||||
|
from .adapters import inventory_to_contract, route_to_contract
|
||||||
|
from .contracts import ContractProgram, ContractScriptCompiler
|
||||||
|
from .replay import replay_ledger
|
||||||
|
from .solver import PlanAction, PlanResult, solve_program
|
||||||
|
from .sync import PlanDelta, PlanLedger, merge_deltas
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ContractProgram",
|
||||||
|
"ContractScriptCompiler",
|
||||||
|
"PlanAction",
|
||||||
|
"PlanResult",
|
||||||
|
"PlanDelta",
|
||||||
|
"PlanLedger",
|
||||||
|
"inventory_to_contract",
|
||||||
|
"merge_deltas",
|
||||||
|
"replay_ledger",
|
||||||
|
"route_to_contract",
|
||||||
|
"solve_program",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .contracts import ContractProgram, TaskSpec
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class InventoryItem:
|
||||||
|
name: str
|
||||||
|
quantity: int
|
||||||
|
criticality: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RouteWaypoint:
|
||||||
|
name: str
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
priority: int
|
||||||
|
|
||||||
|
|
||||||
|
def inventory_to_contract(site: str, items: list[InventoryItem]) -> ContractProgram:
|
||||||
|
capacities = {item.name: item.quantity for item in items}
|
||||||
|
tasks = [TaskSpec(name=f"restock_{item.name}", priority=item.criticality, demand={item.name: max(1, item.quantity // 4 or 1)}) for item in items]
|
||||||
|
return ContractProgram(site=site, capacities=capacities, tasks=tasks, notes=["generated from inventory adapter"])
|
||||||
|
|
||||||
|
|
||||||
|
def route_to_contract(site: str, origin: tuple[float, float], waypoints: list[RouteWaypoint]) -> ContractProgram:
|
||||||
|
route_budget = max(1, len(waypoints))
|
||||||
|
capacities = {"route_steps": route_budget}
|
||||||
|
ordered = sorted(waypoints, key=lambda wp: (-wp.priority, _distance(origin, (wp.x, wp.y)), wp.name))
|
||||||
|
tasks = [TaskSpec(name=f"visit_{wp.name}", priority=wp.priority, demand={"route_steps": 1}) for wp in ordered]
|
||||||
|
return ContractProgram(site=site, capacities=capacities, tasks=tasks, notes=["generated from route adapter"])
|
||||||
|
|
||||||
|
|
||||||
|
def _distance(a: tuple[float, float], b: tuple[float, float]) -> float:
|
||||||
|
return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from hashlib import sha256
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class TaskSpec(BaseModel):
|
||||||
|
name: str
|
||||||
|
priority: int = Field(ge=0)
|
||||||
|
demand: dict[str, int]
|
||||||
|
tags: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@field_validator("demand")
|
||||||
|
@classmethod
|
||||||
|
def _validate_demand(cls, value: dict[str, int]) -> dict[str, int]:
|
||||||
|
if not value:
|
||||||
|
raise ValueError("task demand must not be empty")
|
||||||
|
if any(amount <= 0 for amount in value.values()):
|
||||||
|
raise ValueError("task demand values must be positive")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ContractProgram(BaseModel):
|
||||||
|
site: str
|
||||||
|
capacities: dict[str, int]
|
||||||
|
tasks: list[TaskSpec]
|
||||||
|
notes: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@field_validator("capacities")
|
||||||
|
@classmethod
|
||||||
|
def _validate_capacities(cls, value: dict[str, int]) -> dict[str, int]:
|
||||||
|
if not value:
|
||||||
|
raise ValueError("capacities must not be empty")
|
||||||
|
if any(amount < 0 for amount in value.values()):
|
||||||
|
raise ValueError("capacity values must be non-negative")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PortableModule:
|
||||||
|
"""Canonical portable representation of a compiled contract."""
|
||||||
|
|
||||||
|
program: ContractProgram
|
||||||
|
module_bytes: bytes
|
||||||
|
digest: str
|
||||||
|
metadata: dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class ContractScriptCompiler:
|
||||||
|
"""Parse a tiny ContractScript dialect into a deterministic module."""
|
||||||
|
|
||||||
|
_capacity_re = re.compile(r"^capacity\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s+(?P<amount>\d+)$")
|
||||||
|
_task_re = re.compile(
|
||||||
|
r"^task\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s+priority\s+(?P<priority>\d+)\s+demand\s+(?P<demand>.+)$"
|
||||||
|
)
|
||||||
|
_site_re = re.compile(r'^site\s+"(?P<site>.+)"$')
|
||||||
|
_note_re = re.compile(r'^note\s+"(?P<note>.+)"$')
|
||||||
|
|
||||||
|
def compile(self, source: str) -> PortableModule:
|
||||||
|
site = ""
|
||||||
|
capacities: dict[str, int] = {}
|
||||||
|
tasks: list[TaskSpec] = []
|
||||||
|
notes: list[str] = []
|
||||||
|
|
||||||
|
for raw_line in source.splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if match := self._site_re.match(line):
|
||||||
|
site = match.group("site")
|
||||||
|
continue
|
||||||
|
if match := self._note_re.match(line):
|
||||||
|
notes.append(match.group("note"))
|
||||||
|
continue
|
||||||
|
if match := self._capacity_re.match(line):
|
||||||
|
capacities[match.group("name")] = int(match.group("amount"))
|
||||||
|
continue
|
||||||
|
if match := self._task_re.match(line):
|
||||||
|
tasks.append(
|
||||||
|
TaskSpec(
|
||||||
|
name=match.group("name"),
|
||||||
|
priority=int(match.group("priority")),
|
||||||
|
demand=_parse_demand(match.group("demand")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
raise ValueError(f"unrecognized ContractScript line: {raw_line}")
|
||||||
|
|
||||||
|
if not site:
|
||||||
|
raise ValueError("missing site declaration")
|
||||||
|
|
||||||
|
program = ContractProgram(site=site, capacities=capacities, tasks=sorted(tasks, key=lambda t: (-t.priority, t.name)), notes=notes)
|
||||||
|
payload = program.model_dump(mode="json")
|
||||||
|
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
digest = sha256(canonical).hexdigest()
|
||||||
|
module_bytes = canonical
|
||||||
|
return PortableModule(program=program, module_bytes=module_bytes, digest=digest, metadata={"format": "canonical-json", "version": 1})
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_demand(raw: str) -> dict[str, int]:
|
||||||
|
demand: dict[str, int] = {}
|
||||||
|
for item in raw.split(","):
|
||||||
|
name, amount = item.strip().split("=")
|
||||||
|
demand[name.strip()] = int(amount.strip())
|
||||||
|
return demand
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from .sync import PlanDelta, apply_delta
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ReplayResult:
|
||||||
|
state: dict
|
||||||
|
applied: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
def replay_ledger(initial_state: dict, deltas: list[PlanDelta], secret: str) -> ReplayResult:
|
||||||
|
state = dict(initial_state)
|
||||||
|
applied: list[str] = []
|
||||||
|
for delta in sorted(deltas, key=lambda d: (d.sequence, d.device_id)):
|
||||||
|
if not delta.verify(secret):
|
||||||
|
raise ValueError(f"invalid signature for {delta.device_id}:{delta.sequence}")
|
||||||
|
state = apply_delta(state, delta)
|
||||||
|
applied.append(f"{delta.device_id}:{delta.sequence}")
|
||||||
|
return ReplayResult(state=state, applied=applied)
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from .contracts import ContractProgram, TaskSpec
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlanAction:
|
||||||
|
task: str
|
||||||
|
allocations: dict[str, int]
|
||||||
|
score: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlanResult:
|
||||||
|
site: str
|
||||||
|
selected_tasks: list[PlanAction]
|
||||||
|
remaining_capacity: dict[str, int]
|
||||||
|
audit_log: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def solve_program(program: ContractProgram) -> PlanResult:
|
||||||
|
remaining = dict(program.capacities)
|
||||||
|
selected: list[PlanAction] = []
|
||||||
|
audit_log: list[str] = []
|
||||||
|
|
||||||
|
for task in program.tasks:
|
||||||
|
if _fits(task, remaining):
|
||||||
|
for resource, amount in task.demand.items():
|
||||||
|
remaining[resource] -= amount
|
||||||
|
score = _task_score(task)
|
||||||
|
selected.append(PlanAction(task=task.name, allocations=dict(task.demand), score=score))
|
||||||
|
audit_log.append(f"selected {task.name} score={score:.3f}")
|
||||||
|
else:
|
||||||
|
audit_log.append(f"skipped {task.name} insufficient capacity")
|
||||||
|
|
||||||
|
return PlanResult(site=program.site, selected_tasks=selected, remaining_capacity=remaining, audit_log=audit_log)
|
||||||
|
|
||||||
|
|
||||||
|
def _fits(task: TaskSpec, remaining: dict[str, int]) -> bool:
|
||||||
|
return all(remaining.get(resource, 0) >= amount for resource, amount in task.demand.items())
|
||||||
|
|
||||||
|
|
||||||
|
def _task_score(task: TaskSpec) -> float:
|
||||||
|
demand_weight = sum(task.demand.values())
|
||||||
|
return task.priority / max(demand_weight, 1)
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from hashlib import sha256
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_bytes(payload: dict[str, Any]) -> bytes:
|
||||||
|
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlanDelta:
|
||||||
|
device_id: str
|
||||||
|
sequence: int
|
||||||
|
parents: list[str]
|
||||||
|
changes: dict[str, Any]
|
||||||
|
summary: str
|
||||||
|
timestamp: str
|
||||||
|
signature: str = ""
|
||||||
|
|
||||||
|
def payload(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"device_id": self.device_id,
|
||||||
|
"sequence": self.sequence,
|
||||||
|
"parents": list(self.parents),
|
||||||
|
"changes": self.changes,
|
||||||
|
"summary": self.summary,
|
||||||
|
"timestamp": self.timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
def sign(self, secret: str) -> "PlanDelta":
|
||||||
|
digest = hmac.new(secret.encode("utf-8"), _canonical_bytes(self.payload()), sha256).hexdigest()
|
||||||
|
return PlanDelta(
|
||||||
|
device_id=self.device_id,
|
||||||
|
sequence=self.sequence,
|
||||||
|
parents=list(self.parents),
|
||||||
|
changes=dict(self.changes),
|
||||||
|
summary=self.summary,
|
||||||
|
timestamp=self.timestamp,
|
||||||
|
signature=digest,
|
||||||
|
)
|
||||||
|
|
||||||
|
def verify(self, secret: str) -> bool:
|
||||||
|
expected = hmac.new(secret.encode("utf-8"), _canonical_bytes(self.payload()), sha256).hexdigest()
|
||||||
|
return hmac.compare_digest(expected, self.signature)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanLedger:
|
||||||
|
secret: str
|
||||||
|
state: dict[str, Any] = field(default_factory=dict)
|
||||||
|
entries: list[PlanDelta] = field(default_factory=list)
|
||||||
|
|
||||||
|
def append(self, delta: PlanDelta) -> None:
|
||||||
|
if not delta.verify(self.secret):
|
||||||
|
raise ValueError("invalid plan delta signature")
|
||||||
|
self.state = apply_delta(self.state, delta)
|
||||||
|
self.entries.append(delta)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_deltas(base_state: dict[str, Any], deltas: list[PlanDelta], secret: str) -> dict[str, Any]:
|
||||||
|
state = dict(base_state)
|
||||||
|
for delta in sorted(deltas, key=lambda d: (d.sequence, d.device_id)):
|
||||||
|
if not delta.verify(secret):
|
||||||
|
raise ValueError(f"invalid signature for {delta.device_id}:{delta.sequence}")
|
||||||
|
state = apply_delta(state, delta)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def apply_delta(state: dict[str, Any], delta: PlanDelta) -> dict[str, Any]:
|
||||||
|
next_state = json.loads(json.dumps(state, sort_keys=True))
|
||||||
|
for key, value in delta.changes.items():
|
||||||
|
if isinstance(value, dict) and isinstance(next_state.get(key), dict):
|
||||||
|
merged = dict(next_state[key])
|
||||||
|
merged.update(value)
|
||||||
|
next_state[key] = merged
|
||||||
|
else:
|
||||||
|
next_state[key] = value
|
||||||
|
next_state.setdefault("audit", [])
|
||||||
|
next_state["audit"] = list(next_state["audit"]) + [f"{delta.device_id}:{delta.sequence}:{delta.summary}"]
|
||||||
|
next_state["last_hash"] = sha256(_canonical_bytes(delta.payload())).hexdigest()
|
||||||
|
return next_state
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PYTHONPATH=src python3 -m pytest
|
||||||
|
python3 -m build
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from idea153_aidmesh_mobile_first import ContractScriptCompiler, PlanDelta, PlanLedger, merge_deltas, replay_ledger, solve_program
|
||||||
|
from idea153_aidmesh_mobile_first.adapters import InventoryItem, RouteWaypoint, inventory_to_contract, route_to_contract
|
||||||
|
|
||||||
|
|
||||||
|
def test_compiler_and_solver_select_high_priority_tasks():
|
||||||
|
source = """
|
||||||
|
site "field-clinic-7"
|
||||||
|
capacity meds 10
|
||||||
|
capacity water 6
|
||||||
|
task triage priority 10 demand meds=4, water=2
|
||||||
|
task shelter priority 4 demand water=4
|
||||||
|
"""
|
||||||
|
module = ContractScriptCompiler().compile(source)
|
||||||
|
assert module.digest
|
||||||
|
result = solve_program(module.program)
|
||||||
|
assert [action.task for action in result.selected_tasks] == ["triage", "shelter"]
|
||||||
|
assert result.remaining_capacity == {"meds": 6, "water": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_deltas_sign_merge_and_replay():
|
||||||
|
secret = "shared-key"
|
||||||
|
delta_a = PlanDelta(
|
||||||
|
device_id="truck-1",
|
||||||
|
sequence=1,
|
||||||
|
parents=[],
|
||||||
|
changes={"inventory": {"meds": 12}},
|
||||||
|
summary="restocked meds",
|
||||||
|
timestamp="2026-04-26T20:00:00Z",
|
||||||
|
).sign(secret)
|
||||||
|
delta_b = PlanDelta(
|
||||||
|
device_id="drone-2",
|
||||||
|
sequence=2,
|
||||||
|
parents=[delta_a.signature],
|
||||||
|
changes={"routes": {"drone-2": ["clinic", "depot"]}},
|
||||||
|
summary="published route",
|
||||||
|
timestamp="2026-04-26T20:01:00Z",
|
||||||
|
).sign(secret)
|
||||||
|
|
||||||
|
merged = merge_deltas({}, [delta_b, delta_a], secret)
|
||||||
|
assert merged["inventory"]["meds"] == 12
|
||||||
|
assert merged["routes"]["drone-2"] == ["clinic", "depot"]
|
||||||
|
|
||||||
|
replay = replay_ledger({}, [delta_a, delta_b], secret)
|
||||||
|
assert replay.state["inventory"]["meds"] == 12
|
||||||
|
assert replay.applied == ["truck-1:1", "drone-2:2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_adapters_generate_valid_contracts():
|
||||||
|
inventory_program = inventory_to_contract(
|
||||||
|
"warehouse-1",
|
||||||
|
[InventoryItem(name="bandages", quantity=20, criticality=8), InventoryItem(name="saline", quantity=12, criticality=6)],
|
||||||
|
)
|
||||||
|
route_program = route_to_contract(
|
||||||
|
"airstrip-a",
|
||||||
|
(0.0, 0.0),
|
||||||
|
[RouteWaypoint(name="north-hill", x=2.0, y=3.0, priority=9), RouteWaypoint(name="river-base", x=1.0, y=1.0, priority=4)],
|
||||||
|
)
|
||||||
|
assert solve_program(inventory_program).selected_tasks
|
||||||
|
assert solve_program(route_program).selected_tasks
|
||||||
|
ledger = PlanLedger(secret="shared-key")
|
||||||
|
delta = PlanDelta(
|
||||||
|
device_id="inventory-1",
|
||||||
|
sequence=1,
|
||||||
|
parents=[],
|
||||||
|
changes={"inventory": {"bandages": 20}},
|
||||||
|
summary="inventory snapshot",
|
||||||
|
timestamp="2026-04-26T20:02:00Z",
|
||||||
|
).sign("shared-key")
|
||||||
|
ledger.append(delta)
|
||||||
|
assert ledger.state["inventory"]["bandages"] == 20
|
||||||
Loading…
Reference in New Issue