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