diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3029596 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,27 @@ +# HopeMesh AGENTS + +- Overview + - A production-oriented skeleton for a cross-domain, offline-first resource allocation system with governance and adapters. + +- Architecture + - Core primitives: LocalProblem, SharedSignals, PlanDelta (GoC-style contracts). + - Graph of Contracts (GoC): lightweight in-process graph to connect problems, signals, and actions. + - Adapters: pluggable endpoints for field units, warehouses, transport hubs; starter adapters included. + - Transport: TLS-backed channel abstraction for inter-device communication. + - Governance: skeleton ledger for provenance with per-message signatures (extensible to DID-based identities). + +- Tech Stack + - Language: Python 3.9+ + - Packaging: pyproject.toml (setuptools) + +- Testing and quality gates + - Basic unit tests for core primitives. + - test.sh to run pytest and python -m build for packaging checks. + - AGENTS.md intentionally documents architecture so future agents can contribute without breaking it. + +- How to run locally + - python3 -m pytest + - bash test.sh + +- Publishing rules + - Follow the publish checklist in the main task description; ensure READY_TO_PUBLISH exists when ready. diff --git a/README.md b/README.md index 1721f2b..ddc90b9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,32 @@ -# idea152-hopemesh-federated-privacy +# HopeMesh: Federated, Privacy-Preserving Humanitarian Resource Allocation -Source logic for Idea #152 \ No newline at end of file +Overview +- HopeMesh is a distributed, offline-first platform to coordinate humanitarian aid across agencies in crisis zones with intermittent connectivity. +- It models local needs as LocalProblem, supply signals as SharedSignals, and delta actions as PlanDelta within a Graph-of-Contracts (GoC) architecture. +- The MVP focuses on cross-domain adapters (supply-inventory tracker and field-needs collector) with TLS transport, governance ledger skeleton, and a small cross-domain objective. + +Architecture (high level) +- Core primitives: LocalProblem, SharedSignals, PlanDelta +- Graph-of-Contracts (GoC): a lightweight graph of contracts and adapters +- Adapters: plug in domain-specific data sources and workflows (starter adapters included) +- Governance ledger: per-message provenance and signature trail (skeleton) +- TLS transport: secure channels between field units and hubs + +Tech Stack (by component) +- Language: Python 3.9+ +- Packaging: pyproject.toml (setuptools) with an executable test script +- API surface: lightweight Python classes and simple in-memory data stores + +How to run (local dev) +- Install dependencies: (no heavy deps required yet) +- Run tests: ./test.sh +- Package and build: python3 -m build + +Packaging hooks +- The project name in pyproject.toml is idea152-hopemesh-federated-privacy to satisfy publishing rules. +- The Python importable package is idea152_hopemesh_federated_privacy (underscore style) to align with Python packaging norms. + +Contributing +- See AGENTS.md for architecture, testing commands, and contribution rules. + +License: MIT (example) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..49b37d6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "idea152-hopemesh-federated-privacy" +version = "0.1.0" +description = "Federated, privacy-preserving humanitarian resource allocation prototype (HopeMesh)" +readme = "README.md" +requires-python = ">=3.9" +license = { file = "LICENSE" } + +[project.urls] +Homepage = "https://example.org/hope-mesh" +Repository = "https://github.com/example/hope-mesh" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..1cb2bea --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,10 @@ +"""Temporary namespace for the HopeMesh Python package. +This package provides core primitives and a minimal MVP scaffolding. +""" + +__all__ = [ + "core", + "governance", + "adapters", + "tls_transport", +] diff --git a/src/adapters/__init__.py b/src/adapters/__init__.py new file mode 100644 index 0000000..0576fef --- /dev/null +++ b/src/adapters/__init__.py @@ -0,0 +1 @@ +"""Adapters package for HopeMesh MVP.""" diff --git a/src/adapters/starter/__init__.py b/src/adapters/starter/__init__.py new file mode 100644 index 0000000..c48a97a --- /dev/null +++ b/src/adapters/starter/__init__.py @@ -0,0 +1 @@ +"""Starter adapters for HopeMesh MVP.""" diff --git a/src/adapters/starter/field_needs_collector.py b/src/adapters/starter/field_needs_collector.py new file mode 100644 index 0000000..ad52dde --- /dev/null +++ b/src/adapters/starter/field_needs_collector.py @@ -0,0 +1,32 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import List, Dict +import time + + +@dataclass +class FieldNeedsCollector: + collected: List[Dict] = field(default_factory=list) + + def add_need(self, location: str, need_type: str, quantity: int, time_window: tuple[str, str]) -> None: + need = { + "location": location, + "need_type": need_type, + "quantity": quantity, + "time_window": time_window, + "timestamp": time.time(), + } + self.collected.append(need) + + def to_local_problem(self) -> dict: + # Simple translation to LocalProblem-like dict for MVP testability + if not self.collected: + return {} + n = self.collected[-1] + return { + "location": n["location"], + "need_type": n["need_type"], + "quantity": n["quantity"], + "time_window": n["time_window"], + "priority": 3, + } diff --git a/src/adapters/starter/supply_inventory_tracker.py b/src/adapters/starter/supply_inventory_tracker.py new file mode 100644 index 0000000..48fe020 --- /dev/null +++ b/src/adapters/starter/supply_inventory_tracker.py @@ -0,0 +1,22 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Dict + + +@dataclass +class SupplyInventoryTracker: + inventory: Dict[str, int] = field(default_factory=dict) + + def register_item(self, item: str, quantity: int) -> None: + if item not in self.inventory: + self.inventory[item] = 0 + self.inventory[item] += int(quantity) + + def consume_item(self, item: str, quantity: int) -> bool: + if self.inventory.get(item, 0) < quantity: + return False + self.inventory[item] -= quantity + return True + + def __repr__(self) -> str: + return f"SupplyInventoryTracker(inventory={self.inventory})" diff --git a/src/core.py b/src/core.py new file mode 100644 index 0000000..d6b3a4e --- /dev/null +++ b/src/core.py @@ -0,0 +1,51 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Dict, List, Tuple, Optional + + +@dataclass +class LocalProblem: + location: str + need_type: str + priority: int # 1-5, higher means more urgent + quantity: int + time_window: Tuple[str, str] # (start_iso, end_iso) + + +@dataclass +class SharedSignals: + stock_levels: Dict[str, int] # e.g., {"itemA": 100} + urgency_scores: Dict[str, float] # per-location urgency or need types + + +@dataclass +class PlanDelta: + plan_id: str + actions: List[Dict[str, object]] # simple action descriptors + timestamp: str # ISO timestamp + valid: bool = True + + +class GraphOfContracts: + """Minimal GoC-like graph to connect problems, signals, and deltas.""" + + def __init__(self) -> None: + self.local_problems: List[LocalProblem] = [] + self.shared_signals: Optional[SharedSignals] = None + self.plan_deltas: List[PlanDelta] = [] + + def add_local_problem(self, lp: LocalProblem) -> None: + self.local_problems.append(lp) + + def set_shared_signals(self, ss: SharedSignals) -> None: + self.shared_signals = ss + + def add_plan_delta(self, pd: PlanDelta) -> None: + self.plan_deltas.append(pd) + + def __repr__(self) -> str: + return ( + f"GraphOfContracts(LocalProblems={len(self.local_problems)}, " + f"SharedSignals={'set' if self.shared_signals else 'unset'}, " + f"PlanDeltas={len(self.plan_deltas)})" + ) diff --git a/src/governance.py b/src/governance.py new file mode 100644 index 0000000..c363b3f --- /dev/null +++ b/src/governance.py @@ -0,0 +1,36 @@ +from __future__ import annotations +import hmac +import hashlib +from dataclasses import dataclass +from time import time + +_SHARED_KEY = b"super-secret-key-for-tests" # In production, use DID-based keys or KMS + + +@dataclass +class GovernanceEntry: + message: str + signature: str + timestamp: float + + +class GovernanceLedger: + """Skeleton governance ledger with simple HMAC-based signatures for testability.""" + + def __init__(self, key: bytes | None = None) -> None: + self.key = key or _SHARED_KEY + self.entries: list[GovernanceEntry] = [] + + def sign_message(self, message: str) -> GovernanceEntry: + ts = time() + sig = hmac.new(self.key, msg=message.encode("utf-8"), digestmod=hashlib.sha256).hexdigest() + entry = GovernanceEntry(message=message, signature=sig, timestamp=ts) + self.entries.append(entry) + return entry + + def verify_entry(self, entry: GovernanceEntry) -> bool: + expected = hmac.new(self.key, msg=entry.message.encode("utf-8"), digestmod=hashlib.sha256).hexdigest() + return hmac.compare_digest(expected, entry.signature) + + def __repr__(self) -> str: + return f"GovernanceLedger(entries={len(self.entries)})" diff --git a/src/tls_transport.py b/src/tls_transport.py new file mode 100644 index 0000000..ea18329 --- /dev/null +++ b/src/tls_transport.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +class TLSEndpoint: + """Minimal TLS transport stub for MVP. Not a full TLS server/client. + This provides a placeholder interface that future code can extend to use real TLS sockets. + """ + + def __init__(self, address: str, use_tls: bool = True): + self.address = address + self.use_tls = use_tls + + def send(self, data: bytes) -> None: + # Stub: in a real implementation this would write to a TLS-wrapped socket + if self.use_tls: + # pretend to encrypt and send + _ = data # no-op for placeholder + else: + pass + + def receive(self) -> bytes: + # Stub: return empty bytes for MVP + return b"" diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..441a42d --- /dev/null +++ b/test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +printf "Running tests...\n" +pytest -q +printf "Building package...\n" +python3 -m build diff --git a/test_core.py b/test_core.py new file mode 100644 index 0000000..b80e4d5 --- /dev/null +++ b/test_core.py @@ -0,0 +1,43 @@ +import time +from src.core import LocalProblem, SharedSignals, PlanDelta, GraphOfContracts + + +def test_local_problem_dataclass(): + lp = LocalProblem( + location="Site-A", + need_type="food_rations", + priority=4, + quantity=1000, + time_window=("2026-04-21T00:00:00Z", "2026-04-22T00:00:00Z"), + ) + assert lp.location == "Site-A" + assert lp.need_type == "food_rations" + assert lp.quantity == 1000 + + +def test_shared_signals_and_plan_delta_basics(): + ss = SharedSignals(stock_levels={"itemA": 500}, urgency_scores={"Site-A": 0.8}) + assert ss.stock_levels["itemA"] == 500 + + pd = PlanDelta(plan_id="P1", actions=[{"allocate": {"itemA": 100}}], timestamp=time.time()) + assert pd.plan_id == "P1" + assert isinstance(pd.actions, list) + + +def test_graph_of_contracts_basic_flow(): + go = GraphOfContracts() + lp = LocalProblem( + location="Site-B", + need_type="water", + priority=2, + quantity=200, + time_window=("2026-04-25T00:00:00Z", "2026-04-26T00:00:00Z"), + ) + go.add_local_problem(lp) + ss = SharedSignals(stock_levels={"water": 1000}, urgency_scores={"Site-B": 0.5}) + go.set_shared_signals(ss) + pd = PlanDelta(plan_id="P2", actions=[{"deliver": {"water": 200}}], timestamp=time.time()) + go.add_plan_delta(pd) + assert len(go.local_problems) == 1 + assert go.shared_signals is ss + assert len(go.plan_deltas) == 1