crapkit 0.4.10__tar.gz → 0.4.12__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 (76) hide show
  1. {crapkit-0.4.10 → crapkit-0.4.12}/PKG-INFO +86 -59
  2. crapkit-0.4.10/src/crapkit.egg-info/PKG-INFO → crapkit-0.4.12/README.md +1174 -1186
  3. {crapkit-0.4.10 → crapkit-0.4.12}/pyproject.toml +1 -1
  4. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/__init__.py +1 -1
  5. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/__init__.py +26 -0
  6. crapkit-0.4.12/src/crapkit/cli/_shared.py +228 -0
  7. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/admin.py +273 -34
  8. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/analyses.py +7 -3
  9. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/parser.py +78 -4
  10. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/queue.py +11 -9
  11. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/ratchet_cmds.py +8 -6
  12. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/reports.py +12 -10
  13. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/scoring.py +14 -6
  14. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/verifying.py +55 -16
  15. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/config.py +56 -3
  16. crapkit-0.4.12/src/crapkit/coverage_py.py +164 -0
  17. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/covstream.py +34 -30
  18. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/doctor.py +16 -8
  19. crapkit-0.4.12/src/crapkit/invocation.py +45 -0
  20. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/lanes.py +265 -26
  21. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/mcp_server.py +3 -1
  22. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/mutate_pool.py +57 -3
  23. crapkit-0.4.12/src/crapkit/procs.py +137 -0
  24. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/ratchet.py +32 -5
  25. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/report.py +7 -2
  26. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/scaffold.py +288 -24
  27. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/uncovered.py +3 -1
  28. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/universe.py +10 -1
  29. crapkit-0.4.10/README.md → crapkit-0.4.12/src/crapkit.egg-info/PKG-INFO +1213 -1147
  30. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit.egg-info/SOURCES.txt +1 -0
  31. crapkit-0.4.10/src/crapkit/cli/_shared.py +0 -130
  32. crapkit-0.4.10/src/crapkit/coverage_py.py +0 -87
  33. crapkit-0.4.10/src/crapkit/procs.py +0 -66
  34. {crapkit-0.4.10 → crapkit-0.4.12}/LICENSE +0 -0
  35. {crapkit-0.4.10 → crapkit-0.4.12}/setup.cfg +0 -0
  36. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/__main__.py +0 -0
  37. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/_pygdefer.py +0 -0
  38. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/analyze.py +0 -0
  39. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cache.py +0 -0
  40. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/churn.py +0 -0
  41. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/churn_cache.py +0 -0
  42. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/churn_log.py +0 -0
  43. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/cli/claude_hook.py +0 -0
  44. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/coupling.py +0 -0
  45. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/coupling_cache.py +0 -0
  46. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/coverage_istanbul.py +0 -0
  47. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/diffparse.py +0 -0
  48. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/digest.py +0 -0
  49. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/discover.py +0 -0
  50. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/dup.py +0 -0
  51. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/errors.py +0 -0
  52. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/gitio.py +0 -0
  53. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/hook.py +0 -0
  54. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/junitparse.py +0 -0
  55. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/keys.py +0 -0
  56. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/lizardcognitive.py +0 -0
  57. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/lizardpowershell.py +0 -0
  58. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/lizardrust.py +0 -0
  59. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/lizardshell.py +0 -0
  60. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/merge.py +0 -0
  61. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/mutate.py +0 -0
  62. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/override.py +0 -0
  63. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/packet.py +0 -0
  64. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/ratchet_report.py +0 -0
  65. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/sarif.py +0 -0
  66. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/sarifio.py +0 -0
  67. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/score.py +0 -0
  68. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/snapshot.py +0 -0
  69. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/store.py +0 -0
  70. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/verify.py +0 -0
  71. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/watch.py +0 -0
  72. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit/worklist.py +0 -0
  73. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit.egg-info/dependency_links.txt +0 -0
  74. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit.egg-info/entry_points.txt +0 -0
  75. {crapkit-0.4.10 → crapkit-0.4.12}/src/crapkit.egg-info/requires.txt +0 -0
  76. {crapkit-0.4.10 → crapkit-0.4.12}/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.10
3
+ Version: 0.4.12
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
@@ -42,7 +42,7 @@ Dynamic: license-file
42
42
  [![ci](https://github.com/JeanFrancoisGagne/crapkit/actions/workflows/ci.yml/badge.svg)](https://github.com/JeanFrancoisGagne/crapkit/actions/workflows/ci.yml)
43
43
  [![PyPI](https://img.shields.io/pypi/v/crapkit)](https://pypi.org/project/crapkit/)
44
44
  [![Python](https://img.shields.io/pypi/pyversions/crapkit)](https://pypi.org/project/crapkit/)
45
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
45
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/JeanFrancoisGagne/crapkit/blob/main/LICENSE)
46
46
 
47
47
  ![crapkit init, coverage and worklist --top 5 on a small Python repo, then a shell heredoc adding a function at ccn 7: the per-edit advisory reports it and exits 2, and the commit gate refuses the staged file with exit 6](https://raw.githubusercontent.com/JeanFrancoisGagne/crapkit/main/docs/demo.gif)
48
48
 
@@ -58,6 +58,9 @@ because half the callers are coding agents.
58
58
  CRAP = ccn^2 * (1 - cov)^3 + ccn
59
59
  ```
60
60
 
61
+ The name is not ours: C.R.A.P. (Change Risk Anti-Patterns) was coined for crap4j by
62
+ Alberto Savoia and Bob Evans in 2007.
63
+
61
64
  `ccn` is the smaller of standard and modified cyclomatic complexity, both read off one
62
65
  lizard pass. `cov` is branch coverage inside the function's span; with no branches it
63
66
  falls back to statement coverage, and with no statements to invoked-or-not, so a
@@ -77,7 +80,12 @@ it.
77
80
  ```
78
81
  pip install crapkit
79
82
  cd your-repo
80
- crapkit init # writes crapkit.toml: scopes, a coverage lane, .gitignore lines
83
+ crapkit init # crapkit.toml and .gitignore lines, plus a live coverage lane when it
84
+ # recognizes the runner and a scope speaks its language: pyproject.toml,
85
+ # pytest.ini or setup.cfg for pytest; a test script or vitest/jest in
86
+ # package.json for the JS side
87
+ # without one: the lane comes commented out, init says to declare one,
88
+ # and docs/lanes.md is how to fill it in
81
89
  crapkit coverage # runs the lane, joins coverage, stores a scored run
82
90
  crapkit worklist # the ranked risk map
83
91
  crapkit ratchet seed && git add crapkit.toml crapkit-ratchet.tsv .gitignore
@@ -94,6 +102,9 @@ worklist @ fae4db93108 (run 1, floor ccn>=5, churn 12mo) — 1 active, 0 dormant
94
102
  risk 0.0 ccn 14 ( 14 std) 1c/1a w 0.00 calc/grade.py:7 classify( score , attempts , late , bonus )
95
103
  ```
96
104
 
105
+ `risk 0.0` is what a one-commit repo scores, because churn needs a spread of commits to
106
+ rank and ccn order stands in until then ([Risk](#risk-what-ranks-the-worklist)).
107
+
97
108
  `ratchet seed` signs today's debt at today's score. From then on marks only ever fall, so
98
109
  the repo can get better and never worse while you burn it down.
99
110
 
@@ -106,7 +117,9 @@ command when `pytest_cov` is missing; `pip install "crapkit[py]"` pulls the plug
106
117
  alongside crapkit when the two share a venv. On a Windows PATH holding only the `py`
107
118
  launcher it writes `py`, not a `python3` the lane could never run, and when cmd.exe cannot
108
119
  start the interpreter at all (exit 9009, the Store alias) it names that instead of guessing
109
- at pytest-cov. The two quickstarts below walk a real repo end to end.
120
+ at pytest-cov. A repo that pins no lockfile and carries its own `.venv` gets that venv's
121
+ interpreter in the lane, when that interpreter can import pytest, rather than whichever
122
+ python the shell answers with. The two quickstarts below walk a real repo end to end.
110
123
 
111
124
  **On Windows a lane command is read by cmd.exe**, the shell that will run it, not by sh.
112
125
  Double quotes are the portable quoting. A single-quoted value is refused at config load
@@ -118,7 +131,7 @@ artifact:
118
131
  command = "python -m pytest -m 'not live and not perf' --cov=calc --cov-branch --cov-report=json:.crapkit/cov/py.json"
119
132
 
120
133
  $ crapkit doctor
121
- 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)
134
+ 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); a suite whose testpaths cannot be collected in one process needs one lane per testpath, each with full_suite = false and its own artifact
122
135
  ```
123
136
 
124
137
  Write it `-m "not live and not perf"`. Carets, `&&` and `|` segments, redirections and
@@ -145,9 +158,14 @@ mirror installs fine. Requires Python 3.11 or newer. The `pip install -e ".[dev]
145
158
  [Development](#development) is a different thing: it adds the test extra, for people
146
159
  changing crapkit.
147
160
 
161
+ Scoring runs your own test command on your own machine and reads the artifact it writes.
162
+ There is no network call anywhere in crapkit, so no source, no score and no telemetry
163
+ leaves the box
164
+ ([SECURITY.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/SECURITY.md)).
165
+
148
166
  ```
149
167
  $ crapkit --version
150
- crapkit 0.4.10
168
+ crapkit 0.4.12
151
169
  ```
152
170
 
153
171
  `python -m crapkit` works identically to the console script and is what to use from a
@@ -222,9 +240,11 @@ claude plugin install crapkit@crapkit
222
240
  ```
223
241
 
224
242
  Two commands, installed once per user, and every repo on the machine gets it. The plugin
225
- ships three skills (`crapkit`, `crapkit-recover`, `crapkit-onboard`), the read-only MCP
226
- server, and one advisory PostToolUse hook that names any function an edit pushed over its
227
- ceiling. It adds no files to your repo, and it needs the crapkit CLI on PATH.
243
+ ships three skills, the read-only MCP server, and one advisory PostToolUse hook that names
244
+ any function an edit pushed over its ceiling. Claude reaches two of the skills by itself,
245
+ `crapkit` and `crapkit-recover`; the third you type, as `/crapkit:crapkit-onboard`, because
246
+ wiring a repo up happens once and its description has no business in every turn's window.
247
+ It adds no files to your repo, and it needs the crapkit CLI on PATH.
228
248
 
229
249
  A repo with no `crapkit.toml` costs a silent sub-50 ms no-op per edit. Other agent
230
250
  runtimes have no marketplace: copy `plugin/skills/*` into their skills directory instead.
@@ -375,7 +395,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
375
395
  repos:
376
396
  - repo: https://github.com/JeanFrancoisGagne/crapkit
377
397
  # crapkit's release step rewrites this line to the tag it just cut
378
- rev: v0.4.10
398
+ rev: v0.4.12
379
399
  hooks:
380
400
  - id: crapkit-gate
381
401
  ```
@@ -426,7 +446,7 @@ crapkit: baseline commit a74260f321f is not an ancestor of HEAD (rebase or amend
426
446
 
427
447
  That is exit 4 on a `git clone --depth 1` of a repo whose baseline verifies at full depth.
428
448
  Set `fetch-depth: 0` on the checkout step, which is what crapkit's own
429
- [.github/workflows/ci.yml](.github/workflows/ci.yml) does.
449
+ [.github/workflows/ci.yml](https://github.com/JeanFrancoisGagne/crapkit/blob/main/.github/workflows/ci.yml) does.
430
450
 
431
451
  The whole PR job, on GitHub Actions:
432
452
 
@@ -469,11 +489,11 @@ violation and 0 otherwise. The stderr block above is the same either way.
469
489
  three-record audit: an alert line through `alert_command`, a ratchet entry staged into the
470
490
  commit, and a row in the override log. All three land or nothing does, and an unset
471
491
  `alert_command` refuses the override outright. See
472
- [docs/ratchet.md](docs/ratchet.md#overrides-and-the-audit-trail).
492
+ [docs/ratchet.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md#overrides-and-the-audit-trail).
473
493
 
474
494
  ## The GitHub Action
475
495
 
476
- [action.yml](action.yml) at this repository's root is a composite action, so a reviewer
496
+ [action.yml](https://github.com/JeanFrancoisGagne/crapkit/blob/main/action.yml) at this repository's root is a composite action, so a reviewer
477
497
  sees crapkit's numbers on the pull request without installing anything. Four lines add it
478
498
  to a workflow, and every input has a default:
479
499
 
@@ -481,7 +501,7 @@ to a workflow, and every input has a default:
481
501
  - uses: actions/checkout@v4
482
502
  with:
483
503
  fetch-depth: 0
484
- - uses: JeanFrancoisGagne/crapkit@v0.4.8
504
+ - uses: JeanFrancoisGagne/crapkit@v0.4.12
485
505
  ```
486
506
 
487
507
  The whole job those four lines sit in:
@@ -497,8 +517,11 @@ jobs:
497
517
  - uses: actions/checkout@v4
498
518
  with:
499
519
  fetch-depth: 0 # the diff, and verify's baseline commit
520
+ - uses: actions/setup-python@v5
521
+ with:
522
+ python-version: "3.12" # the interpreter the install below lands in
500
523
  - run: pip install -e ".[dev]" # whatever your lanes need to run
501
- - uses: JeanFrancoisGagne/crapkit@v0.4.8
524
+ - uses: JeanFrancoisGagne/crapkit@v0.4.12
502
525
  with:
503
526
  gate: "false"
504
527
  ```
@@ -513,8 +536,9 @@ baseline's commit. With a shallow clone the file list comes back empty and the c
513
536
  ranks the whole repository instead of the diff.
514
537
 
515
538
  The action installs crapkit from `$GITHUB_ACTION_PATH`, which is its own checkout of the
516
- ref you pinned in `uses:`. So `@v0.4.8` scores your tree with 0.4.8's crapkit rather than
517
- with whatever released last, and pinning a tag is the whole version policy.
539
+ ref you pinned in `uses:`. So a pin left at last month's tag scores your tree with last
540
+ month's crapkit rather than with whatever released since, and pinning a tag is the whole
541
+ version policy; the snippets above name the current release.
518
542
 
519
543
  ### What the comment looks like
520
544
 
@@ -564,7 +588,7 @@ behind the checkout to measure from.
564
588
  | `gate` | `"false"` | `"true"` exits with `crapkit verify`'s own code, so a finding fails the check. Anything else exits 0 and the comment is the whole output |
565
589
  | `delta` | `"true"` | scores the pull request's base commit first, so the verdict covers the commits the pull request adds. Costs a second lane run; `"false"` scores the checkout alone |
566
590
  | `top` | `"5"` | worklist rows rendered in the table |
567
- | `python-version` | `"3.12"` | the interpreter `actions/setup-python` installs crapkit into |
591
+ | `python-version` | `"3.12"` | the interpreter `actions/setup-python` installs crapkit into. Match it to the version your own setup-python step named, or the lanes run on an interpreter your dependencies never reached |
568
592
 
569
593
  `gate: "false"` is the default on purpose. A team adopts the action before it has decided
570
594
  which findings should stop a merge, and a check that fails on day one gets turned off on
@@ -641,22 +665,22 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
641
665
  | Command | What it does |
642
666
  |---|---|
643
667
  | `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. |
644
- | `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). |
668
+ | `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 or that the shell cannot start, naming the word to change; a bare name is looked for on PATH and a runner spelled as a path is looked for under the directory the lane runs in, so `.venv/bin/python` answers the same from any directory you run `doctor` in; 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](https://github.com/JeanFrancoisGagne/crapkit/tree/main/plugin) against the `crapkit` on PATH (the bare name its hooks and MCP server spawn) on both version and hook `--protocol`, and FAILs when PATH carries no `crapkit` at all, 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](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md#doctor---json). |
645
669
  | `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. |
646
- | `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). |
670
+ | `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](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md). |
647
671
  | `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. |
648
672
  | `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. |
649
673
  | `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. |
650
674
  | `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. |
651
675
  | `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). |
652
- | `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. |
676
+ | `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](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#test-attribution-for-explain---tests)). `--json` emits the same content as one `schema` 1 object. |
653
677
  | `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. |
654
- | `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). |
678
+ | `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](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md). |
655
679
  | `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. |
656
680
  | `overrides [--json]` | The override audit trail: who granted what, when, and why. |
657
681
  | `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). |
658
682
  | `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. |
659
- | `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. |
683
+ | `report [--out PATH]` | One self-contained HTML page written to `.crapkit/report.html` (or `--out PATH`, repo-relative, or an absolute path you name), 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. |
660
684
  | `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. |
661
685
  | `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. |
662
686
  | `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. |
@@ -664,7 +688,8 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
664
688
  | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
665
689
  | `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, and `hook-precommit` stays the enforcement point. Exit 2 and an advisory on stderr is the only thing it ever says, one block per judged file (a head line, one line per breaching function, a closing line): 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. The root is the first `crapkit.toml` above the edited file; the walk stops at a `.git` entry, so a worktree never borrows its parent's config. A `Bash` event names no file, so it judges the working tree instead: the dirty or untracked `*.py` files touched in the last 12 seconds, 25 at most, each through the same ladder, and silence for a clean tree or a cwd outside any repo. That half fires only where you register a `Bash` matcher ([The Claude Code plugin](#the-claude-code-plugin)). It opens no snapshot and writes nothing. |
666
690
  | `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. |
667
- | `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). |
691
+ | `help [TOPIC]` | The help git, npm and docker answer to. With no TOPIC it prints the command list; with one it prints that subcommand's own help, the same page as `crapkit TOPIC --help`. A TOPIC that names no subcommand exits 3. |
692
+ | `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](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md#mcp-server). |
668
693
 
669
694
  ## Reading the output
670
695
 
@@ -826,7 +851,7 @@ pip install pytest-cov
826
851
  If your suite drives its own CLI through `subprocess.run`, add `[tool.coverage.run]
827
852
  patch = ["subprocess"]` to `pyproject.toml` and keep `coverage>=7.10.6`: pytest-cov 7.0.0
828
853
  dropped subprocess measurement, so without that key every entry point scores 0% and nothing
829
- warns. [docs/lanes.md](docs/lanes.md) has the whole rule.
854
+ warns. [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md) has the whole rule.
830
855
 
831
856
  ### 1. Scaffold the config
832
857
 
@@ -844,11 +869,11 @@ coverage lane from what the repo already has: a pytest marker file (`pyproject.t
844
869
  `poetry.lock`, `pdm.lock` or `Pipfile.lock` makes the lane `uv run python -m pytest …` (and
845
870
  the matching `run` for the rest), because a bare `python` binds to whichever venv the shell
846
871
  has active rather than the one the repo pins — see
847
- [The interpreter a lane binds to](docs/lanes.md#the-interpreter-a-lane-binds-to). Whatever
872
+ [The interpreter a lane binds to](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#the-interpreter-a-lane-binds-to). Whatever
848
873
  it detects, it also leaves commented templates for the runners it did not find, and those
849
874
  carry the same launcher, so uncommenting one cannot hand the bare `python` back. Every lane
850
875
  it writes reports into `.crapkit/cov/`, which is why the `.gitignore` list is so short: see
851
- [Where artifacts live](docs/lanes.md#where-artifacts-live).
876
+ [Where artifacts live](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#where-artifacts-live).
852
877
 
853
878
  ```toml
854
879
  [crapkit]
@@ -860,11 +885,11 @@ paths = ["calc"]
860
885
  languages = ["python"]
861
886
 
862
887
  [exclude]
863
- globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/*.test.*", "**/*.spec.*", "**/test_*.py", "**/*_test.py", "**/conftest.py", "**/*_test.go", "*.config.ts", "*.config.js", "*.config.mts", "**/*.config.ts", "**/*.config.js", "**/*.config.mts"]
888
+ globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/*.test.*", "**/*.spec.*", "**/test_*.py", "**/*_test.py", "**/conftest.py", "node_modules/**", "dist/**", "build/**", "vendor/**", "*.test.*", "*.spec.*", "test_*.py", "*_test.py", "conftest.py", "*_test.go", "**/*_test.go", "*.config.ts", "*.config.js", "*.config.mts", "**/*.config.ts", "**/*.config.js", "**/*.config.mts"]
864
889
 
865
890
  [[lane]]
866
891
  name = "py"
867
- command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json --junitxml=.crapkit/cov/junit-py.xml"
892
+ command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json --junitxml=.crapkit/cov/junit-py.xml --continue-on-collection-errors"
868
893
  artifact = ".crapkit/cov/py.json"
869
894
  results_artifact = ".crapkit/cov/junit-py.xml"
870
895
  parser = "coveragepy"
@@ -873,7 +898,7 @@ scopes = ["calc"]
873
898
  # Declare one [[lane]] per coverage command, then run `crapkit coverage`.
874
899
  # [[lane]]
875
900
  # name = "js"
876
- # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml"
901
+ # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml"
877
902
  # artifact = ".crapkit/cov/js/coverage-final.json"
878
903
  # results_artifact = ".crapkit/cov/js/junit.xml"
879
904
  # parser = "istanbul"
@@ -886,16 +911,16 @@ calc = "python -m pytest {files} -q -p no:cacheprovider"
886
911
  ```
887
912
 
888
913
  The last block is the one an agent loop needs. `crapkit test-scoped` exits 3 for a file
889
- whose scope declares no template, and [AGENTS.md](AGENTS.md#4-run-the-owning-scopes-tests)
914
+ whose scope declares no template, and [AGENTS.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/AGENTS.md#4-run-the-owning-scopes-tests)
890
915
  makes it step 4 of the burn-down loop. Every key is in
891
- [docs/configuration.md](docs/configuration.md).
916
+ [docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md).
892
917
 
893
918
  ### 2. Check the config against the repo
894
919
 
895
920
  ```
896
921
  $ crapkit doctor
897
922
  ok config keys all recognized
898
- ok scope 'calc': 1 files
923
+ ok scope 'calc': 1 file
899
924
  ok every tracked source file belongs to a scope
900
925
  ok 1 lane(s) declared
901
926
  ok lizard 1.24.0
@@ -934,7 +959,7 @@ $ crapkit next-item
934
959
  `remedy: "decompose"`, `est_splits: 3` (this needs roughly three pieces to fit under 6),
935
960
  and `uncovered_lines` naming the ten lines no test walks. `handle` is the name form to
936
961
  pass back, and `stale: false` says the run still describes HEAD. Every field is in
937
- [docs/agent-json.md](docs/agent-json.md).
962
+ [docs/agent-json.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md).
938
963
 
939
964
  ### 5. Seed the ratchet
940
965
 
@@ -967,11 +992,11 @@ against the trusted baseline: every function the diff touched sits at or under i
967
992
  ceiling, no marked function got worse, and no test that passed in the baseline fails now.
968
993
  Exit 0 advances the baseline and tightens `crapkit-ratchet.tsv` in place, so the repaid
969
994
  mark leaves the file: follow up with `git commit -am "ratchet: classify repaid"`. The full
970
- mark lifecycle is in [docs/ratchet.md](docs/ratchet.md).
995
+ mark lifecycle is in [docs/ratchet.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md).
971
996
 
972
997
  `crapkit next-item` now comes back `empty: true` with a `reasons` object saying which
973
998
  ending you got. That is most of the stop condition, not all of it:
974
- [AGENTS.md](AGENTS.md#the-termination-rule) states the whole rule and reads the rest of
999
+ [AGENTS.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/AGENTS.md#the-termination-rule) states the whole rule and reads the rest of
975
1000
  `reasons`.
976
1001
 
977
1002
  ## Quickstart: TypeScript
@@ -988,16 +1013,16 @@ added to .gitignore: .crapkit/
988
1013
  ```
989
1014
 
990
1015
  The lane `init` wrote is
991
- `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml`.
1016
+ `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml`.
992
1017
  It reads vitest's `json` reporter from `.crapkit/cov/js/coverage-final.json`; the
993
1018
  `reportsDirectory` flag is what keeps that report out of your root. The junit half is the
994
1019
  lane's `results_artifact`, which the crashed-worker and no-new-failures checks read; both
995
1020
  reporters are named because `--reporter=junit` alone would replace the console output you
996
1021
  watch the suite through. Anything that produces
997
- an istanbul `coverage-final.json` works; see [docs/lanes.md](docs/lanes.md) for the
998
- [jest](docs/lanes.md#jest) and [pytest](docs/lanes.md#pytest) recipes, a package
999
- [one directory down](docs/lanes.md#running-from-a-subdirectory), and a
1000
- [crapkit root below the repo top](docs/lanes.md#a-crapkit-root-below-the-repo-top).
1022
+ an istanbul `coverage-final.json` works; see [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md) for the
1023
+ [jest](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#jest) and [pytest](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#pytest) recipes, a package
1024
+ [one directory down](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#running-from-a-subdirectory), and a
1025
+ [crapkit root below the repo top](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#a-crapkit-root-below-the-repo-top).
1001
1026
 
1002
1027
  ### 2. Install a coverage provider
1003
1028
 
@@ -1007,12 +1032,12 @@ exit 5:
1007
1032
 
1008
1033
  ```
1009
1034
  $ crapkit coverage
1010
- crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); full log: /repo/.crapkit/lane-js.log; last output: $ npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml
1035
+ crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); full log: /repo/.crapkit/lane-js.log; last output: $ npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml
1011
1036
 
1012
1037
  MISSING DEPENDENCY Cannot find dependency '@vitest/coverage-v8'
1013
1038
 
1014
1039
  (exit 1)
1015
- crapkit: every lane failed: ...
1040
+ crapkit: every lane failed (1 of 1); the errors are above
1016
1041
  ```
1017
1042
 
1018
1043
  That failure **writes no run**. Every lane failed, so `coverage` exits before it opens a
@@ -1035,10 +1060,12 @@ The artifact crapkit wants is `coverage-final.json`, written by vitest's `json`
1035
1060
  reporter, which is on by default. If your vitest config sets `coverage.reporter`
1036
1061
  explicitly, keep `"json"` in the list.
1037
1062
 
1038
- One more vitest default worth flipping now: it writes **no coverage report at all when the
1039
- run fails**, so a single red test becomes a missing artifact and a lane failure. Set
1040
- `coverage.reportOnFailure = true`. The full config block is in
1041
- [docs/lanes.md](docs/lanes.md#reportonfailure).
1063
+ vitest writes **no coverage report at all when the run fails**. The lane `init` wrote
1064
+ already carries `--coverage.reportOnFailure`, so a red test still produces the artifact.
1065
+ If you write the lane by hand, or you would rather keep the switch beside your other
1066
+ coverage settings, `coverage.reportOnFailure = true` in the vitest config does the same
1067
+ job; either one is enough. The full block is in
1068
+ [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#reportonfailure).
1042
1069
 
1043
1070
  ### 3. Score the repo
1044
1071
 
@@ -1150,22 +1177,22 @@ dropped it once `classify` scored under the ceiling, rewriting the tracked
1150
1177
 
1151
1178
  A verify may also print `warning: N changed line(s) have no coverage` above its verdict;
1152
1179
  that block is advisory unless `diff_uncovered_max` is set
1153
- ([docs/configuration.md](docs/configuration.md)).
1180
+ ([docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md)).
1154
1181
 
1155
1182
  ## Documentation
1156
1183
 
1157
1184
  | Page | Covers |
1158
1185
  |---|---|
1159
- | [The handbook](https://jeanfrancoisgagne.github.io/crapkit/handbook.html) | **Start here for anything deeper.** The illustrated handbook: what crapkit is, how every piece works, and where each command earns its keep. Also at [docs/handbook.html](docs/handbook.html), self-contained, so it opens straight from a clone. |
1160
- | [docs/adoption.md](docs/adoption.md) | The judgment layer over the quickstarts: scope granularity, exclude vs lane, scoped_tests wiring, the first-verify taint hazard. |
1161
- | [docs/configuration.md](docs/configuration.md) | Every `crapkit.toml` key: type, default, and what it does. |
1162
- | [docs/lanes.md](docs/lanes.md) | The lane model, vitest and jest and pytest recipes, artifact reuse, flake retest, containers. |
1163
- | [docs/ratchet.md](docs/ratchet.md) | Seeding, pruning, the git merge driver, metric stamps, debt policy, overrides. |
1164
- | [docs/agent-json.md](docs/agent-json.md) | The machine surface: `schema`, every payload field, real captured examples. |
1165
- | [AGENTS.md](AGENTS.md) | The burn-down loop an agent runs, and the rules for changing crapkit itself. |
1166
- | [plugin/](plugin/) | The Claude Code plugin: three skills, the read-side MCP server, and the advisory PostToolUse hook. |
1186
+ | [The handbook](https://jeanfrancoisgagne.github.io/crapkit/handbook.html) | **Start here for anything deeper.** The illustrated handbook: what crapkit is, how every piece works, and where each command earns its keep. Also at [docs/handbook.html](https://jeanfrancoisgagne.github.io/crapkit/handbook.html), self-contained, so it opens straight from a clone. |
1187
+ | [docs/adoption.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/adoption.md) | The judgment layer over the quickstarts: scope granularity, exclude vs lane, scoped_tests wiring, the first-verify taint hazard. |
1188
+ | [docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md) | Every `crapkit.toml` key: type, default, and what it does. |
1189
+ | [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md) | The lane model, vitest and jest and pytest recipes, artifact reuse, flake retest, containers. |
1190
+ | [docs/ratchet.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md) | Seeding, pruning, the git merge driver, metric stamps, debt policy, overrides. |
1191
+ | [docs/agent-json.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md) | The machine surface: `schema`, every payload field, real captured examples. |
1192
+ | [AGENTS.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/AGENTS.md) | The burn-down loop an agent runs, and the rules for changing crapkit itself. |
1193
+ | [plugin/](https://github.com/JeanFrancoisGagne/crapkit/tree/main/plugin) | The Claude Code plugin: three skills, the read-side MCP server, and the advisory PostToolUse hook. |
1167
1194
 
1168
- [crapkit.schema.json](crapkit.schema.json) is the authority on the config file shape.
1195
+ [crapkit.schema.json](https://github.com/JeanFrancoisGagne/crapkit/blob/main/crapkit.schema.json) is the authority on the config file shape.
1169
1196
 
1170
1197
  ## Development
1171
1198
 
@@ -1179,8 +1206,8 @@ python -m pytest -q
1179
1206
  `pytest-xdist` is not optional: `tests/fixtures/mini_repo` declares a lane that shells out
1180
1207
  to `pytest ... -n 2`, and without it that subprocess dies on an unrecognized `-n`. The
1181
1208
  `git config` line arms the complexity gate on your own commits. Same steps, with what each
1182
- one buys, in [CONTRIBUTING.md](CONTRIBUTING.md).
1209
+ one buys, in [CONTRIBUTING.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/CONTRIBUTING.md).
1183
1210
 
1184
1211
  ## License
1185
1212
 
1186
- MIT. See [LICENSE](LICENSE).
1213
+ MIT. See [LICENSE](https://github.com/JeanFrancoisGagne/crapkit/blob/main/LICENSE).