10 KiB
AGENTS.md — HeurAMS Codebase Guide
Project Overview
HeurAMS (潜进, v0.5.0 "fulcrum/支点") is a Heuristic Auxiliary Memory Scheduler — an open spaced-repetition scheduling platform. Licensed AGPL-3.0. Written in Python 3.10+.
The project uses a physics-inspired data model: nucleon (memory content) + electron (algorithm state) + orbital (review strategy) = atom (runtime memory unit).
Essential Commands
# Setup (uv recommended)
uv sync # Install all deps & dev env
python3 -m pip install -e . # Native pip alternative
# Run
uv run heurams # Verify CLI (just prints help)
uv run tui # Launch Textual TUI
python3 -m heurams.interface # Native pip alternative for TUI
# Formatting
black . --workers=1 # Python (single-thread flag needed)
autoflake --in-place --remove-all-unused-imports --recursive ./src/ --exclude __init__.py
mdformat --number . # Markdown
# Tests (pytest, 128 tests across 9 test files as of v0.5.0)
```bash
uv run pytest # Run all tests
uv run pytest -v --tb=short # Verbose, short tracebacks
uv run pytest tests/test_sm2.py # Single test file
uv run pytest -k "test_interval" # Filter by name
uv run pytest --cov=heurams --cov-report=term # Coverage report (via pytest-cov)
Test structure:
tests/conftest.py— Shared fixtures:timer_context(deterministic time overrides),sample_algodata_sm2,sample_algodata_nsp0tests/test_textproc.py—truncate,domize,undomizetests/test_hasher.py—get_md5,hashtests/test_epath.py—epathnested dict/list read/write accesstests/test_lict.py—Lictcollection (list+dict hybrid, dirty-sync)tests/test_evalizor.py—Evalizereval-based template systemtests/test_base_algorithm.py—BaseAlgorithmdefaults, methods, integritytests/test_sm2.py— SM-2: defaults, revisor (efactor/rept/interval logic), is_due, datestests/test_nsp0.py— NSP-0: defaults, revisor (important/interval), is_duetests/test_electron.py—Electron: init, activation, revisor delegation, dict/list access,from_data
## Code Organization
src/heurams/ ├── init.py, main.py, context.py # Entry point, global ContextVar config ├── interface/ — Textual TUI (screens/, widgets/, css/) ├── kernel/ — Core logic │ ├── algorithms/ — SM-2, NSP-0, SM-15M, FSRS (stub) │ ├── particles/ — Atom, Electron, Nucleon, Orbital data models │ ├── puzzles/ — MCQ, Cloze, Recognition puzzle generators │ ├── reactor/ — Router → Procession → Expander state machines │ ├── repolib/ — Repo class (loads/saves repos from TOML/JSON dirs) │ └── auxiliary/ — Evalizer (eval-based template), Lict (dict+list hybrid) ├── services/ — Config, logger, timer, audio, TTS, favorites, attic, hasher, epath ├── providers/ — Pluggable drivers (TTS: edgetts/base; Audio: playsound/termux) ├── unifront/ — Unified frontend session (mostly stub) └── tools/ — csv2payload, zmqclient
Outside `src/`:
- `data/config/` — Hierarchical TOML config (see below)
- `data/repo/` — Memory repos (one TOML dir per repo set)
- `tests/` — Empty, no test infrastructure yet
## Architecture & Data Flow
### Layers (bottom-up)
1. **Kernel** — Algorithms, data models, puzzle generators, scheduling reactor
2. **Services** — Config, logging, timer, TTS, audio, favorites, persistence
3. **Providers** — Swappable backends (EdgeTTS, playsound, OpenAI LLM stub)
4. **Interface** — Textual TUI (Textual 8.x, CSS-based styling in `.tcss` files)
5. **Unifront** — Abstract frontend session (unimplemented)
### Review Flow
Router (global state machine) └─ contains → Procession(s) — one per phase (quick_review → recognition → final_review) └─ contains → Expander (per atom) — expands puzzle schedule into individual puzzle widgets └─ wraps → Puzzle widget (MCQ, Cloze, Recognition)
States managed via the `transitions` library (states defined in `states.py`).
## Key Patterns & Conventions
### Logging
Every module creates its own logger with `get_logger(__name__)`. Log files rotate at 10MB, up to 5 backups. Logs append to `heurams.log` in the working directory.
### Config System (`ConfigDict`)
Singleton per path. Lazy-loads TOML files on access. Directory convention:
- `_.toml` files = default values for that directory level (merged into parent)
- Files named `*.toml` = lazy-loaded on key access
- Directories = recursive sub-configs
- Access config via `config_var.get()["section"]["key"]`
### Context Management
```python
from heurams.context import config_var, ConfigContext, rootdir, workdir
config_var.get() # Current config (thread-safe)
with ConfigContext(test_config_provider): # Scoped config override
...
State Machines (transitions library)
Three finite state machines with explicit state enums in states.py:
- RouterState: unsure → quick_review → recognition → final_review → finished
- ProcessionState: active → finished
- ExpanderState: exammode → retronly
Physics Metaphor Data Model
- Nucleon: Read-only content container. Wraps payload + typedef via
Evalizer(eval-based template system). Created from(ident, (payload, common))tuples. - Electron: Algorithm-specific memory state. Wraps algodata dict with
BaseAlgorithminterface. Created from(ident, algodata, algo_name)tuples. - Orbital: Review strategy dict defining puzzle probabilities per phase.
- Atom: Runtime assembly of nucleon + electron + orbital + runtime flags. The primary object the UI works with.
Lict Collection
A MutableSequence subclass that maintains both list-like and dict-like access simultaneously. Used extensively for repo data. Appends are (key, value) tuples — keys must be unique strings. Supports lazy sync between internal list and dict representations.
Evalizer Template System
Recursively traverses data structures, evaluating strings prefixed with eval: via Python's eval() in a controlled namespace. Used in nucleon initialization. TODO/warning: noted for being risk-prone.
Repo (Memory Repository)
A directory of TOML/JSON files:
manifest.toml— title, author, package name, descriptiontypedef.toml— Common metadata, puzzle definitions, annotationspayload.toml— Individual memory items (keyed by ident)algodata.json— Algorithm state (keyed by ident, persistent)schedule.toml— Orbital/review schedule definition
Repos are loaded from data/repo/ via Repo.from_repodir(). Created from directory structure, no database required.
Algorithms
| Name | File | Status |
|---|---|---|
| SM-2 | sm2.py |
Working — Classic SuperMemo 1987 |
| NSP-0 | nsp0.py |
Working — Non-spaced filtering scheduler |
| SM-15M | sm15m.py + sm15m_calc.py |
Working — Ported from CoffeeScript SM-15 |
| FSRS | fsrs.py |
Stub only — "尚未实现" |
| Base | base.py |
Abstract base with defaults |
All algorithms are class-method-based (@classmethod), registered in algorithms/__init__.py dict.
Services
- config.py —
ConfigDict(UserDict)singleton, TOML lazy loader - logger.py —
get_logger(name)→ hierarchical loggers underheurams.* - timer.py —
get_daystamp()/get_timestamp()with configurable overrides - epath.py — Dot-notation access to nested dicts (
epath(dct, "a.b.c")) - attic.py — Struct-like pickle persistence (per-ident, supports
<DAYSTAMP>/<TIMESTAMP>placeholders) - hasher.py — MD5 hashing
- textproc.py —
truncate(),domize(),undomize() - audio_service.py — Routes through configured audio provider
- tts_service.py — Routes through configured TTS provider
- favorite_service.py — Favorite manager with JSON5 persistence (singleton)
- exceptions.py —
WTFException - version.py —
ver = "0.5.0",stage = "prototype",codename = "fulcrum"
Important Gotchas
- FSRS is not implemented —
fsrs.pyis just alogger.info("尚未实现")stub. - No tests — The
tests/directory is completely empty. There is no pytest config or test runner. Any code added should establish test infrastructure. black --workers=1— The multi-threaded worker flag has compatibility issues on some platforms.autoflakemust exclude__init__.py— Unused-import removal breaks__init__.pyre-exports.- ConfigDict is a singleton per path — Creating
ConfigDictwith the same path returns the same instance. Don't create instances with default dict argument (raisesWTFException). eval()in Evalizer — The template system useseval()under the hood. Marked as high-risk in comments. Any changes should prioritize replacement.- Don't run
interface/__main__.pydirectly — It will misconfigure Python context. Always run viapython -m heurams.interfaceoruv run tui. - No fast-forward merges — Project policy requires non-fast-forward merging only (
git config merge.ff false). id()is avoided for repo lookup — The dashboard explicitly notesid()can be reused, so it usesrepolinkdict keyed by package name string.- Termux support — There's a separate audio provider for Termux; the project maintains pip-compatibility specifically for Termux (where uv doesn't work well).
- ZMQ debug server — Optional debug feature that opens a REP socket for remote code execution via pickle. Disabled by default.
- Commit messages — Follow Conventional Commits spec. Written in Chinese or English.
Provider System
Providers are swappable implementations registered in __init__.py dicts:
- TTS:
edgetts(Microsoft Edge TTS),basetts(stub) - Audio:
playsound(cross-platform),termux(Android Termux) - LLM:
openai(OpenAI-compatible API), no LLM provider is fully implemented yet
Selection is via data/config/services/*.toml (e.g., provider = "edgetts").
Dependencies (pyproject.toml)
Core: psutil, tabulate, textual>=8.2.3, toml, transitions, zmq
Optional (in requirements.txt, commented out in pyproject.toml): edge-tts, jieba, openai, playsound