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.
- chisel_checker-0.1.1/.gitignore +220 -0
- chisel_checker-0.1.1/.grimp_cache/.gitignore +2 -0
- chisel_checker-0.1.1/.grimp_cache/84239c1bc60433074e3d05309a9d0d09848ffd65.data.json +1 -0
- chisel_checker-0.1.1/.grimp_cache/CACHEDIR.TAG +3 -0
- chisel_checker-0.1.1/.grimp_cache/af276a573254b22776d0a8d1fe8e976b0694e680.data.json +1 -0
- chisel_checker-0.1.1/.grimp_cache/chisel.meta.json +1 -0
- chisel_checker-0.1.1/.grimp_cache/myapp.meta.json +1 -0
- chisel_checker-0.1.1/PKG-INFO +107 -0
- chisel_checker-0.1.1/README.md +92 -0
- chisel_checker-0.1.1/pyproject.toml +32 -0
- chisel_checker-0.1.1/src/chisel/__init__.py +4 -0
- chisel_checker-0.1.1/src/chisel/checker/__init__.py +11 -0
- chisel_checker-0.1.1/src/chisel/checker/config.py +9 -0
- chisel_checker-0.1.1/src/chisel/checker/controllers/check_controller.py +124 -0
- chisel_checker-0.1.1/src/chisel/checker/errors.py +16 -0
- chisel_checker-0.1.1/src/chisel/checker/factory.py +45 -0
- chisel_checker-0.1.1/src/chisel/checker/models/__init__.py +20 -0
- chisel_checker-0.1.1/src/chisel/checker/models/exemption.py +8 -0
- chisel_checker-0.1.1/src/chisel/checker/models/file_info.py +14 -0
- chisel_checker-0.1.1/src/chisel/checker/models/import_edge.py +10 -0
- chisel_checker-0.1.1/src/chisel/checker/models/layer.py +19 -0
- chisel_checker-0.1.1/src/chisel/checker/models/project_info.py +12 -0
- chisel_checker-0.1.1/src/chisel/checker/models/result.py +17 -0
- chisel_checker-0.1.1/src/chisel/checker/models/severity.py +8 -0
- chisel_checker-0.1.1/src/chisel/checker/models/violation.py +13 -0
- chisel_checker-0.1.1/src/chisel/checker/reporter.py +70 -0
- chisel_checker-0.1.1/src/chisel/checker/repositories/exception_registry.py +60 -0
- chisel_checker-0.1.1/src/chisel/checker/repositories/file_discovery.py +119 -0
- chisel_checker-0.1.1/src/chisel/checker/repositories/file_reader.py +8 -0
- chisel_checker-0.1.1/src/chisel/checker/repositories/import_graph.py +100 -0
- chisel_checker-0.1.1/src/chisel/checker/repositories/protocols.py +25 -0
- chisel_checker-0.1.1/src/chisel/checker/services/app_file.py +125 -0
- chisel_checker-0.1.1/src/chisel/checker/services/check_test_structure.py +331 -0
- chisel_checker-0.1.1/src/chisel/checker/services/complexity.py +203 -0
- chisel_checker-0.1.1/src/chisel/checker/services/concurrency.py +80 -0
- chisel_checker-0.1.1/src/chisel/checker/services/config_startup.py +67 -0
- chisel_checker-0.1.1/src/chisel/checker/services/error_flow.py +95 -0
- chisel_checker-0.1.1/src/chisel/checker/services/import_boundary.py +285 -0
- chisel_checker-0.1.1/src/chisel/checker/services/project_structure.py +216 -0
- chisel_checker-0.1.1/src/chisel/checker/services/protocols.py +72 -0
- chisel_checker-0.1.1/src/chisel/checker/services/session.py +76 -0
- chisel_checker-0.1.1/src/chisel/checker/services/structural.py +799 -0
- chisel_checker-0.1.1/src/chisel/checker/services/suppression.py +98 -0
- chisel_checker-0.1.1/src/chisel/cli/main.py +126 -0
- chisel_checker-0.1.1/src/chisel/py.typed +0 -0
- chisel_checker-0.1.1/tests/__init__.py +0 -0
- chisel_checker-0.1.1/tests/conftest.py +9 -0
- chisel_checker-0.1.1/tests/fakes/__init__.py +4 -0
- chisel_checker-0.1.1/tests/fakes/fake_import_graph.py +27 -0
- chisel_checker-0.1.1/tests/unit/__init__.py +0 -0
- chisel_checker-0.1.1/tests/unit/controllers/__init__.py +0 -0
- chisel_checker-0.1.1/tests/unit/controllers/test_check_controller.py +59 -0
- chisel_checker-0.1.1/tests/unit/repositories/__init__.py +0 -0
- chisel_checker-0.1.1/tests/unit/repositories/test_exception_registry.py +173 -0
- chisel_checker-0.1.1/tests/unit/repositories/test_file_discovery.py +74 -0
- chisel_checker-0.1.1/tests/unit/repositories/test_file_reader.py +15 -0
- chisel_checker-0.1.1/tests/unit/repositories/test_import_graph.py +24 -0
- chisel_checker-0.1.1/tests/unit/services/__init__.py +0 -0
- chisel_checker-0.1.1/tests/unit/services/test_app_file.py +70 -0
- chisel_checker-0.1.1/tests/unit/services/test_check_test_structure.py +238 -0
- chisel_checker-0.1.1/tests/unit/services/test_complexity.py +209 -0
- chisel_checker-0.1.1/tests/unit/services/test_concurrency.py +71 -0
- chisel_checker-0.1.1/tests/unit/services/test_config_startup.py +43 -0
- chisel_checker-0.1.1/tests/unit/services/test_error_flow.py +55 -0
- chisel_checker-0.1.1/tests/unit/services/test_import_boundary.py +171 -0
- chisel_checker-0.1.1/tests/unit/services/test_project_structure.py +78 -0
- chisel_checker-0.1.1/tests/unit/services/test_project_structure_coverage.py +84 -0
- chisel_checker-0.1.1/tests/unit/services/test_session.py +51 -0
- chisel_checker-0.1.1/tests/unit/services/test_structural.py +517 -0
- chisel_checker-0.1.1/tests/unit/services/test_suppression.py +74 -0
- chisel_checker-0.1.1/tests/unit/test_cli.py +68 -0
- 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 @@
|
|
|
1
|
+
{"myapp":[]}
|
|
@@ -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,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,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
|
+
)
|