crapkit 0.4.4__tar.gz → 0.4.5__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 (73) hide show
  1. {crapkit-0.4.4 → crapkit-0.4.5}/PKG-INFO +194 -56
  2. crapkit-0.4.4/src/crapkit.egg-info/PKG-INFO → crapkit-0.4.5/README.md +963 -862
  3. {crapkit-0.4.4 → crapkit-0.4.5}/pyproject.toml +23 -6
  4. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/__init__.py +1 -1
  5. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/analyze.py +26 -9
  6. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/churn_cache.py +42 -5
  7. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/churn_log.py +45 -1
  8. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/__init__.py +19 -0
  9. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/admin.py +355 -55
  10. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/analyses.py +49 -5
  11. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/claude_hook.py +8 -1
  12. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/parser.py +9 -3
  13. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/queue.py +62 -29
  14. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/ratchet_cmds.py +29 -13
  15. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/reports.py +2 -1
  16. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/scoring.py +41 -15
  17. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/verifying.py +112 -23
  18. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/config.py +209 -32
  19. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/coupling.py +45 -10
  20. crapkit-0.4.5/src/crapkit/coupling_cache.py +147 -0
  21. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/covstream.py +38 -3
  22. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/dup.py +53 -5
  23. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/gitio.py +179 -23
  24. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/lanes.py +42 -22
  25. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/lizardcognitive.py +90 -6
  26. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/lizardshell.py +3 -3
  27. crapkit-0.4.5/src/crapkit/mutate_pool.py +308 -0
  28. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/packet.py +10 -3
  29. crapkit-0.4.5/src/crapkit/procs.py +66 -0
  30. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/scaffold.py +68 -18
  31. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/store.py +173 -40
  32. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/uncovered.py +96 -8
  33. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/universe.py +61 -23
  34. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/verify.py +28 -4
  35. crapkit-0.4.4/README.md → crapkit-0.4.5/src/crapkit.egg-info/PKG-INFO +1000 -827
  36. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit.egg-info/SOURCES.txt +2 -0
  37. crapkit-0.4.4/src/crapkit/mutate_pool.py +0 -152
  38. {crapkit-0.4.4 → crapkit-0.4.5}/LICENSE +0 -0
  39. {crapkit-0.4.4 → crapkit-0.4.5}/setup.cfg +0 -0
  40. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/__main__.py +0 -0
  41. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/_pygdefer.py +0 -0
  42. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cache.py +0 -0
  43. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/churn.py +0 -0
  44. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/cli/_shared.py +0 -0
  45. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/coverage_istanbul.py +0 -0
  46. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/coverage_py.py +0 -0
  47. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/diffparse.py +0 -0
  48. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/digest.py +0 -0
  49. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/discover.py +0 -0
  50. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/doctor.py +0 -0
  51. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/errors.py +0 -0
  52. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/hook.py +0 -0
  53. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/junitparse.py +0 -0
  54. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/keys.py +0 -0
  55. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/lizardpowershell.py +0 -0
  56. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/lizardrust.py +0 -0
  57. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/mcp_server.py +0 -0
  58. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/merge.py +0 -0
  59. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/mutate.py +0 -0
  60. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/override.py +0 -0
  61. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/ratchet.py +0 -0
  62. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/ratchet_report.py +0 -0
  63. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/report.py +0 -0
  64. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/sarif.py +0 -0
  65. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/sarifio.py +0 -0
  66. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/score.py +0 -0
  67. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/snapshot.py +0 -0
  68. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/watch.py +0 -0
  69. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit/worklist.py +0 -0
  70. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit.egg-info/dependency_links.txt +0 -0
  71. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit.egg-info/entry_points.txt +0 -0
  72. {crapkit-0.4.4 → crapkit-0.4.5}/src/crapkit.egg-info/requires.txt +0 -0
  73. {crapkit-0.4.4 → crapkit-0.4.5}/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.5
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
@@ -96,9 +98,29 @@ out to your own test runner, and the runner needs its coverage package installed
96
98
  `pytest-cov` for pytest, `@vitest/coverage-v8` (pinned to your vitest major) for vitest.
97
99
  Without it the lane produces no artifact and `coverage` exits 5 quoting the runner's own
98
100
  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.
101
+ command when `pytest_cov` is missing; `pip install "crapkit[py]"` pulls the plugin
102
+ alongside crapkit when the two share a venv. On a Windows PATH holding only the `py`
103
+ launcher it writes `py`, not a `python3` the lane could never run, and when cmd.exe cannot
104
+ start the interpreter at all (exit 9009, the Store alias) it names that instead of guessing
105
+ at pytest-cov. The two quickstarts below walk a real repo end to end.
106
+
107
+ **On Windows a lane command is read by cmd.exe**, the shell that will run it, not by sh.
108
+ Double quotes are the portable quoting. A single-quoted value is refused at config load
109
+ with exit 3, because cmd.exe would hand pytest five words and the lane would write no
110
+ artifact:
111
+
112
+ ```
113
+ # the lane in crapkit.toml
114
+ command = "python -m pytest -m 'not live and not perf' --cov=calc --cov-branch --cov-report=json:.crapkit/cov/py.json"
115
+
116
+ $ crapkit doctor
117
+ 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)
118
+ ```
119
+
120
+ Write it `-m "not live and not perf"`. Carets, `&&` and `|` segments, redirections and
121
+ empty quoted arguments all read the way the shell reads them, so a chained lane
122
+ (`cd tests && python -m pytest --cov ...`) is checked one segment at a time. `doctor` reads
123
+ a lane the same way, and FAILs one whose runner will not start.
102
124
 
103
125
  ## Install
104
126
 
@@ -121,30 +143,73 @@ changing crapkit.
121
143
 
122
144
  ```
123
145
  $ crapkit --version
124
- crapkit 0.4.4
146
+ crapkit 0.4.5
125
147
  ```
126
148
 
127
149
  `python -m crapkit` works identically to the console script and is what to use from a
128
150
  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.
151
+ so you never have to `cd` into the repo you are scoring; [Subcommands](#subcommands) shows
152
+ where the flag goes.
153
+
154
+ ## Upgrading from 0.4.4
131
155
 
132
- ### Upgrading on Windows
156
+ **Run `crapkit ratchet seed` first.** Shell cognitive complexity now nests, which is
157
+ analysis version 8, and marks measured under version 7 are not comparable. Until you
158
+ re-seed, `verify` refuses at exit 3:
159
+
160
+ ```
161
+ $ crapkit verify
162
+ 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`
163
+ ```
164
+
165
+ Only shell and PowerShell cognitive numbers move. `ccn` does not, so a re-seed re-stamps
166
+ the file and leaves the marks where they were.
167
+
168
+ Five more things change under you. Three of them need nothing from you:
169
+
170
+ - **New cache files.** `.crapkit/coupling-cache-v1.json` joins `churn-cache-v2.json` and
171
+ `churn-log-v2.z`. A warm 0.4.4 churn cache is adopted once and its file removed, and
172
+ `.crapkit/` is already gitignored, so nothing new reaches your index.
173
+ - **`trend` and `report` write.** Both read a per-run rollup table, filled once per run and
174
+ pruned with its run, instead of rescanning every scored row. A read-only `.crapkit/`
175
+ costs the speedup, never the command.
176
+ - **Nested scopes may move files.** One predicate decides scope ownership now, and the
177
+ deepest declared path wins, so a repo whose `[[scope]]` paths nest inside each other can
178
+ see files change scope, rollup and ceiling on the next scan. Scopes that do not nest see
179
+ no change.
180
+
181
+ The other two put something in front of you:
182
+
183
+ - **`mutate` keeps a worktree pool.** With `mutation_workers > 1` the worker worktrees now
184
+ live under `.crapkit/mutate-pool/` between runs and are re-prepared each run, which is
185
+ the setup cost gone (30.6 s to build four on a 31,459-file repo, 0.46 s to re-prepare
186
+ them). The pool is not size-bounded and nothing sweeps it: `crapkit mutate --drop-pool`
187
+ removes it and exits. Single-worker runs are untouched.
188
+ - **`doctor` WARNs on a lane with no `results_artifact`.** Every `coveragepy` or `istanbul`
189
+ lane written before 0.4.5 gets one, with the two lines that fix it. Coverage is
190
+ unaffected. What the lane cannot feed without a results file is the crashed-worker check
191
+ and the no-new-failures check (exit 8).
192
+
193
+ ### The exe lock on Windows
133
194
 
134
195
  `uv tool upgrade crapkit`, and `pip install -U` into a tool venv, fail with `os error 32`
135
196
  ("The process cannot access the file because it is being used by another process") while a
136
197
  crapkit MCP server is live: an agent session spawns `crapkit.exe mcp`, which holds the
137
198
  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.
199
+ that copy fails, so `crapkit --version` already reports the new version and only the
200
+ launcher is stale. Quit the agent session and rerun the upgrade, or rename the locked exe
201
+ aside (Windows allows renaming a running one) and copy the new one in. Two lines in
202
+ cmd.exe, where both `%` variables expand:
142
203
 
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
204
+ ```bat
205
+ move %USERPROFILE%\.local\bin\crapkit.exe %USERPROFILE%\.local\bin\crapkit.exe.old
206
+ copy %APPDATA%\uv\tools\crapkit\Scripts\crapkit.exe %USERPROFILE%\.local\bin\crapkit.exe
146
207
  ```
147
208
 
209
+ Git Bash has no `move` and passes `%APPDATA%` through as literal text, so that block
210
+ fails there on its first line. Its form is `mv` and `cp` over `"$USERPROFILE"` and
211
+ `"$APPDATA"`, which Git Bash sets to the same two directories.
212
+
148
213
  ## The Claude Code plugin
149
214
 
150
215
  ```
@@ -211,10 +276,21 @@ different powers:
211
276
  | `crapkit verify` | before you push, and in CI | **the verdict.** Gate, ratchet, new test failures, diff coverage, against the trusted baseline |
212
277
 
213
278
  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.
279
+ signed debt never refuses a commit. `verify` is what fails a mark that rises. Since 0.4.5
280
+ its gate exempts a touched function whose fresh CRAP sits **at or under** its mark, the
281
+ rule `rescore --gate` already applied; push it past the mark and the gate fires again. The
282
+ pre-commit hook still exempts on the mark's existence alone, on purpose: a staged blob has
283
+ no coverage, so there is no fresh CRAP to compare against. It reports each exemption count
284
+ on stderr (`staged function(s) carry a ratchet mark and were not gated`), and says the same
285
+ about a staged file no `[[scope]]` claims, so a new top-level directory cannot go ungated
286
+ in silence.
287
+
288
+ **The crapkit root does not have to be the git top.** Since 0.4.5 every git spawn runs with
289
+ `diff.relative=true` and `core.quotePath=false`, so a `crapkit.toml` in `packages/api`
290
+ gates that package's own staged files and names them `app/m.py`, not
291
+ `packages/api/app/m.py`, and a dirty non-ASCII path is a real row rather than an invisible
292
+ one. Before that a nested root matched staged paths against no scope, and a function at
293
+ twice the ceiling committed with a warning.
218
294
 
219
295
  Git runs hooks outside your shell's activated venv. Bare `python` must resolve to an
220
296
  interpreter that has crapkit installed, or spell it out
@@ -268,11 +344,22 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
268
344
  repos:
269
345
  - repo: https://github.com/JeanFrancoisGagne/crapkit
270
346
  # crapkit's release step rewrites this line to the tag it just cut
271
- rev: v0.4.4
347
+ rev: v0.4.5
272
348
  hooks:
273
349
  - id: crapkit-gate
274
350
  ```
275
351
 
352
+ That file arms nothing on its own. The framework writes `.git/hooks/pre-commit` when you
353
+ tell it to, and until then `git commit` runs no gate and says nothing:
354
+
355
+ ```sh
356
+ pip install pre-commit
357
+ pre-commit install
358
+ ```
359
+
360
+ `pre-commit install` is the line every clone needs, the way Route 2 needs its
361
+ `git config core.hooksPath` line.
362
+
276
363
  `rev` is a git ref pre-commit resolves against that remote. Pin a release tag, not a
277
364
  branch: `pre-commit autoupdate` only moves between tags, and a moving `main` would change
278
365
  your gate under you.
@@ -295,6 +382,45 @@ crapkit verify --baseline-tsv crapkit-baseline.tsv --github
295
382
  writes SARIF 2.1.0 for code-scanning upload. Refresh the committed baseline whenever the
296
383
  default branch's verify passes.
297
384
 
385
+ Two things the job has to do before those lines run. **Install crapkit**, `pip install
386
+ crapkit`, and pin the version the way Route 3 pins `rev`: an unpinned install moves your
387
+ gate on whatever day a release lands. **Fetch the whole history.** `actions/checkout`
388
+ clones one commit by default, `verify` reads the diff against the baseline's commit out of
389
+ git, and a shallow clone does not have that commit:
390
+
391
+ ```
392
+ $ crapkit verify --baseline-tsv crapkit-baseline.tsv
393
+ crapkit: baseline commit a74260f321f is not an ancestor of HEAD (rebase or amend rewrote history) — run `crapkit coverage` for a fresh baseline
394
+ ```
395
+
396
+ That is exit 4 on a `git clone --depth 1` of a repo whose baseline verifies at full depth.
397
+ Set `fetch-depth: 0` on the checkout step, which is what crapkit's own
398
+ [.github/workflows/ci.yml](.github/workflows/ci.yml) does.
399
+
400
+ The whole PR job, on GitHub Actions:
401
+
402
+ ```yaml
403
+ on: pull_request
404
+ jobs:
405
+ crapkit:
406
+ runs-on: ubuntu-latest
407
+ steps:
408
+ - uses: actions/checkout@v4
409
+ with:
410
+ fetch-depth: 0 # verify needs the baseline's commit
411
+ - uses: actions/setup-python@v5
412
+ with:
413
+ python-version: "3.12"
414
+ - run: pip install crapkit
415
+ - run: pip install -e ".[dev]" # your own test dependencies
416
+ - run: crapkit verify --baseline-tsv crapkit-baseline.tsv --github
417
+ ```
418
+
419
+ The second install is the one people leave out. `verify` reruns your lanes, so the job
420
+ needs whatever your test command needs: the coverage plugin, `npm ci`, a database, all of
421
+ it. Without them the lane writes no artifact and `verify` exits 5 quoting the runner's own
422
+ error, which is a broken job and not a verdict.
423
+
298
424
  ### What a refusal looks like
299
425
 
300
426
  ```
@@ -339,30 +465,30 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
339
465
  | Command | What it does |
340
466
  |---|---|
341
467
  | `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). |
468
+ | `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
469
  | `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
470
  | `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. |
471
+ | `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. |
472
+ | `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
473
  | `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
474
  | `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. |
475
+ | `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). |
476
+ | `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. |
477
+ | `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
478
  | `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
479
  | `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
480
  | `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. |
481
+ | `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
482
  | `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. |
483
+ | `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
484
  | `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. |
485
+ | `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. |
486
+ | `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
487
  | `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
488
  | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
363
489
  | `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
490
  | `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). |
491
+ | `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
492
 
367
493
  ## Reading the output
368
494
 
@@ -433,9 +559,10 @@ baseline**. `crapkit runs list` marks which one that is today.
433
559
  never qualifies, and neither does a `partial` run (a lane failed, so some scope fell back
434
560
  to `no-lane`) nor a `hook` override record, which carries no scored rows at all. In `runs
435
561
  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.
562
+ `verify` renders a verdict. Four readers ask this one question and get this one answer: the
563
+ baseline pick here, `ratchet seed`, `prune`, and the tighten damping that compares a mark
564
+ against the same commit's previous run. A mark can no longer be signed off a run `verify`
565
+ refused.
439
566
 
440
567
  **What advances it.** Any qualifying run. `coverage` writes one wherever HEAD is, so a
441
568
  dashboard cron advances the baseline exactly as CI does. A passing `verify` advances it
@@ -457,7 +584,7 @@ $ crapkit verify
457
584
  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
585
  verify FAILED @ d89068de7f3 vs baseline 88012a148f6 (2 changed files)
459
586
  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)
587
+ findings: 1 committed / 0 dirty (uncommitted edits and untracked files)
461
588
  ```
462
589
 
463
590
  Run 3 is a `coverage` run somebody took on the tree run 2 refused, and it scores the same
@@ -470,6 +597,14 @@ bypasses the rule, and the run history records which run the verdict used. Nothi
470
597
  touches a repo that has never run `verify`: with no failure to protect, `coverage` alone
471
598
  always advances the baseline.
472
599
 
600
+ **When the id you pass cannot serve.** A `--baseline ID` naming a real run that is not a
601
+ candidate says which run it is, why, and which ones can:
602
+
603
+ ```
604
+ $ crapkit verify --baseline 3
605
+ 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
606
+ ```
607
+
473
608
  ## Exit codes
474
609
 
475
610
  | Code | Meaning |
@@ -477,11 +612,11 @@ always advances the baseline.
477
612
  | 0 | OK. For `verify` and `hook-precommit`: the gate passed. |
478
613
  | 1 | **Overloaded.** Three unrelated things, listed below the table. |
479
614
  | 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. |
615
+ | 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
616
  | 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. |
617
+ | 5 | Tool error: lizard not importable, a lane produced no artifact, 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. |
618
+ | 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. |
619
+ | 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
620
  | 8 | New test failures against the baseline run. Failures the baseline already had do not count. |
486
621
  | 9 | Diff-coverage ceiling breached: `diff_uncovered_max` is set and more changed lines than that never ran. |
487
622
 
@@ -510,9 +645,7 @@ writes runs `pytest --cov` and those flags come from `pytest-cov`:
510
645
  pip install pytest-cov
511
646
  ```
512
647
 
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).
648
+ (`pip install "crapkit[py]"` pulls both at once when crapkit shares the suite's venv.)
516
649
 
517
650
  ### 1. Scaffold the config
518
651
 
@@ -545,16 +678,18 @@ globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/
545
678
 
546
679
  [[lane]]
547
680
  name = "py"
548
- command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json"
681
+ command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json --junitxml=.crapkit/cov/junit-py.xml"
549
682
  artifact = ".crapkit/cov/py.json"
683
+ results_artifact = ".crapkit/cov/junit-py.xml"
550
684
  parser = "coveragepy"
551
685
  scopes = ["calc"]
552
686
 
553
687
  # Declare one [[lane]] per coverage command, then run `crapkit coverage`.
554
688
  # [[lane]]
555
689
  # name = "js"
556
- # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js"
690
+ # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml"
557
691
  # artifact = ".crapkit/cov/js/coverage-final.json"
692
+ # results_artifact = ".crapkit/cov/js/junit.xml"
558
693
  # parser = "istanbul"
559
694
  # scopes = ["<your-scope>"]
560
695
 
@@ -599,11 +734,9 @@ Columns: `risk`, `ccn` with the standard-only ccn in parentheses,
599
734
  `<commits>c/<authors>a` in the churn window with `w<weight>`, `path:line`, the function's
600
735
  long name, then a marker on rows the burn-down queue will not hand out (`ok`, `no-lane`).
601
736
 
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.
737
+ **`worklist` is the risk map, not a to-do list.** It ranks finished rows too, so it does
738
+ not empty when the burn-down does. `next-item` is the other view of that run: it drops the
739
+ `no-lane` rows, ranks by `crap`, and its `empty: true` is the stop condition.
607
740
 
608
741
  ### 4. Take the top item
609
742
 
@@ -669,11 +802,16 @@ added to .gitignore: .crapkit/
669
802
  ```
670
803
 
671
804
  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.
805
+ `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml`.
806
+ It reads vitest's `json` reporter from `.crapkit/cov/js/coverage-final.json`; the
807
+ `reportsDirectory` flag is what keeps that report out of your root. The junit half is the
808
+ lane's `results_artifact`, which the crashed-worker and no-new-failures checks read; both
809
+ reporters are named because `--reporter=junit` alone would replace the console output you
810
+ watch the suite through. Anything that produces
811
+ an istanbul `coverage-final.json` works; see [docs/lanes.md](docs/lanes.md) for the
812
+ [jest](docs/lanes.md#jest) and [pytest](docs/lanes.md#pytest) recipes, a package
813
+ [one directory down](docs/lanes.md#running-from-a-subdirectory), and a
814
+ [crapkit root below the repo top](docs/lanes.md#a-crapkit-root-below-the-repo-top).
677
815
 
678
816
  ### 2. Install a coverage provider
679
817
 
@@ -683,7 +821,7 @@ exit 5:
683
821
 
684
822
  ```
685
823
  $ 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
824
+ 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 --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml
687
825
 
688
826
  MISSING DEPENDENCY Cannot find dependency '@vitest/coverage-v8'
689
827