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..e0d1af1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,22 @@ +# AGENTS + +Architecture overview for AudioLedger Studio. + +- Language: Python 3.9+ (pyproject configured) +- Core modules: + - core.py: DSL parsing and IR data classes (LocalProblemIR, SharedVariableIR, PlanDeltaIR, AuditLog) + - runtime.py: ExecutionGraph and a minimal deterministic allocator + - delta.py: DeltaStore for offline delta-synchronization + - signer.py: Lightweight HMAC-based attestations for audit logs + - registry.py: GraphRegistry primitive with adapters support + - adapters.py: PriceFeedAdapter as a sample adapter + - sonifier.py: Audio cue mapping from risk/allocation state + - tests/ (unit tests) + +- Testing: pytest-based tests to cover DSL parsing, runtime allocation, delta syncing, signing, and registry; + a test.sh script will run tests and Python packaging to verify build integrity. + +- How to contribute: follow these steps + 1) Implement or modify a feature in the core, then run tests via test.sh + 2) Update AGENTS.md if you introduce new components or interfaces + 3) Ensure READY_TO_PUBLISH is created only after the repository is fully ready for publishing diff --git a/README.md b/README.md index 7c77d5d..c5a3cdc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,33 @@ -# idea81-audioledger-studio-verifiable +# idea81_audioledger_studio_verifiable -Source logic for Idea #81 \ No newline at end of file +Open-source platform to express financial market scenarios via a math-friendly DSL, compile to a portable execution graph, and execute offline-first with delta-sync to governance hubs. + +Highlights +- Algebraic DSL for assets, objectives, risk budgets, and policies +- Canonical IR: LocalProblem, SharedVariable, PlanDelta, DualVariables, AuditLog +- WebAssembly-ready runtime (stubbed here for Python-based prototyping) +- Tamper-evident, attestable governance logs via a Signer +- Lightweight Graph-of-Contracts registry with adapters (price feeds, brokers) +- Audio sonification layer mapping risk/allocation/state to audible cues +- Offline-first operation with deterministic delta-sync + +Architecture +- Python-based core with modular separation: + - core.py: DSL parsing and IR data classes + - runtime.py: Execution graph and allocation logic + - delta.py: Delta store and application logic + - signer.py: Attestation helper + - registry.py: Registry for contracts + - adapters.py: Adapters for external data sources + - sonifier.py: Audio cue generation + +Quick Start +- Install: pytest and build tooling (Python packaging) +- Run tests: bash test.sh +- Extend: Add new adapters, DSL features, and testing coverage + +Packaging +- Python package name: idea81_audioledger_studio_verifiable +- pyproject.toml defines build metadata and long_description hook to README.md + +For more details, see AGENTS.md and the unit tests in tests/. diff --git a/idea81_audioledger_studio_verifiable/README_TEMPLATE.md b/idea81_audioledger_studio_verifiable/README_TEMPLATE.md new file mode 100644 index 0000000..69a5e60 --- /dev/null +++ b/idea81_audioledger_studio_verifiable/README_TEMPLATE.md @@ -0,0 +1,3 @@ +# Idea + +This is a placeholder to assist tooling in this sprint. The real README will be upgraded in subsequent tasks. diff --git a/idea81_audioledger_studio_verifiable/__init__.py b/idea81_audioledger_studio_verifiable/__init__.py new file mode 100644 index 0000000..1135750 --- /dev/null +++ b/idea81_audioledger_studio_verifiable/__init__.py @@ -0,0 +1,21 @@ +"""__init__ for idea81_audioledger_studio_verifiable""" +from .core import parse_dsl, LocalProblemIR, SharedVariableIR, PlanDeltaIR, AuditLog +from .runtime import ExecutionGraph, Node +from .delta import DeltaStore +from .signer import Signer +from .registry import GraphRegistry +from .sonifier import Sonifier + +__all__ = [ + "parse_dsl", + "LocalProblemIR", + "SharedVariableIR", + "PlanDeltaIR", + "AuditLog", + "ExecutionGraph", + "Node", + "DeltaStore", + "Signer", + "GraphRegistry", + "Sonifier", +] diff --git a/idea81_audioledger_studio_verifiable/adapters.py b/idea81_audioledger_studio_verifiable/adapters.py new file mode 100644 index 0000000..f20e0af --- /dev/null +++ b/idea81_audioledger_studio_verifiable/adapters.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from typing import Dict + + +class PriceFeedAdapter: + """Simple in-memory price feed adapter.""" + def __init__(self, prices: Dict[str, float] | None = None) -> None: + self._prices = prices or {"AAPL": 150.0, "BTC": 60000.0, "USDC": 1.0} + + def get_prices(self) -> Dict[str, float]: + return dict(self._prices) diff --git a/idea81_audioledger_studio_verifiable/core.py b/idea81_audioledger_studio_verifiable/core.py new file mode 100644 index 0000000..26b3457 --- /dev/null +++ b/idea81_audioledger_studio_verifiable/core.py @@ -0,0 +1,91 @@ +import time +from dataclasses import dataclass, field +from typing import Any, Dict, List + + +@dataclass +class LocalProblemIR: + assets: Dict[str, float] = field(default_factory=dict) + objectives: Dict[str, Any] = field(default_factory=dict) + risk_budgets: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class SharedVariableIR: + variables: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class PlanDeltaIR: + changes: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class AuditLogEntry: + timestamp: float + action: str + details: str + + +class AuditLog: + def __init__(self) -> None: + self.entries: List[AuditLogEntry] = [] + + def add(self, action: str, details: str) -> None: + self.entries.append(AuditLogEntry(time.time(), action, details)) + + def to_json(self) -> str: + # minimal JSON-like representation + items = [ + {"timestamp": e.timestamp, "action": e.action, "details": e.details} + for e in self.entries + ] + return str(items) + + +def parse_dsl(text: str) -> Dict[str, Any]: + assets: Dict[str, float] = {} + objectives: Dict[str, Any] = {} + risk_budgets: Dict[str, Any] = {} + current: str | None = None + for raw_line in text.splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if line.lower().startswith("assets:"): + current = "assets" + continue + if line.lower().startswith("objectives:"): + current = "objectives" + continue + if line.lower().startswith("risk_budgets:") or line.lower().startswith("risk budgets:"): + current = "risk_budgets" + continue + if line.endswith(":"): + # section placeholder + current = line[:-1].lower() + continue + # Parse key: value lines within sections + if ":" in line: + key, val = [p.strip() for p in line.split(":", 1)] + try: + num = float(val) + v = num + except ValueError: + v = val + if current == "assets": + assets[key] = v + elif current == "objectives": + objectives[key] = v + elif current == "risk_budgets" or current == "risk budgets": + risk_budgets[key] = v + else: + # default to assets-like mapping + assets[key] = v + ir = { + "objects": LocalProblemIR(assets=assets, objectives=objectives, risk_budgets=risk_budgets), + "morphisms": SharedVariableIR(variables={}), + "plan_delta": PlanDeltaIR(changes={}), + "audit_log": AuditLog(), + } + return ir diff --git a/idea81_audioledger_studio_verifiable/delta.py b/idea81_audioledger_studio_verifiable/delta.py new file mode 100644 index 0000000..20a5b00 --- /dev/null +++ b/idea81_audioledger_studio_verifiable/delta.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import Any, Dict + + +class DeltaStore: + def __init__(self) -> None: + self.last_full_state: Dict[str, Any] = {} + + def compute_delta(self, current_state: Dict[str, Any], previous_state: Dict[str, Any]) -> Dict[str, Any]: + delta: Dict[str, Any] = {} + # naive shallow diff + keys = set(current_state.keys()) | set(previous_state.keys()) + for k in keys: + cur = current_state.get(k) + prev = previous_state.get(k) + if cur != prev: + delta[k] = cur + return delta + + def apply_delta(self, state: Dict[str, Any], delta: Dict[str, Any]) -> Dict[str, Any]: + new_state = dict(state) + for k, v in delta.items(): + new_state[k] = v + return new_state diff --git a/idea81_audioledger_studio_verifiable/registry.py b/idea81_audioledger_studio_verifiable/registry.py new file mode 100644 index 0000000..296c767 --- /dev/null +++ b/idea81_audioledger_studio_verifiable/registry.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from typing import Any, Dict + + +class GraphRegistry: + def __init__(self) -> None: + self._contracts: Dict[str, Any] = {} + + def register_contract(self, name: str, contract: Any) -> None: + self._contracts[name] = contract + + def get_contract(self, name: str) -> Any | None: + return self._contracts.get(name) + + def list_contracts(self) -> Dict[str, Any]: + return dict(self._contracts) diff --git a/idea81_audioledger_studio_verifiable/runtime.py b/idea81_audioledger_studio_verifiable/runtime.py new file mode 100644 index 0000000..99265de --- /dev/null +++ b/idea81_audioledger_studio_verifiable/runtime.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Any + + +@dataclass +class Node: + id: str + action: str + inputs: Dict[str, Any] = field(default_factory=dict) + outputs: Dict[str, Any] = field(default_factory=dict) + + +class ExecutionGraph: + def __init__(self) -> None: + self.nodes: Dict[str, Node] = {} + self.edges = [] # list of (src, dst) + + def add_node(self, node: Node) -> None: + self.nodes[node.id] = node + + def add_edge(self, src_id: str, dst_id: str) -> None: + self.edges.append((src_id, dst_id)) + + def compile_from_ir(self, ir: Dict[str, Any]) -> None: + # Minimal compilation: create a single allocation node if assets present + assets = getattr(ir.get("objects"), "assets", {}) if isinstance(ir, dict) else {} + total = sum(assets.values()) if isinstance(assets, dict) else 0.0 + self.nodes["allocate"] = Node(id="allocate", action="allocate", inputs={"assets_total": total}) + + def run(self, assets: Dict[str, float], target_total: float) -> Dict[str, float]: + # Simple deterministic allocation: scale assets to meet target_total while preserving ratios + if not assets: + return {} + total = sum(assets.values()) + if total == 0: + return {k: 0.0 for k in assets} + scale = target_total / total if target_total is not None else 1.0 + return {k: v * scale for k, v in assets.items()} diff --git a/idea81_audioledger_studio_verifiable/signer.py b/idea81_audioledger_studio_verifiable/signer.py new file mode 100644 index 0000000..11aa0fb --- /dev/null +++ b/idea81_audioledger_studio_verifiable/signer.py @@ -0,0 +1,20 @@ +import time +import hmac +import hashlib +from dataclasses import dataclass +from typing import Any + + +@dataclass +class AuditLogEntry: + timestamp: float + action: str + details: str + + +class Signer: + def __init__(self, key: str) -> None: + self.key = key.encode("utf-8") + + def sign(self, message: str) -> str: + return hmac.new(self.key, message.encode("utf-8"), hashlib.sha256).hexdigest() diff --git a/idea81_audioledger_studio_verifiable/sonifier.py b/idea81_audioledger_studio_verifiable/sonifier.py new file mode 100644 index 0000000..bda34ce --- /dev/null +++ b/idea81_audioledger_studio_verifiable/sonifier.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import List, Dict, Any + + +class Sonifier: + def map_to_cues(self, risk: float, allocation: Dict[str, float], constraints: Dict[str, Any] | None = None) -> List[Dict[str, Any]]: + cues: List[Dict[str, Any]] = [] + # Simple mapping: higher risk -> alert cue, otherwise steady cue per asset + base_note = 60 # MIDI note C4 + if risk > 0.6: + cues.append({"note": base_note + 12, "duration": 0.5, "type": "alert", "message": "High risk"}) + for i, (asset, val) in enumerate(allocation.items()): + note = base_note + i + cues.append({"note": note, "duration": 0.25, "type": "beat", "asset": asset, "value": val}) + return cues diff --git a/ideas.md b/ideas.md new file mode 100644 index 0000000..f89a287 --- /dev/null +++ b/ideas.md @@ -0,0 +1,2 @@ +This repository is a multi-component Python prototype for AudioLedger Studio. +It demonstrates a minimal but production-minded architecture (DSL, IR, runtime, delta-sync, signing, registry, adapters, and sonification) suitable for further expansion. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a82fe39 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "idea81_audioledger_studio_verifiable" +version = "0.1.0" +description = "AudioLedger Studio: Verifiable Algebraic Market Scenarios with Sonified Governance" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "MIT"} +authors = [{name = "OpenCode AI", email = "noreply@example.com"}] +dependencies = ["pytest"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["idea81_audioledger_studio_verifiable"] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..f5a9f01 --- /dev/null +++ b/test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Running tests..." +pytest -q + +echo "Building package (python -m build)..." +python3 -m build + +echo "All tests passed and package built." diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9d087ea --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests package init.""" diff --git a/tests/test_delta.py b/tests/test_delta.py new file mode 100644 index 0000000..fb4820f --- /dev/null +++ b/tests/test_delta.py @@ -0,0 +1,11 @@ +from idea81_audioledger_studio_verifiable.delta import DeltaStore + + +def test_delta_and_apply(): + ds = DeltaStore() + prev = {"A": 1, "B": 2} + curr = {"A": 1, "B": 3} + delta = ds.compute_delta(curr, prev) + assert delta == {"B": 3} + applied = ds.apply_delta(prev, delta) + assert applied == curr diff --git a/tests/test_dsl.py b/tests/test_dsl.py new file mode 100644 index 0000000..154a081 --- /dev/null +++ b/tests/test_dsl.py @@ -0,0 +1,21 @@ +import math +from idea81_audioledger_studio_verifiable.core import parse_dsl, LocalProblemIR + + +def test_parse_dsl_basic_assets_and_risk(): + text = """ +assets: + AAPL: 100 + BTC: 2 +objectives: + maximize_return: true +risk_budgets: + CVaR: 0.05 +""" + ir = parse_dsl(text) + assert "objects" in ir + obj: LocalProblemIR = ir["objects"] # type: ignore + assert isinstance(obj, LocalProblemIR) + assert obj.assets["AAPL"] == 100 + assert obj.assets["BTC"] == 2 + assert obj.risk_budgets.get("CVaR") == 0.05 diff --git a/tests/test_registry.py b/tests/test_registry.py new file mode 100644 index 0000000..a9844e2 --- /dev/null +++ b/tests/test_registry.py @@ -0,0 +1,11 @@ +from idea81_audioledger_studio_verifiable.registry import GraphRegistry +from idea81_audioledger_studio_verifiable.adapters import PriceFeedAdapter + + +def test_registry_and_adapter(): + reg = GraphRegistry() + adapter = PriceFeedAdapter({"AAPL": 170.0}) + reg.register_contract("price-feed", adapter) + got = reg.get_contract("price-feed") + assert isinstance(got, PriceFeedAdapter) + assert got.get_prices()["AAPL"] == 170.0 diff --git a/tests/test_runtime.py b/tests/test_runtime.py new file mode 100644 index 0000000..a2cecb4 --- /dev/null +++ b/tests/test_runtime.py @@ -0,0 +1,12 @@ +from idea81_audioledger_studio_verifiable.runtime import ExecutionGraph + + +def test_runtime_allocation_scaling(): + g = ExecutionGraph() + assets = {"A": 100.0, "B": 300.0} + g.compile_from_ir({"objects": type("O", (), {"assets": assets})()}) + result = g.run(assets, target_total=400.0) + # Ratios preserved, sum equals target + assert abs(sum(result.values()) - 400.0) < 1e-6 + assert result["A"] == 100.0 * (400.0 / 400.0) + assert result["B"] == 300.0 * (400.0 / 400.0) diff --git a/tests/test_signer.py b/tests/test_signer.py new file mode 100644 index 0000000..fa9d1b6 --- /dev/null +++ b/tests/test_signer.py @@ -0,0 +1,7 @@ +from idea81_audioledger_studio_verifiable.signer import Signer + + +def test_signer_basic(): + signer = Signer("super-secret-key") + sig = signer.sign("audit-log-entry") + assert isinstance(sig, str) and len(sig) >= 64 diff --git a/tests/test_sonifier.py b/tests/test_sonifier.py new file mode 100644 index 0000000..cf763ad --- /dev/null +++ b/tests/test_sonifier.py @@ -0,0 +1,8 @@ +from idea81_audioledger_studio_verifiable.sonifier import Sonifier + + +def test_sonifier_basic(): + s = Sonifier() + cues = s.map_to_cues(0.75, {"A": 10.0, "B": 20.0}) + assert isinstance(cues, list) + assert len(cues) >= 1