Contributing#

Setup#

$ git clone https://github.com/Quantum-ARISE-Acad/qa-materials-cli
$ cd qa-materials-cli
$ python -m venv .venv && .venv/bin/pip install -e ".[dev]"
$ .venv/bin/matmelt --help

Do not install the science stack in the dev environment. That is not an oversight — the entire test suite must pass without SMACT, PyXtal, MACE or AiiDA, because that is what proves the base install is light and the lazy imports are really lazy. The science-dependent tests skip cleanly when the packages are absent, and the AiiDA-touching tests fake the modules outright.

The documentation toolchain lives in its own environment so it can never leak into a user install:

$ python -m venv .venv-docs
$ .venv-docs/bin/pip install --upgrade pip     # --group needs pip >= 25.1
$ .venv-docs/bin/pip install --group docs -e .

The check battery#

Every change ends with all four green:

$ .venv/bin/ruff check src tests docs/diagrams
$ .venv/bin/ruff format --check src tests docs/diagrams
$ .venv/bin/mypy src
$ .venv/bin/pytest

and, for any docs change:

$ .venv-docs/bin/sphinx-build -W -b html docs docs/_build/html

-W turns warnings into errors on purpose: a broken cross-reference or an unparseable docstring must fail rather than land quietly on the site.

A change in behaviour ships with the test that proves it. CI runs the same checks, plus a security job (bandit, pip-audit) and a build job that installs the wheel in a clean environment and runs the CLI from outside the source tree.

How the tests are organised#

tests/unit/ mirrors src/, and tests/integration/ holds the end-to-end proofs. Four files are worth knowing before you change anything:

File

Why it matters

conftest.py

FakeBackend and the shared fixtures — the reason stages are testable with no science

test_backend_contract.py

The ten conformance checks every backend must pass

integration/test_e2e_local.py

The whole tool below the CLI, on fakes only. This stays green.

integration/test_docs_examples.py

Checks the published docs against the real code

The last one runs in both directions: every documented command, flag and config key must exist, and every config field, stage option, backend option and material state must be documented. A new option fails a test until reference/config.md covers it. That is intentional — it is cheaper than discovering an undocumented option a year later.

FakeBackend fabricates each declared output, which is what lets a stage’s full lifecycle run without compute. If you add a stage, its tests almost certainly belong on that rail rather than on real science.

Adding things#

A stage. Write <name>_runner.py first — standard library plus its science package, --help-runnable by hand, exiting 3 with the exact pip install command when its package is missing. Then the thin Stage class, then register, FUNNEL_ORDER and STAGE_OUTPUT_STATE. An AST guard will fail if the runner imports matmelt. See The two contracts.

A backend. Subclass ComputeBackend, implement the six methods, register the kind, and add one branch to the conformance fixture.

A config key. config/schema.py, then reference/config.md.

A diagram. Scripts live in docs/diagrams/; the palette is encoded once in _palette.py so every diagram shares one vocabulary.

$ python docs/diagrams/render.py            # regenerate .dot and .svg
$ python docs/diagrams/render.py --check    # what CI asks: is anything stale?

Rendered SVGs are committed, so building the site never needs Graphviz. Guards check that the committed artifacts match what the scripts produce now, that both themes exist and differ, and that every page embedding a diagram uses both themes with the diagram’s own alt text.

Conventions#

  • Docstrings are NumPy style, and parameter types are never written in them — they come from the annotations, so type information has one source. Attributes entries do carry types.

  • All output through ui/. No print() in core/.

  • Commands return int, they do not call sys.exit.

  • Optional means unknown, never zero.

  • Don’t add a dependency without asking first.

One upstream trap, recorded in docs/conf.py because it has bitten twice: in a function returning None, do not end a docstring with a Notes section following a multi-line Raises. sphinx-autodoc-typehints misplaces the injected return-type field and the build fails confusingly. Put that content in the extended description instead.