build(agent): melter#14fd4b iteration
This commit is contained in:
parent
d83adbf41e
commit
170a2b6d37
|
|
@ -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,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.
|
||||||
31
README.md
31
README.md
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .harness import compute_progress_frame
|
||||||
|
|
||||||
|
__all__ = ["compute_progress_frame"]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import risk_literacy, portfolio
|
||||||
|
|
||||||
|
__all__ = ["risk_literacy", "portfolio"]
|
||||||
|
|
@ -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.",
|
||||||
|
}
|
||||||
|
|
@ -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?",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .market import simulate_price_series
|
||||||
|
|
||||||
|
__all__ = ["simulate_price_series"]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .ledger import Ledger
|
||||||
|
|
||||||
|
__all__ = ["Ledger"]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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"] == []
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue