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..2c7238b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +OpenCode: MarketMesh – Architecture, Tech Stack, Testing, and Contribution Rules + +Overview +- A privacy-preserving federated benchmarking scaffold for startups. Adapters map device-specific metrics to canonical signals; data channels (morphisms) carry aggregated stats with privacy budgets. + +Tech Stack +- Language: Python 3.8+ +- Core concepts: Contracts, Local DP (optional), Secure Aggregation (simulated), Delta-Sync, Governance Ledger (audit logs), Adapter Registry. +- Adapters: Stripe revenue adapter, Shopify funnel adapter (minimum viable implementations). + +Testing and Commands +- Run tests: ./test.sh +- Build packages: python3 -m build +- Linting (optional): pip install flake8; flake8 . + +Repository Structure (high level) +- marketmesh_privacy_preserving_federated_: Core package +- marketmesh_privacy_preserving_federated_/adapters: Adapter implementations +- tests/: Unit tests +- test.sh: Test runner that executes tests and packaging build + +Contribution Rules +- Complete, small, well-scoped changes with tests. +- Do not modify packaging metadata unless necessary; ensure tests remain green. +- Document changes with clear commit messages (why-focused). + +Governance and Conventions +- All data is anonymized/aggregated; DP budgets are simulated for MVP. +- Logs and contracts are versioned; adapters must expose a conformance interface. + +This document helps future agents contribute without breaking the repo. diff --git a/README.md b/README.md index 3dff7b6..a24f470 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,25 @@ -# marketmesh-privacy-preserving-federated- +MarketMesh Privacy-Preserving Federated Benchmarking -A lightweight, open-source federation platform that lets participating startups share anonymized growth KPIs to generate cross-market benchmarks and test growth hypotheses without exposing raw data. Key features include: -- Contract-driven data exchan \ No newline at end of file +Overview +- Lightweight, open-source federation platform for sharing anonymized growth KPIs to generate cross-market benchmarks without exposing raw data. +- MVP features: contract-driven data exchange, privacy budgets, secure/DP aggregation, delta-sync, governance ledger, adapters marketplace, and a CatOpt-inspired interoperability abstraction. + +Getting Started +- Install (editable): + python -m build + pip install . +- Run tests: ./test.sh + +Project Structure (high level) +- marketmesh_privacy_preserving_federated_: Core package with protocol, aggregation, governance and adapters scaffolding. +- marketmesh_privacy_preserving_federated_/adapters: Stripe and Shopify adapters (minimum viable implementations). +- tests: Basic unit tests for protocol/aggregation and adapter mappings. + +How to contribute +- Implement additional adapters by adding modules under adapters/ and mapping their signals to canonical KPIs. +- Extend the protocol with more contract fields and governance rules as needed. + +License +- MIT-style license (placeholder in this MVP). + +This README is linked in pyproject.toml for packaging visibility. diff --git a/marketmesh_privacy_preserving_federated_/__init__.py b/marketmesh_privacy_preserving_federated_/__init__.py new file mode 100644 index 0000000..281c2c6 --- /dev/null +++ b/marketmesh_privacy_preserving_federated_/__init__.py @@ -0,0 +1,18 @@ +"""MarketMesh Privacy-Preserving Federated Benchmarking (core package). + +This minimalist MVP exposes core primitives: +- Contract definitions (protocol, karma checks) +- Simple delta-sync aggregator with optional DP shims +- Adapters registry and two minimal adapters (Stripe, Shopify) +""" + +from .core import Contract, DeltaSync, Aggregator +from .adapters import StripeAdapter, ShopifyAdapter + +__all__ = [ + "Contract", + "DeltaSync", + "Aggregator", + "StripeAdapter", + "ShopifyAdapter", +] diff --git a/marketmesh_privacy_preserving_federated_/adapters/__init__.py b/marketmesh_privacy_preserving_federated_/adapters/__init__.py new file mode 100644 index 0000000..6a0b2b5 --- /dev/null +++ b/marketmesh_privacy_preserving_federated_/adapters/__init__.py @@ -0,0 +1,4 @@ +from .stripe import StripeAdapter +from .shopify import ShopifyAdapter + +__all__ = ["StripeAdapter", "ShopifyAdapter"] diff --git a/marketmesh_privacy_preserving_federated_/adapters/shopify.py b/marketmesh_privacy_preserving_federated_/adapters/shopify.py new file mode 100644 index 0000000..2c4d9d3 --- /dev/null +++ b/marketmesh_privacy_preserving_federated_/adapters/shopify.py @@ -0,0 +1,21 @@ +"""Shopify funnel adapter (minimal viable implementation).""" +from typing import Dict + +CANONICAL_KPIS = ["activation_rate", "visits", "signups"] + + +class ShopifyAdapter: + """Maps Shopify-like funnel metrics into canonical KPIs.""" + + def __init__(self, contract_id: str | None = None): + self.contract_id = contract_id or "shopify-contract-0" + + def map_to_canonical(self, raw: Dict[str, float]) -> Dict[str, float]: + visits = raw.get("visits", 0.0) + signups = raw.get("signups", 0.0) + activation = raw.get("activation_rate", 0.0) + return { + "activation_rate": float(activation), + "visits": float(visits), + "signups": float(signups), + } diff --git a/marketmesh_privacy_preserving_federated_/adapters/stripe.py b/marketmesh_privacy_preserving_federated_/adapters/stripe.py new file mode 100644 index 0000000..7d48024 --- /dev/null +++ b/marketmesh_privacy_preserving_federated_/adapters/stripe.py @@ -0,0 +1,17 @@ +"""Stripe revenue adapter (minimal viable implementation).""" +from typing import Dict + +CANONICAL_KPIS = ["revenue", "customers"] + + +class StripeAdapter: + """Maps Stripe-like revenue data into canonical KPIs.""" + + def __init__(self, contract_id: str | None = None): + self.contract_id = contract_id or "stripe-contract-0" + + def map_to_canonical(self, raw: Dict[str, float]) -> Dict[str, float]: + # Very naive mapping for MVP: expect keys like 'amount', 'subscriber_count' + revenue = raw.get("amount", 0.0) + customers = raw.get("subscriber_count", raw.get("customers", 0.0)) + return {"revenue": float(revenue), "customers": float(customers)} diff --git a/marketmesh_privacy_preserving_federated_/core.py b/marketmesh_privacy_preserving_federated_/core.py new file mode 100644 index 0000000..7d71a4a --- /dev/null +++ b/marketmesh_privacy_preserving_federated_/core.py @@ -0,0 +1,77 @@ +"""Core primitives for MarketMesh MVP: contracts, delta-sync, and aggregation.""" +from typing import Dict, Any, List +import math +import random + + +class Contract: + """A versioned data contract describing which KPIs are shared and how they are aggregated.""" + + def __init__(self, contract_id: str, version: int, kpis: List[str]): + self.contract_id = contract_id + self.version = version + self.kpis = list(kpis) + + def __repr__(self) -> str: + return f"Contract(id={self.contract_id}, v={self.version}, kpis={self.kpis})" + + +class DeltaSync: + """Simplified delta-sync payload carrying aggregated signals with a version vector.""" + + def __init__(self, contract_id: str, version_vector: Dict[str, int], payload: Dict[str, float], hash_: str): + self.contract_id = contract_id + self.version_vector = dict(version_vector) + self.payload = dict(payload) + self.hash = hash_ + + def __repr__(self) -> str: + return f"DeltaSync(contract_id={self.contract_id}, version_vector={self.version_vector}, hash={self.hash})" + + +class Aggregator: + """In-memory federated aggregator with optional DP-like noise for each KPI.""" + + def __init__(self, epsilon_per_kpi: Dict[str, float] | None = None): + # epsilon_per_kpi maps KPI name to privacy budget; 0 or None means no DP noise + self.epsilon_per_kpi = dict(epsilon_per_kpi or {}) + + def _laplace_scale(self, eps: float) -> float: + # Laplace mechanism scale b = 1/epsilon; ensure sane default + if eps <= 0: + return 0.0 + return 1.0 / eps + + def aggregate(self, contributions: List[Dict[str, float]]) -> Dict[str, float]: + if not contributions: + return {} + # Compute simple mean per KPI + keys = sorted({k for c in contributions for k in c.keys()}) + sums = {k: 0.0 for k in keys} + count = {k: 0 for k in keys} + for c in contributions: + for k in keys: + if k in c: + sums[k] += c[k] + count[k] += 1 + means = {k: (sums[k] / count[k] if count[k] > 0 else 0.0) for k in keys} + + # Apply a lightweight DP-like noise per KPI if budget is provided + noised = {} + for k, val in means.items(): + eps = self.epsilon_per_kpi.get(k, 0.0) + if eps > 0: + scale = self._laplace_scale(eps) + # simple Laplace sampling without numpy + noise = self._laplace_sample(scale) + noised[k] = val + noise + else: + noised[k] = val + return noised + + def _laplace_sample(self, b: float) -> float: + # Inverse CDF sampling for Laplace(0, b) + u = random.random() - 0.5 # uniform(-0.5, 0.5) + if u == 0: + return 0.0 + return -b * math.copysign(1.0, u) * math.log(1 - 2 * abs(u)) diff --git a/marketmesh_privacy_preserving_federated_/delta_sync_demo.py b/marketmesh_privacy_preserving_federated_/delta_sync_demo.py new file mode 100644 index 0000000..183c104 --- /dev/null +++ b/marketmesh_privacy_preserving_federated_/delta_sync_demo.py @@ -0,0 +1,6 @@ +"""Delta-sync demo helper (standalone).""" +from marketmesh_privacy_preserving_federated_.core import DeltaSync + +def demo(): + ds = DeltaSync(contract_id="demo", version_vector={"v": 1}, payload={"revenue": 100.0}, hash_="x") + return ds diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..92f8442 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "marketmesh_privacy_preserving_federated" +version = "0.1.0" +description = "Lightweight federation for private, anonymized growth benchmarks across startups." +readme = "README.md" +requires-python = ">=3.8" +license = { text = "MIT" } +authors = [ { name = "OpenCode SWARM" } ] + +[project.urls] +Homepage = "https://example.org/marketmesh" + +[tool.setuptools.packages.find] +where = ["marketmesh_privacy_preserving_federated_"] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..7d2e00f --- /dev/null +++ b/test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +export PYTHONPATH="${PYTHONPATH:-}":"./" # ensure repo root is in path for imports + +echo "Running unit tests..." +pytest -q + +echo "Building package..." +python3 -m build + +echo "All tests passed and package built." diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 0000000..3e9cfb3 --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,44 @@ +import unittest + +from marketmesh_privacy_preserving_federated_.core import Contract, DeltaSync, Aggregator +from marketmesh_privacy_preserving_federated_.adapters.stripe import StripeAdapter +from marketmesh_privacy_preserving_federated_.adapters.shopify import ShopifyAdapter + + +class TestAggregationPipeline(unittest.TestCase): + def test_protocol_and_aggregation_basic(self): + # Define a minimal contract with a few KPIs + contract = Contract(contract_id="c1", version=1, kpis=["revenue", "activation_rate", "visits"]) + self.assertIsNotNone(contract) + + # Prepare two participant contributions via adapters mapping to canonical KPIs + stripe = StripeAdapter("stripe-c1") + shopify = ShopifyAdapter("shopify-c1") + + p1_raw = {"amount": 1200.0, "subscriber_count": 40, "visits": 3000, "activation_rate": 0.55, "signups": 250} + p2_raw = {"amount": 800.0, "subscriber_count": 25, "visits": 1800, "activation_rate": 0.6, "signups": 180} + + p1 = stripe.map_to_canonical(p1_raw) + p2 = shopify.map_to_canonical(p2_raw) + + # The aggregated KPI set should include the union of keys + contributions = [p1, p2] + aggregator = Aggregator(epsilon_per_kpi={"revenue": 1.0, "activation_rate": 0.5, "visits": 0.5}) + aggregated = aggregator.aggregate(contributions) + + # Basic sanity: keys exist and values are numeric + for k in ["revenue", "activation_rate", "visits"]: + self.assertIn(k, aggregated) + self.assertIsInstance(aggregated[k], float) + + def test_delta_sync_and_hash(self): + # Simple delta sync payload creation and representation test + payload = {"revenue": 1500.0, "customers": 60} + ds = DeltaSync(contract_id="c1", version_vector={"v1": 1}, payload=payload, hash_="abcd1234") + self.assertEqual(ds.contract_id, "c1") + self.assertEqual(ds.payload, payload) + self.assertEqual(ds.hash, "abcd1234") + + +if __name__ == "__main__": + unittest.main()