crapkit 0.7.4__tar.gz → 0.7.6__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.
- {crapkit-0.7.4/src/crapkit.egg-info → crapkit-0.7.6}/PKG-INFO +46 -14
- {crapkit-0.7.4 → crapkit-0.7.6}/README.md +44 -12
- {crapkit-0.7.4 → crapkit-0.7.6}/pyproject.toml +2 -2
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/__init__.py +1 -1
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/scoring.py +45 -4
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/mcp_server.py +20 -30
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/score.py +89 -24
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/store.py +1 -1
- {crapkit-0.7.4 → crapkit-0.7.6/src/crapkit.egg-info}/PKG-INFO +46 -14
- {crapkit-0.7.4 → crapkit-0.7.6}/LICENSE +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/setup.cfg +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/__main__.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/_analysis_pool.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/_mcp_stdio.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/_process_family.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/_process_owner.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/_pygdefer.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/_windows_job.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/analyze.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cache.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/churn.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/churn_cache.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/churn_log.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/__init__.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/_shared.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/admin.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/analyses.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/claude_hook.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/maintenance.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/parser.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/queue.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/ratchet_cmds.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/reports.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/cli/verifying.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/config.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/config_contract.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/coupling.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/coupling_cache.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/coverage_istanbul.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/coverage_py.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/covstream.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/diffparse.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/digest.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/doctor.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/dup.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/errors.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/gitio.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/gitpaths.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/hook.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/invocation.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/junitparse.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/keys.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/lanes.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/lizardcognitive.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/lizardpowershell.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/lizardpython.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/lizardrust.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/lizardshell.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/lizardtypescript.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/locks.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/logs.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/merge.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/mutate.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/mutate_pool.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/override.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/packet.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/procs.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/ratchet.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/ratchet_report.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/ratchetfile.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/records.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/report.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/repotext.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/resources.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/retention.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/rootfind.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/sarif.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/sarifio.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/scaffold.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/snapshot.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/uncovered.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/universe.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/verify.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/watch.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit/worklist.py +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit.egg-info/SOURCES.txt +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit.egg-info/dependency_links.txt +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit.egg-info/entry_points.txt +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit.egg-info/requires.txt +0 -0
- {crapkit-0.7.4 → crapkit-0.7.6}/src/crapkit.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crapkit
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.6
|
|
4
4
|
Summary: Scores every function on complexity times uncovered risk, ranks the worst, and blocks commits that add more.
|
|
5
5
|
Author: Jean-Francois Gagne
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/JeanFrancoisGagne/crapkit
|
|
8
|
-
Project-URL: Documentation, https://
|
|
8
|
+
Project-URL: Documentation, https://www.jfgagne.com/crapkit/handbook.html
|
|
9
9
|
Project-URL: Changelog, https://github.com/JeanFrancoisGagne/crapkit/blob/main/CHANGELOG.md
|
|
10
10
|
Project-URL: Issues, https://github.com/JeanFrancoisGagne/crapkit/issues
|
|
11
11
|
Project-URL: Source, https://github.com/JeanFrancoisGagne/crapkit
|
|
@@ -111,6 +111,9 @@ crapkit ratchet seed
|
|
|
111
111
|
git add crapkit.toml crapkit-ratchet.tsv .gitignore
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
Not a Python repo? `uvx crapkit init` runs the same commands and adds nothing to your
|
|
115
|
+
manifest: see [A repo that is not Python](#a-repo-that-is-not-python).
|
|
116
|
+
|
|
114
117
|
`init` detects pytest, Vitest and Jest from the repository's own files. Review the
|
|
115
118
|
generated config before running its commands. When detection leaves a commented
|
|
116
119
|
lane, fill it in using the [lane recipes](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md).
|
|
@@ -182,6 +185,28 @@ pip install git+https://github.com/JeanFrancoisGagne/crapkit.git
|
|
|
182
185
|
pip install .
|
|
183
186
|
```
|
|
184
187
|
|
|
188
|
+
### A repo that is not Python
|
|
189
|
+
|
|
190
|
+
crapkit is a command-line tool, never a dependency of the code it scores. A TypeScript,
|
|
191
|
+
Go or Rust repo adds nothing to its own manifest. With [uv](https://docs.astral.sh/uv/)
|
|
192
|
+
on the machine, `uvx` fetches crapkit into a cache of its own and runs it:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
$ uvx crapkit init
|
|
196
|
+
wrote crapkit.toml with 1 scope(s): src
|
|
197
|
+
detected 1 lane(s) from this repo's own files: js - next: run `crapkit coverage`
|
|
198
|
+
added to .gitignore: .crapkit/
|
|
199
|
+
|
|
200
|
+
$ uvx crapkit coverage
|
|
201
|
+
$ uvx crapkit worklist
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The lane still runs your own test runner, so Vitest or Jest and its coverage package come
|
|
205
|
+
from the repo's `node_modules` as they do today. `uv tool install crapkit` or
|
|
206
|
+
`pipx install crapkit` puts a `crapkit` command on PATH once, which is what the
|
|
207
|
+
[commit gate](#the-gate) and the Claude Code plugin call. uv brings its own Python when
|
|
208
|
+
the machine has none.
|
|
209
|
+
|
|
185
210
|
Requires Python 3.11 or newer and Git on PATH. The CLI has one runtime dependency,
|
|
186
211
|
`lizard>=1.24.0`; a package mirror needs both distributions. Install into the environment
|
|
187
212
|
you intend to use, then check `crapkit --version`. The `pip install -e ".[dev]"` under
|
|
@@ -199,7 +224,7 @@ files. Review those commands before running Crapkit in a repository you do not t
|
|
|
199
224
|
|
|
200
225
|
```
|
|
201
226
|
$ crapkit --version
|
|
202
|
-
crapkit 0.7.
|
|
227
|
+
crapkit 0.7.6
|
|
203
228
|
```
|
|
204
229
|
|
|
205
230
|
`python -m crapkit` works identically to the console script and is what to use from a
|
|
@@ -455,7 +480,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
|
|
|
455
480
|
repos:
|
|
456
481
|
- repo: https://github.com/JeanFrancoisGagne/crapkit
|
|
457
482
|
# crapkit's release step rewrites this line to the tag it just cut
|
|
458
|
-
rev: v0.7.
|
|
483
|
+
rev: v0.7.6
|
|
459
484
|
hooks:
|
|
460
485
|
- id: crapkit-gate
|
|
461
486
|
```
|
|
@@ -563,7 +588,7 @@ to a workflow, and every input has a default:
|
|
|
563
588
|
- uses: actions/checkout@v4
|
|
564
589
|
with:
|
|
565
590
|
fetch-depth: 0
|
|
566
|
-
- uses: JeanFrancoisGagne/crapkit@v0.7.
|
|
591
|
+
- uses: JeanFrancoisGagne/crapkit@v0.7.6
|
|
567
592
|
```
|
|
568
593
|
|
|
569
594
|
The whole job those four lines sit in:
|
|
@@ -583,7 +608,7 @@ jobs:
|
|
|
583
608
|
with:
|
|
584
609
|
python-version: "3.12" # the interpreter the install below lands in
|
|
585
610
|
- run: pip install -e ".[dev]" # whatever your lanes need to run
|
|
586
|
-
- uses: JeanFrancoisGagne/crapkit@v0.7.
|
|
611
|
+
- uses: JeanFrancoisGagne/crapkit@v0.7.6
|
|
587
612
|
with:
|
|
588
613
|
gate: "false"
|
|
589
614
|
```
|
|
@@ -654,7 +679,7 @@ closes it. A verify that passed is one line: `**verify passed.** Run 2 against b
|
|
|
654
679
|
The rows are the ranked worklist for the files the pull request changed, worst first,
|
|
655
680
|
`top` of them, with the rows a finding names listed first. `risk` is ccn times churn
|
|
656
681
|
weight, the number `crapkit worklist` ranks on, and `remedy` is the run's own verdict for
|
|
657
|
-
that function: `decompose`, `add-tests` or `ok`. `(accepted debt)` marks a function the
|
|
682
|
+
that function: `decompose`, `split-lines`, `add-tests` or `ok`. `(accepted debt)` marks a function the
|
|
658
683
|
committed ratchet carries a mark for, so an untouched `legacy_router` does not read like
|
|
659
684
|
the pull request's own new function. A pull request that touches no ranked function gets
|
|
660
685
|
the heading and no table.
|
|
@@ -830,6 +855,7 @@ The coverage summary counts all four as `measured` / `untested` / `no_lane` / `c
|
|
|
830
855
|
| Remedy | Condition | Action |
|
|
831
856
|
|---|---|---|
|
|
832
857
|
| `decompose` | `ccn > ceiling` | Split it. No amount of coverage clears this. |
|
|
858
|
+
| `split-lines` | `ccn <= ceiling`, `crap > ceiling`, and another function shares its source lines | Put each definition on its own lines, then measure again. Coverage cannot tell functions on one line apart, so the score stays at uncovered whatever the tests do. |
|
|
833
859
|
| `add-tests` | `ccn <= ceiling` and `crap > ceiling` | Cover the branches. |
|
|
834
860
|
| `ok` | `crap <= ceiling` | Nothing. |
|
|
835
861
|
|
|
@@ -1268,12 +1294,12 @@ export function classify(row: Row): string {
|
|
|
1268
1294
|
```
|
|
1269
1295
|
$ crapkit rescore src/grade.ts --gate
|
|
1270
1296
|
rescore vs run 1 @ 8bfbe613fcd (coverage STALE, complexity fresh)
|
|
1271
|
-
ccn cov crap remedy
|
|
1272
|
-
5 0% 30.0 add-tests
|
|
1273
|
-
5 0% 30.0 add-tests
|
|
1274
|
-
4 0% 20.0 add-tests
|
|
1275
|
-
4 45% 6.7 add-tests
|
|
1276
|
-
4 75% 4.2 ok
|
|
1297
|
+
ccn cov crap remedy function
|
|
1298
|
+
5 0% 30.0 add-tests src/grade.ts:22 band ( score )
|
|
1299
|
+
5 0% 30.0 add-tests src/grade.ts:38 demote ( letter , row Row )
|
|
1300
|
+
4 0% 20.0 add-tests src/grade.ts:8 penalty ( attempts , late )
|
|
1301
|
+
4 45% 6.7 add-tests src/grade.ts:48 classify ( row Row )
|
|
1302
|
+
4 75% 4.2 ok src/grade.ts:59 average ( scores Array )
|
|
1277
1303
|
```
|
|
1278
1304
|
|
|
1279
1305
|
Exit 0: every piece is at or under 6. The `crap` column is loud because its coverage half
|
|
@@ -1340,7 +1366,7 @@ with no debt.
|
|
|
1340
1366
|
|
|
1341
1367
|
| Page | Covers |
|
|
1342
1368
|
|---|---|
|
|
1343
|
-
| [The handbook](https://
|
|
1369
|
+
| [The handbook](https://www.jfgagne.com/crapkit/handbook.html) | **Start here for anything deeper.** The illustrated handbook: what crapkit is, how every piece works, and where each command earns its keep. Also at [docs/handbook.html](https://www.jfgagne.com/crapkit/handbook.html), self-contained, so it opens straight from a clone. |
|
|
1344
1370
|
| [docs/adoption.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/adoption.md) | The judgment layer over the quickstarts: scope granularity, exclude vs lane, scoped_tests wiring, the first-verify taint hazard. |
|
|
1345
1371
|
| [docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md) | Every `crapkit.toml` key: type, default, and what it does. |
|
|
1346
1372
|
| [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md) | The lane model, vitest and jest and pytest recipes, artifact reuse, flake retest, containers. |
|
|
@@ -1371,6 +1397,12 @@ line arms the complexity gate. See
|
|
|
1371
1397
|
for development and [the verified implementation report](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/architecture/2026-09-07-implementation/REPORT.md)
|
|
1372
1398
|
for complete Windows source and Linux wheel results, focused benchmarks and their limits.
|
|
1373
1399
|
|
|
1400
|
+
## Maintainer and project background
|
|
1401
|
+
|
|
1402
|
+
crapkit is created and maintained by [Jean-François Gagné](https://www.jfgagne.com/).
|
|
1403
|
+
Read the [project background](https://www.jfgagne.com/projects/crapkit/) for the
|
|
1404
|
+
problem it addresses and how it fits into his work on software and AI.
|
|
1405
|
+
|
|
1374
1406
|
## License
|
|
1375
1407
|
|
|
1376
1408
|
MIT. See [LICENSE](https://github.com/JeanFrancoisGagne/crapkit/blob/main/LICENSE).
|
|
@@ -70,6 +70,9 @@ crapkit ratchet seed
|
|
|
70
70
|
git add crapkit.toml crapkit-ratchet.tsv .gitignore
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Not a Python repo? `uvx crapkit init` runs the same commands and adds nothing to your
|
|
74
|
+
manifest: see [A repo that is not Python](#a-repo-that-is-not-python).
|
|
75
|
+
|
|
73
76
|
`init` detects pytest, Vitest and Jest from the repository's own files. Review the
|
|
74
77
|
generated config before running its commands. When detection leaves a commented
|
|
75
78
|
lane, fill it in using the [lane recipes](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md).
|
|
@@ -141,6 +144,28 @@ pip install git+https://github.com/JeanFrancoisGagne/crapkit.git
|
|
|
141
144
|
pip install .
|
|
142
145
|
```
|
|
143
146
|
|
|
147
|
+
### A repo that is not Python
|
|
148
|
+
|
|
149
|
+
crapkit is a command-line tool, never a dependency of the code it scores. A TypeScript,
|
|
150
|
+
Go or Rust repo adds nothing to its own manifest. With [uv](https://docs.astral.sh/uv/)
|
|
151
|
+
on the machine, `uvx` fetches crapkit into a cache of its own and runs it:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
$ uvx crapkit init
|
|
155
|
+
wrote crapkit.toml with 1 scope(s): src
|
|
156
|
+
detected 1 lane(s) from this repo's own files: js - next: run `crapkit coverage`
|
|
157
|
+
added to .gitignore: .crapkit/
|
|
158
|
+
|
|
159
|
+
$ uvx crapkit coverage
|
|
160
|
+
$ uvx crapkit worklist
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The lane still runs your own test runner, so Vitest or Jest and its coverage package come
|
|
164
|
+
from the repo's `node_modules` as they do today. `uv tool install crapkit` or
|
|
165
|
+
`pipx install crapkit` puts a `crapkit` command on PATH once, which is what the
|
|
166
|
+
[commit gate](#the-gate) and the Claude Code plugin call. uv brings its own Python when
|
|
167
|
+
the machine has none.
|
|
168
|
+
|
|
144
169
|
Requires Python 3.11 or newer and Git on PATH. The CLI has one runtime dependency,
|
|
145
170
|
`lizard>=1.24.0`; a package mirror needs both distributions. Install into the environment
|
|
146
171
|
you intend to use, then check `crapkit --version`. The `pip install -e ".[dev]"` under
|
|
@@ -158,7 +183,7 @@ files. Review those commands before running Crapkit in a repository you do not t
|
|
|
158
183
|
|
|
159
184
|
```
|
|
160
185
|
$ crapkit --version
|
|
161
|
-
crapkit 0.7.
|
|
186
|
+
crapkit 0.7.6
|
|
162
187
|
```
|
|
163
188
|
|
|
164
189
|
`python -m crapkit` works identically to the console script and is what to use from a
|
|
@@ -414,7 +439,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
|
|
|
414
439
|
repos:
|
|
415
440
|
- repo: https://github.com/JeanFrancoisGagne/crapkit
|
|
416
441
|
# crapkit's release step rewrites this line to the tag it just cut
|
|
417
|
-
rev: v0.7.
|
|
442
|
+
rev: v0.7.6
|
|
418
443
|
hooks:
|
|
419
444
|
- id: crapkit-gate
|
|
420
445
|
```
|
|
@@ -522,7 +547,7 @@ to a workflow, and every input has a default:
|
|
|
522
547
|
- uses: actions/checkout@v4
|
|
523
548
|
with:
|
|
524
549
|
fetch-depth: 0
|
|
525
|
-
- uses: JeanFrancoisGagne/crapkit@v0.7.
|
|
550
|
+
- uses: JeanFrancoisGagne/crapkit@v0.7.6
|
|
526
551
|
```
|
|
527
552
|
|
|
528
553
|
The whole job those four lines sit in:
|
|
@@ -542,7 +567,7 @@ jobs:
|
|
|
542
567
|
with:
|
|
543
568
|
python-version: "3.12" # the interpreter the install below lands in
|
|
544
569
|
- run: pip install -e ".[dev]" # whatever your lanes need to run
|
|
545
|
-
- uses: JeanFrancoisGagne/crapkit@v0.7.
|
|
570
|
+
- uses: JeanFrancoisGagne/crapkit@v0.7.6
|
|
546
571
|
with:
|
|
547
572
|
gate: "false"
|
|
548
573
|
```
|
|
@@ -613,7 +638,7 @@ closes it. A verify that passed is one line: `**verify passed.** Run 2 against b
|
|
|
613
638
|
The rows are the ranked worklist for the files the pull request changed, worst first,
|
|
614
639
|
`top` of them, with the rows a finding names listed first. `risk` is ccn times churn
|
|
615
640
|
weight, the number `crapkit worklist` ranks on, and `remedy` is the run's own verdict for
|
|
616
|
-
that function: `decompose`, `add-tests` or `ok`. `(accepted debt)` marks a function the
|
|
641
|
+
that function: `decompose`, `split-lines`, `add-tests` or `ok`. `(accepted debt)` marks a function the
|
|
617
642
|
committed ratchet carries a mark for, so an untouched `legacy_router` does not read like
|
|
618
643
|
the pull request's own new function. A pull request that touches no ranked function gets
|
|
619
644
|
the heading and no table.
|
|
@@ -789,6 +814,7 @@ The coverage summary counts all four as `measured` / `untested` / `no_lane` / `c
|
|
|
789
814
|
| Remedy | Condition | Action |
|
|
790
815
|
|---|---|---|
|
|
791
816
|
| `decompose` | `ccn > ceiling` | Split it. No amount of coverage clears this. |
|
|
817
|
+
| `split-lines` | `ccn <= ceiling`, `crap > ceiling`, and another function shares its source lines | Put each definition on its own lines, then measure again. Coverage cannot tell functions on one line apart, so the score stays at uncovered whatever the tests do. |
|
|
792
818
|
| `add-tests` | `ccn <= ceiling` and `crap > ceiling` | Cover the branches. |
|
|
793
819
|
| `ok` | `crap <= ceiling` | Nothing. |
|
|
794
820
|
|
|
@@ -1227,12 +1253,12 @@ export function classify(row: Row): string {
|
|
|
1227
1253
|
```
|
|
1228
1254
|
$ crapkit rescore src/grade.ts --gate
|
|
1229
1255
|
rescore vs run 1 @ 8bfbe613fcd (coverage STALE, complexity fresh)
|
|
1230
|
-
ccn cov crap remedy
|
|
1231
|
-
5 0% 30.0 add-tests
|
|
1232
|
-
5 0% 30.0 add-tests
|
|
1233
|
-
4 0% 20.0 add-tests
|
|
1234
|
-
4 45% 6.7 add-tests
|
|
1235
|
-
4 75% 4.2 ok
|
|
1256
|
+
ccn cov crap remedy function
|
|
1257
|
+
5 0% 30.0 add-tests src/grade.ts:22 band ( score )
|
|
1258
|
+
5 0% 30.0 add-tests src/grade.ts:38 demote ( letter , row Row )
|
|
1259
|
+
4 0% 20.0 add-tests src/grade.ts:8 penalty ( attempts , late )
|
|
1260
|
+
4 45% 6.7 add-tests src/grade.ts:48 classify ( row Row )
|
|
1261
|
+
4 75% 4.2 ok src/grade.ts:59 average ( scores Array )
|
|
1236
1262
|
```
|
|
1237
1263
|
|
|
1238
1264
|
Exit 0: every piece is at or under 6. The `crap` column is loud because its coverage half
|
|
@@ -1299,7 +1325,7 @@ with no debt.
|
|
|
1299
1325
|
|
|
1300
1326
|
| Page | Covers |
|
|
1301
1327
|
|---|---|
|
|
1302
|
-
| [The handbook](https://
|
|
1328
|
+
| [The handbook](https://www.jfgagne.com/crapkit/handbook.html) | **Start here for anything deeper.** The illustrated handbook: what crapkit is, how every piece works, and where each command earns its keep. Also at [docs/handbook.html](https://www.jfgagne.com/crapkit/handbook.html), self-contained, so it opens straight from a clone. |
|
|
1303
1329
|
| [docs/adoption.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/adoption.md) | The judgment layer over the quickstarts: scope granularity, exclude vs lane, scoped_tests wiring, the first-verify taint hazard. |
|
|
1304
1330
|
| [docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md) | Every `crapkit.toml` key: type, default, and what it does. |
|
|
1305
1331
|
| [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md) | The lane model, vitest and jest and pytest recipes, artifact reuse, flake retest, containers. |
|
|
@@ -1330,6 +1356,12 @@ line arms the complexity gate. See
|
|
|
1330
1356
|
for development and [the verified implementation report](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/architecture/2026-09-07-implementation/REPORT.md)
|
|
1331
1357
|
for complete Windows source and Linux wheel results, focused benchmarks and their limits.
|
|
1332
1358
|
|
|
1359
|
+
## Maintainer and project background
|
|
1360
|
+
|
|
1361
|
+
crapkit is created and maintained by [Jean-François Gagné](https://www.jfgagne.com/).
|
|
1362
|
+
Read the [project background](https://www.jfgagne.com/projects/crapkit/) for the
|
|
1363
|
+
problem it addresses and how it fits into his work on software and AI.
|
|
1364
|
+
|
|
1333
1365
|
## License
|
|
1334
1366
|
|
|
1335
1367
|
MIT. See [LICENSE](https://github.com/JeanFrancoisGagne/crapkit/blob/main/LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "crapkit"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.6"
|
|
8
8
|
description = "Scores every function on complexity times uncovered risk, ranks the worst, and blocks commits that add more."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -49,7 +49,7 @@ dependencies = [
|
|
|
49
49
|
|
|
50
50
|
[project.urls]
|
|
51
51
|
Homepage = "https://github.com/JeanFrancoisGagne/crapkit"
|
|
52
|
-
Documentation = "https://
|
|
52
|
+
Documentation = "https://www.jfgagne.com/crapkit/handbook.html"
|
|
53
53
|
Changelog = "https://github.com/JeanFrancoisGagne/crapkit/blob/main/CHANGELOG.md"
|
|
54
54
|
Issues = "https://github.com/JeanFrancoisGagne/crapkit/issues"
|
|
55
55
|
Source = "https://github.com/JeanFrancoisGagne/crapkit"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""crapkit: deterministic CRAP-score framework."""
|
|
2
|
-
__version__ = "0.7.
|
|
2
|
+
__version__ = "0.7.6"
|
|
@@ -281,7 +281,7 @@ def _scored_run(root: Path, cfg, lanes, *, reuse_artifacts: bool, reuse_unchange
|
|
|
281
281
|
`git` is the caller's GitFacts when it already has one — verify asks for the
|
|
282
282
|
dirty set before this runs, and that answer is the one the lanes must see too.
|
|
283
283
|
"""
|
|
284
|
-
from ..score import score_rows
|
|
284
|
+
from ..score import SharedSpanFold, score_rows
|
|
285
285
|
|
|
286
286
|
git = git or GitFacts(root)
|
|
287
287
|
commit, rows, corpus, cache_hits, tool_versions = _build_inventory(root, cfg, git)
|
|
@@ -294,14 +294,55 @@ def _scored_run(root: Path, cfg, lanes, *, reuse_artifacts: bool, reuse_unchange
|
|
|
294
294
|
# Only scopes a SUCCESSFUL lane covers count as measured; a failed lane's
|
|
295
295
|
# scopes fall back to no-lane flags rather than reading as untested code.
|
|
296
296
|
lane_scopes = {s for lane in succeeded for s in lane.scopes}
|
|
297
|
+
shared_spans = SharedSpanFold()
|
|
297
298
|
scored = score_rows(rows, coverage_by_path, lane_scopes=lane_scopes, target=cfg.target,
|
|
298
299
|
scope_targets=cfg.scope_targets,
|
|
299
|
-
cc_only_scopes=cfg.coverage_optional_scopes
|
|
300
|
+
cc_only_scopes=cfg.coverage_optional_scopes,
|
|
301
|
+
shared_spans=shared_spans)
|
|
302
|
+
_note_shared_spans(shared_spans, cfg)
|
|
300
303
|
test_failures = {f for prov in provenance.values() for f in prov.get("failures", ())}
|
|
301
304
|
return _ScoredRun(commit, scored, provenance, lane_errors, test_failures, tool_versions,
|
|
302
305
|
corpus, cache_hits, dead_lines)
|
|
303
306
|
|
|
304
307
|
|
|
308
|
+
_SPANS_NAMED = 3
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _over_target_at_zero(members, cfg):
|
|
312
|
+
"""The worst function on each shared span that its ceiling can fail.
|
|
313
|
+
|
|
314
|
+
Every function on a shared span scores as uncovered, and a function of
|
|
315
|
+
complexity 2 scores 6 at zero coverage, so no ceiling of 6 can fail it.
|
|
316
|
+
The count carries those; the named lines are the ones worth splitting.
|
|
317
|
+
"""
|
|
318
|
+
from ..score import crap
|
|
319
|
+
|
|
320
|
+
for span in members:
|
|
321
|
+
worst = max(span, key=lambda row: row.ccn)
|
|
322
|
+
if crap(worst.ccn, 0.0) > cfg.ceiling_of(worst.scope):
|
|
323
|
+
yield worst
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _note_shared_spans(fold, cfg) -> None:
|
|
327
|
+
"""Name the source line spans more than one function declares.
|
|
328
|
+
|
|
329
|
+
Loud but not fatal, the shape `_note_unanalyzable` settled for unreadable
|
|
330
|
+
files: the join cannot tell whose coverage is whose on such a span, and
|
|
331
|
+
ending the run over it left the consumer repo with no coverage run at all.
|
|
332
|
+
"""
|
|
333
|
+
if not fold.sites:
|
|
334
|
+
return
|
|
335
|
+
over = sorted(_over_target_at_zero(fold.sites, cfg), key=lambda row: (-row.ccn, row.path))
|
|
336
|
+
tail = f"; {len(over)} of them hold a function over its target:" if over else ""
|
|
337
|
+
print(f"crapkit: {len(fold.sites)} source line span(s) hold more than one function; "
|
|
338
|
+
"coverage cannot say whose is whose, so those functions score as uncovered, and "
|
|
339
|
+
f"splitting the definitions onto separate lines measures them{tail}", file=sys.stderr)
|
|
340
|
+
for row in over[:_SPANS_NAMED]:
|
|
341
|
+
print(f"crapkit: {row.path}:{row.start} (complexity {row.ccn})", file=sys.stderr)
|
|
342
|
+
if len(over) > _SPANS_NAMED:
|
|
343
|
+
print(f"crapkit: ... and {len(over) - _SPANS_NAMED} more", file=sys.stderr)
|
|
344
|
+
|
|
345
|
+
|
|
305
346
|
def _run_kind(lanes, cfg, failures) -> str:
|
|
306
347
|
"""A --lane subset or a run with failed lanes must never serve as the
|
|
307
348
|
verify baseline: its lane set differs from what verify runs, so every
|
|
@@ -776,6 +817,6 @@ def cmd_rescore(args: argparse.Namespace) -> int:
|
|
|
776
817
|
def _print_rescore_table(overlay, latest: dict) -> None:
|
|
777
818
|
"""The refactor loop's view: fresh ccn, worst first, stale cov labeled."""
|
|
778
819
|
print(f"rescore vs run {latest['id']} @ {latest['commit'][:11]} (coverage STALE, complexity fresh)")
|
|
779
|
-
print(f" {'ccn':>4} {'cov':>5} {'crap':>8} {'remedy':
|
|
820
|
+
print(f" {'ccn':>4} {'cov':>5} {'crap':>8} {'remedy':11} function")
|
|
780
821
|
for r in sorted(overlay, key=lambda x: (-x.ccn, x.path, x.start)):
|
|
781
|
-
print(f" {r.ccn:>4} {r.cov:>5.0%} {r.crap:>8.1f} {r.remedy:
|
|
822
|
+
print(f" {r.ccn:>4} {r.cov:>5.0%} {r.crap:>8.1f} {r.remedy:11} {r.path}:{r.start} {r.long_name}")
|
|
@@ -30,6 +30,15 @@ _NAME_DESCRIPTION = ("the bare identifier (classify, or route for a Rust "
|
|
|
30
30
|
"printed (classify( score , late )); both resolve, exact "
|
|
31
31
|
"match first")
|
|
32
32
|
|
|
33
|
+
# The scorer's remedy vocabulary, said once: a structured result carrying a value
|
|
34
|
+
# its outputSchema does not list is rejected whole by a validating client.
|
|
35
|
+
_REMEDIES = ("decompose", "split-lines", "add-tests", "ok")
|
|
36
|
+
_REMEDY_DESCRIPTION = ("decompose (ccn over ceiling), split-lines (another function shares its "
|
|
37
|
+
"source lines, so coverage cannot tell them apart and no test lowers the "
|
|
38
|
+
"score until the definitions sit on separate lines), add-tests (coverage "
|
|
39
|
+
"short) or ok (nothing left to do)")
|
|
40
|
+
_REMEDY = {"type": "string", "description": _REMEDY_DESCRIPTION, "enum": _REMEDIES}
|
|
41
|
+
|
|
33
42
|
# The partition a large repo needs before `top` means anything: one --scope
|
|
34
43
|
# per element, exact names as declared in crapkit.toml.
|
|
35
44
|
_SCOPE = {
|
|
@@ -63,10 +72,7 @@ _PACKET_PROPERTIES = {'scope': {'type': 'string', 'description': 'the declared s
|
|
|
63
72
|
'measure this span',
|
|
64
73
|
'enum': ('measured', 'untested', 'no-lane', 'cc-only')},
|
|
65
74
|
'crap': {'type': 'number', 'description': 'the score: ccn^2 x (1 - cov)^3 + ccn'},
|
|
66
|
-
'remedy':
|
|
67
|
-
'description': 'decompose (ccn over ceiling), add-tests (coverage short) or ok '
|
|
68
|
-
'(nothing left to do)',
|
|
69
|
-
'enum': ('decompose', 'add-tests', 'ok')},
|
|
75
|
+
'remedy': _REMEDY,
|
|
70
76
|
'target': {'type': 'integer', 'description': "this scope's effective ccn ceiling"},
|
|
71
77
|
'commits': {'type': 'integer', 'description': 'commits touching the file in the churn window'},
|
|
72
78
|
'authors': {'type': 'integer', 'description': 'distinct authors of those commits'},
|
|
@@ -112,9 +118,9 @@ _WORKLIST_ITEM = {'type': 'object',
|
|
|
112
118
|
'inventory-only run',
|
|
113
119
|
'enum': ('measured', 'untested', 'no-lane', 'cc-only', None)},
|
|
114
120
|
'remedy': {'type': 'string',
|
|
115
|
-
'description': 'decompose, add-tests or ok;
|
|
116
|
-
'
|
|
117
|
-
'enum':
|
|
121
|
+
'description': 'decompose, split-lines, add-tests or ok; every row '
|
|
122
|
+
'but ok reaches get_next_item when a lane measures it',
|
|
123
|
+
'enum': _REMEDIES},
|
|
118
124
|
'crap': {'type': ('number', 'null'),
|
|
119
125
|
'description': 'the score from the ranked run; null on an inventory-only '
|
|
120
126
|
'run'},
|
|
@@ -469,9 +475,9 @@ TOOLS: tuple[dict, ...] = (
|
|
|
469
475
|
"value get_next_item prints")},
|
|
470
476
|
"remedy": {
|
|
471
477
|
"type": "string",
|
|
472
|
-
"description": ("decompose, add-tests or ok: the branch the
|
|
473
|
-
"scored.remedy carries the same value"),
|
|
474
|
-
"enum":
|
|
478
|
+
"description": ("decompose, split-lines, add-tests or ok: the branch the "
|
|
479
|
+
"session takes; scored.remedy carries the same value"),
|
|
480
|
+
"enum": _REMEDIES},
|
|
475
481
|
"target": {
|
|
476
482
|
"type": "integer",
|
|
477
483
|
"description": ("this scope's effective ccn ceiling, the same value as "
|
|
@@ -527,11 +533,7 @@ TOOLS: tuple[dict, ...] = (
|
|
|
527
533
|
"crap": {
|
|
528
534
|
"type": "number",
|
|
529
535
|
"description": "the score: ccn^2 x (1 - cov)^3 + ccn"},
|
|
530
|
-
"remedy":
|
|
531
|
-
"type": "string",
|
|
532
|
-
"description": ("decompose (ccn over ceiling), add-tests (coverage short) or "
|
|
533
|
-
"ok (nothing left to do)"),
|
|
534
|
-
"enum": ("decompose", "add-tests", "ok")}}},
|
|
536
|
+
"remedy": _REMEDY}},
|
|
535
537
|
"source": {
|
|
536
538
|
"type": "string",
|
|
537
539
|
"description": ("the function's own text, start to end inclusive, newlines intact: "
|
|
@@ -590,11 +592,7 @@ TOOLS: tuple[dict, ...] = (
|
|
|
590
592
|
"crap": {
|
|
591
593
|
"type": "number",
|
|
592
594
|
"description": "score"},
|
|
593
|
-
"remedy":
|
|
594
|
-
"type": "string",
|
|
595
|
-
"description": ("decompose (ccn over ceiling), add-tests (coverage short) "
|
|
596
|
-
"or ok (nothing left to do)"),
|
|
597
|
-
"enum": ("decompose", "add-tests", "ok")}}}},
|
|
595
|
+
"remedy": _REMEDY}}},
|
|
598
596
|
"file_totals": {
|
|
599
597
|
"type": "object",
|
|
600
598
|
"description": "the file rolled up",
|
|
@@ -1342,11 +1340,7 @@ TOOLS: tuple[dict, ...] = (
|
|
|
1342
1340
|
"crap": {
|
|
1343
1341
|
"type": "number",
|
|
1344
1342
|
"description": "score from fresh ccn and baseline cov"},
|
|
1345
|
-
"remedy":
|
|
1346
|
-
"type": "string",
|
|
1347
|
-
"description": ("decompose (ccn over ceiling), add-tests (coverage short) "
|
|
1348
|
-
"or ok (nothing left to do)"),
|
|
1349
|
-
"enum": ("decompose", "add-tests", "ok")},
|
|
1343
|
+
"remedy": _REMEDY,
|
|
1350
1344
|
"stale_coverage": {
|
|
1351
1345
|
"type": "boolean",
|
|
1352
1346
|
"description": ("always true: complexity is the working tree's, coverage "
|
|
@@ -1393,11 +1387,7 @@ TOOLS: tuple[dict, ...] = (
|
|
|
1393
1387
|
"crap": {
|
|
1394
1388
|
"type": "number",
|
|
1395
1389
|
"description": "score"},
|
|
1396
|
-
"remedy":
|
|
1397
|
-
"type": "string",
|
|
1398
|
-
"description": ("decompose (ccn over ceiling), add-tests "
|
|
1399
|
-
"(coverage short) or ok (nothing left to do)"),
|
|
1400
|
-
"enum": ("decompose", "add-tests", "ok")},
|
|
1390
|
+
"remedy": _REMEDY,
|
|
1401
1391
|
"key_name": {
|
|
1402
1392
|
"type": "string",
|
|
1403
1393
|
"description": "the ratchet key form of the name"},
|
|
@@ -128,13 +128,20 @@ def _best_match(row: InventoryRow, candidates: list[FnCoverage]) -> FnCoverage |
|
|
|
128
128
|
return best
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
def _remedy(ccn: int, score: float, ceiling: int) -> str:
|
|
131
|
+
def _remedy(ccn: int, score: float, ceiling: int, shared_span: bool = False) -> str:
|
|
132
|
+
"""The first thing that can lower this score. A function sharing its source
|
|
133
|
+
line span with another scores as uncovered whatever its tests do, so
|
|
134
|
+
add-tests there is advice nobody can follow: splitting the definitions is,
|
|
135
|
+
and the run after the split says whether tests are still owed."""
|
|
132
136
|
if ccn > ceiling:
|
|
133
137
|
return "decompose"
|
|
134
|
-
|
|
138
|
+
if score <= ceiling:
|
|
139
|
+
return "ok"
|
|
140
|
+
return "split-lines" if shared_span else "add-tests"
|
|
135
141
|
|
|
136
142
|
|
|
137
|
-
def _finish(row, cov: float, flag: str, *, target: int, scope_targets
|
|
143
|
+
def _finish(row, cov: float, flag: str, *, target: int, scope_targets,
|
|
144
|
+
shared_span: bool = False) -> ScoredRow:
|
|
138
145
|
# cc-only is the pre-commit hook's rule: crap IS ccn, so _remedy can only
|
|
139
146
|
# answer ok or decompose. Feeding it cov=0 through the formula would say
|
|
140
147
|
# add-tests about code no test can reach.
|
|
@@ -146,7 +153,7 @@ def _finish(row, cov: float, flag: str, *, target: int, scope_targets) -> Scored
|
|
|
146
153
|
# built a throwaway dict per row and looked every field up by name.
|
|
147
154
|
return ScoredRow(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7],
|
|
148
155
|
row[8], row[9], row[10],
|
|
149
|
-
cov, flag, score, _remedy(row[7], score, ceiling), row[11], row[12])
|
|
156
|
+
cov, flag, score, _remedy(row[7], score, ceiling, shared_span), row[11], row[12])
|
|
150
157
|
|
|
151
158
|
|
|
152
159
|
def _nearest_overlay(row, candidates) -> list:
|
|
@@ -277,35 +284,87 @@ def overlay_stale_coverage(
|
|
|
277
284
|
if r.flag == "measured":
|
|
278
285
|
by_key.setdefault((r.path, r.long_name), []).append(r)
|
|
279
286
|
|
|
287
|
+
shared = _shared_source_spans(rows, lane_scopes, cc_only_scopes)
|
|
280
288
|
scored = []
|
|
281
289
|
for row in rows:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
scored.append(_finish(row, cov, flag, target=target, scope_targets=scope_targets
|
|
290
|
+
verdict = _cov_without_join(row, lane_scopes, cc_only_scopes)
|
|
291
|
+
cov, flag = verdict or _named_overlay_cov(row, by_key, positions)
|
|
292
|
+
scored.append(_finish(row, cov, flag, target=target, scope_targets=scope_targets,
|
|
293
|
+
shared_span=_on_shared_span(row, verdict, shared)))
|
|
285
294
|
return scored
|
|
286
295
|
|
|
287
296
|
|
|
297
|
+
class SharedSpanFold:
|
|
298
|
+
"""The source line spans more than one function declares in one run.
|
|
299
|
+
|
|
300
|
+
The join cannot tell whose coverage is whose there, so each such function
|
|
301
|
+
scores as uncovered and the run names the spans instead of ending. Filled
|
|
302
|
+
while scoring; the caller reports it once.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
def __init__(self) -> None:
|
|
306
|
+
self.sites: list[list[InventoryRow]] = []
|
|
307
|
+
|
|
308
|
+
def add(self, members: list[InventoryRow]) -> None:
|
|
309
|
+
self.sites.append(members)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _identity(row) -> tuple:
|
|
313
|
+
"""What separates two functions on one span: the name, and the occurrence
|
|
314
|
+
that tells sibling callbacks on a line apart."""
|
|
315
|
+
return row.long_name, row.occurrence
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _join_member(members: list, row) -> None:
|
|
319
|
+
"""A third function on the span joins its members; another copy of one that
|
|
320
|
+
is already there (the same function in a second scope) does not."""
|
|
321
|
+
if all(_identity(member) != _identity(row) for member in members):
|
|
322
|
+
members.append(row)
|
|
323
|
+
|
|
324
|
+
|
|
288
325
|
def _shared_source_spans(rows, lane_scopes: set, cc_only_scopes) -> dict:
|
|
289
|
-
|
|
326
|
+
"""span -> the distinct functions declaring it, for spans more than one does."""
|
|
327
|
+
first, collisions = {}, {}
|
|
290
328
|
for row in rows:
|
|
291
329
|
if _cov_without_join(row, lane_scopes, cc_only_scopes) is not None:
|
|
292
330
|
continue
|
|
293
331
|
span = row.path, row.start, row.end
|
|
294
|
-
|
|
295
|
-
if seen
|
|
296
|
-
collisions[
|
|
332
|
+
seen = first.setdefault(span, row)
|
|
333
|
+
if _identity(seen) != _identity(row):
|
|
334
|
+
_join_member(collisions.setdefault(span, [seen]), row)
|
|
297
335
|
return collisions
|
|
298
336
|
|
|
299
337
|
|
|
300
|
-
def
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
338
|
+
def _ambiguous_spans(shared: dict, coverage_by_path: dict, start_index: dict) -> dict:
|
|
339
|
+
"""The shared spans a measurement would speak about, so the join would hand
|
|
340
|
+
every function on the span one function's number.
|
|
341
|
+
|
|
342
|
+
Through 0.7.4 this raised and ended the run. One consumer repo holds 591
|
|
343
|
+
shared spans, 459 of them measured, so a run died on the first one it met
|
|
344
|
+
and the repo never finished a coverage run at all. Refusing the ambiguous
|
|
345
|
+
number is the specified behaviour; ending the run over it was not, the
|
|
346
|
+
shape _note_unanalyzable settled for unreadable files.
|
|
347
|
+
"""
|
|
348
|
+
ambiguous = {}
|
|
349
|
+
for span, members in shared.items():
|
|
350
|
+
if _span_join_cov(members[0], coverage_by_path, start_index)[1] == "measured":
|
|
351
|
+
ambiguous[span] = members
|
|
352
|
+
return ambiguous
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _joined_cov(row, ambiguous: dict, coverage_by_path: dict,
|
|
356
|
+
start_index: dict) -> tuple[float, str]:
|
|
357
|
+
"""Uncovered on an ambiguous span, never the neighbour's number: the honest
|
|
358
|
+
floor for a function whose measurement cannot be told from another's."""
|
|
359
|
+
if (row.path, row.start, row.end) in ambiguous:
|
|
360
|
+
return 0.0, "untested"
|
|
361
|
+
return _span_join_cov(row, coverage_by_path, start_index)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _on_shared_span(row, verdict, shared: dict) -> bool:
|
|
365
|
+
"""A row some lane measures, on a span it shares. Measured yet or not: tests
|
|
366
|
+
would only make the span measured, and then it scores as uncovered."""
|
|
367
|
+
return verdict is None and (row.path, row.start, row.end) in shared
|
|
309
368
|
|
|
310
369
|
|
|
311
370
|
def score_rows(
|
|
@@ -316,12 +375,18 @@ def score_rows(
|
|
|
316
375
|
target: int = 6,
|
|
317
376
|
scope_targets: dict[str, int] | None = None,
|
|
318
377
|
cc_only_scopes: frozenset[str] = frozenset(),
|
|
378
|
+
shared_spans: SharedSpanFold | None = None,
|
|
319
379
|
) -> list[ScoredRow]:
|
|
320
380
|
start_index = _start_index(coverage_by_path)
|
|
321
|
-
|
|
381
|
+
shared = _shared_source_spans(rows, lane_scopes, cc_only_scopes)
|
|
382
|
+
ambiguous = _ambiguous_spans(shared, coverage_by_path, start_index)
|
|
383
|
+
for members in ambiguous.values():
|
|
384
|
+
if shared_spans is not None:
|
|
385
|
+
shared_spans.add(members)
|
|
322
386
|
scored = []
|
|
323
387
|
for r in rows:
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
scored.append(_finish(r, cov, flag, target=target, scope_targets=scope_targets
|
|
388
|
+
verdict = _cov_without_join(r, lane_scopes, cc_only_scopes)
|
|
389
|
+
cov, flag = verdict or _joined_cov(r, ambiguous, coverage_by_path, start_index)
|
|
390
|
+
scored.append(_finish(r, cov, flag, target=target, scope_targets=scope_targets,
|
|
391
|
+
shared_span=_on_shared_span(r, verdict, shared)))
|
|
327
392
|
return scored
|
|
@@ -70,7 +70,7 @@ _CODE_DDL = """CREATE TABLE IF NOT EXISTS {table} (
|
|
|
70
70
|
# and a store is a file people copy between machines. A name from outside this
|
|
71
71
|
# list is still stored, at a code minted after these.
|
|
72
72
|
_CODE_SEEDS = {"flags": ("measured", "untested", "no-lane", "cc-only"),
|
|
73
|
-
"remedies": ("ok", "add-tests", "decompose")}
|
|
73
|
+
"remedies": ("ok", "add-tests", "decompose", "split-lines")}
|
|
74
74
|
|
|
75
75
|
_SCHEMA = f"""
|
|
76
76
|
CREATE TABLE IF NOT EXISTS runs (
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crapkit
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.6
|
|
4
4
|
Summary: Scores every function on complexity times uncovered risk, ranks the worst, and blocks commits that add more.
|
|
5
5
|
Author: Jean-Francois Gagne
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/JeanFrancoisGagne/crapkit
|
|
8
|
-
Project-URL: Documentation, https://
|
|
8
|
+
Project-URL: Documentation, https://www.jfgagne.com/crapkit/handbook.html
|
|
9
9
|
Project-URL: Changelog, https://github.com/JeanFrancoisGagne/crapkit/blob/main/CHANGELOG.md
|
|
10
10
|
Project-URL: Issues, https://github.com/JeanFrancoisGagne/crapkit/issues
|
|
11
11
|
Project-URL: Source, https://github.com/JeanFrancoisGagne/crapkit
|
|
@@ -111,6 +111,9 @@ crapkit ratchet seed
|
|
|
111
111
|
git add crapkit.toml crapkit-ratchet.tsv .gitignore
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
Not a Python repo? `uvx crapkit init` runs the same commands and adds nothing to your
|
|
115
|
+
manifest: see [A repo that is not Python](#a-repo-that-is-not-python).
|
|
116
|
+
|
|
114
117
|
`init` detects pytest, Vitest and Jest from the repository's own files. Review the
|
|
115
118
|
generated config before running its commands. When detection leaves a commented
|
|
116
119
|
lane, fill it in using the [lane recipes](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md).
|
|
@@ -182,6 +185,28 @@ pip install git+https://github.com/JeanFrancoisGagne/crapkit.git
|
|
|
182
185
|
pip install .
|
|
183
186
|
```
|
|
184
187
|
|
|
188
|
+
### A repo that is not Python
|
|
189
|
+
|
|
190
|
+
crapkit is a command-line tool, never a dependency of the code it scores. A TypeScript,
|
|
191
|
+
Go or Rust repo adds nothing to its own manifest. With [uv](https://docs.astral.sh/uv/)
|
|
192
|
+
on the machine, `uvx` fetches crapkit into a cache of its own and runs it:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
$ uvx crapkit init
|
|
196
|
+
wrote crapkit.toml with 1 scope(s): src
|
|
197
|
+
detected 1 lane(s) from this repo's own files: js - next: run `crapkit coverage`
|
|
198
|
+
added to .gitignore: .crapkit/
|
|
199
|
+
|
|
200
|
+
$ uvx crapkit coverage
|
|
201
|
+
$ uvx crapkit worklist
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The lane still runs your own test runner, so Vitest or Jest and its coverage package come
|
|
205
|
+
from the repo's `node_modules` as they do today. `uv tool install crapkit` or
|
|
206
|
+
`pipx install crapkit` puts a `crapkit` command on PATH once, which is what the
|
|
207
|
+
[commit gate](#the-gate) and the Claude Code plugin call. uv brings its own Python when
|
|
208
|
+
the machine has none.
|
|
209
|
+
|
|
185
210
|
Requires Python 3.11 or newer and Git on PATH. The CLI has one runtime dependency,
|
|
186
211
|
`lizard>=1.24.0`; a package mirror needs both distributions. Install into the environment
|
|
187
212
|
you intend to use, then check `crapkit --version`. The `pip install -e ".[dev]"` under
|
|
@@ -199,7 +224,7 @@ files. Review those commands before running Crapkit in a repository you do not t
|
|
|
199
224
|
|
|
200
225
|
```
|
|
201
226
|
$ crapkit --version
|
|
202
|
-
crapkit 0.7.
|
|
227
|
+
crapkit 0.7.6
|
|
203
228
|
```
|
|
204
229
|
|
|
205
230
|
`python -m crapkit` works identically to the console script and is what to use from a
|
|
@@ -455,7 +480,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
|
|
|
455
480
|
repos:
|
|
456
481
|
- repo: https://github.com/JeanFrancoisGagne/crapkit
|
|
457
482
|
# crapkit's release step rewrites this line to the tag it just cut
|
|
458
|
-
rev: v0.7.
|
|
483
|
+
rev: v0.7.6
|
|
459
484
|
hooks:
|
|
460
485
|
- id: crapkit-gate
|
|
461
486
|
```
|
|
@@ -563,7 +588,7 @@ to a workflow, and every input has a default:
|
|
|
563
588
|
- uses: actions/checkout@v4
|
|
564
589
|
with:
|
|
565
590
|
fetch-depth: 0
|
|
566
|
-
- uses: JeanFrancoisGagne/crapkit@v0.7.
|
|
591
|
+
- uses: JeanFrancoisGagne/crapkit@v0.7.6
|
|
567
592
|
```
|
|
568
593
|
|
|
569
594
|
The whole job those four lines sit in:
|
|
@@ -583,7 +608,7 @@ jobs:
|
|
|
583
608
|
with:
|
|
584
609
|
python-version: "3.12" # the interpreter the install below lands in
|
|
585
610
|
- run: pip install -e ".[dev]" # whatever your lanes need to run
|
|
586
|
-
- uses: JeanFrancoisGagne/crapkit@v0.7.
|
|
611
|
+
- uses: JeanFrancoisGagne/crapkit@v0.7.6
|
|
587
612
|
with:
|
|
588
613
|
gate: "false"
|
|
589
614
|
```
|
|
@@ -654,7 +679,7 @@ closes it. A verify that passed is one line: `**verify passed.** Run 2 against b
|
|
|
654
679
|
The rows are the ranked worklist for the files the pull request changed, worst first,
|
|
655
680
|
`top` of them, with the rows a finding names listed first. `risk` is ccn times churn
|
|
656
681
|
weight, the number `crapkit worklist` ranks on, and `remedy` is the run's own verdict for
|
|
657
|
-
that function: `decompose`, `add-tests` or `ok`. `(accepted debt)` marks a function the
|
|
682
|
+
that function: `decompose`, `split-lines`, `add-tests` or `ok`. `(accepted debt)` marks a function the
|
|
658
683
|
committed ratchet carries a mark for, so an untouched `legacy_router` does not read like
|
|
659
684
|
the pull request's own new function. A pull request that touches no ranked function gets
|
|
660
685
|
the heading and no table.
|
|
@@ -830,6 +855,7 @@ The coverage summary counts all four as `measured` / `untested` / `no_lane` / `c
|
|
|
830
855
|
| Remedy | Condition | Action |
|
|
831
856
|
|---|---|---|
|
|
832
857
|
| `decompose` | `ccn > ceiling` | Split it. No amount of coverage clears this. |
|
|
858
|
+
| `split-lines` | `ccn <= ceiling`, `crap > ceiling`, and another function shares its source lines | Put each definition on its own lines, then measure again. Coverage cannot tell functions on one line apart, so the score stays at uncovered whatever the tests do. |
|
|
833
859
|
| `add-tests` | `ccn <= ceiling` and `crap > ceiling` | Cover the branches. |
|
|
834
860
|
| `ok` | `crap <= ceiling` | Nothing. |
|
|
835
861
|
|
|
@@ -1268,12 +1294,12 @@ export function classify(row: Row): string {
|
|
|
1268
1294
|
```
|
|
1269
1295
|
$ crapkit rescore src/grade.ts --gate
|
|
1270
1296
|
rescore vs run 1 @ 8bfbe613fcd (coverage STALE, complexity fresh)
|
|
1271
|
-
ccn cov crap remedy
|
|
1272
|
-
5 0% 30.0 add-tests
|
|
1273
|
-
5 0% 30.0 add-tests
|
|
1274
|
-
4 0% 20.0 add-tests
|
|
1275
|
-
4 45% 6.7 add-tests
|
|
1276
|
-
4 75% 4.2 ok
|
|
1297
|
+
ccn cov crap remedy function
|
|
1298
|
+
5 0% 30.0 add-tests src/grade.ts:22 band ( score )
|
|
1299
|
+
5 0% 30.0 add-tests src/grade.ts:38 demote ( letter , row Row )
|
|
1300
|
+
4 0% 20.0 add-tests src/grade.ts:8 penalty ( attempts , late )
|
|
1301
|
+
4 45% 6.7 add-tests src/grade.ts:48 classify ( row Row )
|
|
1302
|
+
4 75% 4.2 ok src/grade.ts:59 average ( scores Array )
|
|
1277
1303
|
```
|
|
1278
1304
|
|
|
1279
1305
|
Exit 0: every piece is at or under 6. The `crap` column is loud because its coverage half
|
|
@@ -1340,7 +1366,7 @@ with no debt.
|
|
|
1340
1366
|
|
|
1341
1367
|
| Page | Covers |
|
|
1342
1368
|
|---|---|
|
|
1343
|
-
| [The handbook](https://
|
|
1369
|
+
| [The handbook](https://www.jfgagne.com/crapkit/handbook.html) | **Start here for anything deeper.** The illustrated handbook: what crapkit is, how every piece works, and where each command earns its keep. Also at [docs/handbook.html](https://www.jfgagne.com/crapkit/handbook.html), self-contained, so it opens straight from a clone. |
|
|
1344
1370
|
| [docs/adoption.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/adoption.md) | The judgment layer over the quickstarts: scope granularity, exclude vs lane, scoped_tests wiring, the first-verify taint hazard. |
|
|
1345
1371
|
| [docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md) | Every `crapkit.toml` key: type, default, and what it does. |
|
|
1346
1372
|
| [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md) | The lane model, vitest and jest and pytest recipes, artifact reuse, flake retest, containers. |
|
|
@@ -1371,6 +1397,12 @@ line arms the complexity gate. See
|
|
|
1371
1397
|
for development and [the verified implementation report](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/architecture/2026-09-07-implementation/REPORT.md)
|
|
1372
1398
|
for complete Windows source and Linux wheel results, focused benchmarks and their limits.
|
|
1373
1399
|
|
|
1400
|
+
## Maintainer and project background
|
|
1401
|
+
|
|
1402
|
+
crapkit is created and maintained by [Jean-François Gagné](https://www.jfgagne.com/).
|
|
1403
|
+
Read the [project background](https://www.jfgagne.com/projects/crapkit/) for the
|
|
1404
|
+
problem it addresses and how it fits into his work on software and AI.
|
|
1405
|
+
|
|
1374
1406
|
## License
|
|
1375
1407
|
|
|
1376
1408
|
MIT. See [LICENSE](https://github.com/JeanFrancoisGagne/crapkit/blob/main/LICENSE).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|