maintainability-agent 0.1.0__py3-none-any.whl
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.
- maintainability_agent-0.1.0.dist-info/METADATA +260 -0
- maintainability_agent-0.1.0.dist-info/RECORD +18 -0
- maintainability_agent-0.1.0.dist-info/WHEEL +5 -0
- maintainability_agent-0.1.0.dist-info/entry_points.txt +3 -0
- maintainability_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- maintainability_agent-0.1.0.dist-info/top_level.txt +1 -0
- maintainability_audit/__init__.py +3 -0
- maintainability_audit/__main__.py +4 -0
- maintainability_audit/_metrics_types.py +45 -0
- maintainability_audit/baseline.py +41 -0
- maintainability_audit/cli.py +112 -0
- maintainability_audit/config.py +77 -0
- maintainability_audit/git_tools.py +19 -0
- maintainability_audit/instructions.py +88 -0
- maintainability_audit/metrics.py +249 -0
- maintainability_audit/renderers.py +246 -0
- maintainability_audit/sarif.py +150 -0
- maintainability_audit/scoring.py +47 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maintainability-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Maintainability gate and AI remediation prompt generator for CI.
|
|
5
|
+
Author: Marshall Guillory
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: maintainability,ci,audit,quality,iso-25010,ai-code-review
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-cov>=5; extra == "test"
|
|
21
|
+
Requires-Dist: jsonschema>=4; extra == "test"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
25
|
+
Requires-Dist: jsonschema>=4; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
27
|
+
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# Maintainability Agent
|
|
31
|
+
|
|
32
|
+
A deterministic CI gate + bounded remediation prompt generator for repos that use AI coding agents (Claude, Codex, Cursor, Copilot, Windsurf, …).
|
|
33
|
+
|
|
34
|
+
## Why this exists
|
|
35
|
+
|
|
36
|
+
AI-written code fails in recognizable ways: speculative refactors, duplicated helpers, broad rewrites for narrow bugs, stale comments that sound confident, tests that assert implementation details instead of behavior, architecture drift across modules. SonarQube / CodeClimate / Qlty / ESLint / Ruff / Radon all catch some of this. None of them ship a **bounded prompt back to the agent** that says *"fix only these specific findings, do not refactor outside this scope."*
|
|
37
|
+
|
|
38
|
+
That's the point of this tool:
|
|
39
|
+
|
|
40
|
+
1. Run a deterministic local audit — file size, function size, approximate cyclomatic complexity, duplication, configurable risk patterns, ISO/IEC 25010-inspired 0–5 score.
|
|
41
|
+
2. Emit Markdown, JSON, SARIF, a PR comment, and a baseline for incremental adoption.
|
|
42
|
+
3. Generate an **AI remediation prompt scoped to the actual findings** — bounded, with explicit "don't rewrite the codebase" rules.
|
|
43
|
+
4. Hand that prompt to your agent. Get a small, reviewable fix instead of a 600-line speculative cleanup PR.
|
|
44
|
+
|
|
45
|
+
The remediation prompt is the differentiator. Every other tool in this space stops at "here's a list of findings."
|
|
46
|
+
|
|
47
|
+
## Who it's for
|
|
48
|
+
|
|
49
|
+
- Teams running AI agents in the dev loop who are tired of unbounded agent rewrites and want a CI gate that actively constrains follow-up scope.
|
|
50
|
+
- Repos that want a maintainability gate without paying for SonarQube / CodeClimate / Qlty or sending code to a third party.
|
|
51
|
+
- Solo devs who want a single-binary deterministic audit they can pin in a Makefile, a pre-commit, or a local CI script.
|
|
52
|
+
|
|
53
|
+
## Design principles
|
|
54
|
+
|
|
55
|
+
- **Deterministic first, AI optional.** The audit never calls an LLM by default. The remediation prompt is a generated artifact that you choose to hand to an agent.
|
|
56
|
+
- **Bounded scope.** The remediation prompt explicitly tells the agent to fix the listed findings only — not to embark on architecture cleanup.
|
|
57
|
+
- **No vendor lock-in.** All outputs (Markdown, JSON, SARIF, PR comment) are plain files. Pair this tool with mature analyzers (ESLint, Ruff, Radon, Semgrep, SonarQube, Qlty/Code Climate) — don't replace them.
|
|
58
|
+
- **Pass-the-cost-of-disclosure.** A finding that's "just a warning" never blocks CI alone. Hard gates are configurable + opt-in.
|
|
59
|
+
|
|
60
|
+
See [docs/philosophy.md](docs/philosophy.md) for the longer version.
|
|
61
|
+
|
|
62
|
+
## Self-Audit
|
|
63
|
+
|
|
64
|
+
This repo eats its own dogfood — the tool is run against this codebase as part of CI, and the latest report is checked in at [docs/self-audit.md](docs/self-audit.md):
|
|
65
|
+
|
|
66
|
+
| Metric | Value |
|
|
67
|
+
|---|---:|
|
|
68
|
+
| Overall score | **5.0 / 5 (A+)** |
|
|
69
|
+
| File warnings | 0 |
|
|
70
|
+
| Function warnings | 0 |
|
|
71
|
+
| Duplicate blocks | 0 |
|
|
72
|
+
| Risk findings | 0 |
|
|
73
|
+
| Hard gate failures | 0 |
|
|
74
|
+
|
|
75
|
+
All five ISO/IEC 25010 categories (modularity, reusability, analyzability, modifiability, testability) score 5.0. Regenerate with `maintainability-agent --config maintainability-agent.json --output docs/self-audit.md` (see the file's preamble for the path-sanitization step).
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
Run directly from source:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python3 -m maintainability_audit \
|
|
83
|
+
--root . \
|
|
84
|
+
--config maintainability-agent.json \
|
|
85
|
+
--output maintainability-report.md
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or install editable during development:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python3 -m pip install -e .
|
|
92
|
+
maintainability-audit --root . --config maintainability-agent.json
|
|
93
|
+
maintainability-agent --root . --config maintainability-agent.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Quick Start
|
|
97
|
+
|
|
98
|
+
Copy the example config to your repo root as `maintainability-agent.json`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
cp maintainability-audit.example.json maintainability-agent.json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Run:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
maintainability-agent \
|
|
108
|
+
--config maintainability-agent.json \
|
|
109
|
+
--format markdown \
|
|
110
|
+
--output maintainability-report.md
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Fail CI on hard gates:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
maintainability-agent \
|
|
117
|
+
--config maintainability-agent.json \
|
|
118
|
+
--fail-on-gate \
|
|
119
|
+
--output maintainability-report.md \
|
|
120
|
+
--prompt-output maintainability-remediation-prompt.md \
|
|
121
|
+
--comment-output maintainability-pr-comment.md
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## What It Analyzes
|
|
125
|
+
|
|
126
|
+
The deterministic scanner reads code from your repo (no LLM calls) and produces signals on:
|
|
127
|
+
|
|
128
|
+
- largest files (warn / fail thresholds configurable per-repo)
|
|
129
|
+
- approximate function/class size
|
|
130
|
+
- approximate cyclomatic complexity
|
|
131
|
+
- duplicate blocks (≥ N consecutive non-trivial lines, configurable)
|
|
132
|
+
- configurable risk patterns (regex matchers — TODO/FIXME, `eval(`, `exec(`, custom)
|
|
133
|
+
- expected files present (README, LICENSE, etc. — opt-in hard gate)
|
|
134
|
+
- expected test/lint commands declared in the config (opt-in hard gate)
|
|
135
|
+
- worktree-clean state at audit time (opt-in hard gate)
|
|
136
|
+
- ISO/IEC 25010-inspired 0–5 score per category + overall letter grade
|
|
137
|
+
|
|
138
|
+
The analyzer is intentionally conservative and dependency-free. Mature repos should **pair** this with native tools (ESLint, Ruff, Radon, Semgrep, SonarQube, Qlty / Code Climate) — not replace them. SARIF input from those tools can be folded into this tool's report via `--sarif-input`.
|
|
139
|
+
|
|
140
|
+
## What It Produces
|
|
141
|
+
|
|
142
|
+
Each run can emit any combination of:
|
|
143
|
+
|
|
144
|
+
- `maintainability-report.md` — the full Markdown report with summary, score, hotspots, duplicates, risk findings, external (SARIF) findings.
|
|
145
|
+
- `maintainability-remediation-prompt.md` — bounded AI prompt scoped to the run's findings.
|
|
146
|
+
- `maintainability-pr-comment.md` — short body suitable for a `gh pr comment` post.
|
|
147
|
+
- `maintainability.sarif` — SARIF 2.1.0 output for GitHub Code Scanning ingestion.
|
|
148
|
+
- `maintainability-baseline.json` — fingerprints of current findings, for `--fail-on-new` incremental adoption.
|
|
149
|
+
- Per-tool agent instruction files (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/maintainability.mdc`, `.github/copilot-instructions.md`, `.windsurf/rules/maintainability.md`, `AI-MAINTAINABILITY.md`) via `--init-agent-standards`.
|
|
150
|
+
|
|
151
|
+
## AI Remediation Prompt
|
|
152
|
+
|
|
153
|
+
The runner can generate a bounded prompt for a human developer to give to Claude, Codex, or another coding assistant:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
maintainability-agent \
|
|
157
|
+
--config maintainability-agent.json \
|
|
158
|
+
--output maintainability-report.md \
|
|
159
|
+
--prompt-output maintainability-remediation-prompt.md
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The prompt is designed for AI-written or AI-assisted code reviews. It tells the assistant to:
|
|
163
|
+
|
|
164
|
+
- fix only the highest-value maintainability issues
|
|
165
|
+
- keep the patch small and reviewable
|
|
166
|
+
- preserve existing architecture and behavior
|
|
167
|
+
- add tests where behavior changes
|
|
168
|
+
- report false positives instead of rewriting blindly
|
|
169
|
+
|
|
170
|
+
This makes the CI artifact actionable without letting the audit turn into an unbounded refactor request.
|
|
171
|
+
|
|
172
|
+
## PR and Baseline Workflows
|
|
173
|
+
|
|
174
|
+
PR-only audits, baseline grandfathering, AI-agent instruction
|
|
175
|
+
generation, and reusable agent-standard file generation are covered
|
|
176
|
+
in [PR and Baseline Workflows](docs/pr-and-baseline-workflows.md).
|
|
177
|
+
|
|
178
|
+
## Running Tests
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Sandbox-friendly invocation (works with PYTEST_DISABLE_PLUGIN_AUTOLOAD=1):
|
|
182
|
+
PYTHONPATH=src python3 -m pytest
|
|
183
|
+
|
|
184
|
+
# With coverage gate (matches CI):
|
|
185
|
+
PYTHONPATH=src python3 -m pytest \
|
|
186
|
+
--cov=maintainability_audit --cov-fail-under=92
|
|
187
|
+
|
|
188
|
+
# With ruff lint + pip-audit (matches CI):
|
|
189
|
+
python3 -m pip install -e ".[dev]"
|
|
190
|
+
ruff check src tests
|
|
191
|
+
pip-audit
|
|
192
|
+
PYTHONPATH=src python3 -m pytest --cov=maintainability_audit --cov-fail-under=92
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Coverage is intentionally NOT in `[tool.pytest.ini_options].addopts` so the
|
|
196
|
+
sandbox-friendly invocation (`PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`) doesn't choke
|
|
197
|
+
on `--cov` flags it can't load. Pass coverage flags explicitly when you want
|
|
198
|
+
the gate.
|
|
199
|
+
|
|
200
|
+
## Scoring Standard
|
|
201
|
+
|
|
202
|
+
The audit model is based on ISO/IEC 25010 maintainability:
|
|
203
|
+
|
|
204
|
+
- modularity
|
|
205
|
+
- reusability
|
|
206
|
+
- analyzability
|
|
207
|
+
- modifiability
|
|
208
|
+
- testability
|
|
209
|
+
|
|
210
|
+
See [docs/standard.md](docs/standard.md).
|
|
211
|
+
|
|
212
|
+
## Documentation
|
|
213
|
+
|
|
214
|
+
- [CLI reference](docs/cli.md)
|
|
215
|
+
- [Config schema](docs/config-schema.md)
|
|
216
|
+
- [Philosophy](docs/philosophy.md)
|
|
217
|
+
- [Analyzer adapters](docs/adapters.md)
|
|
218
|
+
- [External quality tools](docs/external-quality-tools.md)
|
|
219
|
+
- [IDE and agent integration](docs/ide-agent-integration.md)
|
|
220
|
+
- [PR and baseline workflows](docs/pr-and-baseline-workflows.md)
|
|
221
|
+
- [Roadmap](docs/roadmap.md)
|
|
222
|
+
|
|
223
|
+
## GitHub Action
|
|
224
|
+
|
|
225
|
+
This repo includes `action.yml`, so it can be used as a composite action after publishing:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
- uses: marshallguillory86/maintainability-agent@v0.1.0
|
|
229
|
+
with:
|
|
230
|
+
config: maintainability-agent.json
|
|
231
|
+
changed-only: main...HEAD
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## GitHub Actions
|
|
235
|
+
|
|
236
|
+
After publishing, copy `.github/workflows/maintainability.yml` into the target repo or adapt it for your local CI.
|
|
237
|
+
|
|
238
|
+
## IDE and Agent Integration
|
|
239
|
+
|
|
240
|
+
See [docs/ide-agent-integration.md](docs/ide-agent-integration.md) for VS Code tasks and integration notes for Copilot, Cursor, Codex, Claude Code, Windsurf, generic agents, local CI, and GitHub Actions.
|
|
241
|
+
|
|
242
|
+
## Local CI
|
|
243
|
+
|
|
244
|
+
For repos that do not use GitHub Actions, use:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
examples/local-ci.sh
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The local CI script enforces test coverage at `>=92%` and writes `coverage.xml` for SonarQube Cloud, Qlty, Codacy, or any other tool that can ingest Python coverage.
|
|
251
|
+
|
|
252
|
+
## Get in Touch
|
|
253
|
+
|
|
254
|
+
- **Bug reports / feature requests / general questions** — open a [GitHub Issue](https://github.com/marshallguillory86/maintainability-agent/issues/new).
|
|
255
|
+
- **Discussion / ideas** — use the [Discussions tab](https://github.com/marshallguillory86/maintainability-agent/discussions) (enable in repo settings if not visible yet).
|
|
256
|
+
- **Security vulnerabilities** — see [`SECURITY.md`](SECURITY.md) and use the [private security advisory flow](https://github.com/marshallguillory86/maintainability-agent/security/advisories/new). Do **not** post vulnerabilities in public issues.
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
maintainability_agent-0.1.0.dist-info/licenses/LICENSE,sha256=SOAOD6SRucfO7P_MWSLyyjwgr7x0s1BDaIskdGU8idI,1074
|
|
2
|
+
maintainability_audit/__init__.py,sha256=CAsYl-VynwMlhvu_kggVnBGhEYDe535sU9t1iztAizA,55
|
|
3
|
+
maintainability_audit/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
|
|
4
|
+
maintainability_audit/_metrics_types.py,sha256=u4ghOpZo5kElxWJv1HHTKT83Typ6xQYj8d2ThRp1tKw,1135
|
|
5
|
+
maintainability_audit/baseline.py,sha256=oL51yElOmR4YYPhLGEnQwNnoMmGDVD20msioAR1Ee6g,1445
|
|
6
|
+
maintainability_audit/cli.py,sha256=Pp_CsaMTLal1fUBdQw3QohvBpwg8CRJ-Blcq2PPs4xQ,5056
|
|
7
|
+
maintainability_audit/config.py,sha256=rAshORNP8iBekt4Ckz6eFg9Oay6LkT1jGLgAnY4UuSc,2285
|
|
8
|
+
maintainability_audit/git_tools.py,sha256=K6Kke3pgZk_bcDKPDPC9mB0MqTUMd4G8wGGyJbVHwC0,553
|
|
9
|
+
maintainability_audit/instructions.py,sha256=RNJEEEf7yBaIEjUvRStU3VH1c5tScw_kgxd3SXUzBJQ,3773
|
|
10
|
+
maintainability_audit/metrics.py,sha256=R5U9ZxWpMa5uT5GZi8uVCjjuBcuHWD_HwHLTYSN4XYI,10102
|
|
11
|
+
maintainability_audit/renderers.py,sha256=fAvwdSVp5TA9arddmFePz4AAUMN5RjHl_LGHkH-vGlE,10539
|
|
12
|
+
maintainability_audit/sarif.py,sha256=CnD6kMHxOh1IlBcn5rdUwJxvznjSqe4wYNaarlbmEfI,5804
|
|
13
|
+
maintainability_audit/scoring.py,sha256=NJdzjtUFbAIC-cwtaV3nyEQ66vzH7g1lJjkqr3OJsCM,1755
|
|
14
|
+
maintainability_agent-0.1.0.dist-info/METADATA,sha256=sLuk3tzu-id-xHxFalTl3LJDlFTALqzDcD7mlcUbrnU,10715
|
|
15
|
+
maintainability_agent-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
+
maintainability_agent-0.1.0.dist-info/entry_points.txt,sha256=obtbqnuhuK9DCe-9307elxX5hrVR4k-KL8-0KaAnay4,128
|
|
17
|
+
maintainability_agent-0.1.0.dist-info/top_level.txt,sha256=h62cy_cvetBemtpg5sL3nxbEOrdcHZmfOSXEVjP2_38,22
|
|
18
|
+
maintainability_agent-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marshall Guillory
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
maintainability_audit
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Internal value-types + regex constants used by ``metrics.py``.
|
|
2
|
+
|
|
3
|
+
Extracted from ``metrics.py`` (2026-05-11) so the metrics module
|
|
4
|
+
stays under the maintainability config's warn threshold for file
|
|
5
|
+
length — eating our own dogfood on the A+ grade. Not a public API;
|
|
6
|
+
import from ``metrics`` if you need any of these symbols externally.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
FUNC_PATTERNS = [
|
|
14
|
+
re.compile(r"^\s*def\s+([A-Za-z_][\w]*)\s*\("),
|
|
15
|
+
re.compile(r"^\s*(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\("),
|
|
16
|
+
re.compile(r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>"),
|
|
17
|
+
re.compile(r"^\s*(?:export\s+default\s+)?class\s+([A-Za-z_$][\w$]*)\b"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
COMPLEXITY_RE = re.compile(r"\b(if|elif|for|while|except|case|catch)\b|&&|\|\||\?")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FileMetric:
|
|
25
|
+
path: str
|
|
26
|
+
lines: int
|
|
27
|
+
status: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class FunctionMetric:
|
|
32
|
+
path: str
|
|
33
|
+
name: str
|
|
34
|
+
start_line: int
|
|
35
|
+
lines: int
|
|
36
|
+
complexity: int
|
|
37
|
+
status: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class RiskFinding:
|
|
42
|
+
path: str
|
|
43
|
+
line: int
|
|
44
|
+
name: str
|
|
45
|
+
text: str
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def finding_fingerprints(report: dict[str, Any]) -> set[str]:
|
|
9
|
+
fingerprints: set[str] = set()
|
|
10
|
+
for item in report.get("largest_files", []):
|
|
11
|
+
if item["status"] == "fail":
|
|
12
|
+
fingerprints.add(f"file-lines:{item['path']}")
|
|
13
|
+
for item in report.get("function_hotspots", []):
|
|
14
|
+
if item["status"] == "fail":
|
|
15
|
+
fingerprints.add(f"function:{item['path']}:{item['name']}:{item['start_line']}")
|
|
16
|
+
for item in report.get("risk_findings", []):
|
|
17
|
+
fingerprints.add(f"risk:{item['path']}:{item['line']}:{item['name']}")
|
|
18
|
+
for item in report.get("duplicate_blocks", []):
|
|
19
|
+
locations = ",".join(item["locations"][:5])
|
|
20
|
+
fingerprints.add(f"duplicate:{locations}")
|
|
21
|
+
return fingerprints
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_baseline(path: str | None) -> set[str]:
|
|
25
|
+
if not path:
|
|
26
|
+
return set()
|
|
27
|
+
baseline_path = Path(path)
|
|
28
|
+
if not baseline_path.exists():
|
|
29
|
+
return set()
|
|
30
|
+
data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
|
31
|
+
return set(data.get("findings", []))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_baseline(path: str, report: dict[str, Any]) -> None:
|
|
35
|
+
data = {
|
|
36
|
+
"version": 1,
|
|
37
|
+
"root": report["root"],
|
|
38
|
+
"score": report.get("score", {}),
|
|
39
|
+
"findings": sorted(finding_fingerprints(report)),
|
|
40
|
+
}
|
|
41
|
+
Path(path).write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .baseline import finding_fingerprints, load_baseline, write_baseline
|
|
9
|
+
from .config import DEFAULT_CONFIG, VERSION, load_config
|
|
10
|
+
from .git_tools import changed_paths
|
|
11
|
+
from .instructions import instruction_path_for_target, write_instruction_pack
|
|
12
|
+
from .metrics import build_report
|
|
13
|
+
from .renderers import (
|
|
14
|
+
render_agent_instructions,
|
|
15
|
+
render_ai_prompt,
|
|
16
|
+
render_markdown,
|
|
17
|
+
render_pr_comment,
|
|
18
|
+
)
|
|
19
|
+
from .sarif import read_sarif_inputs, report_to_sarif
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
23
|
+
parser.add_argument("--version", action="version", version=f"maintainability-agent {VERSION}")
|
|
24
|
+
parser.add_argument("--root", default=".", help="Repository root to scan.")
|
|
25
|
+
parser.add_argument("--config", help="Path to JSON config.")
|
|
26
|
+
parser.add_argument("--format", choices=["json", "markdown"], default="markdown")
|
|
27
|
+
parser.add_argument("--output", help="Output file. Defaults to stdout.")
|
|
28
|
+
parser.add_argument("--prompt-output", help="Optional Markdown prompt for AI-assisted remediation.")
|
|
29
|
+
parser.add_argument("--comment-output", help="Optional Markdown body suitable for a PR comment.")
|
|
30
|
+
parser.add_argument("--agent-instructions-output", help="Optional reusable instructions for AI coding agents.")
|
|
31
|
+
parser.add_argument("--sarif-output", help="Optional SARIF output path for GitHub code scanning.")
|
|
32
|
+
parser.add_argument("--sarif-input", action="append", help="Optional external SARIF file to summarize in reports. Repeatable.")
|
|
33
|
+
parser.add_argument("--changed-only", help="Audit only files changed in a git revspec, for example main...HEAD.")
|
|
34
|
+
parser.add_argument("--baseline", help="Existing baseline JSON. With --fail-on-new, only new findings fail.")
|
|
35
|
+
parser.add_argument("--write-baseline", help="Write current findings to a baseline JSON file.")
|
|
36
|
+
parser.add_argument("--fail-on-new", action="store_true", help="Fail only when findings are not in --baseline.")
|
|
37
|
+
parser.add_argument("--fail-on-gate", action="store_true", help="Exit 1 when hard gates fail.")
|
|
38
|
+
parser.add_argument("--init-agent-standards", action="store_true", help="Write model/tool-specific instruction files and exit.")
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--target",
|
|
41
|
+
action="append",
|
|
42
|
+
choices=["generic", "claude-code", "codex", "cursor", "copilot", "windsurf"],
|
|
43
|
+
help="Instruction target. Repeatable. Used with --init-agent-standards.",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument("--instructions-output-dir", default=".", help="Directory for generated instruction files.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_outputs(args: argparse.Namespace, report: dict, rendered: str) -> None:
|
|
49
|
+
if args.output:
|
|
50
|
+
Path(args.output).write_text(rendered + "\n", encoding="utf-8")
|
|
51
|
+
else:
|
|
52
|
+
print(rendered)
|
|
53
|
+
if args.prompt_output:
|
|
54
|
+
Path(args.prompt_output).write_text(render_ai_prompt(report) + "\n", encoding="utf-8")
|
|
55
|
+
if args.comment_output:
|
|
56
|
+
Path(args.comment_output).write_text(render_pr_comment(report) + "\n", encoding="utf-8")
|
|
57
|
+
if args.agent_instructions_output:
|
|
58
|
+
Path(args.agent_instructions_output).write_text(render_agent_instructions(report) + "\n", encoding="utf-8")
|
|
59
|
+
if args.write_baseline:
|
|
60
|
+
write_baseline(args.write_baseline, report)
|
|
61
|
+
if args.sarif_output:
|
|
62
|
+
Path(args.sarif_output).write_text(json.dumps(report_to_sarif(report), indent=2) + "\n", encoding="utf-8")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def audit_exit_code(args: argparse.Namespace, report: dict) -> int:
|
|
66
|
+
if args.fail_on_new:
|
|
67
|
+
baseline = load_baseline(args.baseline)
|
|
68
|
+
if finding_fingerprints(report) - baseline:
|
|
69
|
+
return 1
|
|
70
|
+
if args.fail_on_gate and report["hard_gate_failures"]:
|
|
71
|
+
return 1
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main(argv: list[str] | None = None) -> int:
|
|
76
|
+
parser = argparse.ArgumentParser()
|
|
77
|
+
add_arguments(parser)
|
|
78
|
+
args = parser.parse_args(argv)
|
|
79
|
+
|
|
80
|
+
root = Path(args.root).resolve()
|
|
81
|
+
config = load_config(args.config)
|
|
82
|
+
if args.init_agent_standards:
|
|
83
|
+
targets = args.target or ["generic", "claude-code", "codex", "cursor", "copilot", "windsurf"]
|
|
84
|
+
write_instruction_pack(targets, Path(args.instructions_output_dir).resolve(), config)
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
only_paths = changed_paths(root, args.changed_only) if args.changed_only else None
|
|
88
|
+
external_findings = read_sarif_inputs(args.sarif_input)
|
|
89
|
+
report = build_report(root, config, only_paths=only_paths, changed_revspec=args.changed_only, external_findings=external_findings)
|
|
90
|
+
rendered = json.dumps(report, indent=2, sort_keys=True) if args.format == "json" else render_markdown(report)
|
|
91
|
+
write_outputs(args, report, rendered)
|
|
92
|
+
return audit_exit_code(args, report)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
__all__ = [
|
|
96
|
+
"add_arguments",
|
|
97
|
+
"audit_exit_code",
|
|
98
|
+
"build_report",
|
|
99
|
+
"changed_paths",
|
|
100
|
+
"DEFAULT_CONFIG",
|
|
101
|
+
"finding_fingerprints",
|
|
102
|
+
"instruction_path_for_target",
|
|
103
|
+
"load_baseline",
|
|
104
|
+
"load_config",
|
|
105
|
+
"main",
|
|
106
|
+
"read_sarif_inputs",
|
|
107
|
+
"report_to_sarif",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
sys.exit(main())
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
VERSION = "0.1.0"
|
|
8
|
+
|
|
9
|
+
PROJECT_URL = "https://github.com/marshallguillory86/maintainability-agent"
|
|
10
|
+
|
|
11
|
+
DEFAULT_CONFIG: dict[str, Any] = {
|
|
12
|
+
"paths": {
|
|
13
|
+
"include_extensions": [".py", ".js", ".jsx", ".ts", ".tsx", ".html", ".css", ".md"],
|
|
14
|
+
"exclude_patterns": [
|
|
15
|
+
".git/",
|
|
16
|
+
"node_modules/",
|
|
17
|
+
".venv/",
|
|
18
|
+
"venv/",
|
|
19
|
+
"dist/",
|
|
20
|
+
"build/",
|
|
21
|
+
"coverage/",
|
|
22
|
+
"__pycache__/",
|
|
23
|
+
"maintainability-baseline.json",
|
|
24
|
+
"maintainability-report.md",
|
|
25
|
+
"maintainability-remediation-prompt.md",
|
|
26
|
+
"maintainability-pr-comment.md",
|
|
27
|
+
"maintainability.sarif",
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
"thresholds": {
|
|
31
|
+
"max_file_lines": 800,
|
|
32
|
+
"warn_file_lines": 400,
|
|
33
|
+
"max_function_lines": 80,
|
|
34
|
+
"warn_function_lines": 50,
|
|
35
|
+
"max_complexity": 15,
|
|
36
|
+
"warn_complexity": 10,
|
|
37
|
+
"max_duplicate_blocks": 20,
|
|
38
|
+
"duplicate_block_lines": 8,
|
|
39
|
+
},
|
|
40
|
+
"hard_gates": {
|
|
41
|
+
"require_test_command": False,
|
|
42
|
+
"require_readme": True,
|
|
43
|
+
"require_clean_worktree": False,
|
|
44
|
+
},
|
|
45
|
+
"expected_files": ["README.md"],
|
|
46
|
+
"expected_commands": {"test": [], "lint": []},
|
|
47
|
+
"risk_patterns": [
|
|
48
|
+
{
|
|
49
|
+
"name": "debt-marker",
|
|
50
|
+
"pattern": r"\b(TODO|FIXME|HACK)\b",
|
|
51
|
+
"extensions": [".py", ".js", ".jsx", ".ts", ".tsx", ".html", ".css", ".md"],
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"instruction_pack": {
|
|
55
|
+
"project_name": "this repository",
|
|
56
|
+
"strictness": "high",
|
|
57
|
+
"test_policy": "tests for meaningful behavior changes",
|
|
58
|
+
"architecture_notes": [],
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def deep_update(base: dict[str, Any], override: dict[str, Any]) -> None:
|
|
64
|
+
for key, value in override.items():
|
|
65
|
+
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
|
66
|
+
deep_update(base[key], value)
|
|
67
|
+
else:
|
|
68
|
+
base[key] = value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_config(path: str | None) -> dict[str, Any]:
|
|
72
|
+
config = json.loads(json.dumps(DEFAULT_CONFIG))
|
|
73
|
+
if not path:
|
|
74
|
+
return config
|
|
75
|
+
user_config = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
76
|
+
deep_update(config, user_config)
|
|
77
|
+
return config
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_git(args: list[str], cwd: Path) -> str:
|
|
9
|
+
try:
|
|
10
|
+
return subprocess.check_output(["git", *args], cwd=cwd, text=True, stderr=subprocess.DEVNULL).strip()
|
|
11
|
+
except Exception:
|
|
12
|
+
return ""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def changed_paths(root: Path, revspec: str) -> set[str]:
|
|
16
|
+
output = run_git(["diff", "--name-only", revspec], root)
|
|
17
|
+
if not output:
|
|
18
|
+
return set()
|
|
19
|
+
return {line.strip().replace(os.sep, "/") for line in output.splitlines() if line.strip()}
|