---
description: The locked technical defaults, the boundaries that are not negotiable, and the four decisions that reversed an earlier one — with the evidence that reversed them.
---

# Design decisions

What was chosen, and what was chosen *instead*. Decisions that reversed an
earlier one are kept here with their evidence, because a reversal without its
reason tends to get re-reversed.

## Locked defaults

| Area | Choice |
|---|---|
| CLI | Typer |
| Output | Rich, confined to `ui/` |
| Config | pydantic v2, path-tagged errors |
| State | SQLite only, WAL, atomic claim |
| Remote data | declarative rsync/SSH staging, behind a transport seam |
| Packaging | src-layout, hatchling |
| License | MIT |
| Quality | ruff + mypy (strict) + pytest |

These are settled. Changing one is a project-level decision, not a refactor.

## Boundaries that are not negotiable

**Clean-room.** MatMelt shares no code with the production pipeline it mirrors
architecturally. Nothing is imported or copied from that tree. The **amendment**
worth knowing: the boundary is about *code*. `aiida-core`, `aiida-mydftbp` and
`ase` are declared user-owned runtime requirements of the optional `calculate`
stage — lazily imported inside the runner, never at package level, never in the
dev environment, with every AiiDA-touching test faking the modules.

**The base install stays light.** `pip install matmelt` must not drag in torch,
and the package never bundles a model. The science stack is the user's, in the
user's environment. What MatMelt owns is the automation *around* it: each stage
declares its requirements, `doctor` and `run` report exactly what is missing and
the exact command to fix it, and a model download is always disclosed and
consented to — never silent, never a silent failure.

**No secrets in git, ever.** Any config value may be an `env:NAME` reference;
`health_check` never leaks one into its detail.

**No new dependencies without asking.** The runtime list is four packages
(typer, rich, pydantic, pyyaml) and it is meant to stay short.

## Reversals, with their evidence

**Shared filesystem → declarative staging.** The original design assumed a
remote saw the same paths over NFS. Inverted before Phase 4: staging is the
default and `shared_filesystem: true` is the opt-in. A shared filesystem is a
property of *some* clusters, and assuming it excludes every machine that is
merely reachable by SSH.

**Mid-funnel `stop_after`: retire → rest.** Originally a stopped run marked its
materials `DONE`. That retired 550 real materials into a state no later run
could claim, and continuing the study required editing the database by hand. Now
materials rest at the stop stage's output state, and only a run through the
configured last stage retires anything
({doc}`../explanation/funnel-and-stop-after`).

**`calculate`: deferred → optional stage.** Higher-fidelity calculation was
first deferred past v1, then added as a *skippable* stage rather than a required
one. Configs written before it existed behave identically today, which is the
property that made adding it safe.

**Compose sizes: binaries → all sizes.** `combination_sizes` defaulted to `[2]`.
Omitting it now means every size from 1 to the element count, and size 1 is
legal — which is what makes elemental allotropes reachable.

**`sphinx-click` → `sphinxcontrib-typer`.** Typer 0.20+ vendors its own copy of
click, so `sphinx-click`'s isinstance checks all fail and its directive emits an
**empty section without raising** — a hole `-W` cannot catch. Documented in
`docs/conf.py` so it is not re-adopted.

## Decisions that look odd until explained

**Threads are not used in core.** Parallelism is at the process level: jobs are
OS subprocesses, so one bad calculation cannot take the tool down, and the loop
stays single-threaded and debuggable. Concurrency is added where a need is
demonstrated, never speculatively.

**Runners duplicate a little logic rather than import a shared module.** They
must be self-contained to be shippable and hand-runnable, and that is worth more
than deduplicating a formula parser.

**Physical-sanity bounds are constants, not options.** Configurable bounds make
verdicts incomparable across a database, which defeats screening at scale
({doc}`../explanation/the-science`).

**`_mcli` survived the rename.** The project became MatMelt, but the data
namespace in every `record.json` and CIF did not, because renaming it would
break every record already produced and every downstream consumer. One
deliberate inconsistency, documented rather than tidied.

**The version lives in two places.** `pyproject.toml` and `__init__.py` — and
they must be bumped together, because `--version` reads the second while the
release guard checks the first. A stale `__init__` once shipped a package
misreporting its own version. Deriving it from installed metadata is the obvious
follow-up.

## Related

- {doc}`../explanation/index` — the reasoning behind the architecture itself.
- {doc}`contributing` — working within these decisions.
