build(agent): melter#14fd4b iteration

This commit is contained in:
agent-14fd4b738639d573 2026-04-24 17:59:59 +02:00
parent d83adbf41e
commit 170a2b6d37
18 changed files with 387 additions and 2 deletions

21
.gitignore vendored Normal file
View File

@ -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

38
AGENTS.md Normal file
View File

@ -0,0 +1,38 @@
AGENTS.md
========
Repository purpose
------------------
This repo hosts the InvestLearn Studio prototype: a verifiable, offline-first investment education & practice engine. The code here is intentionally small but structured to be extended by additional agents in the swarm.
Architecture & Tech Stack
------------------------
- Language: Python 3.8+
- Packaging: pyproject.toml (setuptools backend)
- Key packages: pandas (analytics), pytest (tests)
- Source layout: `src/idea128_investlearn_studio_verifiable`
Key components
--------------
- dsl.py: simple DSL parser for learner objectives
- modules/: learning modules (risk_literacy, portfolio)
- simulation/: market simulation engine
- sync/: lightweight cryptographic ledger for attestations
- analytics/: toy analytics harness using pandas
Testing & Commands
------------------
Run the full verification and packaging pipeline locally:
```
./test.sh
```
What agents must follow
-----------------------
1. Read this file before changing packaging or tests. 2. Do not remove `test.sh` or change its behavior without updating the CI maintainers. 3. When adding dependencies, update `pyproject.toml` and ensure tests still run quickly. 4. Keep changes minimal and localized. 5. If you're adding new data files, update MANIFEST or package_data accordingly.

View File

@ -1,3 +1,30 @@
# idea128-investlearn-studio-verifiable
InvestLearn Studio (prototype)
================================
Source logic for Idea #128
InvestLearn Studio is an offline-first, verifiable learning engine for investment education.
This repository contains a focused, well-tested chunk of functionality for the MVP:
- A small DSL parser to declare learning objectives and preferences
- Two learning modules: risk literacy and basic portfolio concepts
- A deterministic market simulation engine (seeded RNG)
- A tiny verifiable ledger to attest to completed modules and quiz results
- A toy analytics harness using pandas
Usage
-----
Run tests and build the package:
```
./test.sh
```
Project structure
-----------------
- src/idea128_investlearn_studio_verifiable: core package
- tests/: pytest tests
- AGENTS.md: contribution and architecture guidance for agents
License: MIT (prototype)

17
pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "idea128_investlearn_studio_verifiable"
version = "0.1.0"
description = "InvestLearn Studio: Verifiable, Offline-First Investment Education & Practice Engine (prototype)"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"pandas>=1.3.0",
"pytest>=6.0",
]
[tool.setuptools.packages.find]
where = ["src"]

View File

@ -0,0 +1,17 @@
"""InvestLearn Studio core package
This package contains a minimal prototype of the DSL, learning modules,
simulation engine, ledger, and analytics harness.
"""
__all__ = [
"dsl",
"modules",
"simulation",
"sync",
"analytics",
]
from . import dsl, modules, simulation, sync, analytics
__version__ = "0.1.0"

View File

@ -0,0 +1,3 @@
from .harness import compute_progress_frame
__all__ = ["compute_progress_frame"]

View File

@ -0,0 +1,16 @@
"""Toy analytics harness using pandas.
compute_progress_frame takes a list of module instances and returns a
pandas.DataFrame summarizing progress per learner/module.
"""
from typing import List, Dict, Any
import pandas as pd
def compute_progress_frame(instances: List[Dict[str, Any]]) -> pd.DataFrame:
# instances: list of dicts { learner_id, module_id, progress }
df = pd.DataFrame(instances)
if df.empty:
return pd.DataFrame(columns=["learner_id", "module_id", "progress"])
grouped = df.groupby(["learner_id", "module_id"]).agg({"progress": "mean"}).reset_index()
return grouped

View File

@ -0,0 +1,58 @@
"""Simple DSL parser for declaring learning objectives.
DSL format (very small subset):
objective <id>:
title = "..."
risk = low|medium|high
steps = ["step1", "step2"]
This parser produces a dict with the declared fields.
"""
from typing import Dict, Any, List
import re
class DSLParseError(Exception):
pass
def parse(dsl_text: str) -> Dict[str, Any]:
lines = [l.strip() for l in dsl_text.strip().splitlines() if l.strip()]
if not lines:
raise DSLParseError("empty DSL")
header = lines[0]
m = re.match(r"objective\s+(\w+):", header)
if not m:
raise DSLParseError("expected 'objective <id>:' header")
obj_id = m.group(1)
result: Dict[str, Any] = {"id": obj_id}
for line in lines[1:]:
if line.startswith("title"):
m = re.match(r'title\s*=\s*"([^"]+)"', line)
if not m:
raise DSLParseError("malformed title")
result["title"] = m.group(1)
elif line.startswith("risk"):
m = re.match(r"risk\s*=\s*(low|medium|high)", line)
if not m:
raise DSLParseError("malformed risk")
result["risk"] = m.group(1)
elif line.startswith("steps"):
m = re.match(r"steps\s*=\s*\[(.*)\]", line)
if not m:
raise DSLParseError("malformed steps")
inner = m.group(1).strip()
if not inner:
result["steps"] = []
else:
parts = [p.strip().strip('"') for p in inner.split(",")]
result["steps"] = parts
else:
# ignore unknown lines for forward compat
continue
return result

View File

@ -0,0 +1,3 @@
from . import risk_literacy, portfolio
__all__ = ["risk_literacy", "portfolio"]

View File

@ -0,0 +1,37 @@
"""Basic portfolio concepts learning module (toy implementation)
"""
from dataclasses import dataclass, field
from typing import List, Dict
@dataclass
class LearningModuleInstance:
id: str
title: str
steps: List[str]
completed_steps: List[str] = field(default_factory=list)
def progress(self) -> float:
if not self.steps:
return 0.0
return len(self.completed_steps) / len(self.steps)
def complete_step(self, step: str):
if step in self.steps and step not in self.completed_steps:
self.completed_steps.append(step)
class LearningModule:
def __init__(self, id: str, title: str, steps: List[str]):
self.id = id
self.title = title
self.steps = steps
def instantiate(self) -> LearningModuleInstance:
return LearningModuleInstance(id=self.id, title=self.title, steps=list(self.steps))
def quiz(self) -> Dict[str, str]:
return {
"q1": "What is an asset allocation?",
"q2": "Name two broad asset classes.",
}

View File

@ -0,0 +1,41 @@
"""Risk literacy learning module (toy implementation)
Exposes a LearningModule class which can be instantiated to produce
LearningModuleInstance objects that track progress and produce a tiny quiz.
"""
from dataclasses import dataclass, field
from typing import List, Dict
@dataclass
class LearningModuleInstance:
id: str
title: str
steps: List[str]
completed_steps: List[str] = field(default_factory=list)
def progress(self) -> float:
if not self.steps:
return 0.0
return len(self.completed_steps) / len(self.steps)
def complete_step(self, step: str):
if step in self.steps and step not in self.completed_steps:
self.completed_steps.append(step)
class LearningModule:
def __init__(self, id: str, title: str, steps: List[str]):
self.id = id
self.title = title
self.steps = steps
def instantiate(self) -> LearningModuleInstance:
return LearningModuleInstance(id=self.id, title=self.title, steps=list(self.steps))
def quiz(self) -> Dict[str, str]:
# Tiny deterministic quiz
return {
"q1": "What is diversification?",
"q2": "Does higher expected return always mean lower risk?",
}

View File

@ -0,0 +1,3 @@
from .market import simulate_price_series
__all__ = ["simulate_price_series"]

View File

@ -0,0 +1,16 @@
"""Tiny deterministic market simulation engine.
simulate_price_series(seed, n_steps, start_price) -> list[float]
"""
from typing import List
import random
def simulate_price_series(seed: int, n_steps: int = 10, start_price: float = 100.0) -> List[float]:
rnd = random.Random(seed)
prices = [float(start_price)]
for _ in range(n_steps - 1):
# small random walk multiplicative returns
ret = rnd.uniform(-0.05, 0.05)
prices.append(prices[-1] * (1 + ret))
return prices

View File

@ -0,0 +1,3 @@
from .ledger import Ledger
__all__ = ["Ledger"]

View File

@ -0,0 +1,33 @@
"""Lightweight append-only ledger with SHA256 chaining for attestations.
This is intentionally tiny: each entry contains a payload and a chain hash
linking to the previous entry so the ledger is tamper-evident in a simple way.
"""
import hashlib
import json
from typing import List, Dict, Any, Optional
class Ledger:
def __init__(self):
self.entries: List[Dict[str, Any]] = []
def _chain_hash(self, prev_hash: Optional[str], payload: Dict[str, Any]) -> str:
data = json.dumps({"prev": prev_hash, "payload": payload}, sort_keys=True)
return hashlib.sha256(data.encode("utf-8")).hexdigest()
def append(self, payload: Dict[str, Any]) -> Dict[str, Any]:
prev = self.entries[-1]["hash"] if self.entries else None
h = self._chain_hash(prev, payload)
entry = {"payload": payload, "prev": prev, "hash": h}
self.entries.append(entry)
return entry
def verify(self) -> bool:
prev = None
for e in self.entries:
expected = self._chain_hash(prev, e["payload"]) # type: ignore[arg-type]
if expected != e["hash"]:
return False
prev = e["hash"]
return True

11
test.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Running tests..."
# Ensure src/ is on PYTHONPATH so tests can import the package in-place
PYTHONPATH="src" pytest -q
echo "Building package..."
python3 -m build
echo "All done."

26
tests/test_dsl.py Normal file
View File

@ -0,0 +1,26 @@
from idea128_investlearn_studio_verifiable import dsl
def test_parse_basic_objective():
text = '''
objective o1:
title = "Intro to Risk"
risk = low
steps = ["what is risk", "diversification"]
'''
parsed = dsl.parse(text)
assert parsed["id"] == "o1"
assert parsed["title"] == "Intro to Risk"
assert parsed["risk"] == "low"
assert parsed["steps"] == ["what is risk", "diversification"]
def test_parse_empty_steps():
text = '''
objective x:
title = "Empty"
risk = medium
steps = []
'''
parsed = dsl.parse(text)
assert parsed["steps"] == []

View File

@ -0,0 +1,15 @@
from idea128_investlearn_studio_verifiable import simulation, sync
def test_simulation_deterministic():
a = simulation.simulate_price_series(seed=42, n_steps=5, start_price=100.0)
b = simulation.simulate_price_series(seed=42, n_steps=5, start_price=100.0)
assert a == b
assert len(a) == 5
def test_ledger_append_and_verify():
ledger = sync.Ledger()
ledger.append({"event": "module_completed", "module": "risk"})
ledger.append({"event": "quiz_passed", "module": "portfolio", "score": 0.8})
assert ledger.verify()