build(agent): c3po#b883b4 iteration
This commit is contained in:
parent
703e3ecbfe
commit
28ae65a4e7
|
|
@ -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,27 @@
|
||||||
|
# MediMesh Contributor Guide
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
- `src/medimesh/contracts.py`: Graph-of-Contracts registry and schema conformance checks.
|
||||||
|
- `src/medimesh/solver.py`: Distributed allocator and policy weighting for scarce medical resources.
|
||||||
|
- `src/medimesh/governance.py`: DID-style identities, attestations, and tamper-evident SQLite ledger.
|
||||||
|
- `src/medimesh/sync.py`: Offline-first delta storage and deterministic replay.
|
||||||
|
- `src/medimesh/adapters.py`: Starter hospital inventory and EMS/logistics adapters.
|
||||||
|
- `src/medimesh/scenario.py`: End-to-end toy scenario for validation and demos.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- Python 3.10+
|
||||||
|
- `networkx` for contract graph modeling
|
||||||
|
- `cryptography` for Ed25519 signing and verification
|
||||||
|
- SQLite via the standard library for local durable state
|
||||||
|
- `pytest` for tests
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Keep changes small and deterministic.
|
||||||
|
- Preserve privacy boundaries: patient data stays local and out of global coordination objects.
|
||||||
|
- Prefer aggregated signals over site-level reconstruction.
|
||||||
|
- Maintain hash-chain integrity in sync and governance stores.
|
||||||
|
- Update tests alongside behavior changes.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
- Install and test: `bash test.sh`
|
||||||
|
- Run the toy scenario: `python -m medimesh`
|
||||||
35
README.md
35
README.md
|
|
@ -1,3 +1,34 @@
|
||||||
# idea150-medimesh-federated-privacy
|
# MediMesh
|
||||||
|
|
||||||
Source logic for Idea #150
|
MediMesh is a federated coordination platform for disaster-affected health systems.
|
||||||
|
It keeps site-level data local, exchanges only aggregated signals, and uses a canonical contract registry to coordinate hospitals, clinics, EMS, pharmacies, and supply caches under privacy and governance constraints.
|
||||||
|
|
||||||
|
## What It Provides
|
||||||
|
|
||||||
|
- Graph-of-Contracts registry for schema and adapter governance
|
||||||
|
- Distributed allocator for scarce resources using policy-weighted coordination
|
||||||
|
- SQLite-backed tamper-evident governance log with cryptographic attestations
|
||||||
|
- Offline-first delta storage with deterministic replay for audits
|
||||||
|
- Starter hospital inventory and EMS/logistics adapters
|
||||||
|
- End-to-end toy scenario with a shortage shock
|
||||||
|
|
||||||
|
## Package Layout
|
||||||
|
|
||||||
|
- `medimesh.contracts`: canonical contracts and registry
|
||||||
|
- `medimesh.solver`: local and distributed allocation logic
|
||||||
|
- `medimesh.governance`: identities, credentials, and ledger
|
||||||
|
- `medimesh.sync`: offline delta sync and replay
|
||||||
|
- `medimesh.adapters`: reference controllers
|
||||||
|
- `medimesh.scenario`: demo orchestration
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash test.sh
|
||||||
|
python -m medimesh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Packaging
|
||||||
|
|
||||||
|
- Python distribution name: `idea150-medimesh-federated-privacy`
|
||||||
|
- `readme = "README.md"` is wired into `pyproject.toml`
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "idea150-medimesh-federated-privacy"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Federated, privacy-preserving coordination for medical resource allocation in low-connectivity environments"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"networkx>=3.2",
|
||||||
|
"cryptography>=42.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
test = ["pytest>=8.0"]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
package-dir = {"" = "src"}
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""MediMesh federated coordination toolkit."""
|
||||||
|
|
||||||
|
from .adapters import EMSDispatchLogisticsController, HospitalInventoryController
|
||||||
|
from .contracts import ContractRegistry, DataContract, GovernancePolicy
|
||||||
|
from .governance import Credential, GovernanceLedger, Identity, IdentityAuthority
|
||||||
|
from .scenario import run_toy_scenario
|
||||||
|
from .solver import DistributedAllocator, HumanitarianPolicy, SiteState
|
||||||
|
from .sync import OfflineDeltaStore
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ContractRegistry",
|
||||||
|
"DataContract",
|
||||||
|
"GovernancePolicy",
|
||||||
|
"Credential",
|
||||||
|
"GovernanceLedger",
|
||||||
|
"Identity",
|
||||||
|
"IdentityAuthority",
|
||||||
|
"DistributedAllocator",
|
||||||
|
"HumanitarianPolicy",
|
||||||
|
"SiteState",
|
||||||
|
"OfflineDeltaStore",
|
||||||
|
"HospitalInventoryController",
|
||||||
|
"EMSDispatchLogisticsController",
|
||||||
|
"run_toy_scenario",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .scenario import run_toy_scenario
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print(json.dumps(run_toy_scenario(), indent=2, sort_keys=True))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Mapping
|
||||||
|
|
||||||
|
from .solver import AllocationResult, DistributedAllocator, HumanitarianPolicy, SiteState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class HospitalInventoryController:
|
||||||
|
site_id: str
|
||||||
|
region: str
|
||||||
|
on_hand: float
|
||||||
|
forecast_demand: float
|
||||||
|
minimum_service: float = 0.0
|
||||||
|
|
||||||
|
def to_site_state(self) -> SiteState:
|
||||||
|
return SiteState(
|
||||||
|
site_id=self.site_id,
|
||||||
|
region=self.region,
|
||||||
|
on_hand=self.on_hand,
|
||||||
|
forecast_demand=self.forecast_demand,
|
||||||
|
minimum_service=self.minimum_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
def recommend(self, allocator: DistributedAllocator, total_available: float, policy: HumanitarianPolicy) -> AllocationResult:
|
||||||
|
return allocator.allocate([self.to_site_state()], total_available=total_available, policy=policy)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EMSDispatchLogisticsController:
|
||||||
|
dispatch_base: str
|
||||||
|
region: str
|
||||||
|
|
||||||
|
def prioritize_transfers(self, allocations: Mapping[str, float], shortage_signal: float) -> dict[str, float]:
|
||||||
|
if shortage_signal <= 0:
|
||||||
|
return {site_id: 0.0 for site_id in allocations}
|
||||||
|
total = sum(max(0.0, amount) for amount in allocations.values()) or 1.0
|
||||||
|
return {site_id: round(shortage_signal * amount / total, 4) for site_id, amount in allocations.items()}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
||||||
|
|
||||||
|
import networkx as nx
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DataContract:
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
required_fields: tuple[str, ...]
|
||||||
|
optional_fields: tuple[str, ...] = ()
|
||||||
|
owner: str = ""
|
||||||
|
classification: str = "internal"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def node_id(self) -> str:
|
||||||
|
return f"{self.name}@{self.version}"
|
||||||
|
|
||||||
|
def validate(self, payload: Mapping[str, Any]) -> list[str]:
|
||||||
|
missing = [field for field in self.required_fields if field not in payload]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"payload missing required fields: {', '.join(missing)}")
|
||||||
|
return sorted(set(payload.keys()) - set(self.required_fields) - set(self.optional_fields))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class GovernancePolicy:
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
triage_priority: float = 1.0
|
||||||
|
equity_weight: float = 1.0
|
||||||
|
continuity_weight: float = 1.0
|
||||||
|
regional_floor: float = 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def node_id(self) -> str:
|
||||||
|
return f"policy:{self.name}@{self.version}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AdapterContractBinding:
|
||||||
|
adapter_name: str
|
||||||
|
reads: tuple[str, ...]
|
||||||
|
writes: tuple[str, ...]
|
||||||
|
governed_by: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
class ContractRegistry:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.graph: nx.DiGraph = nx.DiGraph()
|
||||||
|
|
||||||
|
def register_contract(self, contract: DataContract) -> None:
|
||||||
|
self.graph.add_node(
|
||||||
|
contract.node_id,
|
||||||
|
kind="contract",
|
||||||
|
contract=contract,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register_policy(self, policy: GovernancePolicy) -> None:
|
||||||
|
self.graph.add_node(
|
||||||
|
policy.node_id,
|
||||||
|
kind="policy",
|
||||||
|
policy=policy,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register_adapter(self, binding: AdapterContractBinding) -> None:
|
||||||
|
self.graph.add_node(
|
||||||
|
binding.adapter_name,
|
||||||
|
kind="adapter",
|
||||||
|
binding=binding,
|
||||||
|
)
|
||||||
|
for contract_id in binding.reads:
|
||||||
|
self.graph.add_edge(contract_id, binding.adapter_name, relation="feeds")
|
||||||
|
for contract_id in binding.writes:
|
||||||
|
self.graph.add_edge(binding.adapter_name, contract_id, relation="produces")
|
||||||
|
for policy_id in binding.governed_by:
|
||||||
|
self.graph.add_edge(policy_id, binding.adapter_name, relation="governs")
|
||||||
|
|
||||||
|
def register_flow(self, upstream: str, downstream: str, relation: str = "depends_on") -> None:
|
||||||
|
self.graph.add_edge(upstream, downstream, relation=relation)
|
||||||
|
|
||||||
|
def conformance_check(self, contract_id: str, payload: Mapping[str, Any]) -> list[str]:
|
||||||
|
contract = self.resolve_contract(contract_id)
|
||||||
|
return contract.validate(payload)
|
||||||
|
|
||||||
|
def resolve_contract(self, contract_id: str) -> DataContract:
|
||||||
|
node = self.graph.nodes[contract_id]
|
||||||
|
if node.get("kind") != "contract":
|
||||||
|
raise KeyError(f"{contract_id} is not a registered contract")
|
||||||
|
return node["contract"]
|
||||||
|
|
||||||
|
def lineage(self, node_id: str) -> list[str]:
|
||||||
|
return list(nx.ancestors(self.graph, node_id))
|
||||||
|
|
||||||
|
def topology(self) -> dict[str, list[tuple[str, str]]]:
|
||||||
|
return {
|
||||||
|
node: sorted((neighbor, data.get("relation", "")) for _, neighbor, data in self.graph.out_edges(node, data=True))
|
||||||
|
for node in self.graph.nodes
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable, Mapping
|
||||||
|
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||||
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(value: Any) -> str:
|
||||||
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_bytes(*parts: bytes) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
for part in parts:
|
||||||
|
digest.update(part)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Identity:
|
||||||
|
did: str
|
||||||
|
public_key: bytes
|
||||||
|
private_key: Ed25519PrivateKey
|
||||||
|
|
||||||
|
def sign(self, payload: bytes) -> bytes:
|
||||||
|
return self.private_key.sign(payload)
|
||||||
|
|
||||||
|
def verify(self, payload: bytes, signature: bytes) -> None:
|
||||||
|
Ed25519PublicKey.from_public_bytes(self.public_key).verify(signature, payload)
|
||||||
|
|
||||||
|
|
||||||
|
class IdentityAuthority:
|
||||||
|
@staticmethod
|
||||||
|
def create(label: str) -> Identity:
|
||||||
|
private_key = Ed25519PrivateKey.generate()
|
||||||
|
public_key = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||||
|
fingerprint = hashlib.sha256(public_key).hexdigest()[:24]
|
||||||
|
did = f"did:medimesh:{label}:{fingerprint}"
|
||||||
|
return Identity(did=did, public_key=public_key, private_key=private_key)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Credential:
|
||||||
|
subject_did: str
|
||||||
|
issuer_did: str
|
||||||
|
scopes: tuple[str, ...]
|
||||||
|
issued_at: str
|
||||||
|
expires_at: str
|
||||||
|
signature: str
|
||||||
|
|
||||||
|
def is_valid(self, now: datetime | None = None) -> bool:
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
expires = datetime.fromisoformat(self.expires_at)
|
||||||
|
return now <= expires
|
||||||
|
|
||||||
|
|
||||||
|
class GovernanceLedger:
|
||||||
|
def __init__(self, db_path: str | Path = ":memory:") -> None:
|
||||||
|
self.db_path = str(db_path)
|
||||||
|
self.conn = sqlite3.connect(self.db_path)
|
||||||
|
self.conn.row_factory = sqlite3.Row
|
||||||
|
self._ensure_schema()
|
||||||
|
|
||||||
|
def _ensure_schema(self) -> None:
|
||||||
|
self.conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS ledger_entries (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
ts TEXT NOT NULL,
|
||||||
|
actor_did TEXT NOT NULL,
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
prev_hash TEXT NOT NULL,
|
||||||
|
entry_hash TEXT NOT NULL,
|
||||||
|
signature TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def latest_hash(self) -> str:
|
||||||
|
row = self.conn.execute("SELECT entry_hash FROM ledger_entries ORDER BY id DESC LIMIT 1").fetchone()
|
||||||
|
return row["entry_hash"] if row else "GENESIS"
|
||||||
|
|
||||||
|
def append(self, actor: Identity, event_type: str, payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
ts = datetime.now(timezone.utc).isoformat()
|
||||||
|
prev_hash = self.latest_hash()
|
||||||
|
payload_json = _canonical_json(payload)
|
||||||
|
entry_hash = _hash_bytes(prev_hash.encode(), ts.encode(), actor.did.encode(), event_type.encode(), payload_json.encode())
|
||||||
|
signature = actor.sign(entry_hash.encode())
|
||||||
|
record = {
|
||||||
|
"ts": ts,
|
||||||
|
"actor_did": actor.did,
|
||||||
|
"event_type": event_type,
|
||||||
|
"payload": payload_json,
|
||||||
|
"prev_hash": prev_hash,
|
||||||
|
"entry_hash": entry_hash,
|
||||||
|
"signature": base64.b64encode(signature).decode(),
|
||||||
|
}
|
||||||
|
self.conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO ledger_entries (ts, actor_did, event_type, payload, prev_hash, entry_hash, signature)
|
||||||
|
VALUES (:ts, :actor_did, :event_type, :payload, :prev_hash, :entry_hash, :signature)
|
||||||
|
""",
|
||||||
|
record,
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
return record
|
||||||
|
|
||||||
|
def entries(self) -> list[dict[str, Any]]:
|
||||||
|
rows = self.conn.execute("SELECT * FROM ledger_entries ORDER BY id ASC").fetchall()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
def verify_chain(self, identity_lookup: Mapping[str, Identity]) -> bool:
|
||||||
|
previous = "GENESIS"
|
||||||
|
for entry in self.entries():
|
||||||
|
payload = entry["payload"]
|
||||||
|
recalculated = _hash_bytes(
|
||||||
|
previous.encode(),
|
||||||
|
entry["ts"].encode(),
|
||||||
|
entry["actor_did"].encode(),
|
||||||
|
entry["event_type"].encode(),
|
||||||
|
payload.encode(),
|
||||||
|
)
|
||||||
|
if recalculated != entry["entry_hash"]:
|
||||||
|
return False
|
||||||
|
actor = identity_lookup.get(entry["actor_did"])
|
||||||
|
if actor is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
actor.verify(entry["entry_hash"].encode(), base64.b64decode(entry["signature"]))
|
||||||
|
except InvalidSignature:
|
||||||
|
return False
|
||||||
|
previous = entry["entry_hash"]
|
||||||
|
return True
|
||||||
|
|
||||||
|
def issue_credential(self, issuer: Identity, subject_did: str, scopes: Iterable[str], ttl_seconds: int = 900) -> Credential:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
expires = now.timestamp() + ttl_seconds
|
||||||
|
body = {
|
||||||
|
"subject_did": subject_did,
|
||||||
|
"issuer_did": issuer.did,
|
||||||
|
"scopes": tuple(scopes),
|
||||||
|
"issued_at": now.isoformat(),
|
||||||
|
"expires_at": datetime.fromtimestamp(expires, tz=timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
signature = base64.b64encode(issuer.sign(_canonical_json(body).encode())).decode()
|
||||||
|
return Credential(signature=signature, **body)
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
from .adapters import EMSDispatchLogisticsController, HospitalInventoryController
|
||||||
|
from .contracts import AdapterContractBinding, ContractRegistry, DataContract, GovernancePolicy
|
||||||
|
from .governance import GovernanceLedger, IdentityAuthority
|
||||||
|
from .solver import DistributedAllocator, HumanitarianPolicy, SiteState
|
||||||
|
from .sync import OfflineDeltaStore
|
||||||
|
|
||||||
|
|
||||||
|
def run_toy_scenario() -> dict:
|
||||||
|
registry = ContractRegistry()
|
||||||
|
inventory_contract = DataContract(
|
||||||
|
name="inventory_snapshot",
|
||||||
|
version="1.0",
|
||||||
|
required_fields=("site_id", "resource", "on_hand", "forecast_demand"),
|
||||||
|
optional_fields=("minimum_service",),
|
||||||
|
owner="hospital",
|
||||||
|
)
|
||||||
|
dispatch_contract = DataContract(
|
||||||
|
name="dispatch_plan",
|
||||||
|
version="1.0",
|
||||||
|
required_fields=("site_id", "transfer_priority", "shortage_signal"),
|
||||||
|
owner="ems",
|
||||||
|
)
|
||||||
|
policy = GovernancePolicy(name="humanitarian", version="1.0", triage_priority=1.5, equity_weight=1.2, continuity_weight=1.1)
|
||||||
|
registry.register_contract(inventory_contract)
|
||||||
|
registry.register_contract(dispatch_contract)
|
||||||
|
registry.register_policy(policy)
|
||||||
|
registry.register_adapter(
|
||||||
|
AdapterContractBinding(
|
||||||
|
adapter_name="hospital-inventory-controller",
|
||||||
|
reads=(inventory_contract.node_id,),
|
||||||
|
writes=(dispatch_contract.node_id,),
|
||||||
|
governed_by=(policy.node_id,),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
authority = IdentityAuthority.create("medimesh")
|
||||||
|
ledger = GovernanceLedger()
|
||||||
|
delta_store = OfflineDeltaStore()
|
||||||
|
allocator = DistributedAllocator(iterations=10)
|
||||||
|
policy_runtime = HumanitarianPolicy(equity_weight=1.2, continuity_weight=1.1, triage_weight=1.5)
|
||||||
|
|
||||||
|
sites = [
|
||||||
|
SiteState("hospital-a", "north", 32, 46, equity_weight=1.4, continuity_weight=1.2, triage_priority=1.5, minimum_service=10),
|
||||||
|
SiteState("hospital-b", "central", 18, 40, equity_weight=1.1, continuity_weight=1.5, triage_priority=1.2, minimum_service=8),
|
||||||
|
SiteState("clinic-c", "south", 7, 21, equity_weight=1.8, continuity_weight=0.9, triage_priority=1.4, minimum_service=4),
|
||||||
|
SiteState("pharmacy-d", "east", 15, 12, equity_weight=0.9, continuity_weight=1.0, triage_priority=0.8, minimum_service=2),
|
||||||
|
]
|
||||||
|
|
||||||
|
ledger.append(authority, "scenario.initialized", {"sites": [site.site_id for site in sites]})
|
||||||
|
for site in sites:
|
||||||
|
delta_store.append(site.site_id, "set", {"on_hand": site.on_hand, "forecast_demand": site.forecast_demand}, authority.did)
|
||||||
|
|
||||||
|
shortage_shock = 28.0
|
||||||
|
result = allocator.allocate(sites, total_available=52.0 - shortage_shock, policy=policy_runtime)
|
||||||
|
ems = EMSDispatchLogisticsController(dispatch_base="ems-hub-1", region="central")
|
||||||
|
transfers = ems.prioritize_transfers(result.allocations, shortage_signal=shortage_shock)
|
||||||
|
|
||||||
|
ledger.append(authority, "allocation.issued", {"allocations": result.allocations, "shortage_shock": shortage_shock})
|
||||||
|
for site_id, allocation in result.allocations.items():
|
||||||
|
delta_store.append(site_id, "increment", {"allocated": allocation}, authority.did)
|
||||||
|
|
||||||
|
hospital = HospitalInventoryController("hospital-a", "north", 32, 46, minimum_service=10)
|
||||||
|
hospital_result = hospital.recommend(allocator, total_available=24.0, policy=policy_runtime)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"registry_nodes": sorted(registry.graph.nodes),
|
||||||
|
"ledger_verified": ledger.verify_chain({authority.did: authority}),
|
||||||
|
"delta_replay": {site.site_id: delta_store.replay(site.site_id) for site in sites},
|
||||||
|
"allocation": result.allocations,
|
||||||
|
"unmet": result.unmet_demand,
|
||||||
|
"transfers": transfers,
|
||||||
|
"hospital_recommendation": hospital_result.allocations,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterable, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SiteState:
|
||||||
|
site_id: str
|
||||||
|
region: str
|
||||||
|
on_hand: float
|
||||||
|
forecast_demand: float
|
||||||
|
equity_weight: float = 1.0
|
||||||
|
continuity_weight: float = 1.0
|
||||||
|
triage_priority: float = 1.0
|
||||||
|
minimum_service: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class HumanitarianPolicy:
|
||||||
|
equity_weight: float = 1.0
|
||||||
|
continuity_weight: float = 1.0
|
||||||
|
triage_weight: float = 1.0
|
||||||
|
regional_floor: float = 0.0
|
||||||
|
|
||||||
|
def site_prior(self, site: SiteState) -> float:
|
||||||
|
return (
|
||||||
|
site.equity_weight * self.equity_weight
|
||||||
|
+ site.continuity_weight * self.continuity_weight
|
||||||
|
+ site.triage_priority * self.triage_weight
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AllocationResult:
|
||||||
|
allocations: dict[str, float]
|
||||||
|
unmet_demand: dict[str, float]
|
||||||
|
total_allocated: float
|
||||||
|
iterations: int
|
||||||
|
|
||||||
|
|
||||||
|
class DistributedAllocator:
|
||||||
|
def __init__(self, iterations: int = 8, step_size: float = 0.5) -> None:
|
||||||
|
self.iterations = iterations
|
||||||
|
self.step_size = step_size
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _project_to_sum(values: list[float], total: float, lowers: list[float], uppers: list[float]) -> list[float]:
|
||||||
|
if not values:
|
||||||
|
return []
|
||||||
|
low = min(v - u for v, u in zip(values, uppers)) - abs(total)
|
||||||
|
high = max(v - l for v, l in zip(values, lowers)) + abs(total)
|
||||||
|
for _ in range(80):
|
||||||
|
shift = (low + high) / 2.0
|
||||||
|
projected = [min(upper, max(lower, value - shift)) for value, lower, upper in zip(values, lowers, uppers)]
|
||||||
|
current = sum(projected)
|
||||||
|
if abs(current - total) < 1e-8:
|
||||||
|
return projected
|
||||||
|
if current > total:
|
||||||
|
low = shift
|
||||||
|
else:
|
||||||
|
high = shift
|
||||||
|
shift = (low + high) / 2.0
|
||||||
|
return [min(upper, max(lower, value - shift)) for value, lower, upper in zip(values, lowers, uppers)]
|
||||||
|
|
||||||
|
def allocate(self, sites: Iterable[SiteState], total_available: float, policy: HumanitarianPolicy | None = None) -> AllocationResult:
|
||||||
|
policy = policy or HumanitarianPolicy()
|
||||||
|
sites = list(sites)
|
||||||
|
if not sites:
|
||||||
|
return AllocationResult({}, {}, 0.0, 0)
|
||||||
|
|
||||||
|
lowers = [max(0.0, site.minimum_service) for site in sites]
|
||||||
|
uppers = [max(lower, min(site.forecast_demand, site.on_hand + total_available)) for site, lower in zip(sites, lowers)]
|
||||||
|
weights = [max(0.1, policy.site_prior(site)) for site in sites]
|
||||||
|
total = min(total_available, sum(uppers))
|
||||||
|
x = [min(upper, max(lower, total * weight / sum(weights))) for weight, lower, upper in zip(weights, lowers, uppers)]
|
||||||
|
duals = [0.0 for _ in sites]
|
||||||
|
|
||||||
|
for _ in range(self.iterations):
|
||||||
|
proposals = []
|
||||||
|
for idx, site in enumerate(sites):
|
||||||
|
target = min(site.forecast_demand, site.on_hand + total_available)
|
||||||
|
prior = policy.site_prior(site)
|
||||||
|
local_update = x[idx] + self.step_size * ((target - x[idx]) / max(1.0, target) + prior - duals[idx])
|
||||||
|
proposals.append(local_update)
|
||||||
|
x = self._project_to_sum(proposals, total, lowers, uppers)
|
||||||
|
duals = [dual + proposal - alloc for dual, proposal, alloc in zip(duals, proposals, x)]
|
||||||
|
|
||||||
|
allocations = {site.site_id: round(value, 4) for site, value in zip(sites, x)}
|
||||||
|
unmet = {site.site_id: round(max(0.0, site.forecast_demand - allocations[site.site_id]), 4) for site in sites}
|
||||||
|
return AllocationResult(allocations=allocations, unmet_demand=unmet, total_allocated=round(sum(x), 4), iterations=self.iterations)
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable, Mapping
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(value: Any) -> str:
|
||||||
|
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_chain(*parts: str) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
for part in parts:
|
||||||
|
digest.update(part.encode())
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DeltaRecord:
|
||||||
|
site_id: str
|
||||||
|
sequence: int
|
||||||
|
kind: str
|
||||||
|
payload: dict[str, Any]
|
||||||
|
author_did: str
|
||||||
|
prev_hash: str
|
||||||
|
hash: str
|
||||||
|
|
||||||
|
|
||||||
|
class OfflineDeltaStore:
|
||||||
|
def __init__(self, db_path: str | Path = ":memory:") -> None:
|
||||||
|
self.db_path = str(db_path)
|
||||||
|
self.conn = sqlite3.connect(self.db_path)
|
||||||
|
self.conn.row_factory = sqlite3.Row
|
||||||
|
self._ensure_schema()
|
||||||
|
|
||||||
|
def _ensure_schema(self) -> None:
|
||||||
|
self.conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS deltas (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
site_id TEXT NOT NULL,
|
||||||
|
sequence INTEGER NOT NULL,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
author_did TEXT NOT NULL,
|
||||||
|
prev_hash TEXT NOT NULL,
|
||||||
|
hash TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_deltas_site_sequence ON deltas(site_id, sequence)")
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def latest_sequence(self, site_id: str) -> int:
|
||||||
|
row = self.conn.execute("SELECT MAX(sequence) AS sequence FROM deltas WHERE site_id = ?", (site_id,)).fetchone()
|
||||||
|
return int(row["sequence"] or 0)
|
||||||
|
|
||||||
|
def latest_hash(self, site_id: str) -> str:
|
||||||
|
row = self.conn.execute("SELECT hash FROM deltas WHERE site_id = ? ORDER BY sequence DESC LIMIT 1", (site_id,)).fetchone()
|
||||||
|
return row["hash"] if row else "GENESIS"
|
||||||
|
|
||||||
|
def append(self, site_id: str, kind: str, payload: Mapping[str, Any], author_did: str) -> DeltaRecord:
|
||||||
|
sequence = self.latest_sequence(site_id) + 1
|
||||||
|
prev_hash = self.latest_hash(site_id)
|
||||||
|
payload_json = _canonical_json(payload)
|
||||||
|
hash_value = _hash_chain(site_id, str(sequence), kind, payload_json, author_did, prev_hash)
|
||||||
|
self.conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO deltas (site_id, sequence, kind, payload, author_did, prev_hash, hash)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(site_id, sequence, kind, payload_json, author_did, prev_hash, hash_value),
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
return DeltaRecord(site_id, sequence, kind, dict(payload), author_did, prev_hash, hash_value)
|
||||||
|
|
||||||
|
def records(self, site_id: str) -> list[DeltaRecord]:
|
||||||
|
rows = self.conn.execute("SELECT * FROM deltas WHERE site_id = ? ORDER BY sequence ASC", (site_id,)).fetchall()
|
||||||
|
return [
|
||||||
|
DeltaRecord(
|
||||||
|
site_id=row["site_id"],
|
||||||
|
sequence=row["sequence"],
|
||||||
|
kind=row["kind"],
|
||||||
|
payload=json.loads(row["payload"]),
|
||||||
|
author_did=row["author_did"],
|
||||||
|
prev_hash=row["prev_hash"],
|
||||||
|
hash=row["hash"],
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
def delta_since(self, site_id: str, sequence: int) -> list[DeltaRecord]:
|
||||||
|
rows = self.conn.execute(
|
||||||
|
"SELECT * FROM deltas WHERE site_id = ? AND sequence > ? ORDER BY sequence ASC",
|
||||||
|
(site_id, sequence),
|
||||||
|
).fetchall()
|
||||||
|
return [
|
||||||
|
DeltaRecord(
|
||||||
|
site_id=row["site_id"],
|
||||||
|
sequence=row["sequence"],
|
||||||
|
kind=row["kind"],
|
||||||
|
payload=json.loads(row["payload"]),
|
||||||
|
author_did=row["author_did"],
|
||||||
|
prev_hash=row["prev_hash"],
|
||||||
|
hash=row["hash"],
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
def replay(self, site_id: str) -> dict[str, Any]:
|
||||||
|
state: dict[str, Any] = {}
|
||||||
|
for record in self.records(site_id):
|
||||||
|
if record.kind == "set":
|
||||||
|
state.update(record.payload)
|
||||||
|
elif record.kind == "increment":
|
||||||
|
for key, value in record.payload.items():
|
||||||
|
state[key] = state.get(key, 0) + value
|
||||||
|
else:
|
||||||
|
state[record.kind] = record.payload
|
||||||
|
return state
|
||||||
|
|
||||||
|
def verify_chain(self, site_id: str) -> bool:
|
||||||
|
previous = "GENESIS"
|
||||||
|
for record in self.records(site_id):
|
||||||
|
recalculated = _hash_chain(record.site_id, str(record.sequence), record.kind, _canonical_json(record.payload), record.author_did, previous)
|
||||||
|
if recalculated != record.hash:
|
||||||
|
return False
|
||||||
|
previous = record.hash
|
||||||
|
return True
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
python3 -m pip install -e .[test]
|
||||||
|
python3 -m pip install build
|
||||||
|
pytest
|
||||||
|
python3 -m build
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from medimesh.contracts import AdapterContractBinding, ContractRegistry, DataContract, GovernancePolicy
|
||||||
|
|
||||||
|
|
||||||
|
def test_contract_registry_conformance_and_lineage() -> None:
|
||||||
|
registry = ContractRegistry()
|
||||||
|
contract = DataContract("inventory_snapshot", "1.0", ("site_id", "on_hand"), ("forecast_demand",))
|
||||||
|
policy = GovernancePolicy("humanitarian", "1.0")
|
||||||
|
registry.register_contract(contract)
|
||||||
|
registry.register_policy(policy)
|
||||||
|
registry.register_adapter(AdapterContractBinding("adapter", (contract.node_id,), (), (policy.node_id,)))
|
||||||
|
|
||||||
|
assert registry.conformance_check(contract.node_id, {"site_id": "a", "on_hand": 10, "forecast_demand": 8}) == []
|
||||||
|
assert contract.node_id in registry.lineage("adapter")
|
||||||
|
assert policy.node_id in registry.lineage("adapter")
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
from medimesh.governance import GovernanceLedger, IdentityAuthority
|
||||||
|
from medimesh.sync import OfflineDeltaStore
|
||||||
|
|
||||||
|
|
||||||
|
def test_governance_chain_verifies_and_credentials_are_short_lived() -> None:
|
||||||
|
authority = IdentityAuthority.create("tester")
|
||||||
|
ledger = GovernanceLedger()
|
||||||
|
ledger.append(authority, "site.registered", {"site_id": "hospital-a"})
|
||||||
|
cred = ledger.issue_credential(authority, "did:medimesh:subject", ["read:inventory"], ttl_seconds=60)
|
||||||
|
|
||||||
|
assert cred.is_valid()
|
||||||
|
assert ledger.verify_chain({authority.did: authority})
|
||||||
|
|
||||||
|
|
||||||
|
def test_delta_sync_replay_is_deterministic() -> None:
|
||||||
|
store = OfflineDeltaStore()
|
||||||
|
store.append("hospital-a", "set", {"on_hand": 10}, "did:medimesh:test")
|
||||||
|
store.append("hospital-a", "increment", {"on_hand": -3}, "did:medimesh:test")
|
||||||
|
|
||||||
|
assert store.verify_chain("hospital-a")
|
||||||
|
assert store.replay("hospital-a")["on_hand"] == 7
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from medimesh.scenario import run_toy_scenario
|
||||||
|
from medimesh.solver import DistributedAllocator, HumanitarianPolicy, SiteState
|
||||||
|
|
||||||
|
|
||||||
|
def test_allocator_respects_supply_and_bounds() -> None:
|
||||||
|
allocator = DistributedAllocator(iterations=6)
|
||||||
|
sites = [
|
||||||
|
SiteState("a", "north", 10, 12, minimum_service=3),
|
||||||
|
SiteState("b", "south", 5, 8, minimum_service=2),
|
||||||
|
]
|
||||||
|
|
||||||
|
result = allocator.allocate(sites, total_available=9.0, policy=HumanitarianPolicy())
|
||||||
|
|
||||||
|
assert round(result.total_allocated, 4) <= 9.0
|
||||||
|
assert result.allocations["a"] >= 3
|
||||||
|
assert result.allocations["b"] >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_toy_scenario_runs_end_to_end() -> None:
|
||||||
|
outcome = run_toy_scenario()
|
||||||
|
|
||||||
|
assert outcome["ledger_verified"] is True
|
||||||
|
assert "hospital-a" in outcome["allocation"]
|
||||||
|
assert outcome["registry_nodes"]
|
||||||
Loading…
Reference in New Issue