smelt
Static architecture guardrails for Python. Describe your features, layers and roles in
smelt.yaml; smelt check reports every import and construct that breaks them, with the
exact location, the allowed alternative and a hint on how to fix it.
Installation
uv tool install smelt-cli
# or install into your project's environment:
pip install smelt-cli
The PyPI package is named smelt-cli; the CLI command and Python package are both
named smelt. To run without installing, use uvx --from smelt-cli smelt check.
Usage
smelt check # whole project, text output
smelt check --changed # only files changed against HEAD (incl. untracked)
smelt check --changed --base origin/main --format json
smelt context voice # architecture briefing for a feature or path
smelt explain SMT101 # rationale, examples and config knobs of a rule
smelt rules # all rules with defaults
smelt debt # record today's violations as known debt
smelt debt --prune # drop debt entries that were fixed
smelt init # draft a config from packages or uv workspace members
smelt debt lets an existing project adopt smelt incrementally: with
findings.debt: .smelt/debt.json in smelt.yaml, smelt check only fails on new violations, and SMT903 reports entries that
were fixed and can leave the file.
Exit codes: 0 clean, 1 violations at or above --fail-on, 2 config or usage error.
Configuration
smelt.yaml has one section per question:
| Section | Answers |
|---|---|
project |
Where is the code? (source_roots, test_roots; packages are discovered) |
architecture |
What shape should it have? (features, layers, shared, composition root, cross-feature relationships, cycles, roles) |
conventions |
How is code named, placed and tested? (naming, packages, tests) |
integrations |
Where does a framework get special treatment? (dependency_injection) |
analysis |
How does smelt read the code? (imports, types) |
rules |
How loud is a finding? Severities only, by rule name or code |
findings |
How are existing findings handled? (debt, ignore, suppressions) |
smelt config show prints the resolved config with all defaults; docs/configuration.md explains the layout.
For a uv workspace, run smelt init at the workspace root. It reads
tool.uv.workspace.members, finds each member's source and test roots, and drafts one
configuration for all packages. Review the generated policy before adopting its findings:
feature/layer boundaries are inferred, not a declaration of your intended architecture.
For example, feature-local DI providers can be allowed to use the DI framework while
remaining in their original feature and layer:
architecture:
features: {root: backend.features}
composition_root: [backend.main, backend.lifespan]
cross_feature:
default: deny
allow:
- from: session.presentation
to: auth.presentation
integrations:
dependency_injection:
frameworks: [dishka] # always allowed in the composition root
allowed_in: ["backend.features.*.infrastructure.di"]
The object form permits only that directional feature/layer relationship. The shorter
"presentation -> presentation" form remains available when a global layer-pair
exception is intended. allowed_in entries are dotted module patterns (* is one
segment, ** any number) and also cover their submodules; unlike the composition root,
those modules keep all feature and layer rules.
For an architecture-first adoption, smelt check --select SMT1,SMT3 focuses on
dependency and structure findings. To silence noisier testing rules persistently, use
severity overrides such as rules: {private-access: off, interaction-assertion: off} (rule names or codes) and review them later.
Silence a single finding inline, always with a reason:
from gateway.infra.sql import Repo # smelt: ignore[SMT101] -- migration tracked in #123
With conventions.tests.layout: mirror, every test file must mirror a source module by its path:
tests/billing/test_invoice.py needs app/billing/invoice.py, and a package test
tests/billing/test_billing.py needs app/billing/. Not every module needs a test, but a
test whose source is missing or elsewhere is an error. Deliberately unmirrored tests go in
conventions.tests.unmirrored (e.g. ["tests/integration/**"]);
conventions.tests.mirror_suffixes: true also allows test_invoice_<topic>.py.
conventions.tests.mirror sets the convention relative to the test
root: the default {path}/test_{module}.py drops the root package, {root}/{path}/test_{module}.py
keeps it, and unit/{path}/{module}_test.py puts tests under tests/unit/ with a suffix.
Every rule has a page under docs/rules, and smelt.schema.json gives editors
autocompletion for smelt.yaml.
Python versions
Smelt runs on Python 3.12 to 3.14 and parses your code with the Python it runs on. Code
that uses newer syntax (3.14's except A, B: or t-strings, 3.13's type parameter defaults)
needs smelt on that version, e.g. uvx -p 3.14 --from smelt-cli smelt check; the syntax error says so when
requires-python or .python-version targets a newer Python.
Optional type information
Role detection is nominal by default: a class is an adapter when it inherits a port. With
analysis:
types: pyright # needs pyright on PATH; pyright_command overrides how it is run
Smelt also asks pyright whether a class satisfies a port structurally, so a duck-typed adapter is found too. It is never required: without it, every rule still runs.
Using Smelt with coding agents
Add this to your AGENTS.md or CLAUDE.md:
Before implementing, run `smelt context <feature>` to see where code belongs.
After every change, run `smelt check --changed --format json`.
Do not finish while errors remain. Use `smelt explain <code>` when unsure.
Suggested loop: smelt context <feature> → edit → smelt check --changed → fix → tests →
pre-commit → CI (full check). Prefer the cheapest verification that gives sufficient
confidence: a rename needs smelt plus a type checker, new behavior needs one focused
regression test.
pre-commit
repos:
- repo: https://github.com/mathisarends/smelt
rev: v0.1.0
hooks:
- id: smelt
GitHub Actions
CI always checks the whole repository, because cycles and transitive rules cannot be judged from a diff alone.
- uses: astral-sh/setup-uv@v6
- run: uvx --from smelt-cli smelt check --format github
# optional: code scanning
- run: uvx --from smelt-cli smelt check --format sarif > smelt.sarif || true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: smelt.sarif
Development
Requires uv.
uv sync # create .venv and install dev dependencies
uv run pre-commit install
Common commands:
uv run pytest # run tests
uv run pytest --cov # run tests with coverage
uv run ruff check --fix . # lint
uv run ruff format . # format
uv run mypy # type-check
uv run pre-commit run --all-files # run all hooks
uv run smelt check # smelt checks itself
uv run python scripts/generate.py # refresh smelt.schema.json and docs/rules/
Commit messages follow Conventional Commits.
Metadata
Release files for smelt-cli 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| smelt_cli-0.1.0.tar.gz | 82.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| smelt_cli-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 202.3 kB
Release files / smelt_cli-0.1.0.tar.gz
| Download URL | smelt_cli-0.1.0.tar.gz |
|---|---|
| Size | 82.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
90933e1b354169ca8c5d41daf4c6401fb6fc055fb3b10cefa807f29479dfebc5
|
|
BLAKE2b-256 checksum How to use checksums |
28dbfdab10ec8ca7477e58870a6688d061b05cd78f693ca52997cba6b0f1a6df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Release files / smelt_cli-0.1.0-py3-none-any.whl
| Download URL | smelt_cli-0.1.0-py3-none-any.whl |
|---|---|
| Size | 120.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cdc3062273a0b4e5941b6e3937b82e3d600dbeedcba53f36863c8abaa2d35583
|
|
BLAKE2b-256 checksum How to use checksums |
d4c080eb723c4251ce5106f13a38c500644a4bc67e6fd81136e0cd846b5e6e24
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|