smelt-cli 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. smelt_cli-0.1.2/PKG-INFO +274 -0
  2. smelt_cli-0.1.2/README.md +259 -0
  3. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/pyproject.toml +8 -3
  4. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/pyproject.toml.orig +8 -3
  5. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/analysis/__init__.py +2 -10
  6. smelt_cli-0.1.2/smelt/analysis/context.py +56 -0
  7. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/analysis/files.py +4 -1
  8. smelt_cli-0.1.2/smelt/analysis/syntax.py +66 -0
  9. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/cli/app.py +96 -23
  10. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/cli/commands/__init__.py +1 -4
  11. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/cli/commands/adopt.py +52 -10
  12. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/cli/commands/discover.py +20 -14
  13. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/cli/commands/info.py +22 -15
  14. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/cli/support.py +19 -2
  15. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/config/loader.py +15 -1
  16. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/config/models.py +156 -203
  17. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/config/validation.py +25 -49
  18. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/diagnostics/debt.py +48 -10
  19. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/diagnostics/dedupe.py +3 -4
  20. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/diagnostics/render/machine.py +28 -0
  21. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/diagnostics/render/text.py +68 -4
  22. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/diagnostics/report.py +2 -0
  23. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/diagnostics/violation.py +4 -7
  24. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/docs.py +1 -1
  25. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/engine/briefing.py +160 -103
  26. smelt_cli-0.1.2/smelt/engine/changes.py +57 -0
  27. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/engine/check.py +133 -61
  28. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/engine/inference.py +242 -134
  29. smelt_cli-0.1.2/smelt/engine/mirror_inference.py +95 -0
  30. smelt_cli-0.1.2/smelt/engine/paths.py +101 -0
  31. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/model/architecture.py +30 -33
  32. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/base.py +2 -0
  33. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/common.py +29 -38
  34. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/dependencies/composition_root.py +5 -50
  35. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/dependencies/cycles.py +34 -9
  36. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/dependencies/features.py +28 -11
  37. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/dependencies/layers.py +14 -23
  38. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/meta.py +3 -3
  39. smelt_cli-0.1.2/smelt/rules/registry.py +32 -0
  40. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/structure/layout.py +39 -51
  41. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/structure/naming.py +2 -2
  42. smelt_cli-0.1.2/smelt/rules/testing/location.py +388 -0
  43. smelt_cli-0.1.0/PKG-INFO +0 -199
  44. smelt_cli-0.1.0/README.md +0 -187
  45. smelt_cli-0.1.0/smelt/analysis/context.py +0 -106
  46. smelt_cli-0.1.0/smelt/analysis/roles.py +0 -122
  47. smelt_cli-0.1.0/smelt/analysis/syntax.py +0 -246
  48. smelt_cli-0.1.0/smelt/analysis/types.py +0 -181
  49. smelt_cli-0.1.0/smelt/cli/commands/verify.py +0 -60
  50. smelt_cli-0.1.0/smelt/engine/architecture_map.py +0 -125
  51. smelt_cli-0.1.0/smelt/engine/changes.py +0 -81
  52. smelt_cli-0.1.0/smelt/engine/verify.py +0 -77
  53. smelt_cli-0.1.0/smelt/rules/code/common.py +0 -119
  54. smelt_cli-0.1.0/smelt/rules/code/construction.py +0 -172
  55. smelt_cli-0.1.0/smelt/rules/code/container.py +0 -104
  56. smelt_cli-0.1.0/smelt/rules/code/inheritance.py +0 -99
  57. smelt_cli-0.1.0/smelt/rules/code/roles.py +0 -100
  58. smelt_cli-0.1.0/smelt/rules/code/self_reference.py +0 -104
  59. smelt_cli-0.1.0/smelt/rules/registry.py +0 -135
  60. smelt_cli-0.1.0/smelt/rules/structure/roles.py +0 -63
  61. smelt_cli-0.1.0/smelt/rules/testing/__init__.py +0 -0
  62. smelt_cli-0.1.0/smelt/rules/testing/api.py +0 -115
  63. smelt_cli-0.1.0/smelt/rules/testing/bloat.py +0 -84
  64. smelt_cli-0.1.0/smelt/rules/testing/common.py +0 -238
  65. smelt_cli-0.1.0/smelt/rules/testing/location.py +0 -253
  66. smelt_cli-0.1.0/smelt/rules/testing/mocks.py +0 -294
  67. smelt_cli-0.1.0/smelt/rules/testing/patching.py +0 -152
  68. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/__init__.py +0 -0
  69. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/__main__.py +0 -0
  70. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/analysis/graphs.py +0 -0
  71. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/analysis/imports.py +0 -0
  72. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/analysis/parsing.py +0 -0
  73. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/cli/__init__.py +0 -0
  74. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/cli/commands/check.py +0 -0
  75. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/config/__init__.py +0 -0
  76. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/config/discovery.py +0 -0
  77. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/config/errors.py +0 -0
  78. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/config/patterns.py +0 -0
  79. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/config/schema.py +0 -0
  80. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/diagnostics/__init__.py +0 -0
  81. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/diagnostics/render/__init__.py +0 -0
  82. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/diagnostics/suppressions.py +0 -0
  83. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/engine/__init__.py +0 -0
  84. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/model/__init__.py +0 -0
  85. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/py.typed +0 -0
  86. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/__init__.py +0 -0
  87. {smelt_cli-0.1.0/smelt/rules/code → smelt_cli-0.1.2/smelt/rules/dependencies}/__init__.py +0 -0
  88. {smelt_cli-0.1.0 → smelt_cli-0.1.2}/smelt/rules/dependencies/third_party.py +0 -0
  89. {smelt_cli-0.1.0/smelt/rules/dependencies → smelt_cli-0.1.2/smelt/rules/structure}/__init__.py +0 -0
  90. {smelt_cli-0.1.0/smelt/rules/structure → smelt_cli-0.1.2/smelt/rules/testing}/__init__.py +0 -0
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.3
2
+ Name: smelt-cli
3
+ Version: 0.1.2
4
+ Summary: Static guardrails for Python codebases maintained by humans and coding agents.
5
+ Author: Mathis Arends
6
+ Author-email: Mathis Arends <mathisarends27@gmail.com>
7
+ Requires-Dist: grimp>=3.17
8
+ Requires-Dist: pydantic>=2.13.5
9
+ Requires-Dist: pyyaml>=6.0.3
10
+ Requires-Python: >=3.12
11
+ Project-URL: Repository, https://github.com/mathisarends/smelt
12
+ Project-URL: Issues, https://github.com/mathisarends/smelt/issues
13
+ Project-URL: Documentation, https://github.com/mathisarends/smelt#readme
14
+ Description-Content-Type: text/markdown
15
+
16
+ # smelt
17
+
18
+ Architecture guardrails for Python, built for reviewing what coding agents change. Describe
19
+ your features and layers in `smelt.yaml`; `smelt check` reports two kinds of findings, each
20
+ with the exact location, what is allowed instead and how to fix it:
21
+
22
+ - **Boundaries:** an import that crosses a layer, a feature or the composition root.
23
+ - **Test mirroring:** a test file that does not sit at the mirrored path of a source module.
24
+
25
+ ## Installation
26
+
27
+ Requires Python 3.12 or newer. Until this project has a PyPI release under its own
28
+ distribution name, install from this repository:
29
+
30
+ ```bash
31
+ uv tool install git+https://github.com/mathisarends/smelt.git
32
+ smelt --version
33
+ ```
34
+
35
+ From a local checkout, use `uv tool install .`. The PyPI package named `smelt` belongs
36
+ to a different project; `pip install smelt` and bare `uvx smelt` install that project.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ uv tool install smelt-cli
42
+ # or install into your project's environment:
43
+ pip install smelt-cli
44
+ ```
45
+
46
+ The PyPI package is named `smelt-cli`; the CLI command and Python package are both
47
+ named `smelt`. To run without installing, use `uvx --from smelt-cli smelt check`.
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ smelt init # draft smelt.yaml from the code (also uv workspaces)
53
+ smelt check # whole project
54
+ smelt check --changed # only what the working tree introduced since HEAD
55
+ smelt check --changed --base origin/main --format json
56
+ smelt context user # what applies to a feature, module or path
57
+ smelt explain SMT101 # rationale, examples and config keys of a rule
58
+ smelt rules # all rules with their default severity
59
+ smelt debt # record today's violations as known debt
60
+ smelt debt --prune # drop debt entries that were fixed
61
+ smelt config show # the resolved config
62
+ smelt config schema # the JSON schema of smelt.yaml
63
+ ```
64
+
65
+ Exit codes: `0` clean, `1` violations at or above `--fail-on`, `2` config or usage error.
66
+ A config entry that points at nothing (a mistyped package, a missing test root) is a
67
+ config error, not a silently disabled rule.
68
+ Explicit check paths must exist and contain analyzed source or test files; unknown
69
+ `--select`/`--ignore` prefixes are usage errors. JSON includes the reporting scope, file
70
+ count and active rule codes. `--config PATH` works before or after a subcommand.
71
+ With `--format json`, an exit `2` prints a JSON document as well, so an agent needs no
72
+ text parser: `{"schema_version": 1, "status": "error", "error": {...}}` with `kind`
73
+ (`usage`, `config` or `analysis`), `message`, the rejected `input` (`option`, `value`)
74
+ and, for config errors, the `config` file with each issue's YAML `location`. That covers
75
+ argument errors (`--fail-on bogus`, an unknown flag) whenever `--format json` is on the
76
+ command line; an invalid `--format` value itself is reported as argparse text.
77
+
78
+ `--changed` checks the base commit (HEAD, or the merge-base with `--base`) with today's
79
+ config and reports only violations that are new, wherever they show up. Old violations in
80
+ an edited file stay quiet, a new import cycle does not.
81
+ Changing an import's comment, formatting or alias does not make its existing boundary
82
+ violation new; an additional identical violating import still counts as new.
83
+
84
+ ## A DDD layout
85
+
86
+ ```yaml
87
+ version: 1
88
+ project:
89
+ root_packages: [backend, agent]
90
+ source_roots: [backend/src, libs/agent/src]
91
+ test_roots: [backend/tests, libs/agent/tests]
92
+
93
+ architecture:
94
+ features: {root: backend.features} # every child package is a feature
95
+ shared: [backend.shared, backend.env] # importable everywhere, imports no feature
96
+ composition_root: [backend.main, backend.app]
97
+ wiring: [backend.features.*.infrastructure.di]
98
+ modules: # packages outside the features
99
+ backend.platform: infrastructure
100
+ agent: infrastructure
101
+ layers:
102
+ domain: {path: domain}
103
+ application: {path: application, may_depend_on: [domain]}
104
+ infrastructure: {path: infrastructure, may_depend_on: [domain, application]}
105
+ presentation: {path: presentation, may_depend_on: [application, domain]}
106
+ cross_feature:
107
+ default: deny
108
+ allow:
109
+ - "application -> application" # between all features
110
+ - {from: session.presentation, to: auth.presentation}
111
+
112
+ tests:
113
+ layout: mirror
114
+ mirror: "{root}/{path}/test_{module}.py"
115
+ ```
116
+
117
+ - **Layers** hold inside every feature: `domain` must not import `infrastructure`.
118
+ - **Features** may only import each other through `cross_feature.allow`. The string form
119
+ allows a layer pair for all features, the object form one direction between two.
120
+ - **Central packages** such as a platform or a workspace library get a layer under
121
+ `modules`. The layer rules then apply between them and the features (a feature's domain
122
+ must not import `backend.platform`), and they must not import features.
123
+ - **Wiring** modules (whole-segment `*` allowed) keep their feature and layer and may cross
124
+ layer boundaries. Feature wiring may also cross feature boundaries. Composition roots,
125
+ other wiring modules and their package facades may import declared wiring. Central or
126
+ shared wiring keeps the prohibition on importing features.
127
+ - Every module should belong somewhere: an unclassified module is exempt from all
128
+ layer and feature boundary checks, so SMT305 warns about it. Composition-root/wiring
129
+ import restrictions and configured cycle checks still apply.
130
+
131
+ `smelt init` infers most of this: features, layers, shared and settings modules, the
132
+ composition root including an app factory, wiring patterns, central packages by name and
133
+ the mirror pattern the existing tests follow. Review it before adopting its findings.
134
+ Its starter policy explicitly allows third-party packages and checks direct imports.
135
+ Review these decisions: to keep frameworks out of the core, set e.g.
136
+ `domain.third_party: {default: deny, allow: [pydantic]}` under `architecture.layers`.
137
+ Set `architecture.imports.transitive: true` to also detect layer dependencies through
138
+ re-exports, such as a domain importing an infrastructure provider from a feature's
139
+ `__init__.py`. Review the suggested `application -> application` pair too: it permits
140
+ that dependency between **every** feature.
141
+
142
+ `smelt context` describes the policy that applies to its target, including wiring and
143
+ composition-root exceptions, third-party defaults and direct/transitive coverage. It
144
+ accepts feature names, dotted module names and source paths, including planned `.py`
145
+ files under existing source packages. If a syntax error prevents analysis, the briefing
146
+ is still available and its violation counts are marked unavailable (`violations: null`
147
+ and `analysis_error` in JSON). Central and wiring modules have explicit JSON flags.
148
+
149
+ ## Test mirroring
150
+
151
+ With `tests.layout: mirror` the path of a test decides: `tests/billing/test_invoice.py`
152
+ needs `app/billing/invoice.py`, and a package test `tests/billing/test_billing.py` needs
153
+ `app/billing/`. Not every module needs a test, but a test whose source is missing or
154
+ elsewhere is an error. The message names the fix where it can: the right directory, the
155
+ right file name (`test_session_infrastructure_repository.py` should be named
156
+ `test_repository.py`), a neighbouring module with a similar name, or a missing `{root}` in
157
+ the pattern.
158
+
159
+ `tests.mirror` sets the convention relative to the test root: the default
160
+ `{path}/test_{module}.py` drops the root package, `{root}/{path}/test_{module}.py` keeps it,
161
+ and `unit/{path}/{module}_test.py` puts tests under `tests/unit/` with a suffix. In a uv
162
+ workspace each test root mirrors the package of its own member. Deliberately unmirrored
163
+ tests go in `tests.unmirrored` (e.g. `["tests/e2e/**"]`); `tests.mirror_suffixes: true`
164
+ also allows `test_invoice_<topic>.py`.
165
+
166
+ Mirroring repeats file names: `auth/presentation/test_router.py` and
167
+ `user/presentation/test_router.py`. pytest's default import mode only tells them apart
168
+ inside packages, so without `__init__.py` in the test directories it stops with "import
169
+ file mismatch". Prefixing the names (`test_auth_presentation_router.py`) works around that
170
+ but breaks the mirror; set the import mode instead, which `smelt init` points out:
171
+
172
+ ```toml
173
+ [tool.pytest.ini_options]
174
+ addopts = ["--import-mode=importlib"]
175
+ ```
176
+
177
+ ## Adopting it in an existing project
178
+
179
+ A first check often reports dozens of findings that come down to a few decisions. Each
180
+ import finding carries an `edge` in JSON (`"billing.application -> voice.domain"`; for
181
+ cycles, the cycle), and the text output ends with the edges behind several findings.
182
+ Settle each edge once: fix the code, or record an intended dependency as policy, e.g. a
183
+ `cross_feature.allow` entry. Accept what remains with a baseline:
184
+
185
+ `smelt debt` records today's violations in `.smelt/debt.json` and sets `debt:` in
186
+ `smelt.yaml`. `smelt check` then fails only on new violations, and SMT903 reports entries
187
+ that were fixed and can leave the file (`smelt debt --prune`).
188
+ Existing debt files remain readable. Run `smelt debt --prune` once before editing imports
189
+ covered by an older baseline to upgrade their fingerprints; pruning accepts no new debt.
190
+ New import fingerprints survive comments, aliases and formatting changes.
191
+
192
+ Override severities by rule name or code, e.g. `rules: {unclassified-module: off}`.
193
+
194
+ Silence a single finding inline, always with a reason:
195
+
196
+ ```python
197
+ from gateway.infra.sql import Repo # smelt: ignore[SMT101] -- migration tracked in #123
198
+ ```
199
+
200
+ Every rule has a page under [docs/rules](docs/rules/), and `smelt.schema.json` gives editors
201
+ autocompletion and a description for every key of `smelt.yaml`.
202
+
203
+ ## Python versions
204
+
205
+ Smelt runs on Python 3.12 to 3.14 and parses your code with the Python it runs on. Code
206
+ that uses newer syntax (3.14's `except A, B:` or t-strings, 3.13's type parameter defaults)
207
+ needs smelt on that version, e.g. `uvx -p 3.14 --from smelt-cli smelt check`; the syntax error says so when
208
+ `requires-python` or `.python-version` targets a newer Python.
209
+
210
+ ## Using Smelt with coding agents
211
+
212
+ Add this to your `AGENTS.md` or `CLAUDE.md`:
213
+
214
+ ```md
215
+ Before implementing, run `smelt context <feature>` to see where code belongs.
216
+ After every change, run `smelt check --changed --format json`.
217
+ Do not finish while errors remain. Use `smelt explain <code>` when unsure.
218
+ ```
219
+
220
+ Suggested loop: `smelt context <feature>` → edit → `smelt check --changed` → fix → tests →
221
+ pre-commit → CI (full check). Prefer the cheapest verification that gives sufficient
222
+ confidence: a rename needs smelt plus a type checker, new behavior needs one focused
223
+ regression test.
224
+
225
+ ## pre-commit
226
+
227
+ ```yaml
228
+ repos:
229
+ - repo: https://github.com/mathisarends/smelt
230
+ rev: v0.1.2
231
+ hooks:
232
+ - id: smelt
233
+ ```
234
+
235
+ The hook checks the whole project when Python files or `smelt.yaml` / `smelt.yml`
236
+ change. Checking only changed filenames can miss violations in modules that import them.
237
+
238
+ ## GitHub Actions
239
+
240
+ CI checks the whole repository; `--changed` is for the agent loop.
241
+
242
+ ```yaml
243
+ - uses: astral-sh/setup-uv@v6
244
+ - run: uvx --from smelt-cli smelt check --format github
245
+ # optional: code scanning
246
+ - run: uvx --from smelt-cli smelt check --format sarif > smelt.sarif || true
247
+ - uses: github/codeql-action/upload-sarif@v3
248
+ with:
249
+ sarif_file: smelt.sarif
250
+ ```
251
+
252
+ ## Development
253
+
254
+ Requires [uv](https://docs.astral.sh/uv/).
255
+
256
+ ```bash
257
+ uv sync # create .venv and install dev dependencies
258
+ uv run pre-commit install
259
+ ```
260
+
261
+ Common commands:
262
+
263
+ ```bash
264
+ uv run pytest # run tests
265
+ uv run pytest --cov # run tests with coverage
266
+ uv run ruff check --fix . # lint
267
+ uv run ruff format . # format
268
+ uv run mypy # type-check
269
+ uv run pre-commit run --all-files # run all hooks
270
+ uv run smelt check # smelt checks itself
271
+ uv run python scripts/generate.py # refresh smelt.schema.json and docs/rules/
272
+ ```
273
+
274
+ Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/).
@@ -0,0 +1,259 @@
1
+ # smelt
2
+
3
+ Architecture guardrails for Python, built for reviewing what coding agents change. Describe
4
+ your features and layers in `smelt.yaml`; `smelt check` reports two kinds of findings, each
5
+ with the exact location, what is allowed instead and how to fix it:
6
+
7
+ - **Boundaries:** an import that crosses a layer, a feature or the composition root.
8
+ - **Test mirroring:** a test file that does not sit at the mirrored path of a source module.
9
+
10
+ ## Installation
11
+
12
+ Requires Python 3.12 or newer. Until this project has a PyPI release under its own
13
+ distribution name, install from this repository:
14
+
15
+ ```bash
16
+ uv tool install git+https://github.com/mathisarends/smelt.git
17
+ smelt --version
18
+ ```
19
+
20
+ From a local checkout, use `uv tool install .`. The PyPI package named `smelt` belongs
21
+ to a different project; `pip install smelt` and bare `uvx smelt` install that project.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ uv tool install smelt-cli
27
+ # or install into your project's environment:
28
+ pip install smelt-cli
29
+ ```
30
+
31
+ The PyPI package is named `smelt-cli`; the CLI command and Python package are both
32
+ named `smelt`. To run without installing, use `uvx --from smelt-cli smelt check`.
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ smelt init # draft smelt.yaml from the code (also uv workspaces)
38
+ smelt check # whole project
39
+ smelt check --changed # only what the working tree introduced since HEAD
40
+ smelt check --changed --base origin/main --format json
41
+ smelt context user # what applies to a feature, module or path
42
+ smelt explain SMT101 # rationale, examples and config keys of a rule
43
+ smelt rules # all rules with their default severity
44
+ smelt debt # record today's violations as known debt
45
+ smelt debt --prune # drop debt entries that were fixed
46
+ smelt config show # the resolved config
47
+ smelt config schema # the JSON schema of smelt.yaml
48
+ ```
49
+
50
+ Exit codes: `0` clean, `1` violations at or above `--fail-on`, `2` config or usage error.
51
+ A config entry that points at nothing (a mistyped package, a missing test root) is a
52
+ config error, not a silently disabled rule.
53
+ Explicit check paths must exist and contain analyzed source or test files; unknown
54
+ `--select`/`--ignore` prefixes are usage errors. JSON includes the reporting scope, file
55
+ count and active rule codes. `--config PATH` works before or after a subcommand.
56
+ With `--format json`, an exit `2` prints a JSON document as well, so an agent needs no
57
+ text parser: `{"schema_version": 1, "status": "error", "error": {...}}` with `kind`
58
+ (`usage`, `config` or `analysis`), `message`, the rejected `input` (`option`, `value`)
59
+ and, for config errors, the `config` file with each issue's YAML `location`. That covers
60
+ argument errors (`--fail-on bogus`, an unknown flag) whenever `--format json` is on the
61
+ command line; an invalid `--format` value itself is reported as argparse text.
62
+
63
+ `--changed` checks the base commit (HEAD, or the merge-base with `--base`) with today's
64
+ config and reports only violations that are new, wherever they show up. Old violations in
65
+ an edited file stay quiet, a new import cycle does not.
66
+ Changing an import's comment, formatting or alias does not make its existing boundary
67
+ violation new; an additional identical violating import still counts as new.
68
+
69
+ ## A DDD layout
70
+
71
+ ```yaml
72
+ version: 1
73
+ project:
74
+ root_packages: [backend, agent]
75
+ source_roots: [backend/src, libs/agent/src]
76
+ test_roots: [backend/tests, libs/agent/tests]
77
+
78
+ architecture:
79
+ features: {root: backend.features} # every child package is a feature
80
+ shared: [backend.shared, backend.env] # importable everywhere, imports no feature
81
+ composition_root: [backend.main, backend.app]
82
+ wiring: [backend.features.*.infrastructure.di]
83
+ modules: # packages outside the features
84
+ backend.platform: infrastructure
85
+ agent: infrastructure
86
+ layers:
87
+ domain: {path: domain}
88
+ application: {path: application, may_depend_on: [domain]}
89
+ infrastructure: {path: infrastructure, may_depend_on: [domain, application]}
90
+ presentation: {path: presentation, may_depend_on: [application, domain]}
91
+ cross_feature:
92
+ default: deny
93
+ allow:
94
+ - "application -> application" # between all features
95
+ - {from: session.presentation, to: auth.presentation}
96
+
97
+ tests:
98
+ layout: mirror
99
+ mirror: "{root}/{path}/test_{module}.py"
100
+ ```
101
+
102
+ - **Layers** hold inside every feature: `domain` must not import `infrastructure`.
103
+ - **Features** may only import each other through `cross_feature.allow`. The string form
104
+ allows a layer pair for all features, the object form one direction between two.
105
+ - **Central packages** such as a platform or a workspace library get a layer under
106
+ `modules`. The layer rules then apply between them and the features (a feature's domain
107
+ must not import `backend.platform`), and they must not import features.
108
+ - **Wiring** modules (whole-segment `*` allowed) keep their feature and layer and may cross
109
+ layer boundaries. Feature wiring may also cross feature boundaries. Composition roots,
110
+ other wiring modules and their package facades may import declared wiring. Central or
111
+ shared wiring keeps the prohibition on importing features.
112
+ - Every module should belong somewhere: an unclassified module is exempt from all
113
+ layer and feature boundary checks, so SMT305 warns about it. Composition-root/wiring
114
+ import restrictions and configured cycle checks still apply.
115
+
116
+ `smelt init` infers most of this: features, layers, shared and settings modules, the
117
+ composition root including an app factory, wiring patterns, central packages by name and
118
+ the mirror pattern the existing tests follow. Review it before adopting its findings.
119
+ Its starter policy explicitly allows third-party packages and checks direct imports.
120
+ Review these decisions: to keep frameworks out of the core, set e.g.
121
+ `domain.third_party: {default: deny, allow: [pydantic]}` under `architecture.layers`.
122
+ Set `architecture.imports.transitive: true` to also detect layer dependencies through
123
+ re-exports, such as a domain importing an infrastructure provider from a feature's
124
+ `__init__.py`. Review the suggested `application -> application` pair too: it permits
125
+ that dependency between **every** feature.
126
+
127
+ `smelt context` describes the policy that applies to its target, including wiring and
128
+ composition-root exceptions, third-party defaults and direct/transitive coverage. It
129
+ accepts feature names, dotted module names and source paths, including planned `.py`
130
+ files under existing source packages. If a syntax error prevents analysis, the briefing
131
+ is still available and its violation counts are marked unavailable (`violations: null`
132
+ and `analysis_error` in JSON). Central and wiring modules have explicit JSON flags.
133
+
134
+ ## Test mirroring
135
+
136
+ With `tests.layout: mirror` the path of a test decides: `tests/billing/test_invoice.py`
137
+ needs `app/billing/invoice.py`, and a package test `tests/billing/test_billing.py` needs
138
+ `app/billing/`. Not every module needs a test, but a test whose source is missing or
139
+ elsewhere is an error. The message names the fix where it can: the right directory, the
140
+ right file name (`test_session_infrastructure_repository.py` should be named
141
+ `test_repository.py`), a neighbouring module with a similar name, or a missing `{root}` in
142
+ the pattern.
143
+
144
+ `tests.mirror` sets the convention relative to the test root: the default
145
+ `{path}/test_{module}.py` drops the root package, `{root}/{path}/test_{module}.py` keeps it,
146
+ and `unit/{path}/{module}_test.py` puts tests under `tests/unit/` with a suffix. In a uv
147
+ workspace each test root mirrors the package of its own member. Deliberately unmirrored
148
+ tests go in `tests.unmirrored` (e.g. `["tests/e2e/**"]`); `tests.mirror_suffixes: true`
149
+ also allows `test_invoice_<topic>.py`.
150
+
151
+ Mirroring repeats file names: `auth/presentation/test_router.py` and
152
+ `user/presentation/test_router.py`. pytest's default import mode only tells them apart
153
+ inside packages, so without `__init__.py` in the test directories it stops with "import
154
+ file mismatch". Prefixing the names (`test_auth_presentation_router.py`) works around that
155
+ but breaks the mirror; set the import mode instead, which `smelt init` points out:
156
+
157
+ ```toml
158
+ [tool.pytest.ini_options]
159
+ addopts = ["--import-mode=importlib"]
160
+ ```
161
+
162
+ ## Adopting it in an existing project
163
+
164
+ A first check often reports dozens of findings that come down to a few decisions. Each
165
+ import finding carries an `edge` in JSON (`"billing.application -> voice.domain"`; for
166
+ cycles, the cycle), and the text output ends with the edges behind several findings.
167
+ Settle each edge once: fix the code, or record an intended dependency as policy, e.g. a
168
+ `cross_feature.allow` entry. Accept what remains with a baseline:
169
+
170
+ `smelt debt` records today's violations in `.smelt/debt.json` and sets `debt:` in
171
+ `smelt.yaml`. `smelt check` then fails only on new violations, and SMT903 reports entries
172
+ that were fixed and can leave the file (`smelt debt --prune`).
173
+ Existing debt files remain readable. Run `smelt debt --prune` once before editing imports
174
+ covered by an older baseline to upgrade their fingerprints; pruning accepts no new debt.
175
+ New import fingerprints survive comments, aliases and formatting changes.
176
+
177
+ Override severities by rule name or code, e.g. `rules: {unclassified-module: off}`.
178
+
179
+ Silence a single finding inline, always with a reason:
180
+
181
+ ```python
182
+ from gateway.infra.sql import Repo # smelt: ignore[SMT101] -- migration tracked in #123
183
+ ```
184
+
185
+ Every rule has a page under [docs/rules](docs/rules/), and `smelt.schema.json` gives editors
186
+ autocompletion and a description for every key of `smelt.yaml`.
187
+
188
+ ## Python versions
189
+
190
+ Smelt runs on Python 3.12 to 3.14 and parses your code with the Python it runs on. Code
191
+ that uses newer syntax (3.14's `except A, B:` or t-strings, 3.13's type parameter defaults)
192
+ needs smelt on that version, e.g. `uvx -p 3.14 --from smelt-cli smelt check`; the syntax error says so when
193
+ `requires-python` or `.python-version` targets a newer Python.
194
+
195
+ ## Using Smelt with coding agents
196
+
197
+ Add this to your `AGENTS.md` or `CLAUDE.md`:
198
+
199
+ ```md
200
+ Before implementing, run `smelt context <feature>` to see where code belongs.
201
+ After every change, run `smelt check --changed --format json`.
202
+ Do not finish while errors remain. Use `smelt explain <code>` when unsure.
203
+ ```
204
+
205
+ Suggested loop: `smelt context <feature>` → edit → `smelt check --changed` → fix → tests →
206
+ pre-commit → CI (full check). Prefer the cheapest verification that gives sufficient
207
+ confidence: a rename needs smelt plus a type checker, new behavior needs one focused
208
+ regression test.
209
+
210
+ ## pre-commit
211
+
212
+ ```yaml
213
+ repos:
214
+ - repo: https://github.com/mathisarends/smelt
215
+ rev: v0.1.2
216
+ hooks:
217
+ - id: smelt
218
+ ```
219
+
220
+ The hook checks the whole project when Python files or `smelt.yaml` / `smelt.yml`
221
+ change. Checking only changed filenames can miss violations in modules that import them.
222
+
223
+ ## GitHub Actions
224
+
225
+ CI checks the whole repository; `--changed` is for the agent loop.
226
+
227
+ ```yaml
228
+ - uses: astral-sh/setup-uv@v6
229
+ - run: uvx --from smelt-cli smelt check --format github
230
+ # optional: code scanning
231
+ - run: uvx --from smelt-cli smelt check --format sarif > smelt.sarif || true
232
+ - uses: github/codeql-action/upload-sarif@v3
233
+ with:
234
+ sarif_file: smelt.sarif
235
+ ```
236
+
237
+ ## Development
238
+
239
+ Requires [uv](https://docs.astral.sh/uv/).
240
+
241
+ ```bash
242
+ uv sync # create .venv and install dev dependencies
243
+ uv run pre-commit install
244
+ ```
245
+
246
+ Common commands:
247
+
248
+ ```bash
249
+ uv run pytest # run tests
250
+ uv run pytest --cov # run tests with coverage
251
+ uv run ruff check --fix . # lint
252
+ uv run ruff format . # format
253
+ uv run mypy # type-check
254
+ uv run pre-commit run --all-files # run all hooks
255
+ uv run smelt check # smelt checks itself
256
+ uv run python scripts/generate.py # refresh smelt.schema.json and docs/rules/
257
+ ```
258
+
259
+ Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "smelt-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Static guardrails for Python codebases maintained by humans and coding agents."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -14,11 +14,16 @@ dependencies = [
14
14
  name = "Mathis Arends"
15
15
  email = "mathisarends27@gmail.com"
16
16
 
17
+ [project.urls]
18
+ Repository = "https://github.com/mathisarends/smelt"
19
+ Issues = "https://github.com/mathisarends/smelt/issues"
20
+ Documentation = "https://github.com/mathisarends/smelt#readme"
21
+
17
22
  [project.scripts]
18
23
  smelt = "smelt.cli:main"
19
24
 
20
25
  [build-system]
21
- requires = ["uv_build>=0.12.15,<0.13"]
26
+ requires = ["uv_build>=0.12.19,<0.13"]
22
27
  build-backend = "uv_build"
23
28
 
24
29
  [tool.uv.build-backend]
@@ -112,6 +117,6 @@ dev = [
112
117
  "pre-commit>=4.6",
113
118
  "pytest>=9.1",
114
119
  "pytest-cov>=7.1",
115
- "ruff>=0.16.8",
120
+ "ruff>=0.16.9",
116
121
  "types-pyyaml>=6.0.12.20260906",
117
122
  ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "smelt-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Static guardrails for Python codebases maintained by humans and coding agents."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Mathis Arends", email = "mathisarends27@gmail.com" }]
@@ -11,11 +11,16 @@ dependencies = [
11
11
  "pyyaml>=6.0.3",
12
12
  ]
13
13
 
14
+ [project.urls]
15
+ Repository = "https://github.com/mathisarends/smelt"
16
+ Issues = "https://github.com/mathisarends/smelt/issues"
17
+ Documentation = "https://github.com/mathisarends/smelt#readme"
18
+
14
19
  [project.scripts]
15
20
  smelt = "smelt.cli:main"
16
21
 
17
22
  [build-system]
18
- requires = ["uv_build>=0.12.15,<0.13"]
23
+ requires = ["uv_build>=0.12.19,<0.13"]
19
24
  build-backend = "uv_build"
20
25
 
21
26
  [tool.uv.build-backend]
@@ -28,7 +33,7 @@ dev = [
28
33
  "pre-commit>=4.6",
29
34
  "pytest>=9.1",
30
35
  "pytest-cov>=7.1",
31
- "ruff>=0.16.8",
36
+ "ruff>=0.16.9",
32
37
  "types-pyyaml>=6.0.12.20260906",
33
38
  ]
34
39
 
@@ -1,27 +1,19 @@
1
- from smelt.analysis.context import AnalysisContext, ChangeSet, FileChange, Index
1
+ from smelt.analysis.context import AnalysisContext, Index
2
2
  from smelt.analysis.files import FileIndex, SourceFile, TestFile
3
3
  from smelt.analysis.imports import ImportDetail, ImportIndex, is_stdlib
4
4
  from smelt.analysis.parsing import AnalysisError
5
- from smelt.analysis.roles import RoleIndex
6
- from smelt.analysis.syntax import ClassInfo, ModuleSyntax, SyntaxIndex
7
- from smelt.analysis.types import PyrightTypes, TypeIndex
5
+ from smelt.analysis.syntax import ModuleSyntax, SyntaxIndex
8
6
 
9
7
  __all__ = [
10
8
  "AnalysisContext",
11
9
  "AnalysisError",
12
- "ChangeSet",
13
- "ClassInfo",
14
- "FileChange",
15
10
  "FileIndex",
16
11
  "ImportDetail",
17
12
  "ImportIndex",
18
13
  "Index",
19
14
  "ModuleSyntax",
20
- "PyrightTypes",
21
- "RoleIndex",
22
15
  "SourceFile",
23
16
  "SyntaxIndex",
24
17
  "TestFile",
25
- "TypeIndex",
26
18
  "is_stdlib",
27
19
  ]