lanorme 0.5.1__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 (37) hide show
  1. lanorme-0.5.1/.gitignore +9 -0
  2. lanorme-0.5.1/CHANGELOG.md +250 -0
  3. lanorme-0.5.1/LICENSE +21 -0
  4. lanorme-0.5.1/PKG-INFO +220 -0
  5. lanorme-0.5.1/README.md +195 -0
  6. lanorme-0.5.1/benchmarks/README.md +30 -0
  7. lanorme-0.5.1/docs/RULES.md +521 -0
  8. lanorme-0.5.1/pyproject.toml +98 -0
  9. lanorme-0.5.1/src/lanorme/__init__.py +139 -0
  10. lanorme-0.5.1/src/lanorme/__main__.py +8 -0
  11. lanorme-0.5.1/src/lanorme/checks/__init__.py +5 -0
  12. lanorme-0.5.1/src/lanorme/checks/attribute_access.py +188 -0
  13. lanorme-0.5.1/src/lanorme/checks/comments.py +368 -0
  14. lanorme-0.5.1/src/lanorme/checks/domain_terms.py +231 -0
  15. lanorme-0.5.1/src/lanorme/checks/duplication.py +237 -0
  16. lanorme-0.5.1/src/lanorme/checks/file_limits.py +400 -0
  17. lanorme-0.5.1/src/lanorme/checks/forbidden_paths.py +87 -0
  18. lanorme-0.5.1/src/lanorme/checks/layer_deps.py +280 -0
  19. lanorme-0.5.1/src/lanorme/checks/meta.py +191 -0
  20. lanorme-0.5.1/src/lanorme/checks/named_args.py +240 -0
  21. lanorme-0.5.1/src/lanorme/checks/naming_consistency.py +345 -0
  22. lanorme-0.5.1/src/lanorme/checks/pattern_divergence.py +299 -0
  23. lanorme-0.5.1/src/lanorme/checks/port_coverage.py +462 -0
  24. lanorme-0.5.1/src/lanorme/checks/prose.py +256 -0
  25. lanorme-0.5.1/src/lanorme/checks/restating.py +336 -0
  26. lanorme-0.5.1/src/lanorme/checks/secrets.py +280 -0
  27. lanorme-0.5.1/src/lanorme/checks/security_calls.py +397 -0
  28. lanorme-0.5.1/src/lanorme/checks/security_patterns.py +406 -0
  29. lanorme-0.5.1/src/lanorme/checks/stale_paths.py +165 -0
  30. lanorme-0.5.1/src/lanorme/checks/stray_artifacts.py +180 -0
  31. lanorme-0.5.1/src/lanorme/checks/strong_types.py +278 -0
  32. lanorme-0.5.1/src/lanorme/checks/test_coverage.py +176 -0
  33. lanorme-0.5.1/src/lanorme/checks/test_style.py +287 -0
  34. lanorme-0.5.1/src/lanorme/cli.py +518 -0
  35. lanorme-0.5.1/src/lanorme/discovery.py +101 -0
  36. lanorme-0.5.1/tests/fixtures/configured/pyproject.toml +2 -0
  37. lanorme-0.5.1/tests/fixtures/prose/pyproject.toml +2 -0
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+ benchmarks/.corpora/
@@ -0,0 +1,250 @@
1
+ # Changelog
2
+
3
+ All notable user-facing changes to LaNorme. The public API is the set of
4
+ rule codes that may appear in `select` / `ignore` / `per-file-ignores`
5
+ and the configuration keys under `[tool.lanorme]`. See the Versioning
6
+ section in the README for the breaking-change policy.
7
+
8
+ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.5.1]
13
+
14
+ ### Changed
15
+
16
+ - First release published to PyPI: `uv tool install lanorme` (or
17
+ `pipx install lanorme` / `pip install lanorme`). Publishing runs through
18
+ PyPI Trusted Publishing from GitHub Actions on each GitHub Release, so no
19
+ API token is stored.
20
+ - The project moved to the `lanorme` GitHub organisation; all project URLs
21
+ now point to `github.com/lanorme/lanorme` (the old paths redirect).
22
+
23
+ ## [0.5.0]
24
+
25
+ ### Added
26
+
27
+ - `attribute_access` check, opt-in (default-off), advisory warnings:
28
+ - `ATTR-001`: `hasattr(x, "name")` with a literal identifier name (duck
29
+ typing; prefer a `runtime_checkable` Protocol with `isinstance`, or
30
+ EAFP).
31
+ - `ATTR-002`: `getattr(x, "name")` (no default), `setattr(x, "name", v)`,
32
+ or `delattr(x, "name")` with a literal identifier name (use direct
33
+ attribute access `x.name`).
34
+ - High-confidence cases only: three-argument `getattr` with a default,
35
+ dunder names, non-identifier names, and files under `tests/` are exempt.
36
+ Dynamic (non-literal) names are exempt unless `flag_dynamic = true`.
37
+ Enable via `[tool.lanorme.attribute_access] enabled = true`. The default
38
+ was chosen by measuring the rule against the Python standard library,
39
+ where `hasattr` is dominated by legitimate platform/feature detection
40
+ (no Protocol fix), so the check ships off.
41
+ - `Configurable` protocol in the public API: a `runtime_checkable` Protocol
42
+ for checks that accept a `[tool.lanorme.<name>]` settings table. The CLI
43
+ now selects configurable checks with `isinstance(check, Configurable)`.
44
+
45
+ ## [0.4.0]
46
+
47
+ ### Added
48
+
49
+ - Top-level `[tool.lanorme] source_root` key. It decouples the architectural
50
+ root from the scan target for the two layout-aware checks (`layer_deps` and
51
+ `port_coverage`) only. With `source_root = "src/pkg"`, a single
52
+ `lanorme check .` from the repo root classifies layers under
53
+ `src/pkg/domain/`, `src/pkg/api/`, etc., while every other check keeps
54
+ scanning the whole tree. `composition_root`, `ports_dir`, and
55
+ `adapter_roots` are interpreted relative to `source_root`. A scanned file
56
+ that is not under `source_root` is layer-exempt (skipped by `layer_deps` /
57
+ `port_coverage`, never flagged), but is still seen by every other check.
58
+ Reported violation paths stay relative to the scan target, so `--exclude`,
59
+ `[tool.lanorme.per-file-ignores]`, and `# noqa` line up unchanged.
60
+ `source_root` is resolved against the scan target (for the intended
61
+ `lanorme check .` from the repo root these are the same directory).
62
+ - `exclude` globs are now honoured at file-discovery time, not only in the
63
+ post-filter. An excluded directory is pruned during the walk, so a large
64
+ excluded subtree (`postman/**`, `docs/generated/**`, ...) is no longer read.
65
+ The CLI still post-filters by the same globs as a safety net.
66
+
67
+ ### Changed
68
+
69
+ - A built-in set of never-source directories is pruned during every check's
70
+ tree walk regardless of configuration: `.git`, `.venv`, `venv`,
71
+ `node_modules`, `__pycache__`, `dist`, `build`, `.ruff_cache`,
72
+ `.pytest_cache`, `.mypy_cache`. This makes `lanorme check .` fast out of the
73
+ box (it no longer descends into a virtualenv or build tree). This is a
74
+ behaviour change not gated behind a config key: a project that deliberately
75
+ kept first-party `.py` files under one of these directory names would no
76
+ longer have them scanned. The `stray_artifacts` check already pruned the
77
+ same set, so its `JUNK` rules are unaffected.
78
+
79
+ ## [0.3.0]
80
+
81
+ ### Added
82
+
83
+ - `layer_deps` and `port_coverage` are now configurable via
84
+ `[tool.lanorme.layer_deps]` and `[tool.lanorme.port_coverage]`. All keys
85
+ are optional and the built-in defaults reproduce the previous behaviour.
86
+ - `layer_deps`: `composition_root` (path globs), `layers`, and a nested
87
+ `[tool.lanorme.layer_deps.allowed]` table mapping each layer to the
88
+ layers it may import.
89
+ - `port_coverage`: `ports_dir`, `adapter_roots`, `composition_root`
90
+ (path globs), `skip_files`, `ports_without_impl`.
91
+
92
+ ### Changed
93
+
94
+ - The composition root is now matched with `fnmatch` globs against the
95
+ source-relative path, in both `layer_deps` (LAYER-005) and
96
+ `port_coverage` (PORT-003), instead of a directory `startswith` /
97
+ substring test. A module **file** such as `api/dependencies.py` can now
98
+ be a composition root; previously only an `api/dependencies/` **directory**
99
+ was recognised. The defaults match the same paths as before, so projects
100
+ that do not set the new keys see no change.
101
+ - LAYER-005 rule text changed from "only `api/dependencies/` may import
102
+ from infrastructure" to "only the composition root may import from
103
+ infrastructure" (it is now configurable).
104
+ - `port_coverage` now scans each adapter root **recursively** (`rglob`),
105
+ where it previously scanned only the top level of
106
+ `infrastructure/services/`. A project with adapter files in
107
+ subdirectories of an adapter root may now see those files evaluated by
108
+ PORT-001 and contribute to PORT-002 coverage. For a flat
109
+ `infrastructure/services/*.py` layout there is no change. This is the one
110
+ behaviour change not gated behind a config key.
111
+
112
+ ## [0.2.0]
113
+
114
+ ### Changed
115
+
116
+ - `CMT-001` recall pushed from 0.667 to 1.000 by extending the comment-as-
117
+ code parser with wrapping strategies for the shapes `ast.parse` rejects
118
+ standalone: block headers (`if x:`, `for x in y:`, `def f():`, `class C:`)
119
+ are tried with a `pass` body; `try:` adds a synthetic `except`; `elif` /
120
+ `else` / `except` / `finally` are tried inside their parent block; bare
121
+ `return` / `yield` / `raise` are tried inside a synthetic `def _():`; and
122
+ decorator lines (`@foo`) are tried followed by `def _(): pass`. Also
123
+ added `ast.If` / `ast.Try` / `ast.Match` / `ast.Return` to the code-node
124
+ whitelist. Scored: **F1 = 0.793 -> 0.992** (P = 0.978 -> 0.985,
125
+ R = 0.667 -> 1.000).
126
+ - `CMT-005` moved from the `comments` check to a new `restating` check.
127
+ Same rule code, same precision-funnel detector (P = 1.000, R = 0.418,
128
+ F1 = 0.589). Configuration key changed from `[tool.lanorme.comments]
129
+ restating = true` to `[tool.lanorme.restating] enabled = true`.
130
+ - `SQL-001` rewritten AST-based: only flags raw SQL that reaches a
131
+ database execution sink (`.execute` / `.executemany` /
132
+ `.executescript` on a DB-shaped receiver, or `read_sql` /
133
+ `read_sql_query`). Unwraps `text(...)` constructors, resolves
134
+ module-level and function-local string constants, treats `+` /
135
+ `%`-formatted / `.format`-built SQL as interpolated, and recognises
136
+ parameterised executes (placeholder + params arg) as safe.
137
+ Scored against the bundled corpus: **F1 = 0.761 -> 1.000**
138
+ (P = 0.686 -> 1.000, R = 0.854 -> 1.000).
139
+ - `SECRETPY-001` rewritten AST-based: flags credential-named
140
+ assignments (variable, dict-literal key, call kwarg) plus shape-only
141
+ matches (PEM, JWT, Bearer, DB-URL-with-creds, vendor-prefixed
142
+ tokens for AWS / GitHub / Slack / Stripe). Placeholder markers
143
+ (`<your-...>`, `REPLACE_ME`, `example`, ...) skip a value unless it
144
+ is high-entropy enough (32+ chars, mixed case, digits) to defeat the
145
+ marker. Scored: **F1 = 0.658 -> 1.000** (P = 0.758 -> 1.000,
146
+ R = 0.581 -> 1.000).
147
+ - `SECRETPY-001` moved from the `security_patterns` check to a new
148
+ `secrets` check. Rule code unchanged; the check name changed.
149
+ Users running `lanorme check . --check=security_patterns` no longer
150
+ get SECRETPY-001 flags; run `--check=secrets` instead, or rely on
151
+ the default-on full run.
152
+
153
+ ### Added
154
+
155
+ - `lanorme rule <CODE>` CLI subcommand. Prints the matching section of
156
+ `docs/RULES.md` (bundled into the wheel) for one rule code; exits 2
157
+ with a helpful pointer if the code is unknown.
158
+ - Project metadata for distribution: `authors`, `[project.urls]`
159
+ (homepage / repository / issues / changelog), `Development Status ::
160
+ 4 - Beta`, Python 3.14 classifier, `Typing :: Typed`, additional
161
+ keywords.
162
+ - `[dependency-groups] dev = ["pytest>=8"]`: contributors install with
163
+ `uv sync --group dev` and run the unit suite with
164
+ `uv run pytest tests/unit`.
165
+ - `[tool.hatch.build.targets.sdist]` selects the publishable artefacts
166
+ (src, README, CHANGELOG, LICENSE, docs/RULES.md, pyproject) for the
167
+ source distribution.
168
+ - `security_calls` check, six default-on dangerous-call rules, each one
169
+ AST node and precision-first: `SHELL-001` (subprocess `shell=True`,
170
+ `os.system`, `os.popen`), `DESERIAL-001` (pickle / marshal / dill /
171
+ `yaml.load` without `SafeLoader`), `EVAL-001` (`eval` / `exec` /
172
+ `compile` on a non-literal first argument), `CRYPTO-001` (`md5` / `sha1`
173
+ used for security, deprecated TLS protocol constants), `TLS-001`
174
+ (`verify=False` in `requests` / `httpx` / `aiohttp`, `ssl.CERT_NONE`,
175
+ `ssl._create_unverified_context`), `DEBUG-001` (`Flask` / `FastAPI`
176
+ constructors and `app.run` calls with `debug=True`, `DEBUG = True` at
177
+ module scope in `*settings.py` / `*config.py`).
178
+ - `[tool.lanorme.per-file-ignores]` TOML table: map a path glob to a
179
+ list of rule codes (full code such as `SQL-001` or a category prefix
180
+ such as `SQL`) that should never fire for matching files.
181
+ - `# noqa` inline suppression. Bare `# noqa` silences any rule on the
182
+ line it sits on; `# noqa: CODE1,CODE2` silences only the listed codes
183
+ (full codes or category prefixes). Case-insensitive.
184
+ - `test_style` check with `AAA-001` (test functions must carry inline
185
+ Arrange / Act / Assert section comments, or Given / When / Then) and
186
+ `AAA-002` (test functions in the same file must not share an identical
187
+ arrange prefix). Default-off; enable via
188
+ `[tool.lanorme.test_style] enabled = true`.
189
+ - `CHANGELOG.md`: `docs/RULES.md` per-rule reference, versioning policy
190
+ in README.
191
+
192
+ ### Changed (breaking)
193
+
194
+ - **Rule renames** so each rule's code matches its actual scope:
195
+ - `AUTH-001` &rarr; `AUTHN-001` (the check verifies authentication
196
+ presence; it does not measure authorisation).
197
+ - `TEST-001` &rarr; `TESTFILE-001` (it verifies that a `test_*.py`
198
+ partner exists; it does not measure coverage).
199
+ - `SECRET-001` &rarr; `SECRETPY-001` (Python-source scope only;
200
+ `.env` / `*.yaml` / `*.ipynb` are out of scope until a future
201
+ `SECRET-002` / `SECRET-003` lands).
202
+ - **Default demotions** based on the multi-reviewer audit
203
+ (`docs/audit/SUMMARY.md`). All previously default-on, now opt-in:
204
+ - `NAMING-001` (repository CRUD prefixes). Opt-in via
205
+ `[tool.lanorme.naming_consistency] repo_crud = true`.
206
+ - `NAMING-002` (service CRUD prefixes). Opt-in via
207
+ `service_crud = true`. These two rules actively conflicted with
208
+ `TERM-NNN` on a serious DDD project; the audit's biggest single
209
+ finding.
210
+ - `KWARG-001` (`bare *` on every multi-argument function). Opt-in
211
+ via `[tool.lanorme.named_args] enabled = true`.
212
+ - `AAA-001` / `AAA-002`. Opt-in via
213
+ `[tool.lanorme.test_style] enabled = true`.
214
+ - **Earlier rename pass** (commit `4703490`):
215
+ - `SEC-001` &rarr; `AUTH-001` (later renamed again, see above).
216
+ - `SEC-002` &rarr; `SQL-001`, `SEC-003` &rarr; `SECRET-001` (later
217
+ renamed again, see above).
218
+ - `SIZE-004` &rarr; `COMPLEXITY-001` (cyclomatic complexity is not a
219
+ size).
220
+ - `SIZE-005` &rarr; `PARAM-001` (parameter count is not a size).
221
+ - `PATTERN-001` &rarr; `IMPORT-001`, `PATTERN-002` &rarr; `TYPING-001`
222
+ (later removed), `PATTERN-004` &rarr; `ENDPOINT-001`.
223
+ - `PROJ-001` &rarr; `PATH-001`.
224
+ - `ART-001` / `ART-002` &rarr; `JUNK-001` / `JUNK-002`.
225
+ - `NAMED-001` &rarr; `KWARG-001`.
226
+ - **CMT-003 / CMT-004 unified under PROSE-001 / PROSE-003** (one rule
227
+ code per intent regardless of file type). The `comments` check now
228
+ emits `PROSE-001` and `PROSE-003` on Python comments and docstrings
229
+ when `em_dash` / `emoji` are enabled; the `prose` check still emits
230
+ the same codes on Markdown.
231
+
232
+ ### Removed (breaking)
233
+
234
+ - `TEST-002` (weak blank-line AAA heuristic). Superseded by `AAA-001`
235
+ (comment-marker AAA in the `test_style` check, stronger signal).
236
+ - `TYPING-001` (no `TYPE_CHECKING` outside model files). Three of four
237
+ reviewers in the audit flagged the rule's premise as inverting the
238
+ community typing consensus (ruff's `TCH` family actively promotes
239
+ `TYPE_CHECKING` guards). The helper was also unwired in practice. May
240
+ return as an opt-in with the premise inverted (encourage rather than
241
+ forbid).
242
+
243
+ ## [0.1.0] - initial commit `aaef1f5`
244
+
245
+ First public-shape release. Eighteen checks extracted from a private
246
+ codebase, fully de-identified, with a configurable CLI (`lanorme check`,
247
+ `lanorme rules`), TOML-driven configuration (`[tool.lanorme]` in
248
+ `pyproject.toml` or a dedicated `lanorme.toml`), and a plugin model
249
+ through `[project.entry-points."lanorme.checks"]` or
250
+ `[tool.lanorme] plugins = [...]`.
lanorme-0.5.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LaNorme contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
lanorme-0.5.1/PKG-INFO ADDED
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: lanorme
3
+ Version: 0.5.1
4
+ Summary: La norme: a configurable, pluggable architecture and code-quality linter for Python.
5
+ Project-URL: Homepage, https://github.com/lanorme/lanorme
6
+ Project-URL: Repository, https://github.com/lanorme/lanorme
7
+ Project-URL: Issues, https://github.com/lanorme/lanorme/issues
8
+ Project-URL: Changelog, https://github.com/lanorme/lanorme/blob/main/CHANGELOG.md
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: architecture,checks,code-quality,ddd,hexagonal,linter,static-analysis
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.13
24
+ Description-Content-Type: text/markdown
25
+
26
+ # LaNorme
27
+
28
+ A linter for Python. It checks the usual things, dead code, file and function
29
+ size, complexity, weak types, hardcoded secrets, dangerous calls, and a few
30
+ things most linters do not: hexagonal layer boundaries, ports-and-adapters
31
+ wiring, and a project's own naming vocabulary.
32
+
33
+ Standard library only. No runtime dependencies. Python 3.13+.
34
+
35
+ ```console
36
+ $ lanorme check .
37
+ ```
38
+
39
+ ## Install
40
+
41
+ From PyPI:
42
+
43
+ ```console
44
+ uv tool install lanorme # or: pipx install lanorme, pip install lanorme
45
+ ```
46
+
47
+ Run it once without installing anything:
48
+
49
+ ```console
50
+ uvx lanorme check .
51
+ ```
52
+
53
+ Or install straight from source:
54
+
55
+ ```console
56
+ uv tool install "git+https://github.com/lanorme/lanorme@v0.5.1"
57
+ ```
58
+
59
+ Releases are tagged `vX.Y.Z`; see the [releases page](https://github.com/lanorme/lanorme/releases) for notes.
60
+
61
+ ## Usage
62
+
63
+ ```console
64
+ lanorme check [PATHS...] # run every enabled check (default path: .)
65
+ lanorme check . --check=secrets # run one check by name
66
+ lanorme check . --select TYPE,AUTHN # only these rule codes or categories
67
+ lanorme check . --ignore NAMING-003 # skip specific rules
68
+ lanorme check . --output-format json
69
+ lanorme rules # list every registered rule
70
+ lanorme rule SQL-001 # show the reference for one rule
71
+ ```
72
+
73
+ Exit code is `1` when any check fails, `0` when the tree is clean.
74
+
75
+ A run looks like this:
76
+
77
+ ```console
78
+ $ lanorme check src/
79
+ [FAIL] secrets
80
+ VIOLATION: app.py:8 — Hardcoded credential value bound to 'API_KEY'
81
+ Rule: SECRETPY-001: No hardcoded secrets in source code
82
+ Fix: Read the value from an environment variable, secrets manager, or settings module
83
+ ```
84
+
85
+ ### Suppressing a finding
86
+
87
+ A `# noqa` at the end of a line silences every rule on that line; `# noqa: CODE`
88
+ silences only the listed codes (a full code like `SQL-001` or a category like
89
+ `SQL`):
90
+
91
+ ```python
92
+ def legacy_handler(req): # noqa: KWARG-001
93
+ return req.text # noqa
94
+ ```
95
+
96
+ For whole directories, use the per-file table in your config (below).
97
+
98
+ ## Configuration
99
+
100
+ LaNorme walks up from the target path looking for config: a dedicated
101
+ `lanorme.toml`, otherwise a `[tool.lanorme]` table in `pyproject.toml`. Command
102
+ line flags win over both.
103
+
104
+ ```toml
105
+ [tool.lanorme]
106
+ select = ["ALL"] # rule codes or categories to run
107
+ ignore = ["NAMING-003"] # rule codes or categories to skip
108
+ exclude = ["postman/**", "vendor/*"] # path globs, pruned at walk time
109
+ source_root = "src/myproject" # architectural root for layer_deps/port_coverage
110
+ plugins = ["myproject.checks.house_rules"] # extra check modules to load
111
+
112
+ # Silence specific rules for matching paths (the file is still scanned).
113
+ [tool.lanorme.per-file-ignores]
114
+ "tests/**/*.py" = ["AAA", "SECRETPY"]
115
+ "alembic/**/*.py" = ["SQL"]
116
+ "notebooks/*.py" = ["KWARG", "DRY"]
117
+
118
+ # Each per-check table is handed to that check.
119
+ [tool.lanorme.stray_artifacts]
120
+ extensions = [".zip", ".pdf"] # also flag these (JUNK-002)
121
+ allow = ["docs/diagram.png"] # never flag these (globs)
122
+
123
+ [tool.lanorme.forbidden_paths]
124
+ dirs = ["legacy_src", "build_artifacts"] # these directories must not exist
125
+
126
+ [[tool.lanorme.domain_terms.rules]]
127
+ id = "TERM-001"
128
+ canonical = "Account"
129
+ forbidden = ["Acct", "Acnt"]
130
+ ```
131
+
132
+ `exclude` globs are pruned during the walk, not just filtered from output, so a
133
+ large excluded subtree is never read. A built-in set of never-source
134
+ directories (`.git`, `.venv`, `venv`, `node_modules`, `__pycache__`, `dist`,
135
+ `build`, `.ruff_cache`, `.pytest_cache`, `.mypy_cache`) is always pruned, so
136
+ `lanorme check .` is fast out of the box.
137
+
138
+ `source_root` applies only to the two layout-aware checks (`layer_deps`,
139
+ `port_coverage`). It lets you run `lanorme check .` from the repo root while the
140
+ hexagonal layers live under a nested package: layers are classified relative to
141
+ `source_root`, files outside it are layer-exempt, and `composition_root` /
142
+ `ports_dir` / `adapter_roots` are read relative to it. Every other check still
143
+ scans the whole tree.
144
+
145
+ ## What it checks
146
+
147
+ `lanorme rules` prints the live list. Each rule, what it catches and does not,
148
+ its config, and where measured its precision and recall on the bundled test
149
+ corpora, is in [`docs/RULES.md`](docs/RULES.md).
150
+
151
+ On by default, on any project, no config needed:
152
+
153
+ | Rule | Catches |
154
+ |---|---|
155
+ | `CMT-001/002` | commented-out code, over-long comment blocks |
156
+ | `DRY-001` | near-duplicate function bodies |
157
+ | `SIZE-001..003` / `COMPLEXITY-001` / `PARAM-001` | file, function and class size; cyclomatic complexity; parameter count |
158
+ | `IMPORT-001` / `ENDPOINT-001` | imports inside function bodies; deeply nested endpoints |
159
+ | `NAMING-003/004` | HTTP-verb-to-handler match; boolean-prefix predicates |
160
+ | `TYPE-001..003` | `dict[str, Any]`, bare containers, untyped `**kwargs` |
161
+ | `AUTHN-001` / `SQL-001` / `SECRETPY-001` | mutation endpoints without an auth dependency; raw SQL at a database call; hardcoded secrets in `.py` |
162
+ | `SHELL-001` / `DESERIAL-001` / `EVAL-001` / `CRYPTO-001` / `TLS-001` / `DEBUG-001` | shell injection, unsafe deserialisation, `eval`/`exec`, weak hashes, disabled TLS, debug mode |
163
+ | `JUNK-001/002` | screenshots, scratch files, OS junk, stray binaries |
164
+ | `TESTFILE-001` | a production module with no `test_*.py` partner |
165
+ | `META-001..005` | the checks themselves emit well-formed output |
166
+
167
+ Off until you turn them on:
168
+
169
+ | Rule | Why |
170
+ |---|---|
171
+ | `LAYER-001..005` | needs a layered layout (`domain/ application/ infrastructure/ api/`) |
172
+ | `PORT-001..003` | needs an `application/ports/` directory |
173
+ | `TERM-NNN` | needs a vocabulary in `[tool.lanorme.domain_terms]` |
174
+ | `PATH-001` / `STALE-001` | need forbidden dirs / stale tokens configured |
175
+ | `KWARG-001` | keyword-only call sites; a strong house style |
176
+ | `NAMING-001/002` | CRUD method prefixes; conflicts with domain naming |
177
+ | `AAA-001/002` | Arrange-Act-Assert markers and DRY in tests |
178
+ | `CMT-005` | restating-comment detector; experimental, precision-first |
179
+ | `ATTR-001/002` | `hasattr`/`getattr`/`setattr` with a literal attribute name; a missing-type smell |
180
+ | `PROSE-001..003` | em dashes, US spelling and emoji in Markdown or comments |
181
+
182
+ ## Writing a check
183
+
184
+ A check is any object with `name`, `description`, `rules`, and a `run` method:
185
+
186
+ ```python
187
+ from lanorme import CheckResult, Status, Violation, register
188
+
189
+
190
+ class MyCheck:
191
+ name = "my_check"
192
+ description = "What it enforces"
193
+ rules = ["MYCODE-001: the rule, in one line"]
194
+
195
+ def run(self, *, src_root: str) -> CheckResult:
196
+ violations: list[Violation] = []
197
+ # inspect files under src_root
198
+ status = Status.FAIL if violations else Status.PASS
199
+ return CheckResult(check=self.name, status=status, violations=violations)
200
+
201
+
202
+ register(MyCheck())
203
+ ```
204
+
205
+ Drop it in `lanorme/checks/`, ship it under the `lanorme.checks` entry-point
206
+ group, or point at it with `[tool.lanorme] plugins = [...]`. LaNorme finds it
207
+ and runs it.
208
+
209
+ ## Versioning
210
+
211
+ The public surface is the rule codes you put in `select` / `ignore` /
212
+ `per-file-ignores` and the config keys under `[tool.lanorme]`. Renaming a rule,
213
+ dropping one, or turning a default-on rule off is a breaking change; adding a
214
+ rule or a new config key with a sensible default is not. Before 1.0, breaking
215
+ changes land in minor releases and are listed in
216
+ [`CHANGELOG.md`](CHANGELOG.md).
217
+
218
+ ## License
219
+
220
+ MIT. See [`LICENSE`](LICENSE).