chisel-checker 0.1.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 (72) hide show
  1. chisel_checker-0.1.1/.gitignore +220 -0
  2. chisel_checker-0.1.1/.grimp_cache/.gitignore +2 -0
  3. chisel_checker-0.1.1/.grimp_cache/84239c1bc60433074e3d05309a9d0d09848ffd65.data.json +1 -0
  4. chisel_checker-0.1.1/.grimp_cache/CACHEDIR.TAG +3 -0
  5. chisel_checker-0.1.1/.grimp_cache/af276a573254b22776d0a8d1fe8e976b0694e680.data.json +1 -0
  6. chisel_checker-0.1.1/.grimp_cache/chisel.meta.json +1 -0
  7. chisel_checker-0.1.1/.grimp_cache/myapp.meta.json +1 -0
  8. chisel_checker-0.1.1/PKG-INFO +107 -0
  9. chisel_checker-0.1.1/README.md +92 -0
  10. chisel_checker-0.1.1/pyproject.toml +32 -0
  11. chisel_checker-0.1.1/src/chisel/__init__.py +4 -0
  12. chisel_checker-0.1.1/src/chisel/checker/__init__.py +11 -0
  13. chisel_checker-0.1.1/src/chisel/checker/config.py +9 -0
  14. chisel_checker-0.1.1/src/chisel/checker/controllers/check_controller.py +124 -0
  15. chisel_checker-0.1.1/src/chisel/checker/errors.py +16 -0
  16. chisel_checker-0.1.1/src/chisel/checker/factory.py +45 -0
  17. chisel_checker-0.1.1/src/chisel/checker/models/__init__.py +20 -0
  18. chisel_checker-0.1.1/src/chisel/checker/models/exemption.py +8 -0
  19. chisel_checker-0.1.1/src/chisel/checker/models/file_info.py +14 -0
  20. chisel_checker-0.1.1/src/chisel/checker/models/import_edge.py +10 -0
  21. chisel_checker-0.1.1/src/chisel/checker/models/layer.py +19 -0
  22. chisel_checker-0.1.1/src/chisel/checker/models/project_info.py +12 -0
  23. chisel_checker-0.1.1/src/chisel/checker/models/result.py +17 -0
  24. chisel_checker-0.1.1/src/chisel/checker/models/severity.py +8 -0
  25. chisel_checker-0.1.1/src/chisel/checker/models/violation.py +13 -0
  26. chisel_checker-0.1.1/src/chisel/checker/reporter.py +70 -0
  27. chisel_checker-0.1.1/src/chisel/checker/repositories/exception_registry.py +60 -0
  28. chisel_checker-0.1.1/src/chisel/checker/repositories/file_discovery.py +119 -0
  29. chisel_checker-0.1.1/src/chisel/checker/repositories/file_reader.py +8 -0
  30. chisel_checker-0.1.1/src/chisel/checker/repositories/import_graph.py +100 -0
  31. chisel_checker-0.1.1/src/chisel/checker/repositories/protocols.py +25 -0
  32. chisel_checker-0.1.1/src/chisel/checker/services/app_file.py +125 -0
  33. chisel_checker-0.1.1/src/chisel/checker/services/check_test_structure.py +331 -0
  34. chisel_checker-0.1.1/src/chisel/checker/services/complexity.py +203 -0
  35. chisel_checker-0.1.1/src/chisel/checker/services/concurrency.py +80 -0
  36. chisel_checker-0.1.1/src/chisel/checker/services/config_startup.py +67 -0
  37. chisel_checker-0.1.1/src/chisel/checker/services/error_flow.py +95 -0
  38. chisel_checker-0.1.1/src/chisel/checker/services/import_boundary.py +285 -0
  39. chisel_checker-0.1.1/src/chisel/checker/services/project_structure.py +216 -0
  40. chisel_checker-0.1.1/src/chisel/checker/services/protocols.py +72 -0
  41. chisel_checker-0.1.1/src/chisel/checker/services/session.py +76 -0
  42. chisel_checker-0.1.1/src/chisel/checker/services/structural.py +799 -0
  43. chisel_checker-0.1.1/src/chisel/checker/services/suppression.py +98 -0
  44. chisel_checker-0.1.1/src/chisel/cli/main.py +126 -0
  45. chisel_checker-0.1.1/src/chisel/py.typed +0 -0
  46. chisel_checker-0.1.1/tests/__init__.py +0 -0
  47. chisel_checker-0.1.1/tests/conftest.py +9 -0
  48. chisel_checker-0.1.1/tests/fakes/__init__.py +4 -0
  49. chisel_checker-0.1.1/tests/fakes/fake_import_graph.py +27 -0
  50. chisel_checker-0.1.1/tests/unit/__init__.py +0 -0
  51. chisel_checker-0.1.1/tests/unit/controllers/__init__.py +0 -0
  52. chisel_checker-0.1.1/tests/unit/controllers/test_check_controller.py +59 -0
  53. chisel_checker-0.1.1/tests/unit/repositories/__init__.py +0 -0
  54. chisel_checker-0.1.1/tests/unit/repositories/test_exception_registry.py +173 -0
  55. chisel_checker-0.1.1/tests/unit/repositories/test_file_discovery.py +74 -0
  56. chisel_checker-0.1.1/tests/unit/repositories/test_file_reader.py +15 -0
  57. chisel_checker-0.1.1/tests/unit/repositories/test_import_graph.py +24 -0
  58. chisel_checker-0.1.1/tests/unit/services/__init__.py +0 -0
  59. chisel_checker-0.1.1/tests/unit/services/test_app_file.py +70 -0
  60. chisel_checker-0.1.1/tests/unit/services/test_check_test_structure.py +238 -0
  61. chisel_checker-0.1.1/tests/unit/services/test_complexity.py +209 -0
  62. chisel_checker-0.1.1/tests/unit/services/test_concurrency.py +71 -0
  63. chisel_checker-0.1.1/tests/unit/services/test_config_startup.py +43 -0
  64. chisel_checker-0.1.1/tests/unit/services/test_error_flow.py +55 -0
  65. chisel_checker-0.1.1/tests/unit/services/test_import_boundary.py +171 -0
  66. chisel_checker-0.1.1/tests/unit/services/test_project_structure.py +78 -0
  67. chisel_checker-0.1.1/tests/unit/services/test_project_structure_coverage.py +84 -0
  68. chisel_checker-0.1.1/tests/unit/services/test_session.py +51 -0
  69. chisel_checker-0.1.1/tests/unit/services/test_structural.py +517 -0
  70. chisel_checker-0.1.1/tests/unit/services/test_suppression.py +74 -0
  71. chisel_checker-0.1.1/tests/unit/test_cli.py +68 -0
  72. chisel_checker-0.1.1/uv.lock +243 -0
@@ -0,0 +1,220 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ *.lcov
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ # Pipfile.lock
97
+
98
+ # UV
99
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # uv.lock
103
+
104
+ # poetry
105
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
107
+ # commonly ignored for libraries.
108
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109
+ # poetry.lock
110
+ # poetry.toml
111
+
112
+ # pdm
113
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
114
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
115
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
116
+ # pdm.lock
117
+ # pdm.toml
118
+ .pdm-python
119
+ .pdm-build/
120
+
121
+ # pixi
122
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
123
+ # pixi.lock
124
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
125
+ # in the .venv directory. It is recommended not to include this directory in version control.
126
+ .pixi/*
127
+ !.pixi/config.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule*
134
+ celerybeat.pid
135
+
136
+ # Redis
137
+ *.rdb
138
+ *.aof
139
+ *.pid
140
+
141
+ # RabbitMQ
142
+ mnesia/
143
+ rabbitmq/
144
+ rabbitmq-data/
145
+
146
+ # ActiveMQ
147
+ activemq-data/
148
+
149
+ # SageMath parsed files
150
+ *.sage.py
151
+
152
+ # Environments
153
+ .env
154
+ .envrc
155
+ .venv
156
+ env/
157
+ venv/
158
+ ENV/
159
+ env.bak/
160
+ venv.bak/
161
+
162
+ # Spyder project settings
163
+ .spyderproject
164
+ .spyproject
165
+
166
+ # Rope project settings
167
+ .ropeproject
168
+
169
+ # mkdocs documentation
170
+ /site
171
+
172
+ # mypy
173
+ .mypy_cache/
174
+ .dmypy.json
175
+ dmypy.json
176
+
177
+ # Pyre type checker
178
+ .pyre/
179
+
180
+ # pytype static type analyzer
181
+ .pytype/
182
+
183
+ # Cython debug symbols
184
+ cython_debug/
185
+
186
+ # PyCharm
187
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
188
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
189
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
190
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
191
+ # .idea/
192
+
193
+ # Abstra
194
+ # Abstra is an AI-powered process automation framework.
195
+ # Ignore directories containing user credentials, local state, and settings.
196
+ # Learn more at https://abstra.io/docs
197
+ .abstra/
198
+
199
+ # Visual Studio Code
200
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
201
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
202
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
203
+ # you could uncomment the following to ignore the entire vscode folder
204
+ # .vscode/
205
+ # Temporary file for partial code execution
206
+ tempCodeRunnerFile.py
207
+
208
+ # Ruff stuff:
209
+ .ruff_cache/
210
+
211
+ # PyPI configuration file
212
+ .pypirc
213
+
214
+ # Marimo
215
+ marimo/_static/
216
+ marimo/_lsp/
217
+ __marimo__/
218
+
219
+ # Streamlit
220
+ .streamlit/secrets.toml
@@ -0,0 +1,2 @@
1
+ # Automatically created by Grimp.
2
+ *
@@ -0,0 +1,3 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag automatically created by Grimp.
3
+ # For information about cache directory tags see https://bford.info/cachedir/
@@ -0,0 +1 @@
1
+ {"chisel.checker.models.file_info":[["pathlib",4,"from pathlib import Path"],["ast",2,"import ast"],["chisel.checker.models.layer",6,"from chisel.checker.models.layer import Layer"],["dataclasses",3,"from dataclasses import dataclass, field"]],"chisel.checker.models.exemption":[["dataclasses",1,"from dataclasses import dataclass, field"]],"chisel.checker.models":[["chisel.checker.models.import_edge",4,"from chisel.checker.models.import_edge import ImportEdge"],["chisel.checker.models.violation",9,"from chisel.checker.models.violation import Violation"],["chisel.checker.models.exemption",2,"from chisel.checker.models.exemption import Exemption"],["chisel.checker.models.project_info",6,"from chisel.checker.models.project_info import ProjectInfo"],["chisel.checker.models.file_info",3,"from chisel.checker.models.file_info import FileInfo"],["chisel.checker.models.layer",5,"from chisel.checker.models.layer import Layer"],["chisel.checker.models.severity",8,"from chisel.checker.models.severity import Severity"],["chisel.checker.models.result",7,"from chisel.checker.models.result import CheckResult"]],"chisel.checker":[["chisel.checker.reporter",3,"from chisel.checker.reporter import Reporter"],["chisel.checker.factory",2,"from chisel.checker.factory import CheckerFactory"]],"chisel.checker.models.project_info":[["dataclasses",2,"from dataclasses import dataclass, field"],["chisel.checker.models.file_info",5,"from chisel.checker.models.file_info import FileInfo"],["pathlib",3,"from pathlib import Path"]],"chisel.checker.errors":[],"chisel.checker.models.import_edge":[["dataclasses",2,"from dataclasses import dataclass"]],"chisel.checker.models.severity":[["enum",2,"from enum import Enum"]],"chisel.checker.config":[["pathlib",3,"from pathlib import Path"],["dataclasses",2,"from dataclasses import dataclass, field"]],"chisel.checker.reporter":[["chisel.checker.models.violation",10,"from chisel.checker.models.violation import Violation"],["json",2,"import json"],["chisel.checker.models.result",8,"from chisel.checker.models.result import CheckResult"],["chisel.checker.models.severity",9,"from chisel.checker.models.severity import Severity"],["dataclasses",3,"from dataclasses import dataclass"],["rich",5,"from rich.console import Console"],["rich",6,"from rich.table import Table"]],"chisel.checker.models.violation":[["dataclasses",2,"from dataclasses import dataclass"],["chisel.checker.models.severity",4,"from chisel.checker.models.severity import Severity"]],"chisel.checker.models.result":[["dataclasses",2,"from dataclasses import dataclass, field"],["chisel.checker.models.violation",4,"from chisel.checker.models.violation import Violation"]],"chisel.checker.models.layer":[["enum",2,"from enum import Enum"]],"chisel.checker.factory":[["chisel",7,"from chisel.checker.services.app_file import AppFileService"],["chisel",6,"from chisel.checker.repositories.protocols import IImportGraph"],["chisel",16,"from chisel.checker.services.structural import StructuralService"],["chisel",14,"from chisel.checker.services.protocols import ICheckerService"],["chisel",5,"from chisel.checker.repositories.import_graph import ImportGraph"],["dataclasses",2,"from dataclasses import dataclass, field"],["chisel",11,"from chisel.checker.services.error_flow import ErrorFlowService"],["chisel",8,"from chisel.checker.services.complexity import ComplexityService"],["chisel",17,"from chisel.checker.services.suppression import SuppressionService"],["chisel",13,"from chisel.checker.services.project_structure import ProjectStructureService"],["chisel",4,"from chisel.checker.controllers.check_controller import CheckController"],["chisel",18,"from chisel.checker.services.check_test_structure import CheckTestStructureService"],["chisel",12,"from chisel.checker.services.import_boundary import ImportBoundaryService"],["chisel",9,"from chisel.checker.services.concurrency import ConcurrencyService"],["chisel",15,"from chisel.checker.services.session import SessionService"],["chisel",10,"from chisel.checker.services.config_startup import ConfigStartupService"]],"chisel":[["chisel.checker",2,"from chisel.checker import check_project"]]}
@@ -0,0 +1 @@
1
+ {"chisel.checker.models": 1780432167.138717, "chisel.checker.config": 1780432167.138717, "chisel.checker.models.result": 1780432167.1388886, "chisel.checker.models.project_info": 1780432167.1388886, "chisel.checker.reporter": 1780432167.1388886, "chisel.checker": 1780432167.138681, "chisel.checker.models.file_info": 1780432167.1388886, "chisel.checker.factory": 1780432167.138717, "chisel.checker.models.violation": 1780432167.1388886, "chisel": 1780432167.1386375, "chisel.checker.models.import_edge": 1780432167.1388886, "chisel.checker.errors": 1780432167.138717, "chisel.checker.models.exemption": 1780432167.1388886, "chisel.checker.models.severity": 1780432167.1388886, "chisel.checker.models.layer": 1780432167.1388886}
@@ -0,0 +1 @@
1
+ {"myapp": 1780432174.5208745}
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: chisel_checker
3
+ Version: 0.1.1
4
+ Summary: Opinionated architecture constraint checker
5
+ Author-email: ChidiRNweke <chidi125@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: grimp>=3.0
8
+ Requires-Dist: radon>=6.0
9
+ Requires-Dist: rich>=13.0
10
+ Requires-Dist: typer>=0.12
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == 'dev'
13
+ Requires-Dist: tomli>=2.0; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # chisel
17
+
18
+ Opinionated architecture constraint checker for FastAPI backends. Designed to catch the patterns agents get wrong — use it as a pre-commit hook to block violations, or run it interactively to steer an agent while it's working.
19
+
20
+ It pairs with [agent skills](https://github.com/ChidiRnweke/chisel/tree/master/skills): the skill teaches the agent the right pattern, the checker enforces it deterministically. Run `chisel explain <rule-id>` to get fix guidance the agent can consume directly.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install chisel
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```bash
31
+ chisel check ./backend # check a project
32
+ chisel rules # list all ~55 rules grouped by category
33
+ chisel explain structural:print-banned # detailed fix guidance
34
+ chisel check . --json # machine-readable output for agents
35
+ chisel check . --no-strict # skip src-layout enforcement
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ | Command | Description |
41
+ |---|---|
42
+ | `chisel check [path]` | Scan a project for architectural violations |
43
+ | `chisel check . --json` | Output violations as structured JSON |
44
+ | `chisel check . --strict/--no-strict` | Toggle src-layout and build-config enforcement |
45
+ | `chisel rules` | List all rules, grouped by category |
46
+ | `chisel rules --json` | Machine-readable rule listing |
47
+ | `chisel explain <rule-id>` | Detailed description + fix guidance for a rule |
48
+ | `chisel explain <category>` | All rules in a category (e.g. `structural`) |
49
+
50
+ ## What gets checked
51
+
52
+ | Category | What it enforces |
53
+ |---|---|
54
+ | **Import Boundaries** | Layer-based import restrictions — services can't import SQLAlchemy, routes can't import services, `HTTPException` only in `error_handlers.py`, concrete services only in `factory.py` |
55
+ | **Structural Invariants** | No `getattr`/`setattr`, no `%` string formatting, no f-strings in logger calls, no `print()` in `src/`, `@dataclass` uses `slots=True`, `AppFactory` has zero logic, `match/case` only in error handlers |
56
+ | **Complexity** | Controller method ≤30 LoC, route handler ≤20 LoC, factory cyclomatic complexity = 1, `app.py` ≤50 LoC |
57
+ | **Concurrency** | `asyncio.gather` banned unconditionally — use `asyncio.TaskGroup` |
58
+ | **Error Flow** | `AppError` subclasses never contain HTTP status codes; status codes decided only in `error_handlers.py` |
59
+ | **Project Structure** | `pyproject.toml` only (no `setup.py`), env vars read at startup only via `AppConfig`, no `.py` files at project root |
60
+ | **Test Structure** | Test files only under `tests/unit/`, `tests/integration/`, `tests/e2e/`; one `assert` per test; no mocking libraries; test names describe invariants; `pytest.mark.skip` needs a reason; no `TestClient` outside e2e |
61
+
62
+ ## Exceptions
63
+
64
+ Create a `chisel-exceptions.toml` at your project root:
65
+
66
+ ```toml
67
+ [[exceptions]]
68
+ files = ["src/legacy/*.py", "src/cli/main.py"]
69
+ rules = ["structural:print-banned"]
70
+ reason = "CLI requires stdout output"
71
+ ```
72
+
73
+ `*` matches all rules. A category prefix like `structural` matches all rules in that category.
74
+
75
+ Inline suppression per line: `# noqa: rule-id — reason`.
76
+
77
+ ## Severity tiers
78
+
79
+ | Tier | Behaviour |
80
+ |---|---|
81
+ | **ERROR** | Blocks commit, no override. Import boundary violations, `asyncio.gather`, `getattr`/`setattr`, f-string in logger, `print()` in `src/`. |
82
+ | **WARNING** | Blocks commit, suppressible with a reason comment. Controller/route LoC thresholds, test coverage gaps. |
83
+ | **INFO** | Logged, non-blocking. |
84
+
85
+ ## How it works
86
+
87
+ ```
88
+ models/ Pure data — Violation, Severity, FileInfo, ProjectInfo
89
+ services/ One service per rule category, self-describing via describe_rules()
90
+ repositories/ File discovery, import graph analysis (grimp)
91
+ controllers/ Orchestrates all services, filters exceptions, suppresses via noqa
92
+ factory.py Zero-logic DI — wires all services into the controller
93
+ cli/main.py Typer-based CLI — check, rules, explain
94
+ reporter.py Coloured terminal output via rich
95
+ ```
96
+
97
+ Adding a rule requires one new check method in a service + one `describe_rules()` entry. The CLI discovers it automatically.
98
+
99
+ ## Development
100
+
101
+ ```bash
102
+ pip install -e ".[dev]"
103
+ pytest tests/ -q
104
+ ```
105
+
106
+ The checker is self-validating: `chisel check . --strict` produces zero violations on its own source.
107
+
@@ -0,0 +1,92 @@
1
+ # chisel
2
+
3
+ Opinionated architecture constraint checker for FastAPI backends. Designed to catch the patterns agents get wrong — use it as a pre-commit hook to block violations, or run it interactively to steer an agent while it's working.
4
+
5
+ It pairs with [agent skills](https://github.com/ChidiRnweke/chisel/tree/master/skills): the skill teaches the agent the right pattern, the checker enforces it deterministically. Run `chisel explain <rule-id>` to get fix guidance the agent can consume directly.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install chisel
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ chisel check ./backend # check a project
17
+ chisel rules # list all ~55 rules grouped by category
18
+ chisel explain structural:print-banned # detailed fix guidance
19
+ chisel check . --json # machine-readable output for agents
20
+ chisel check . --no-strict # skip src-layout enforcement
21
+ ```
22
+
23
+ ## Commands
24
+
25
+ | Command | Description |
26
+ |---|---|
27
+ | `chisel check [path]` | Scan a project for architectural violations |
28
+ | `chisel check . --json` | Output violations as structured JSON |
29
+ | `chisel check . --strict/--no-strict` | Toggle src-layout and build-config enforcement |
30
+ | `chisel rules` | List all rules, grouped by category |
31
+ | `chisel rules --json` | Machine-readable rule listing |
32
+ | `chisel explain <rule-id>` | Detailed description + fix guidance for a rule |
33
+ | `chisel explain <category>` | All rules in a category (e.g. `structural`) |
34
+
35
+ ## What gets checked
36
+
37
+ | Category | What it enforces |
38
+ |---|---|
39
+ | **Import Boundaries** | Layer-based import restrictions — services can't import SQLAlchemy, routes can't import services, `HTTPException` only in `error_handlers.py`, concrete services only in `factory.py` |
40
+ | **Structural Invariants** | No `getattr`/`setattr`, no `%` string formatting, no f-strings in logger calls, no `print()` in `src/`, `@dataclass` uses `slots=True`, `AppFactory` has zero logic, `match/case` only in error handlers |
41
+ | **Complexity** | Controller method ≤30 LoC, route handler ≤20 LoC, factory cyclomatic complexity = 1, `app.py` ≤50 LoC |
42
+ | **Concurrency** | `asyncio.gather` banned unconditionally — use `asyncio.TaskGroup` |
43
+ | **Error Flow** | `AppError` subclasses never contain HTTP status codes; status codes decided only in `error_handlers.py` |
44
+ | **Project Structure** | `pyproject.toml` only (no `setup.py`), env vars read at startup only via `AppConfig`, no `.py` files at project root |
45
+ | **Test Structure** | Test files only under `tests/unit/`, `tests/integration/`, `tests/e2e/`; one `assert` per test; no mocking libraries; test names describe invariants; `pytest.mark.skip` needs a reason; no `TestClient` outside e2e |
46
+
47
+ ## Exceptions
48
+
49
+ Create a `chisel-exceptions.toml` at your project root:
50
+
51
+ ```toml
52
+ [[exceptions]]
53
+ files = ["src/legacy/*.py", "src/cli/main.py"]
54
+ rules = ["structural:print-banned"]
55
+ reason = "CLI requires stdout output"
56
+ ```
57
+
58
+ `*` matches all rules. A category prefix like `structural` matches all rules in that category.
59
+
60
+ Inline suppression per line: `# noqa: rule-id — reason`.
61
+
62
+ ## Severity tiers
63
+
64
+ | Tier | Behaviour |
65
+ |---|---|
66
+ | **ERROR** | Blocks commit, no override. Import boundary violations, `asyncio.gather`, `getattr`/`setattr`, f-string in logger, `print()` in `src/`. |
67
+ | **WARNING** | Blocks commit, suppressible with a reason comment. Controller/route LoC thresholds, test coverage gaps. |
68
+ | **INFO** | Logged, non-blocking. |
69
+
70
+ ## How it works
71
+
72
+ ```
73
+ models/ Pure data — Violation, Severity, FileInfo, ProjectInfo
74
+ services/ One service per rule category, self-describing via describe_rules()
75
+ repositories/ File discovery, import graph analysis (grimp)
76
+ controllers/ Orchestrates all services, filters exceptions, suppresses via noqa
77
+ factory.py Zero-logic DI — wires all services into the controller
78
+ cli/main.py Typer-based CLI — check, rules, explain
79
+ reporter.py Coloured terminal output via rich
80
+ ```
81
+
82
+ Adding a rule requires one new check method in a service + one `describe_rules()` entry. The CLI discovers it automatically.
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ pip install -e ".[dev]"
88
+ pytest tests/ -q
89
+ ```
90
+
91
+ The checker is self-validating: `chisel check . --strict` produces zero violations on its own source.
92
+
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "chisel_checker"
3
+ version = "0.1.1"
4
+ description = "Opinionated architecture constraint checker"
5
+ readme = {file = "README.md", content-type = "text/markdown"}
6
+ authors = [
7
+ { name = "ChidiRNweke", email = "chidi125@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "grimp>=3.0",
12
+ "radon>=6.0",
13
+ "typer>=0.12",
14
+ "rich>=13.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=8.0",
20
+ "tomli>=2.0",
21
+ ]
22
+
23
+ [project.scripts]
24
+ chisel = "chisel.cli.main:main"
25
+
26
+ [build-system]
27
+ requires = ["hatchling"]
28
+ build-backend = "hatchling.build"
29
+
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/chisel"]
@@ -0,0 +1,4 @@
1
+
2
+ from chisel.checker import check_project
3
+
4
+ __all__ = ["check_project"]
@@ -0,0 +1,11 @@
1
+
2
+ from chisel.checker.factory import CheckerFactory
3
+ from chisel.checker.reporter import Reporter
4
+
5
+
6
+ def check_project(project_path: str) -> None:
7
+ factory = CheckerFactory()
8
+ controller = factory.create_controller()
9
+ result = controller.check(project_path)
10
+ reporter = Reporter()
11
+ reporter.report(result)
@@ -0,0 +1,9 @@
1
+
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class CheckerConfig:
8
+ target_path: Path = field(default_factory=Path.cwd)
9
+ strict: bool = True
@@ -0,0 +1,124 @@
1
+
2
+ import ast
3
+ import sys
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from chisel.checker.errors import ImportGraphError
8
+ from chisel.checker.models.project_info import ProjectInfo
9
+ from chisel.checker.models.result import CheckResult
10
+ from chisel.checker.models.severity import Severity
11
+ from chisel.checker.models.violation import Violation
12
+ from chisel.checker.repositories.file_discovery import FileDiscovery
13
+ from chisel.checker.repositories.file_reader import FileReader
14
+ from chisel.checker.repositories.import_graph import ImportGraph
15
+ from chisel.checker.repositories.protocols import IImportGraph
16
+ from chisel.checker.services.protocols import ICheckerService, ISuppressionService
17
+
18
+
19
+ from chisel.checker.repositories.exception_registry import ExceptionRegistry
20
+
21
+
22
+ @dataclass(slots=True)
23
+ class CheckController:
24
+ _suppression: ISuppressionService
25
+ _services: list[ICheckerService] = field(default_factory=list)
26
+ _import_graph: IImportGraph = field(default_factory=ImportGraph) # noqa
27
+
28
+ def check(self, project_path: str) -> CheckResult:
29
+ root = Path(project_path).resolve()
30
+ project = self._prepare_project(root)
31
+ violations: list[Violation] = []
32
+
33
+ if project.package_name:
34
+ try:
35
+ self._build_import_graph(root, project.package_name)
36
+ except ImportGraphError as exc:
37
+ violations.append(
38
+ Violation(
39
+ file="<import-graph>",
40
+ line=0,
41
+ severity=Severity.WARNING,
42
+ rule_id="import-graph:build-failed",
43
+ message=str(exc),
44
+ )
45
+ )
46
+
47
+ violations.extend(self._run_services(project))
48
+ violations = self._apply_exceptions(root, violations)
49
+ sources = self._collect_sources(project)
50
+ active = self._suppression.check(violations, sources)
51
+ return self._summarize(active, len(project.files))
52
+
53
+ def _prepare_project(self, root: Path) -> ProjectInfo:
54
+ discovery = FileDiscovery()
55
+ project = discovery.discover(root)
56
+ reader = FileReader()
57
+ populated = [
58
+ self._load_file(f, root, reader) for f in project.files
59
+ ]
60
+ return ProjectInfo(
61
+ root_path=project.root_path,
62
+ files=populated,
63
+ package_name=project.package_name,
64
+ )
65
+
66
+ def _load_file(
67
+ self, file_info, root: Path, reader: FileReader
68
+ ):
69
+ try:
70
+ source = reader.read(root / file_info.path)
71
+ return type(file_info)(
72
+ path=file_info.path,
73
+ layer=file_info.layer,
74
+ source=source,
75
+ ast_tree=ast.parse(source),
76
+ )
77
+ except Exception:
78
+ return file_info
79
+
80
+ def _build_import_graph(self, root: Path, package_name: str) -> None:
81
+ src = root / "src"
82
+ src_path = str(src) if src.is_dir() else str(root)
83
+ original_path = list(sys.path)
84
+ try:
85
+ if src_path not in sys.path:
86
+ sys.path.insert(0, src_path)
87
+ self._import_graph.build(root, package_name)
88
+ except Exception as exc:
89
+ raise ImportGraphError(
90
+ f"Failed to build import graph for "
91
+ f"'{package_name}': {exc}"
92
+ ) from exc
93
+ finally:
94
+ sys.path = original_path
95
+
96
+ def _run_services(self, project: ProjectInfo) -> list[Violation]:
97
+ violations: list[Violation] = []
98
+ for service in self._services:
99
+ violations.extend(service.check(project))
100
+ return violations
101
+
102
+ def _collect_sources(self, project: ProjectInfo) -> dict[str, str]:
103
+ return {str(f.path): f.source for f in project.files}
104
+
105
+ def _apply_exceptions(
106
+ self, root: Path, violations: list[Violation]
107
+ ) -> list[Violation]:
108
+ registry = ExceptionRegistry()
109
+ registry.load(root)
110
+ return registry.filter(violations)
111
+
112
+ def _summarize(
113
+ self, violations: list[Violation], files_checked: int
114
+ ) -> CheckResult:
115
+ errors = sum(1 for v in violations if v.severity == Severity.ERROR)
116
+ warnings = sum(1 for v in violations if v.severity == Severity.WARNING)
117
+ info = sum(1 for v in violations if v.severity == Severity.INFO)
118
+ return CheckResult(
119
+ violations=violations,
120
+ errors=errors,
121
+ warnings=warnings,
122
+ info=info,
123
+ files_checked=files_checked,
124
+ )
@@ -0,0 +1,16 @@
1
+
2
+
3
+ class CheckerError(Exception):
4
+ pass
5
+
6
+
7
+ class FileNotFoundError(CheckerError):
8
+ pass
9
+
10
+
11
+ class ImportGraphError(CheckerError):
12
+ pass
13
+
14
+
15
+ class ConfigError(CheckerError):
16
+ pass