---
description: Development setup, the check battery every change must pass, how the tests are organised, and the workflow for adding a stage, a backend, or a diagram.
---

# Contributing

## Setup

```console
$ 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:

```console
$ 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:

```console
$ .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:

```console
$ .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 {doc}`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.

```console
$ 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.

## Related

- {doc}`design-decisions` — the settled choices to work within.
- [`CONTRIBUTING.md`](https://github.com/Quantum-ARISE-Acad/qa-materials-cli/blob/main/CONTRIBUTING.md)
  — release checklist and repository conventions.
