crapkit 0.4.4__tar.gz → 0.4.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.
Files changed (74) hide show
  1. {crapkit-0.4.4 → crapkit-0.4.6}/PKG-INFO +208 -59
  2. crapkit-0.4.4/src/crapkit.egg-info/PKG-INFO → crapkit-0.4.6/README.md +972 -862
  3. crapkit-0.4.6/pyproject.toml +104 -0
  4. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/__init__.py +1 -1
  5. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/analyze.py +26 -9
  6. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/churn_cache.py +42 -5
  7. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/churn_log.py +45 -1
  8. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/__init__.py +22 -0
  9. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/admin.py +428 -57
  10. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/analyses.py +49 -5
  11. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/claude_hook.py +8 -1
  12. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/parser.py +9 -3
  13. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/queue.py +62 -29
  14. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/ratchet_cmds.py +29 -13
  15. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/reports.py +2 -1
  16. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/scoring.py +41 -15
  17. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/verifying.py +112 -23
  18. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/config.py +209 -32
  19. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/coupling.py +45 -10
  20. crapkit-0.4.6/src/crapkit/coupling_cache.py +147 -0
  21. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/covstream.py +48 -6
  22. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/dup.py +53 -5
  23. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/gitio.py +179 -23
  24. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/lanes.py +268 -28
  25. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/lizardcognitive.py +90 -6
  26. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/lizardshell.py +3 -3
  27. crapkit-0.4.6/src/crapkit/mutate_pool.py +308 -0
  28. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/packet.py +10 -3
  29. crapkit-0.4.6/src/crapkit/procs.py +66 -0
  30. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/scaffold.py +140 -31
  31. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/store.py +173 -40
  32. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/uncovered.py +96 -8
  33. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/universe.py +61 -23
  34. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/verify.py +28 -4
  35. crapkit-0.4.4/README.md → crapkit-0.4.6/src/crapkit.egg-info/PKG-INFO +1011 -827
  36. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit.egg-info/SOURCES.txt +2 -0
  37. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit.egg-info/requires.txt +2 -0
  38. crapkit-0.4.4/pyproject.toml +0 -70
  39. crapkit-0.4.4/src/crapkit/mutate_pool.py +0 -152
  40. {crapkit-0.4.4 → crapkit-0.4.6}/LICENSE +0 -0
  41. {crapkit-0.4.4 → crapkit-0.4.6}/setup.cfg +0 -0
  42. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/__main__.py +0 -0
  43. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/_pygdefer.py +0 -0
  44. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cache.py +0 -0
  45. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/churn.py +0 -0
  46. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/cli/_shared.py +0 -0
  47. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/coverage_istanbul.py +0 -0
  48. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/coverage_py.py +0 -0
  49. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/diffparse.py +0 -0
  50. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/digest.py +0 -0
  51. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/discover.py +0 -0
  52. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/doctor.py +0 -0
  53. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/errors.py +0 -0
  54. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/hook.py +0 -0
  55. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/junitparse.py +0 -0
  56. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/keys.py +0 -0
  57. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/lizardpowershell.py +0 -0
  58. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/lizardrust.py +0 -0
  59. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/mcp_server.py +0 -0
  60. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/merge.py +0 -0
  61. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/mutate.py +0 -0
  62. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/override.py +0 -0
  63. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/ratchet.py +0 -0
  64. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/ratchet_report.py +0 -0
  65. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/report.py +0 -0
  66. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/sarif.py +0 -0
  67. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/sarifio.py +0 -0
  68. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/score.py +0 -0
  69. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/snapshot.py +0 -0
  70. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/watch.py +0 -0
  71. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit/worklist.py +0 -0
  72. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit.egg-info/dependency_links.txt +0 -0
  73. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit.egg-info/entry_points.txt +0 -0
  74. {crapkit-0.4.4 → crapkit-0.4.6}/src/crapkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crapkit
3
- Version: 0.4.4
3
+ Version: 0.4.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
@@ -9,18 +9,20 @@ Project-URL: Documentation, https://jeanfrancoisgagne.github.io/crapkit/handbook
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
12
- Keywords: complexity,coverage,crap,code-quality,technical-debt,cyclomatic,ratchet,pre-commit,claude-code
12
+ Keywords: complexity,coverage,crap,code-quality,technical-debt,cyclomatic,ratchet,pre-commit,claude-code,mcp,mutation-testing,static-analysis
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Environment :: Console
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
19
20
  Classifier: Programming Language :: Python :: 3.11
20
21
  Classifier: Programming Language :: Python :: 3.12
21
22
  Classifier: Programming Language :: Python :: 3.13
22
23
  Classifier: Topic :: Software Development :: Quality Assurance
23
24
  Classifier: Topic :: Software Development :: Testing
25
+ Classifier: Topic :: Software Development :: Version Control :: Git
24
26
  Requires-Python: >=3.11
25
27
  Description-Content-Type: text/markdown
26
28
  License-File: LICENSE
@@ -29,8 +31,10 @@ Provides-Extra: dev
29
31
  Requires-Dist: pytest>=8; extra == "dev"
30
32
  Requires-Dist: pytest-cov>=5; extra == "dev"
31
33
  Requires-Dist: pytest-xdist>=3; extra == "dev"
34
+ Requires-Dist: coverage>=7.10.6; extra == "dev"
32
35
  Provides-Extra: py
33
36
  Requires-Dist: pytest-cov>=5; extra == "py"
37
+ Requires-Dist: coverage>=7.10.6; extra == "py"
34
38
  Dynamic: license-file
35
39
 
36
40
  # crapkit
@@ -96,9 +100,29 @@ out to your own test runner, and the runner needs its coverage package installed
96
100
  `pytest-cov` for pytest, `@vitest/coverage-v8` (pinned to your vitest major) for vitest.
97
101
  Without it the lane produces no artifact and `coverage` exits 5 quoting the runner's own
98
102
  error. For pytest, `init` probes the python its lane will run and prints the install
99
- command when `pytest_cov` is missing; `pip install 'crapkit[py]'` pulls the plugin
100
- alongside crapkit when the two share a venv. The two quickstarts below walk a real repo
101
- end to end.
103
+ command when `pytest_cov` is missing; `pip install "crapkit[py]"` pulls the plugin
104
+ alongside crapkit when the two share a venv. On a Windows PATH holding only the `py`
105
+ launcher it writes `py`, not a `python3` the lane could never run, and when cmd.exe cannot
106
+ start the interpreter at all (exit 9009, the Store alias) it names that instead of guessing
107
+ at pytest-cov. The two quickstarts below walk a real repo end to end.
108
+
109
+ **On Windows a lane command is read by cmd.exe**, the shell that will run it, not by sh.
110
+ Double quotes are the portable quoting. A single-quoted value is refused at config load
111
+ with exit 3, because cmd.exe would hand pytest five words and the lane would write no
112
+ artifact:
113
+
114
+ ```
115
+ # the lane in crapkit.toml
116
+ command = "python -m pytest -m 'not live and not perf' --cov=calc --cov-branch --cov-report=json:.crapkit/cov/py.json"
117
+
118
+ $ crapkit doctor
119
+ crapkit: lane 'py': positional argument 'live' narrows a full-suite coverage run; drop it, attach it to the flag it belongs to (-n8, --numprocesses=8), or set full_suite = false deliberately (cmd.exe does not treat ' as a quote: write the value in double quotes)
120
+ ```
121
+
122
+ Write it `-m "not live and not perf"`. Carets, `&&` and `|` segments, redirections and
123
+ empty quoted arguments all read the way the shell reads them, so a chained lane
124
+ (`cd tests && python -m pytest --cov ...`) is checked one segment at a time. `doctor` reads
125
+ a lane the same way, and FAILs one whose runner will not start.
102
126
 
103
127
  ## Install
104
128
 
@@ -121,30 +145,73 @@ changing crapkit.
121
145
 
122
146
  ```
123
147
  $ crapkit --version
124
- crapkit 0.4.4
148
+ crapkit 0.4.6
125
149
  ```
126
150
 
127
151
  `python -m crapkit` works identically to the console script and is what to use from a
128
152
  source checkout. Every subcommand accepts `--repo PATH` (default: the current directory),
129
- so you never have to `cd` into the repo you are scoring. The flag goes after the
130
- subcommand; [Subcommands](#subcommands) shows both orders.
153
+ so you never have to `cd` into the repo you are scoring; [Subcommands](#subcommands) shows
154
+ where the flag goes.
155
+
156
+ ## Upgrading from 0.4.4
131
157
 
132
- ### Upgrading on Windows
158
+ **Run `crapkit ratchet seed` first.** Shell cognitive complexity now nests, which is
159
+ analysis version 8, and marks measured under version 7 are not comparable. Until you
160
+ re-seed, `verify` refuses at exit 3:
161
+
162
+ ```
163
+ $ crapkit verify
164
+ crapkit: ratchet marks were recorded under [crapkit-analysis=7 lizard=1.24.0] but this run measures [crapkit-analysis=8 lizard=1.24.0] — CRAP scores are not comparable across metric versions; re-baseline with `crapkit ratchet seed`
165
+ ```
166
+
167
+ Only shell and PowerShell cognitive numbers move. `ccn` does not, so a re-seed re-stamps
168
+ the file and leaves the marks where they were.
169
+
170
+ Five more things change under you. Three of them need nothing from you:
171
+
172
+ - **New cache files.** `.crapkit/coupling-cache-v1.json` joins `churn-cache-v2.json` and
173
+ `churn-log-v2.z`. A warm 0.4.4 churn cache is adopted once and its file removed, and
174
+ `.crapkit/` is already gitignored, so nothing new reaches your index.
175
+ - **`trend` and `report` write.** Both read a per-run rollup table, filled once per run and
176
+ pruned with its run, instead of rescanning every scored row. A read-only `.crapkit/`
177
+ costs the speedup, never the command.
178
+ - **Nested scopes may move files.** One predicate decides scope ownership now, and the
179
+ deepest declared path wins, so a repo whose `[[scope]]` paths nest inside each other can
180
+ see files change scope, rollup and ceiling on the next scan. Scopes that do not nest see
181
+ no change.
182
+
183
+ The other two put something in front of you:
184
+
185
+ - **`mutate` keeps a worktree pool.** With `mutation_workers > 1` the worker worktrees now
186
+ live under `.crapkit/mutate-pool/` between runs and are re-prepared each run, which is
187
+ the setup cost gone (30.6 s to build four on a 31,459-file repo, 0.46 s to re-prepare
188
+ them). The pool is not size-bounded and nothing sweeps it: `crapkit mutate --drop-pool`
189
+ removes it and exits. Single-worker runs are untouched.
190
+ - **`doctor` WARNs on a lane with no `results_artifact`.** Every `coveragepy` or `istanbul`
191
+ lane written before 0.4.5 gets one, with the two lines that fix it. Coverage is
192
+ unaffected. What the lane cannot feed without a results file is the crashed-worker check
193
+ and the no-new-failures check (exit 8).
194
+
195
+ ### The exe lock on Windows
133
196
 
134
197
  `uv tool upgrade crapkit`, and `pip install -U` into a tool venv, fail with `os error 32`
135
198
  ("The process cannot access the file because it is being used by another process") while a
136
199
  crapkit MCP server is live: an agent session spawns `crapkit.exe mcp`, which holds the
137
200
  launcher, and Windows will not overwrite a running executable. The venv upgrades before
138
- that copy fails, so `crapkit --version` already reports the new version and the launcher is
139
- the only stale piece. Quit the agent session and rerun the upgrade, or rename the locked
140
- exe aside (Windows allows renaming a running one) and copy the new one in; the `.old` file
141
- goes at the next reboot.
201
+ that copy fails, so `crapkit --version` already reports the new version and only the
202
+ launcher is stale. Quit the agent session and rerun the upgrade, or rename the locked exe
203
+ aside (Windows allows renaming a running one) and copy the new one in. Two lines in
204
+ cmd.exe, where both `%` variables expand:
142
205
 
143
- ```
144
- mv ~/.local/bin/crapkit.exe ~/.local/bin/crapkit.exe.old
145
- cp %APPDATA%/uv/tools/crapkit/Scripts/crapkit.exe ~/.local/bin/crapkit.exe
206
+ ```bat
207
+ move %USERPROFILE%\.local\bin\crapkit.exe %USERPROFILE%\.local\bin\crapkit.exe.old
208
+ copy %APPDATA%\uv\tools\crapkit\Scripts\crapkit.exe %USERPROFILE%\.local\bin\crapkit.exe
146
209
  ```
147
210
 
211
+ Git Bash has no `move` and passes `%APPDATA%` through as literal text, so that block
212
+ fails there on its first line. Its form is `mv` and `cp` over `"$USERPROFILE"` and
213
+ `"$APPDATA"`, which Git Bash sets to the same two directories.
214
+
148
215
  ## The Claude Code plugin
149
216
 
150
217
  ```
@@ -211,10 +278,21 @@ different powers:
211
278
  | `crapkit verify` | before you push, and in CI | **the verdict.** Gate, ratchet, new test failures, diff coverage, against the trusted baseline |
212
279
 
213
280
  Both hooks exempt a function the committed ratchet already carries a mark for, so touching
214
- signed debt never refuses a commit. `verify` is what fails a mark that rises. The
215
- pre-commit hook reports each exemption count on stderr (`staged function(s) carry a
216
- ratchet mark and were not gated`), and says the same about a staged file no `[[scope]]`
217
- claims, so a new top-level directory cannot go ungated in silence.
281
+ signed debt never refuses a commit. `verify` is what fails a mark that rises. Since 0.4.5
282
+ its gate exempts a touched function whose fresh CRAP sits **at or under** its mark, the
283
+ rule `rescore --gate` already applied; push it past the mark and the gate fires again. The
284
+ pre-commit hook still exempts on the mark's existence alone, on purpose: a staged blob has
285
+ no coverage, so there is no fresh CRAP to compare against. It reports each exemption count
286
+ on stderr (`staged function(s) carry a ratchet mark and were not gated`), and says the same
287
+ about a staged file no `[[scope]]` claims, so a new top-level directory cannot go ungated
288
+ in silence.
289
+
290
+ **The crapkit root does not have to be the git top.** Since 0.4.5 every git spawn runs with
291
+ `diff.relative=true` and `core.quotePath=false`, so a `crapkit.toml` in `packages/api`
292
+ gates that package's own staged files and names them `app/m.py`, not
293
+ `packages/api/app/m.py`, and a dirty non-ASCII path is a real row rather than an invisible
294
+ one. Before that a nested root matched staged paths against no scope, and a function at
295
+ twice the ceiling committed with a warning.
218
296
 
219
297
  Git runs hooks outside your shell's activated venv. Bare `python` must resolve to an
220
298
  interpreter that has crapkit installed, or spell it out
@@ -268,11 +346,22 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
268
346
  repos:
269
347
  - repo: https://github.com/JeanFrancoisGagne/crapkit
270
348
  # crapkit's release step rewrites this line to the tag it just cut
271
- rev: v0.4.4
349
+ rev: v0.4.6
272
350
  hooks:
273
351
  - id: crapkit-gate
274
352
  ```
275
353
 
354
+ That file arms nothing on its own. The framework writes `.git/hooks/pre-commit` when you
355
+ tell it to, and until then `git commit` runs no gate and says nothing:
356
+
357
+ ```sh
358
+ pip install pre-commit
359
+ pre-commit install
360
+ ```
361
+
362
+ `pre-commit install` is the line every clone needs, the way Route 2 needs its
363
+ `git config core.hooksPath` line.
364
+
276
365
  `rev` is a git ref pre-commit resolves against that remote. Pin a release tag, not a
277
366
  branch: `pre-commit autoupdate` only moves between tags, and a moving `main` would change
278
367
  your gate under you.
@@ -295,6 +384,45 @@ crapkit verify --baseline-tsv crapkit-baseline.tsv --github
295
384
  writes SARIF 2.1.0 for code-scanning upload. Refresh the committed baseline whenever the
296
385
  default branch's verify passes.
297
386
 
387
+ Two things the job has to do before those lines run. **Install crapkit**, `pip install
388
+ crapkit`, and pin the version the way Route 3 pins `rev`: an unpinned install moves your
389
+ gate on whatever day a release lands. **Fetch the whole history.** `actions/checkout`
390
+ clones one commit by default, `verify` reads the diff against the baseline's commit out of
391
+ git, and a shallow clone does not have that commit:
392
+
393
+ ```
394
+ $ crapkit verify --baseline-tsv crapkit-baseline.tsv
395
+ crapkit: baseline commit a74260f321f is not an ancestor of HEAD (rebase or amend rewrote history) — run `crapkit coverage` for a fresh baseline
396
+ ```
397
+
398
+ That is exit 4 on a `git clone --depth 1` of a repo whose baseline verifies at full depth.
399
+ Set `fetch-depth: 0` on the checkout step, which is what crapkit's own
400
+ [.github/workflows/ci.yml](.github/workflows/ci.yml) does.
401
+
402
+ The whole PR job, on GitHub Actions:
403
+
404
+ ```yaml
405
+ on: pull_request
406
+ jobs:
407
+ crapkit:
408
+ runs-on: ubuntu-latest
409
+ steps:
410
+ - uses: actions/checkout@v4
411
+ with:
412
+ fetch-depth: 0 # verify needs the baseline's commit
413
+ - uses: actions/setup-python@v5
414
+ with:
415
+ python-version: "3.12"
416
+ - run: pip install crapkit
417
+ - run: pip install -e ".[dev]" # your own test dependencies
418
+ - run: crapkit verify --baseline-tsv crapkit-baseline.tsv --github
419
+ ```
420
+
421
+ The second install is the one people leave out. `verify` reruns your lanes, so the job
422
+ needs whatever your test command needs: the coverage plugin, `npm ci`, a database, all of
423
+ it. Without them the lane writes no artifact and `verify` exits 5 quoting the runner's own
424
+ error, which is a broken job and not a verdict.
425
+
298
426
  ### What a refusal looks like
299
427
 
300
428
  ```
@@ -339,30 +467,30 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
339
467
  | Command | What it does |
340
468
  |---|---|
341
469
  | `init` | Sniffs tracked source into per-directory scopes, writes a self-validated starter `crapkit.toml` whose lanes report into `.crapkit/cov/`, and appends `.crapkit/` plus each runner's own droppings to `.gitignore`. Writes a live `[[lane]]` when it can detect the test runner, otherwise a commented template. Refuses to clobber an existing config. |
342
- | `doctor [--show-files] [--json] [--tune] [--plugin-root PATH]` | Checks the config still describes the repo: unknown keys (with the accepted spellings), zero-file scopes, tracked source no scope claims, scopes no lane covers, lane cwds and commands that no longer resolve, lizard importable, oversized files. It WARNs on a lane writing its artifact at the repo root, a committed hook under `core.hooksPath` that is not executable in the index, a directory whose functions are all `untested` while its tests exist, and a scope a lane measures with no `[crapkit.scoped_tests]` template behind it, which is the loop's step 4 with nothing to run. `--tune` prints suggested parallelism knobs and writes nothing. `--plugin-root PATH` reads no repo at all: it checks an installed [plugin](plugin/) against this CLI on both version and hook `--protocol`, one line per disagreement and silence when they agree. See [docs/agent-json.md](docs/agent-json.md#doctor---json). |
470
+ | `doctor [--show-files] [--json] [--tune] [--plugin-root [PATH]]` | Checks the config still describes the repo: unknown keys (with the accepted spellings), zero-file scopes, tracked source no scope claims, scopes no lane covers, lane cwds and commands that no longer resolve, lizard importable, oversized files. It reads each lane command with the shell that will run it, so a quoted interpreter path is one word and a runner after `&&` is checked too, and it FAILs a lane whose runner does not resolve on PATH or that the shell cannot start, naming the word to change; each distinct runner is probed once, not once per lane. It WARNs on a lane writing its artifact at the repo root, a `coveragepy` or `istanbul` lane with no `results_artifact` (the crashed-worker and no-new-failures checks are off for it, whichever runner the lane spells), a committed hook under `core.hooksPath` that is not executable in the index, a directory whose functions are all `untested` while its tests exist, and a scope a lane measures with no `[crapkit.scoped_tests]` template behind it, which is the loop's step 4 with nothing to run. `--tune` prints suggested parallelism knobs and writes nothing. `--plugin-root PATH` reads no repo at all: it checks an installed [plugin](plugin/) against this CLI on both version and hook `--protocol`, one line per disagreement and silence when they agree; PATH is the plugin root or any directory above it, `~/.claude` included (only manifests named `crapkit` count, and the newest install wins), and with no PATH it looks in Claude Code's plugin cache. A root it found rather than one you typed is named first, as `crapkit doctor: checking PATH`. See [docs/agent-json.md](docs/agent-json.md#doctor---json). |
343
471
  | `inventory [--db PATH] [--export PATH] [--json]` | One lizard pass over every in-scope file into a SQLite snapshot run, cached by content hash. `--db` is the only way to point crapkit at a store outside `.crapkit/`, and only this command accepts it. |
344
472
  | `coverage [--lane NAME] [--reuse-artifacts] [--reuse-unchanged] [--export PATH] [--sarif PATH] [--github] [--json]` | Runs the lanes, joins branch coverage onto a fresh inventory, writes a scored run. A failed lane is recorded, not fatal: its scopes fall back to `no-lane` and the run is typed `partial`, so it can never serve as a baseline. See [docs/lanes.md](docs/lanes.md). |
345
- | `verify [--baseline ID \| --base REF \| --baseline-tsv PATH] [--emit-baseline PATH] [--override REASON] [--reuse-artifacts] [--reuse-unchanged] [--no-tighten] [--sarif PATH] [--github] [--json]` | The full verdict against the trusted baseline: gate on touched functions, ratchet, no new test failures, optional diff-coverage ceiling. The three baseline selectors are mutually exclusive; `--baseline ID` also bypasses the taint rule ([The trusted baseline](#the-trusted-baseline)), and `--baseline-tsv` reads a commit-stamped file so a fresh clone verifies with no store. `--no-tighten` passes the verdict without rewriting the ratchet. Findings a dirty tree produced are tagged `dirty` and counted apart. |
346
- | `worklist [--top N] [--scope NAME] [--batches N] [--json]` | The risk map: every admitted function ranked by `ccn * churn weight`, floored by `worklist_floor`, with hot simple code and anything over its ceiling admitted past that floor. It ranks finished rows and `no-lane` rows too, marked `ok` and `no-lane`, so it never empties; `next-item` carries the stop condition. `--scope NAME` (repeatable) is exact, not a substring. `--batches N` **adds** a `batches[]` view cutting the active list into at most N file-disjoint batches with co-changing files kept together; the normal keys stay. |
473
+ | `verify [--baseline ID \| --base REF \| --baseline-tsv PATH] [--emit-baseline PATH] [--override REASON] [--reuse-artifacts] [--reuse-unchanged] [--no-tighten] [--sarif PATH] [--github] [--json]` | The full verdict against the trusted baseline: gate on touched functions, ratchet, no new test failures, optional diff-coverage ceiling. The three baseline selectors are mutually exclusive; `--baseline ID` also bypasses the taint rule ([The trusted baseline](#the-trusted-baseline)), and `--baseline-tsv` reads a commit-stamped file so a fresh clone verifies with no store. `--no-tighten` passes the verdict without rewriting the ratchet. Findings a dirty tree produced are tagged `dirty` and counted apart. It reads each istanbul artifact once for coverage, dead lines and its digest, and skips the artifact walk on an empty diff; skipping the whole run on an unchanged tree was measured and rejected, because a key made of HEAD plus the dirty names cannot see a second edit to a file that was already dirty. |
474
+ | `worklist [--top N] [--scope NAME] [--batches N] [--json]` | The risk map: every admitted function ranked by `ccn * churn weight`, floored by `worklist_floor`, with hot simple code and anything over its ceiling admitted past that floor. It ranks finished rows and `no-lane` rows too, marked `ok` and `no-lane`, so it never empties; `next-item` carries the stop condition. `--scope NAME` (repeatable) is exact, not a substring. `--batches N` **adds** a `batches[]` view cutting the active list into at most N file-disjoint batches with co-changing files kept together, off the same cached pairs `coupling` reads; the normal keys stay. |
347
475
  | `next-item [--top N] [--exclude FRAG] [--scope NAME] [--claim]` | The actionable queue as JSON, with churn, budget estimates and uncovered lines. Same run and same admission floor as `worklist`, a different view of it: `no-lane` rows are skipped and counted in `skipped_no_lane`, and what is left is ranked by `crap` descending rather than by risk, so the item it hands out is often not the worklist's first row. `--exclude FRAG` (repeatable) skips items whose path or function name contains FRAG; `--scope NAME` (repeatable) is exact, not a substring. `--claim` holds what it hands out so a second session skips it. `stale` is true when the ranked run's commit is not HEAD, the same field `worklist` carries. Every item carries a `handle`: the bare identifier, or `(anonymous)#N` for a function with no name, which is the name form that survives the edit the item asks for. |
348
476
  | `claims [list \| release PATH NAME \| release --all] [--json]` | The open claims, and the way to hand one back without waiting for a verify. `release` takes the bare identifier, the whole long name, or the `handle` the claim was taken under, which is the only one that picks out a single `(anonymous)` claim. |
349
- | `brief FILE NAME [--batch N] [--json]` | The start-editing packet for one function: its own `source` text, every function in the file, the scored row and the scope ceiling, the ratchet mark and what the gate will bind on, uncovered lines, duplication twins, file churn, coupling partners, the config's notes, and the literal commands for the rest of the loop. Plus `handle`, `remedy` and the same `est_splits` / `est_uncovered_paths` the queue prints, and a `commands.refresh` that writes a run (`refresh_writes_run`) rather than re-reading the stale one. `NAME` takes the bare identifier, the long name `next-item` printed, the function's start line, `(anonymous)#N` for a function printed `(anonymous)` counting the file's anonymous functions from the top, or `NAME#2` for the second of several functions a file gives one name to. `--batch N` drops the positionals and emits `packets[]` instead: the top N of the queue, built from one read of the store. |
350
- | `explain FILE NAME [--history] [--tests] [--json]` | A function's score across runs plus its mark. `NAME` resolves exact first: a function whose bare identifier or long name is exactly `NAME` wins, and only when nothing matches exactly does it fall back to a prefix match, so `route` explains `route` rather than every `route_*` beside it. `--history` adds the commits that touched it (`git log -L`), each carrying its message `body`, `--tests` the tests that covered it, which needs coverage.py contexts turned on ([recipe](docs/lanes.md#test-attribution-for-explain---tests)). `--json` emits the same content as one `schema` 1 object. |
351
- | `rescore FILE ... [--gate] [--json]` | Fresh complexity for named files over the latest run's stale coverage, joined by name. Advisory: it writes no run. `--gate` applies the pre-commit hook's policy to the same selection the hook uses (functions the tree changed since HEAD), minus functions a ratchet mark already covers, and exits 6. |
477
+ | `brief FILE NAME [--batch N] [--json]` | The start-editing packet for one function: its own `source` text, every function in the file, the scored row and the scope ceiling, the ratchet mark and what the gate will bind on, uncovered lines, duplication twins, file churn, coupling partners, the config's notes, and the literal commands for the rest of the loop. Plus `handle`, `remedy` and the same `est_splits` / `est_uncovered_paths` the queue prints, and a `commands.refresh` that writes a run (`refresh_writes_run`) rather than re-reading the stale one. `NAME` takes the bare identifier, the long name `next-item` printed, the function's start line, `(anonymous)#N` for a function printed `(anonymous)` counting the file's anonymous functions from the top, or `NAME#2` for the second of several functions a file gives one name to. `--batch N` drops the positionals and emits `packets[]` instead: the top N of the queue, built from one read of the store and one duplication pass over the snapshot for the whole batch (batch of 5: 11.8 s to 5.2 s, output byte-identical to five separate calls). |
478
+ | `explain FILE NAME [--history] [--tests] [--json]` | A function's score across runs plus its mark. `NAME` resolves exact first: a function whose bare identifier or long name is exactly `NAME` wins, and only when nothing matches exactly does it fall back to a prefix match, so `route` explains `route` rather than every `route_*` beside it. It also takes the function's start line, the form `brief` takes, which is how you open one printed `(anonymous)`. `--history` adds the commits that touched it (`git log -L`), each carrying its message `body`, `--tests` the tests that covered it, which needs coverage.py contexts turned on ([recipe](docs/lanes.md#test-attribution-for-explain---tests)). `--json` emits the same content as one `schema` 1 object. |
479
+ | `rescore FILE ... [--gate] [--json]` | Fresh complexity for named files over the latest run's stale coverage, joined by name. Advisory: it writes no run. `--gate` applies the pre-commit hook's policy to the same selection the hook uses (functions the tree changed since HEAD), minus functions whose CRAP sits at or under their ratchet mark, and exits 6. A marked function past its mark is gated; the pre-commit hook exempts on the mark's existence instead, because a staged blob has no coverage to score. |
352
480
  | `ratchet seed \| prune \| merge \| move \| report [--enforce] [--json]` | The mark lifecycle: seed new debt, prune gone code (a mark whose file git renamed follows it), merge as a git driver, move re-paths marks, report reads burn-down from the file's own git history. See [docs/ratchet.md](docs/ratchet.md). |
353
481
  | `runs [list \| prune [--keep N]] [--json]` | Run history, and retention. `list` marks the run `verify` compares against today `baseline`, and prints `verdict=-` for a run that produces no verdict rather than one that failed. See [The trusted baseline](#the-trusted-baseline). `--keep` (default 5) is a floor on the newest trusted runs, not a cap: the digest pair, every passing verify baseline, every run an override names, and the newest non-hook run are kept too. `prune` VACUUMs afterwards. |
354
482
  | `overrides [--json]` | The override audit trail: who granted what, when, and why. |
355
- | `trend [--json]` | Totals per trusted run: functions, over-target count, CRAP load, average, per-scope rollup. |
483
+ | `trend [--json]` | Totals per trusted run: functions, over-target count, CRAP load, average, per-scope rollup. It reads a per-run rollup table rather than rescanning every scored row, and fills that table for any run missing one, so it writes to the store (best effort: a read-only `.crapkit/` costs the speed, not the command). |
356
484
  | `digest [--alert]` | The delta between the two newest runs with identical lane sets. Silent when nothing changed. `--alert` pipes the body to `alert_command` on stdin. Plain lines, never JSON. |
357
- | `report [--out PATH]` | One self-contained HTML page written to `.crapkit/report.html` (or `--out PATH`, repo-relative), with the path printed on stdout. It renders what `worklist --json` and `trend --json` already answer at their defaults: the ranked worklist capped at `worklist_top`, the per-scope grades off the newest run, the trend series, and a banner naming every stale lane. It measures nothing and opens no network connection. Per-function CRAP and coverage are absent because no repo-wide payload carries them; each row prints the `crapkit explain` call that does. |
485
+ | `report [--out PATH]` | One self-contained HTML page written to `.crapkit/report.html` (or `--out PATH`, repo-relative), with the path printed on stdout. It renders what `worklist --json` and `trend --json` already answer at their defaults: the ranked worklist capped at `worklist_top`, the per-scope grades off the newest run, the trend series, and a banner naming every stale lane. It measures nothing and opens no network connection. Per-function CRAP and coverage are absent because no repo-wide payload carries them; each row prints the `crapkit explain` call that does. It reads the same per-run rollups `trend` does, and writes them on the same terms. |
358
486
  | `duplication [--min-lines N] [--similarity F] [--top N] [--json]` | Near-duplicate functions by normalized line shingles with containment scoring. Defaults: `--min-lines 8`, `--similarity 0.8`, `--top 50`. `--top` truncates the list. A function and a function nested inside it never pair: their spans nest, they score 1.0 by construction, and nobody can deduplicate a factory from its own closure. |
359
- | `coupling [--min-support N] [--min-confidence F] [--top N] [--json]` | File pairs that keep landing in the same commits. Defaults: `--min-support 5` shared commits, `--min-confidence 0.5` max-direction ratio, `--top 50`. Bulk commits never couple pairs, and a young repo returns nothing at the default support. |
360
- | `mutate [--files F ...] [--max-mutants N] [--json]` | Diff-scoped mutation testing: flips comparisons, boundary shifts, boolean connectives and boolean literals on changed lines, runs `mutation_command` per mutant, lists survivors. `--files` replaces diff scope with the whole file. `--max-mutants` (default 100) caps the run and the cap warning goes to stderr only, so `mutants` in `--json` is the capped count. Shell and PowerShell files are refused by name on stderr rather than mutated: `<` and `>` are redirections there, not comparisons. |
487
+ | `coupling [--min-support N] [--min-confidence F] [--top N] [--json]` | File pairs that keep landing in the same commits. Defaults: `--min-support 5` shared commits, `--min-confidence 0.5` max-direction ratio, `--top 50`. Bulk commits never couple pairs, and a young repo returns nothing at the default support. The ranked pairs are cached in `.crapkit/coupling-cache-v1.json`, keyed on HEAD, the churn window, today's UTC date, the path format and a digest of the tracked set, and shared with `brief` and `worklist --batches` (warm: 1.05 s to 0.11 s on a 72k-commit repo). The date is part of that key, so the first run after midnight UTC rebuilds the pairs on an unchanged HEAD. `--top` reads the cache, because it truncates that same order; `--min-support` or `--min-confidence` off their defaults ask a wider question than the file answers, so they bypass it and recompute. |
488
+ | `mutate [--files F ...] [--max-mutants N] [--drop-pool] [--json]` | Diff-scoped mutation testing: flips comparisons, boundary shifts, boolean connectives and boolean literals on changed lines, runs `mutation_command` per mutant, lists survivors. `--files` replaces diff scope with the whole file. `--max-mutants` (default 100) caps the run and the cap warning goes to stderr only, so `mutants` in `--json` is the capped count. Shell and PowerShell files are refused by name on stderr rather than mutated: `<` and `>` are redirections there, not comparisons. With `mutation_workers > 1` the worker worktrees are kept at `.crapkit/mutate-pool/` and re-prepared per run (30.6 s to build four on a 31,459-file repo, 0.46 s to re-prepare them); `--drop-pool` removes them and exits. |
361
489
  | `test-scoped FILE ...` | Runs each owning scope's `[crapkit.scoped_tests]` template on the files (quoted, longest-prefix scope wins). A template with no `{files}` runs as written, which is how a scope whose tests live outside its own paths runs its whole suite. Exit code only; a nonzero runner exits 1. |
362
490
  | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
363
491
  | `claude-hook [--protocol N]` | Reads one Claude Code PostToolUse payload from stdin and judges the file it edited: ccn against the scope ceiling, on functions the edit changed, minus functions a ratchet mark already covers. Advisory only. The edit has landed, nothing is blocked, and `hook-precommit` stays the enforcement point. Exit 2 with three lines on stderr is the only thing it ever says: no `crapkit.toml` above the edited file, an unscoped file, mid-rebase or mid-merge, a `--protocol` other than 1, source that parses to no functions, or any internal failure all exit 0 in silence. Takes no `--repo`, because the root is the first `crapkit.toml` above the edited file and the upward walk stops at a `.git` entry, so a worktree never borrows its parent's config. It opens no snapshot and writes nothing. |
364
492
  | `watch [--interval SECONDS] [--cycles N]` | Rescores tracked files as they change (mtime polling, default 2s, subprocess-isolated so a half-saved syntax error never kills the watcher). `--cycles N` polls exactly N times and exits 0; without it the loop runs until ctrl-c. |
365
- | `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing nine read-only tools. See [docs/agent-json.md](docs/agent-json.md#mcp-server). |
493
+ | `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing nine read-only tools. Every tool shells to the CLI's own `--json` surface, so the MCP view cannot drift from what the CLI reports. Answering from a kept in-process store was benchmarked and rejected: a packet's `source` would go stale behind the edit it describes. See [docs/agent-json.md](docs/agent-json.md#mcp-server). |
366
494
 
367
495
  ## Reading the output
368
496
 
@@ -433,9 +561,10 @@ baseline**. `crapkit runs list` marks which one that is today.
433
561
  never qualifies, and neither does a `partial` run (a lane failed, so some scope fell back
434
562
  to `no-lane`) nor a `hook` override record, which carries no scored rows at all. In `runs
435
563
  list`, `verdict=-` marks a run that produces no verdict rather than one that failed: only
436
- `verify` renders a verdict. Three readers ask this one question and get this one answer:
437
- the baseline pick here, `ratchet seed` and `prune`, and the tighten damping that compares a
438
- mark against the same commit's previous run.
564
+ `verify` renders a verdict. Four readers ask this one question and get this one answer: the
565
+ baseline pick here, `ratchet seed`, `prune`, and the tighten damping that compares a mark
566
+ against the same commit's previous run. A mark can no longer be signed off a run `verify`
567
+ refused.
439
568
 
440
569
  **What advances it.** Any qualifying run. `coverage` writes one wherever HEAD is, so a
441
570
  dashboard cron advances the baseline exactly as CI does. A passing `verify` advances it
@@ -457,7 +586,7 @@ $ crapkit verify
457
586
  warning: run 3 is not the baseline: verify run 2 FAILED with 1 finding(s) and no passing verify has cleared it since — measuring against run 1 @ 88012a148f6 instead, so those findings stay visible. Fix them, or pass `--baseline 3` to accept the newer run deliberately.
458
587
  verify FAILED @ d89068de7f3 vs baseline 88012a148f6 (2 changed files)
459
588
  GATE crap 72.0 ccn 8 cov 0% calc/legacy.py:7 legacy_router( a , b , c , d , e ) -> decompose
460
- findings: 1 committed / 0 dirty (uncommitted tracked edits)
589
+ findings: 1 committed / 0 dirty (uncommitted edits and untracked files)
461
590
  ```
462
591
 
463
592
  Run 3 is a `coverage` run somebody took on the tree run 2 refused, and it scores the same
@@ -470,6 +599,14 @@ bypasses the rule, and the run history records which run the verdict used. Nothi
470
599
  touches a repo that has never run `verify`: with no failure to protect, `coverage` alone
471
600
  always advances the baseline.
472
601
 
602
+ **When the id you pass cannot serve.** A `--baseline ID` naming a real run that is not a
603
+ candidate says which run it is, why, and which ones can:
604
+
605
+ ```
606
+ $ crapkit verify --baseline 3
607
+ crapkit: run 3 is an inventory run (no coverage was measured) and cannot serve as a baseline; trusted runs: 1, 2; pass `--baseline 2` for the newest
608
+ ```
609
+
473
610
  ## Exit codes
474
611
 
475
612
  | Code | Meaning |
@@ -477,11 +614,11 @@ always advances the baseline.
477
614
  | 0 | OK. For `verify` and `hook-precommit`: the gate passed. |
478
615
  | 1 | **Overloaded.** Three unrelated things, listed below the table. |
479
616
  | 2 | Usage error from argparse: unknown flag, missing positional. Raised before crapkit's own error handling. |
480
- | 3 | Config error: `crapkit.toml` missing or unparseable, an unknown language or parser, a ratchet metric-stamp mismatch, a `test-scoped` file under no scope or under a scope with no template. |
617
+ | 3 | Config error: `crapkit.toml` missing or unparseable, an unknown language or parser, a lane command the shell that runs it reads as a narrowed suite, a ratchet metric-stamp mismatch ([Upgrading from 0.4.4](#upgrading-from-044)), a `test-scoped` file under no scope or under a scope with no template. |
481
618
  | 4 | Git error: not a repository, a baseline commit rewritten out of the history. |
482
- | 5 | Tool error: lizard not importable, a lane produced no artifact, a lane timed out past its retries, an override alert command failed. |
483
- | 6 | Gate violation. A function the diff touched is over its ceiling, or `rescore --gate` found one, or `hook-precommit` did. |
484
- | 7 | Ratchet regression. A marked function scores worse than its recorded high-water mark, touched or not. |
619
+ | 5 | Tool error: lizard not importable, a lane produced no artifact or one that measured a different tree, a lane timed out past its retries, an override alert command failed. A `timeout_seconds` kills the whole process tree, so no orphan suite keeps running behind the failure. |
620
+ | 6 | Gate violation. A function the diff touched is over its ceiling and past any ratchet mark it carries: an edit that leaves a marked function at or under its mark is the debt the repo signed for and is exempt. Also `rescore --gate`, which applies the same rule, and `hook-precommit`, which exempts on the mark's existence instead. |
621
+ | 7 | Ratchet regression the diff never touched. A marked function scores worse than its recorded high-water mark; a touched one past its mark reports 6. |
485
622
  | 8 | New test failures against the baseline run. Failures the baseline already had do not count. |
486
623
  | 9 | Diff-coverage ceiling breached: `diff_uncovered_max` is set and more changed lines than that never ran. |
487
624
 
@@ -510,9 +647,12 @@ writes runs `pytest --cov` and those flags come from `pytest-cov`:
510
647
  pip install pytest-cov
511
648
  ```
512
649
 
513
- (or `pip install 'crapkit[py]'` to pull both at once, when crapkit lives in the same venv
514
- as the suite — skip it and `init` prints this same fix when its probe finds the plugin
515
- missing).
650
+ (`pip install "crapkit[py]"` pulls both at once when crapkit shares the suite's venv.)
651
+
652
+ If your suite drives its own CLI through `subprocess.run`, add `[tool.coverage.run]
653
+ patch = ["subprocess"]` to `pyproject.toml` and keep `coverage>=7.10.6`: pytest-cov 7.0.0
654
+ dropped subprocess measurement, so without that key every entry point scores 0% and nothing
655
+ warns. [docs/lanes.md](docs/lanes.md) has the whole rule.
516
656
 
517
657
  ### 1. Scaffold the config
518
658
 
@@ -526,9 +666,13 @@ added to .gitignore: .crapkit/, .coverage, __pycache__/
526
666
  `init` sniffs tracked source into one scope per top-level source directory, and detects a
527
667
  coverage lane from what the repo already has: a pytest marker file (`pyproject.toml`,
528
668
  `pytest.ini`, `setup.cfg`) writes a live `[[lane]]`, and so does a `test` script or
529
- `vitest`/`jest` in `package.json`. Whatever it detects, it also leaves commented templates
530
- for the runners it did not find. Every lane it writes reports into `.crapkit/cov/`, which
531
- is why the `.gitignore` list is so short: see
669
+ `vitest`/`jest` in `package.json`. A lockfile beside them names the environment: `uv.lock`,
670
+ `poetry.lock`, `pdm.lock` or `Pipfile.lock` makes the lane `uv run python -m pytest …` (and
671
+ the matching `run` for the rest), because a bare `python` binds to whichever venv the shell
672
+ has active rather than the one the repo pins — see
673
+ [The interpreter a lane binds to](docs/lanes.md#the-interpreter-a-lane-binds-to). Whatever
674
+ it detects, it also leaves commented templates for the runners it did not find. Every lane
675
+ it writes reports into `.crapkit/cov/`, which is why the `.gitignore` list is so short: see
532
676
  [Where artifacts live](docs/lanes.md#where-artifacts-live).
533
677
 
534
678
  ```toml
@@ -545,16 +689,18 @@ globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/
545
689
 
546
690
  [[lane]]
547
691
  name = "py"
548
- command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json"
692
+ command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json --junitxml=.crapkit/cov/junit-py.xml"
549
693
  artifact = ".crapkit/cov/py.json"
694
+ results_artifact = ".crapkit/cov/junit-py.xml"
550
695
  parser = "coveragepy"
551
696
  scopes = ["calc"]
552
697
 
553
698
  # Declare one [[lane]] per coverage command, then run `crapkit coverage`.
554
699
  # [[lane]]
555
700
  # name = "js"
556
- # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js"
701
+ # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml"
557
702
  # artifact = ".crapkit/cov/js/coverage-final.json"
703
+ # results_artifact = ".crapkit/cov/js/junit.xml"
558
704
  # parser = "istanbul"
559
705
  # scopes = ["<your-scope>"]
560
706
 
@@ -599,11 +745,9 @@ Columns: `risk`, `ccn` with the standard-only ccn in parentheses,
599
745
  `<commits>c/<authors>a` in the churn window with `w<weight>`, `path:line`, the function's
600
746
  long name, then a marker on rows the burn-down queue will not hand out (`ok`, `no-lane`).
601
747
 
602
- **`worklist` is the risk map, not a to-do list.** It ranks every function it admits,
603
- finished ones included, so it does not empty when the burn-down finishes. `next-item` is
604
- the other view: same run, same admission floor, but it drops the `no-lane` rows and ranks
605
- by `crap` descending instead of by risk. Its `empty: true` is the stop condition; the
606
- worklist has none.
748
+ **`worklist` is the risk map, not a to-do list.** It ranks finished rows too, so it does
749
+ not empty when the burn-down does. `next-item` is the other view of that run: it drops the
750
+ `no-lane` rows, ranks by `crap`, and its `empty: true` is the stop condition.
607
751
 
608
752
  ### 4. Take the top item
609
753
 
@@ -669,11 +813,16 @@ added to .gitignore: .crapkit/
669
813
  ```
670
814
 
671
815
  The lane `init` wrote is
672
- `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js`. It reads
673
- vitest's `json` reporter from `.crapkit/cov/js/coverage-final.json`; the
674
- `reportsDirectory` flag is what keeps that report out of your root. Anything that produces
675
- an istanbul `coverage-final.json` works; see [docs/lanes.md](docs/lanes.md) for jest,
676
- pytest, monorepo and per-package recipes.
816
+ `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml`.
817
+ It reads vitest's `json` reporter from `.crapkit/cov/js/coverage-final.json`; the
818
+ `reportsDirectory` flag is what keeps that report out of your root. The junit half is the
819
+ lane's `results_artifact`, which the crashed-worker and no-new-failures checks read; both
820
+ reporters are named because `--reporter=junit` alone would replace the console output you
821
+ watch the suite through. Anything that produces
822
+ an istanbul `coverage-final.json` works; see [docs/lanes.md](docs/lanes.md) for the
823
+ [jest](docs/lanes.md#jest) and [pytest](docs/lanes.md#pytest) recipes, a package
824
+ [one directory down](docs/lanes.md#running-from-a-subdirectory), and a
825
+ [crapkit root below the repo top](docs/lanes.md#a-crapkit-root-below-the-repo-top).
677
826
 
678
827
  ### 2. Install a coverage provider
679
828
 
@@ -683,7 +832,7 @@ exit 5:
683
832
 
684
833
  ```
685
834
  $ crapkit coverage
686
- crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); last output: $ npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js
835
+ crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); full log: /repo/.crapkit/lane-js.log; last output: $ npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml
687
836
 
688
837
  MISSING DEPENDENCY Cannot find dependency '@vitest/coverage-v8'
689
838