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