crapkit 0.4.3__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.3 → crapkit-0.4.5}/PKG-INFO +198 -51
  2. crapkit-0.4.3/src/crapkit.egg-info/PKG-INFO → crapkit-0.4.5/README.md +963 -853
  3. {crapkit-0.4.3 → crapkit-0.4.5}/pyproject.toml +27 -5
  4. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/__init__.py +1 -1
  5. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/analyze.py +26 -9
  6. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/churn_cache.py +52 -10
  7. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/churn_log.py +63 -7
  8. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/__init__.py +21 -0
  9. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/admin.py +388 -41
  10. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/analyses.py +49 -5
  11. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/claude_hook.py +8 -1
  12. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/parser.py +9 -3
  13. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/queue.py +62 -29
  14. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/ratchet_cmds.py +29 -13
  15. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/reports.py +2 -1
  16. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/scoring.py +41 -15
  17. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/verifying.py +112 -23
  18. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/config.py +236 -11
  19. {crapkit-0.4.3 → 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.3 → crapkit-0.4.5}/src/crapkit/covstream.py +38 -3
  22. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/dup.py +53 -5
  23. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/gitio.py +187 -25
  24. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/lanes.py +42 -22
  25. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/lizardcognitive.py +90 -6
  26. {crapkit-0.4.3 → 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.3 → 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.3 → crapkit-0.4.5}/src/crapkit/scaffold.py +68 -18
  31. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/store.py +173 -40
  32. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/uncovered.py +96 -8
  33. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/universe.py +61 -23
  34. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/verify.py +28 -4
  35. crapkit-0.4.3/README.md → crapkit-0.4.5/src/crapkit.egg-info/PKG-INFO +1000 -820
  36. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit.egg-info/SOURCES.txt +2 -0
  37. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit.egg-info/requires.txt +3 -0
  38. crapkit-0.4.3/src/crapkit/mutate_pool.py +0 -152
  39. {crapkit-0.4.3 → crapkit-0.4.5}/LICENSE +0 -0
  40. {crapkit-0.4.3 → crapkit-0.4.5}/setup.cfg +0 -0
  41. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/__main__.py +0 -0
  42. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/_pygdefer.py +0 -0
  43. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cache.py +0 -0
  44. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/churn.py +0 -0
  45. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/cli/_shared.py +0 -0
  46. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/coverage_istanbul.py +0 -0
  47. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/coverage_py.py +0 -0
  48. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/diffparse.py +0 -0
  49. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/digest.py +0 -0
  50. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/discover.py +0 -0
  51. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/doctor.py +0 -0
  52. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/errors.py +0 -0
  53. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/hook.py +0 -0
  54. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/junitparse.py +0 -0
  55. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/keys.py +0 -0
  56. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/lizardpowershell.py +0 -0
  57. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/lizardrust.py +0 -0
  58. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/mcp_server.py +0 -0
  59. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/merge.py +0 -0
  60. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/mutate.py +0 -0
  61. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/override.py +0 -0
  62. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/ratchet.py +0 -0
  63. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/ratchet_report.py +0 -0
  64. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/report.py +0 -0
  65. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/sarif.py +0 -0
  66. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/sarifio.py +0 -0
  67. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/score.py +0 -0
  68. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/snapshot.py +0 -0
  69. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/watch.py +0 -0
  70. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit/worklist.py +0 -0
  71. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit.egg-info/dependency_links.txt +0 -0
  72. {crapkit-0.4.3 → crapkit-0.4.5}/src/crapkit.egg-info/entry_points.txt +0 -0
  73. {crapkit-0.4.3 → 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.3
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
@@ -29,6 +31,8 @@ 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
+ Provides-Extra: py
35
+ Requires-Dist: pytest-cov>=5; extra == "py"
32
36
  Dynamic: license-file
33
37
 
34
38
  # crapkit
@@ -93,7 +97,30 @@ the repo can get better and never worse while you burn it down.
93
97
  out to your own test runner, and the runner needs its coverage package installed:
94
98
  `pytest-cov` for pytest, `@vitest/coverage-v8` (pinned to your vitest major) for vitest.
95
99
  Without it the lane produces no artifact and `coverage` exits 5 quoting the runner's own
96
- error. The two quickstarts below walk a real repo end to end.
100
+ error. For pytest, `init` probes the python its lane will run and prints the install
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.
97
124
 
98
125
  ## Install
99
126
 
@@ -116,30 +143,73 @@ changing crapkit.
116
143
 
117
144
  ```
118
145
  $ crapkit --version
119
- crapkit 0.4.3
146
+ crapkit 0.4.5
120
147
  ```
121
148
 
122
149
  `python -m crapkit` works identically to the console script and is what to use from a
123
150
  source checkout. Every subcommand accepts `--repo PATH` (default: the current directory),
124
- so you never have to `cd` into the repo you are scoring. The flag goes after the
125
- 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
126
155
 
127
- ### 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
128
194
 
129
195
  `uv tool upgrade crapkit`, and `pip install -U` into a tool venv, fail with `os error 32`
130
196
  ("The process cannot access the file because it is being used by another process") while a
131
197
  crapkit MCP server is live: an agent session spawns `crapkit.exe mcp`, which holds the
132
198
  launcher, and Windows will not overwrite a running executable. The venv upgrades before
133
- that copy fails, so `crapkit --version` already reports the new version and the launcher is
134
- the only stale piece. Quit the agent session and rerun the upgrade, or rename the locked
135
- exe aside (Windows allows renaming a running one) and copy the new one in; the `.old` file
136
- 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:
137
203
 
138
- ```
139
- mv ~/.local/bin/crapkit.exe ~/.local/bin/crapkit.exe.old
140
- 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
141
207
  ```
142
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
+
143
213
  ## The Claude Code plugin
144
214
 
145
215
  ```
@@ -206,10 +276,21 @@ different powers:
206
276
  | `crapkit verify` | before you push, and in CI | **the verdict.** Gate, ratchet, new test failures, diff coverage, against the trusted baseline |
207
277
 
208
278
  Both hooks exempt a function the committed ratchet already carries a mark for, so touching
209
- signed debt never refuses a commit. `verify` is what fails a mark that rises. The
210
- pre-commit hook reports each exemption count on stderr (`staged function(s) carry a
211
- ratchet mark and were not gated`), and says the same about a staged file no `[[scope]]`
212
- 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.
213
294
 
214
295
  Git runs hooks outside your shell's activated venv. Bare `python` must resolve to an
215
296
  interpreter that has crapkit installed, or spell it out
@@ -263,11 +344,22 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
263
344
  repos:
264
345
  - repo: https://github.com/JeanFrancoisGagne/crapkit
265
346
  # crapkit's release step rewrites this line to the tag it just cut
266
- rev: v0.4.3
347
+ rev: v0.4.5
267
348
  hooks:
268
349
  - id: crapkit-gate
269
350
  ```
270
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
+
271
363
  `rev` is a git ref pre-commit resolves against that remote. Pin a release tag, not a
272
364
  branch: `pre-commit autoupdate` only moves between tags, and a moving `main` would change
273
365
  your gate under you.
@@ -290,6 +382,45 @@ crapkit verify --baseline-tsv crapkit-baseline.tsv --github
290
382
  writes SARIF 2.1.0 for code-scanning upload. Refresh the committed baseline whenever the
291
383
  default branch's verify passes.
292
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
+
293
424
  ### What a refusal looks like
294
425
 
295
426
  ```
@@ -334,30 +465,30 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
334
465
  | Command | What it does |
335
466
  |---|---|
336
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. |
337
- | `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). |
338
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. |
339
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). |
340
- | `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. |
341
- | `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. |
342
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. |
343
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. |
344
- | `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. |
345
- | `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. |
346
- | `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. |
347
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). |
348
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. |
349
480
  | `overrides [--json]` | The override audit trail: who granted what, when, and why. |
350
- | `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). |
351
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. |
352
- | `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. |
353
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. |
354
- | `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. |
355
- | `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. |
356
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. |
357
488
  | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
358
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. |
359
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. |
360
- | `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). |
361
492
 
362
493
  ## Reading the output
363
494
 
@@ -428,9 +559,10 @@ baseline**. `crapkit runs list` marks which one that is today.
428
559
  never qualifies, and neither does a `partial` run (a lane failed, so some scope fell back
429
560
  to `no-lane`) nor a `hook` override record, which carries no scored rows at all. In `runs
430
561
  list`, `verdict=-` marks a run that produces no verdict rather than one that failed: only
431
- `verify` renders a verdict. Three readers ask this one question and get this one answer:
432
- the baseline pick here, `ratchet seed` and `prune`, and the tighten damping that compares a
433
- 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.
434
566
 
435
567
  **What advances it.** Any qualifying run. `coverage` writes one wherever HEAD is, so a
436
568
  dashboard cron advances the baseline exactly as CI does. A passing `verify` advances it
@@ -452,7 +584,7 @@ $ crapkit verify
452
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.
453
585
  verify FAILED @ d89068de7f3 vs baseline 88012a148f6 (2 changed files)
454
586
  GATE crap 72.0 ccn 8 cov 0% calc/legacy.py:7 legacy_router( a , b , c , d , e ) -> decompose
455
- findings: 1 committed / 0 dirty (uncommitted tracked edits)
587
+ findings: 1 committed / 0 dirty (uncommitted edits and untracked files)
456
588
  ```
457
589
 
458
590
  Run 3 is a `coverage` run somebody took on the tree run 2 refused, and it scores the same
@@ -465,6 +597,14 @@ bypasses the rule, and the run history records which run the verdict used. Nothi
465
597
  touches a repo that has never run `verify`: with no failure to protect, `coverage` alone
466
598
  always advances the baseline.
467
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
+
468
608
  ## Exit codes
469
609
 
470
610
  | Code | Meaning |
@@ -472,11 +612,11 @@ always advances the baseline.
472
612
  | 0 | OK. For `verify` and `hook-precommit`: the gate passed. |
473
613
  | 1 | **Overloaded.** Three unrelated things, listed below the table. |
474
614
  | 2 | Usage error from argparse: unknown flag, missing positional. Raised before crapkit's own error handling. |
475
- | 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. |
476
616
  | 4 | Git error: not a repository, a baseline commit rewritten out of the history. |
477
- | 5 | Tool error: lizard not importable, a lane produced no artifact, a lane timed out past its retries, an override alert command failed. |
478
- | 6 | Gate violation. A function the diff touched is over its ceiling, or `rescore --gate` found one, or `hook-precommit` did. |
479
- | 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. |
480
620
  | 8 | New test failures against the baseline run. Failures the baseline already had do not count. |
481
621
  | 9 | Diff-coverage ceiling breached: `diff_uncovered_max` is set and more changed lines than that never ran. |
482
622
 
@@ -505,6 +645,8 @@ writes runs `pytest --cov` and those flags come from `pytest-cov`:
505
645
  pip install pytest-cov
506
646
  ```
507
647
 
648
+ (`pip install "crapkit[py]"` pulls both at once when crapkit shares the suite's venv.)
649
+
508
650
  ### 1. Scaffold the config
509
651
 
510
652
  ```
@@ -536,16 +678,18 @@ globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/
536
678
 
537
679
  [[lane]]
538
680
  name = "py"
539
- 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"
540
682
  artifact = ".crapkit/cov/py.json"
683
+ results_artifact = ".crapkit/cov/junit-py.xml"
541
684
  parser = "coveragepy"
542
685
  scopes = ["calc"]
543
686
 
544
687
  # Declare one [[lane]] per coverage command, then run `crapkit coverage`.
545
688
  # [[lane]]
546
689
  # name = "js"
547
- # 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"
548
691
  # artifact = ".crapkit/cov/js/coverage-final.json"
692
+ # results_artifact = ".crapkit/cov/js/junit.xml"
549
693
  # parser = "istanbul"
550
694
  # scopes = ["<your-scope>"]
551
695
 
@@ -590,11 +734,9 @@ Columns: `risk`, `ccn` with the standard-only ccn in parentheses,
590
734
  `<commits>c/<authors>a` in the churn window with `w<weight>`, `path:line`, the function's
591
735
  long name, then a marker on rows the burn-down queue will not hand out (`ok`, `no-lane`).
592
736
 
593
- **`worklist` is the risk map, not a to-do list.** It ranks every function it admits,
594
- finished ones included, so it does not empty when the burn-down finishes. `next-item` is
595
- the other view: same run, same admission floor, but it drops the `no-lane` rows and ranks
596
- by `crap` descending instead of by risk. Its `empty: true` is the stop condition; the
597
- 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.
598
740
 
599
741
  ### 4. Take the top item
600
742
 
@@ -660,11 +802,16 @@ added to .gitignore: .crapkit/
660
802
  ```
661
803
 
662
804
  The lane `init` wrote is
663
- `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js`. It reads
664
- vitest's `json` reporter from `.crapkit/cov/js/coverage-final.json`; the
665
- `reportsDirectory` flag is what keeps that report out of your root. Anything that produces
666
- an istanbul `coverage-final.json` works; see [docs/lanes.md](docs/lanes.md) for jest,
667
- 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).
668
815
 
669
816
  ### 2. Install a coverage provider
670
817
 
@@ -674,7 +821,7 @@ exit 5:
674
821
 
675
822
  ```
676
823
  $ crapkit coverage
677
- 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
678
825
 
679
826
  MISSING DEPENDENCY Cannot find dependency '@vitest/coverage-v8'
680
827