Module map#
src/matmelt/
├── cli.py __main__.py Typer app — a thin dispatcher, no logic
├── commands/ one module per command, each run_<name>() -> int
│ init doctor run status monitor results retry cif update
├── core/
│ ├── jobspec.py JobSpec / Handle / JobResult / Resources / …
│ ├── stages/ the funnel
│ │ ├── base.py the Stage contract
│ │ ├── registry.py FUNNEL_ORDER, OPTIONAL_STAGES, STAGE_OUTPUT_STATE
│ │ ├── compose.py structure.py screen.py calculate.py extract.py
│ │ └── *_runner.py self-contained scripts, shipped per job
│ ├── orchestrator.py the pass loop
│ ├── state.py MaterialState + StateStore (SQLite)
│ ├── preflight.py dependency / backend / model / AiiDA probes
│ ├── monitor.py build_snapshot — pure, no Rich, no SQL
│ ├── cpusample.py /proc + nvidia-smi sampling, best-effort
│ ├── cif.py stdlib .gen → CIF writer
│ └── seeds.py element_combinations
├── backends/ base.py registry.py local.py ssh_exec.py
├── config/ schema.py loader.py
└── ui/ console.py monitor.py monitor_live.py
The dependency rule#
Arrows point inward, and there is exactly one direction:
cli.py → commands/ → core/ , backends/ , config/
↑
ui/ (rendering only; core never imports it)
core/ never imports commands/, cli.py or ui/. That is what lets the
orchestrator, the stages and the state store be tested without a terminal, and
what keeps a rendering change from being able to break a pipeline.
All human-facing output goes through ui/. No print() in core/, no Rich
console constructed anywhere else. The point is not tidiness: it is that
--json output stays parseable because nothing else can write to stdout behind
its back. Deprecation notices and progress go to a separate stderr console for
the same reason.
What belongs where#
cli.py wires Typer options to run_<command>() functions and returns
their exit codes. When it starts containing logic, that logic belongs in
commands/.
commands/ is the only layer allowed to know about both config and core. A
command loads config, builds a runtime, calls into core, and hands values to
ui/ for display. Each returns an int — the exit code — rather than calling
sys.exit, which is what makes commands testable as functions.
core/ is the tool. It takes plain objects: the orchestrator receives a list
of stages and a {stage_name: backend} mapping, and has no idea a YAML file
exists. config/loader.py is the seam that turns a file into those objects.
core/stages/*_runner.py are not library modules. They are programs, staged
to a backend and executed there, and they import the standard library plus their
own science package — never matmelt. A test parses each runner’s imports and
fails if that is violated, because the property is invisible until a remote job
breaks.
backends/ knows how to run an argv somewhere and bring files back. It
knows nothing about materials, stages or science.
ui/ turns values into Rich renderables. It computes nothing: core/monitor.py
builds the snapshot, ui/monitor.py draws it.
Where a change usually goes#
Change |
Where |
|---|---|
New CLI flag |
|
New stage option |
that stage’s |
New science step |
a new |
New compute target |
a |
New config key |
|
New displayed value |
|