crapkit 0.6.0__tar.gz → 0.7.2__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 (96) hide show
  1. {crapkit-0.6.0/src/crapkit.egg-info → crapkit-0.7.2}/PKG-INFO +130 -90
  2. {crapkit-0.6.0 → crapkit-0.7.2}/README.md +126 -88
  3. {crapkit-0.6.0 → crapkit-0.7.2}/pyproject.toml +6 -6
  4. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/__init__.py +1 -1
  5. crapkit-0.7.2/src/crapkit/_analysis_pool.py +190 -0
  6. crapkit-0.7.2/src/crapkit/_mcp_stdio.py +208 -0
  7. crapkit-0.7.2/src/crapkit/_process_family.py +83 -0
  8. crapkit-0.7.2/src/crapkit/_process_owner.py +129 -0
  9. crapkit-0.7.2/src/crapkit/_windows_job.py +98 -0
  10. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/analyze.py +228 -63
  11. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cache.py +14 -5
  12. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/churn.py +5 -43
  13. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/churn_cache.py +10 -26
  14. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/churn_log.py +13 -15
  15. crapkit-0.7.2/src/crapkit/cli/__init__.py +10 -0
  16. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/_shared.py +47 -6
  17. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/admin.py +68 -63
  18. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/analyses.py +1 -1
  19. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/claude_hook.py +50 -34
  20. crapkit-0.7.2/src/crapkit/cli/maintenance.py +39 -0
  21. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/parser.py +9 -1
  22. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/queue.py +111 -52
  23. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/ratchet_cmds.py +46 -9
  24. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/reports.py +21 -30
  25. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/scoring.py +53 -39
  26. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/cli/verifying.py +82 -98
  27. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/config.py +86 -120
  28. crapkit-0.7.2/src/crapkit/config_contract.py +317 -0
  29. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/coupling.py +6 -24
  30. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/coupling_cache.py +3 -2
  31. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/coverage_istanbul.py +62 -87
  32. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/coverage_py.py +14 -59
  33. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/covstream.py +182 -36
  34. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/diffparse.py +5 -6
  35. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/digest.py +9 -2
  36. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/doctor.py +6 -17
  37. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/dup.py +80 -27
  38. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/gitio.py +178 -49
  39. crapkit-0.7.2/src/crapkit/gitpaths.py +39 -0
  40. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/hook.py +7 -4
  41. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/junitparse.py +36 -1
  42. crapkit-0.7.2/src/crapkit/keys.py +171 -0
  43. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/lanes.py +186 -77
  44. crapkit-0.7.2/src/crapkit/lizardtypescript.py +142 -0
  45. crapkit-0.7.2/src/crapkit/locks.py +36 -0
  46. crapkit-0.7.2/src/crapkit/logs.py +130 -0
  47. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/mcp_server.py +142 -336
  48. crapkit-0.7.2/src/crapkit/merge.py +35 -0
  49. crapkit-0.7.2/src/crapkit/mutate.py +298 -0
  50. crapkit-0.7.2/src/crapkit/mutate_pool.py +499 -0
  51. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/override.py +45 -14
  52. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/packet.py +79 -45
  53. crapkit-0.7.2/src/crapkit/procs.py +533 -0
  54. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/ratchet.py +114 -14
  55. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/ratchet_report.py +34 -22
  56. crapkit-0.7.2/src/crapkit/ratchetfile.py +84 -0
  57. crapkit-0.7.2/src/crapkit/records.py +55 -0
  58. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/report.py +19 -9
  59. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/repotext.py +5 -1
  60. crapkit-0.7.2/src/crapkit/resources.py +141 -0
  61. crapkit-0.7.2/src/crapkit/retention.py +186 -0
  62. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/sarif.py +10 -3
  63. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/scaffold.py +4 -79
  64. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/score.py +101 -29
  65. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/snapshot.py +5 -3
  66. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/store.py +329 -134
  67. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/uncovered.py +30 -39
  68. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/universe.py +25 -5
  69. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/verify.py +25 -1
  70. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/worklist.py +37 -19
  71. {crapkit-0.6.0 → crapkit-0.7.2/src/crapkit.egg-info}/PKG-INFO +130 -90
  72. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit.egg-info/SOURCES.txt +15 -1
  73. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit.egg-info/requires.txt +3 -1
  74. crapkit-0.6.0/src/crapkit/cli/__init__.py +0 -412
  75. crapkit-0.6.0/src/crapkit/discover.py +0 -384
  76. crapkit-0.6.0/src/crapkit/keys.py +0 -86
  77. crapkit-0.6.0/src/crapkit/merge.py +0 -77
  78. crapkit-0.6.0/src/crapkit/mutate.py +0 -183
  79. crapkit-0.6.0/src/crapkit/mutate_pool.py +0 -362
  80. crapkit-0.6.0/src/crapkit/procs.py +0 -137
  81. {crapkit-0.6.0 → crapkit-0.7.2}/LICENSE +0 -0
  82. {crapkit-0.6.0 → crapkit-0.7.2}/setup.cfg +0 -0
  83. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/__main__.py +0 -0
  84. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/_pygdefer.py +0 -0
  85. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/errors.py +0 -0
  86. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/invocation.py +0 -0
  87. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/lizardcognitive.py +0 -0
  88. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/lizardpowershell.py +0 -0
  89. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/lizardrust.py +0 -0
  90. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/lizardshell.py +0 -0
  91. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/rootfind.py +0 -0
  92. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/sarifio.py +0 -0
  93. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit/watch.py +0 -0
  94. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit.egg-info/dependency_links.txt +0 -0
  95. {crapkit-0.6.0 → crapkit-0.7.2}/src/crapkit.egg-info/entry_points.txt +0 -0
  96. {crapkit-0.6.0 → crapkit-0.7.2}/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.6.0
3
+ Version: 0.7.2
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
@@ -30,8 +30,10 @@ Requires-Dist: lizard>=1.24.0
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: pytest>=8; extra == "dev"
32
32
  Requires-Dist: pytest-cov>=5; extra == "dev"
33
- Requires-Dist: pytest-xdist>=3; extra == "dev"
33
+ Requires-Dist: pytest-xdist>=3.7; extra == "dev"
34
34
  Requires-Dist: coverage>=7.10.6; extra == "dev"
35
+ Requires-Dist: PyYAML>=6; extra == "dev"
36
+ Requires-Dist: Pillow; extra == "dev"
35
37
  Provides-Extra: py
36
38
  Requires-Dist: pytest-cov>=5; extra == "py"
37
39
  Requires-Dist: coverage>=7.10.6; extra == "py"
@@ -54,8 +56,8 @@ worst ones by how often the file changes, and blocks commits that add more. It r
54
56
  Python, TypeScript, TSX, JavaScript, Swift, Go, Rust, shell, PowerShell, C and C++,
55
57
  Objective-C, Vue, Java and Zig through [lizard](https://github.com/terryyin/lizard), and
56
58
  joins per-function branch coverage from the istanbul or coverage.py artifact your own test
57
- command already writes. Every read command speaks sorted-keys JSON on a pinned schema,
58
- because half the callers are coding agents.
59
+ command already writes. JSON commands use sorted keys and a versioned schema for
60
+ scripts, coding agents and the optional MCP server.
59
61
 
60
62
  ```
61
63
  CRAP = ccn^2 * (1 - cov)^3 + ccn
@@ -86,6 +88,14 @@ a wall of red. Next to crap4py, radon, xenon, wily and SonarQube:
86
88
  crapkit scores **git-tracked files only**. Source you have not `git add`ed is invisible to
87
89
  it.
88
90
 
91
+ | Start with | When |
92
+ |---|---|
93
+ | [Install](#install) and [the 60-second start](#the-60-second-start) | You want the first score in an existing Git repository. |
94
+ | [Python](#quickstart-python) or [TypeScript](#quickstart-typescript) quickstart | You want a worked example from setup through a passing verify. |
95
+ | [Adoption](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/adoption.md) | You need to choose scopes, wire tests or introduce a ratchet to existing debt. |
96
+ | [Upgrading](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/upgrading.md) | You already have saved runs, ratchet marks or an installed plugin. |
97
+ | [Subcommands](#subcommands) and [JSON/MCP reference](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md) | You are scripting commands or connecting a coding agent. |
98
+
89
99
  ---
90
100
 
91
101
  ## The 60-second start
@@ -93,17 +103,20 @@ it.
93
103
  ```
94
104
  pip install crapkit
95
105
  cd your-repo
96
- crapkit init # crapkit.toml and .gitignore lines, plus a live coverage lane when it
97
- # recognizes the runner and a scope speaks its language: pyproject.toml,
98
- # pytest.ini or setup.cfg for pytest; a test script or vitest/jest in
99
- # package.json for the JS side
100
- # without one: the lane comes commented out, init says to declare one,
101
- # and docs/lanes.md is how to fill it in
106
+ crapkit init # write crapkit.toml and ignore measurement output
107
+ crapkit doctor # check scopes, test commands and coverage dependencies
102
108
  crapkit coverage # runs the lane, joins coverage, stores a scored run
103
109
  crapkit worklist # the ranked risk map
104
- crapkit ratchet seed && git add crapkit.toml crapkit-ratchet.tsv .gitignore
110
+ crapkit ratchet seed
111
+ git add crapkit.toml crapkit-ratchet.tsv .gitignore
105
112
  ```
106
113
 
114
+ `init` detects pytest, Vitest and Jest from the repository's own files. Review the
115
+ generated config before running its commands. When detection leaves a commented
116
+ lane, fill it in using the [lane recipes](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md).
117
+ Commit the adoption files, then run `crapkit verify` to establish a passing verdict.
118
+ Install the [commit gate](#the-gate) when the config and ratchet are ready.
119
+
107
120
  `coverage` scores, `worklist` ranks:
108
121
 
109
122
  ```
@@ -169,19 +182,24 @@ pip install git+https://github.com/JeanFrancoisGagne/crapkit.git
169
182
  pip install .
170
183
  ```
171
184
 
172
- Every route pulls one dependency, `lizard>=1.24.0`, a normal PyPI wheel, so an offline
173
- mirror installs fine. Requires Python 3.11 or newer. The `pip install -e ".[dev]"` under
185
+ Requires Python 3.11 or newer and Git on PATH. The CLI has one runtime dependency,
186
+ `lizard>=1.24.0`; a package mirror needs both distributions. Install into the environment
187
+ you intend to use, then check `crapkit --version`. The `pip install -e ".[dev]"` under
174
188
  [Development](#development) is a different thing: it adds the test extra, for people
175
189
  changing crapkit.
176
190
 
177
- Scoring runs your own test command on your own machine and reads the artifact it writes.
178
- There is no network call anywhere in crapkit, so no source, no score and no telemetry
179
- leaves the box
191
+ Python projects can install `pip install "crapkit[py]"` in their test environment to
192
+ include pytest-cov and subprocess-capable coverage.py. A separate tool installation
193
+ still needs the coverage plugin in the environment that runs the suite.
194
+
195
+ Analysis and scoring run locally and send no telemetry. Configured lane, mutation
196
+ and alert commands run with your permissions and can contact services or change
197
+ files. Review those commands before running Crapkit in a repository you do not trust
180
198
  ([SECURITY.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/SECURITY.md)).
181
199
 
182
200
  ```
183
201
  $ crapkit --version
184
- crapkit 0.6.0
202
+ crapkit 0.7.2
185
203
  ```
186
204
 
187
205
  `python -m crapkit` works identically to the console script and is what to use from a
@@ -190,64 +208,34 @@ at or above the current directory, so a monorepo workspace finds the root's), an
190
208
  you never have to `cd` into the repo you are scoring; [Subcommands](#subcommands) shows
191
209
  where the flag goes.
192
210
 
193
- ## Upgrading from 0.4.4
211
+ ## Upgrading
194
212
 
195
- **Run `crapkit ratchet seed` first.** Shell cognitive complexity now nests, which is
196
- analysis version 8, and marks measured under version 7 are not comparable. Until you
197
- re-seed, `verify` refuses at exit 3:
213
+ Keep the CLI and plugin versions aligned, measure fresh coverage after upgrading,
214
+ and review any ratchet identity refusal before reseeding. The current reader is
215
+ analysis version 10; older JavaScript and TypeScript callback marks can require a
216
+ reviewed mapping. Follow the [upgrade guide](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/upgrading.md)
217
+ for saved state, portable records and Windows launcher locks.
218
+
219
+ ### Upgrading from 0.4.4
220
+
221
+ This historical example describes the 0.4.4 to 0.4.5 transition, from analysis
222
+ version 7 to 8. It is retained to explain older refusal messages:
198
223
 
199
224
  ```
200
225
  $ crapkit verify
201
226
  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`
202
227
  ```
203
228
 
204
- Only shell and PowerShell cognitive numbers move. `ccn` does not, so a re-seed re-stamps
205
- the file and leaves the marks where they were.
206
-
207
- Five more things change under you. Three of them need nothing from you:
208
-
209
- - **New cache files.** `.crapkit/coupling-cache-v1.json` joins `churn-cache-v2.json` and
210
- `churn-log-v2.z`. A warm 0.4.4 churn cache is adopted once and its file removed, and
211
- `.crapkit/` is already gitignored, so nothing new reaches your index.
212
- - **`trend` and `report` write.** Both read a per-run rollup table, filled once per run and
213
- pruned with its run, instead of rescanning every scored row. A read-only `.crapkit/`
214
- costs the speedup, never the command.
215
- - **Nested scopes may move files.** One predicate decides scope ownership now, and the
216
- deepest declared path wins, so a repo whose `[[scope]]` paths nest inside each other can
217
- see files change scope, rollup and ceiling on the next scan. Scopes that do not nest see
218
- no change.
219
-
220
- The other two put something in front of you:
221
-
222
- - **`mutate` keeps a worktree pool.** With `mutation_workers > 1` the worker worktrees now
223
- live under `.crapkit/mutate-pool/` between runs and are re-prepared each run, which is
224
- the setup cost gone (30.6 s to build four on a 31,459-file repo, 0.46 s to re-prepare
225
- them). The pool is not size-bounded and nothing sweeps it: `crapkit mutate --drop-pool`
226
- removes it and exits. Single-worker runs are untouched.
227
- - **`doctor` WARNs on a lane with no `results_artifact`.** Every `coveragepy` or `istanbul`
228
- lane written before 0.4.5 gets one, with the two lines that fix it. Coverage is
229
- unaffected. What the lane cannot feed without a results file is the crashed-worker check
230
- and the no-new-failures check (exit 8).
229
+ That transition changed cognitive complexity, not `ccn` or the CRAP formula.
230
+ Later reader changes also affect function identity. Use the current upgrade guide
231
+ when moving from any older release to today's reader.
231
232
 
232
233
  ### The exe lock on Windows
233
234
 
234
- `uv tool upgrade crapkit`, and `pip install -U` into a tool venv, fail with `os error 32`
235
- ("The process cannot access the file because it is being used by another process") while a
236
- crapkit MCP server is live: an agent session spawns `crapkit.exe mcp`, which holds the
237
- launcher, and Windows will not overwrite a running executable. The venv upgrades before
238
- that copy fails, so `crapkit --version` already reports the new version and only the
239
- launcher is stale. Quit the agent session and rerun the upgrade, or rename the locked exe
240
- aside (Windows allows renaming a running one) and copy the new one in. Two lines in
241
- cmd.exe, where both `%` variables expand:
242
-
243
- ```bat
244
- move %USERPROFILE%\.local\bin\crapkit.exe %USERPROFILE%\.local\bin\crapkit.exe.old
245
- copy %APPDATA%\uv\tools\crapkit\Scripts\crapkit.exe %USERPROFILE%\.local\bin\crapkit.exe
246
- ```
247
-
248
- Git Bash has no `move` and passes `%APPDATA%` through as literal text, so that block
249
- fails there on its first line. Its form is `mv` and `cp` over `"$USERPROFILE"` and
250
- `"$APPDATA"`, which Git Bash sets to the same two directories.
235
+ An active MCP server can hold `crapkit.exe` open and make an upgrade fail with
236
+ Windows error 32. Stop that server or its agent session, rerun the upgrade with
237
+ the same installer, then restart the client. See the
238
+ [Windows upgrade procedure](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/upgrading.md#windows-launcher-locks).
251
239
 
252
240
  ## The Claude Code plugin
253
241
 
@@ -257,14 +245,23 @@ claude plugin install crapkit@crapkit
257
245
  ```
258
246
 
259
247
  Two commands, installed once per user, and every repo on the machine gets it. The plugin
260
- ships three skills, the read-only MCP server, and one advisory PostToolUse hook that names
248
+ ships three skills, the read-side MCP server, and one advisory PostToolUse hook that names
261
249
  any function an edit pushed over its ceiling. Claude reaches two of the skills by itself,
262
250
  `crapkit` and `crapkit-recover`; the third you type, as `/crapkit:crapkit-onboard`, because
263
251
  wiring a repo up happens once and its description has no business in every turn's window.
264
252
  It adds no files to your repo, and it needs the crapkit CLI on PATH.
265
253
 
266
- A repo with no `crapkit.toml` costs a silent sub-50 ms no-op per edit. Other agent
267
- runtimes have no marketplace: copy `plugin/skills/*` into their skills directory instead.
254
+ A repo with no `crapkit.toml` costs a silent sub-50 ms no-op per edit. After upgrading
255
+ the CLI, refresh the marketplace before updating the installed plugin:
256
+
257
+ ```
258
+ claude plugin marketplace update crapkit
259
+ claude plugin update crapkit@crapkit --scope user
260
+ crapkit doctor --plugin-root
261
+ ```
262
+
263
+ Restart existing Claude Code sessions to apply the plugin update. The check above
264
+ compares installed files with the CLI on PATH; it does not reload a running session.
268
265
 
269
266
  The hook registers on `Edit|Write`, which is every write that names a file. An agent that
270
267
  writes its source through a shell heredoc names none, so a `Bash` event is judged off the
@@ -293,6 +290,31 @@ on crapkit's own checkout, and more on a bigger tree. What comes back is the dir
293
290
  untracked `*.py` files written in the last 12 seconds, 25 at most, each judged the way an
294
291
  edit is. Python only, so a TypeScript or Go repo pays the two spawns and hears nothing.
295
292
 
293
+ ### Codex
294
+
295
+ Codex can install the same marketplace's plugin through its own manager:
296
+
297
+ ```
298
+ codex plugin marketplace add https://github.com/JeanFrancoisGagne/crapkit.git
299
+ codex plugin add crapkit@crapkit
300
+ ```
301
+
302
+ Use the three skills and MCP server in Codex. The advisory hook instructions above
303
+ configure Claude Code's PostToolUse event.
304
+ To refresh an existing Codex installation:
305
+
306
+ ```
307
+ codex plugin marketplace upgrade crapkit
308
+ codex plugin add crapkit@crapkit
309
+ codex plugin list --marketplace crapkit --json
310
+ ```
311
+
312
+ Check the installed Codex plugin with an explicit `crapkit doctor --plugin-root PATH`.
313
+ See [plugin upgrades](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/upgrading.md#plugin-and-mcp-clients)
314
+ for choosing that path and starting a fresh MCP session. A runtime with a skills
315
+ directory but no compatible marketplace can copy `plugin/skills/*` instead; other
316
+ MCP clients use the [stdio setup](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md#mcp-server).
317
+
296
318
  ## Languages
297
319
 
298
320
  14 languages, two coverage parsers. Coverage joins where a parser exists; everything else
@@ -331,10 +353,20 @@ lizard #494), so crapkit counts each non-wildcard arm like a C `case` and retire
331
353
  override the day upstream fixes it. The cognitive column charges that same block once,
332
354
  the way Sonar charges a `switch`.
333
355
 
356
+ Expression arrows in arrays and argument lists are measured separately. In TypeScript,
357
+ wrap an arrow body in parentheses when it contains `<` before a comma, such as
358
+ `x => (pair<T,U>(x))` or `x => (x < 0)`. Without that delimiter, analysis refuses
359
+ the file because this reader cannot distinguish type arguments from an expression
360
+ separator. Generic arrow parameter declarations remain supported.
361
+
362
+ Functions on the same line have separate occurrence identifiers. Existing ratchet
363
+ marks with ambiguous old identities require a reviewed mapping; see
364
+ [same-line function identity](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md#same-line-function-identity).
365
+
334
366
  ## The gate
335
367
 
336
- Four surfaces ask the same question, ccn against the scope's ceiling, with four
337
- different powers:
368
+ Use the advisory while editing, the gate when committing, and `verify` for the
369
+ full verdict. The preview and hooks differ in what their available evidence can prove:
338
370
 
339
371
  | Surface | Fires | Power |
340
372
  |---|---|---|
@@ -353,12 +385,10 @@ on stderr (`staged function(s) carry a ratchet mark and were not gated`), and sa
353
385
  about a staged file no `[[scope]]` claims, so a new top-level directory cannot go ungated
354
386
  in silence.
355
387
 
356
- **The crapkit root does not have to be the git top.** Since 0.4.5 every git spawn runs with
357
- `diff.relative=true` and `core.quotePath=false`, so a `crapkit.toml` in `packages/api`
358
- gates that package's own staged files and names them `app/m.py`, not
359
- `packages/api/app/m.py`, and a dirty non-ASCII path is a real row rather than an invisible
360
- one. Before that a nested root matched staged paths against no scope, and a function at
361
- twice the ceiling committed with a warning.
388
+ **The Crapkit root can sit below the Git top.** A config in `packages/api` gates
389
+ that package's staged files as project-relative paths such as `app/m.py`.
390
+ [Path and root rules](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md#file-paths-and-root-discovery)
391
+ also cover absolute arguments, literal filenames and Git diff settings.
362
392
 
363
393
  Git runs hooks outside your shell's activated venv. Bare `python` must resolve to an
364
394
  interpreter that has crapkit installed, or spell it out
@@ -425,7 +455,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
425
455
  repos:
426
456
  - repo: https://github.com/JeanFrancoisGagne/crapkit
427
457
  # crapkit's release step rewrites this line to the tag it just cut
428
- rev: v0.6.0
458
+ rev: v0.7.2
429
459
  hooks:
430
460
  - id: crapkit-gate
431
461
  ```
@@ -533,7 +563,7 @@ to a workflow, and every input has a default:
533
563
  - uses: actions/checkout@v4
534
564
  with:
535
565
  fetch-depth: 0
536
- - uses: JeanFrancoisGagne/crapkit@v0.6.0
566
+ - uses: JeanFrancoisGagne/crapkit@v0.7.2
537
567
  ```
538
568
 
539
569
  The whole job those four lines sit in:
@@ -553,7 +583,7 @@ jobs:
553
583
  with:
554
584
  python-version: "3.12" # the interpreter the install below lands in
555
585
  - run: pip install -e ".[dev]" # whatever your lanes need to run
556
- - uses: JeanFrancoisGagne/crapkit@v0.6.0
586
+ - uses: JeanFrancoisGagne/crapkit@v0.7.2
557
587
  with:
558
588
  gate: "false"
559
589
  ```
@@ -724,6 +754,10 @@ on PATH fails that step. Both leave the rendered text in the job log.
724
754
 
725
755
  ## Subcommands
726
756
 
757
+ `crapkit clean --dry-run --json` previews policy-based cleanup. See
758
+ [resource policies](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/resources.md)
759
+ for shared analysis workers, process lifetime, bounded logs and retained evidence.
760
+
727
761
  Every subcommand takes `--repo PATH`, and the flag goes **after** the subcommand. Without
728
762
  it the root is the nearest `crapkit.toml` at or above the current directory
729
763
  ([ADR 0002](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/adr/0002-configuration-is-found-upward-nearest-wins.md)): from a
@@ -750,6 +784,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
750
784
 
751
785
  | Command | What it does |
752
786
  |---|---|
787
+ | `clean [--dry-run] [--json]` | Applies configured retention to recognized idle default test evidence and recovers abandoned temporary mutation worktrees. Preserves active runs, caller-managed output and intentional mutation pools. `--dry-run` reports planned removals. |
753
788
  | `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. |
754
789
  | `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). |
755
790
  | `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. |
@@ -767,15 +802,15 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
767
802
  | `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). |
768
803
  | `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. |
769
804
  | `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. Every row carries the function's CRAP and coverage, and prints the `crapkit explain` call for the rest: dark lines, history, the mark. It reads the same per-run rollups `trend` does, and writes them on the same terms. |
770
- | `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. |
805
+ | `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`. Ties have a stable order across hash seeds. A positive `--top` bounds retained candidates and output; dense inputs still require pair comparisons. A function and its nested closure never pair. |
771
806
  | `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. |
772
- | `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. Both lists pass through the scored corpus first, the same predicate `coverage` uses (scopes, excludes, the test-file cut, `max_file_bytes`): a test file, an excluded path, a file over `max_file_bytes` or a file no scope claims is named on stderr and never mutated, `--json` lists it under `outside_corpus`, and when nothing is left stdout says `nothing to mutate` at exit 0 without starting the suite. `--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. |
807
+ | `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. Both lists pass through the scored corpus first, the same predicate `coverage` uses (scopes, excludes, the test-file cut, `max_file_bytes`): a test file, an excluded path, a file over `max_file_bytes` or a file no scope claims is named on stderr and never mutated, `--json` lists it under `outside_corpus`, and when nothing is left stdout says `nothing to mutate` at exit 0 without starting the suite. `--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. Every worker uses a kept worktree, including one; see [mutation worktrees](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md#mutation-worktrees). `--drop-pool` removes them and exits. |
773
808
  | `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. |
774
809
  | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
775
810
  | `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. |
776
811
  | `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. |
777
812
  | `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. |
778
- | `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing twelve read-only tools named `verb_noun`, each with a title and a documented output schema. 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). |
813
+ | `mcp` | A stdio MCP server with no extra dependency, exposing twelve read-side tools named `verb_noun`, each with a title and output schema. Tools call the CLI to inspect current scores, source and edited-file gates. They take no claims and run no verification; calls can write caches or store metadata. See [the MCP contract and setup](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md#mcp-server). |
779
814
 
780
815
  ## Reading the output
781
816
 
@@ -1147,7 +1182,7 @@ exit 5:
1147
1182
 
1148
1183
  ```
1149
1184
  $ crapkit coverage
1150
- 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
1185
+ crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); lane 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
1151
1186
 
1152
1187
  MISSING DEPENDENCY Cannot find dependency '@vitest/coverage-v8'
1153
1188
 
@@ -1309,11 +1344,14 @@ with no debt.
1309
1344
  | [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. |
1310
1345
  | [docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md) | Every `crapkit.toml` key: type, default, and what it does. |
1311
1346
  | [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. |
1347
+ | [docs/resources.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/resources.md) | Worker budgets, command cleanup, log rotation, test evidence retention and safe cleanup. |
1312
1348
  | [docs/ratchet.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md) | Seeding, pruning, the git merge driver, metric stamps, debt policy, overrides. |
1349
+ | [docs/upgrading.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/upgrading.md) | Existing installations: analysis and key versions, saved state, plugin alignment and Windows upgrades. |
1350
+ | [docs/portable-records.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/portable-records.md) | Lossless exports, portable baselines and ratchets, including filenames with delimiters. |
1313
1351
  | [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. |
1314
1352
  | [docs/comparison.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/comparison.md) | Where crapkit sits next to radon, xenon, wily, coverage.py and SonarQube, and how they run together. |
1315
1353
  | [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. |
1316
- | [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. |
1354
+ | [plugin/](https://github.com/JeanFrancoisGagne/crapkit/tree/main/plugin) | Three skills and the MCP server for Claude Code and Codex, with advisory PostToolUse hook instructions for Claude Code. |
1317
1355
 
1318
1356
  [crapkit.schema.json](https://github.com/JeanFrancoisGagne/crapkit/blob/main/crapkit.schema.json) is the authority on the config file shape.
1319
1357
 
@@ -1321,15 +1359,17 @@ with no debt.
1321
1359
 
1322
1360
  ```
1323
1361
  pip install -e ".[dev]"
1324
- pip install pytest-xdist
1325
1362
  git config core.hooksPath git-hooks
1326
- python -m pytest -q
1363
+ python tools/testing/run.py
1327
1364
  ```
1328
1365
 
1329
- `pytest-xdist` is not optional: `tests/fixtures/mini_repo` declares a lane that shells out
1330
- to `pytest ... -n 2`, and without it that subprocess dies on an unrecognized `-n`. The
1331
- `git config` line arms the complexity gate on your own commits. Same steps, with what each
1332
- one buys, in [CONTRIBUTING.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/CONTRIBUTING.md).
1366
+ The dev extra includes pytest, pytest-cov, pytest-xdist and coverage.py. The shared
1367
+ runner owns the unit and E2E schedule; use `--unit-workers 1` for serial unit
1368
+ reproduction or `--coverage` for combined branch coverage and JUnit. The `git config`
1369
+ line arms the complexity gate. See
1370
+ [CONTRIBUTING.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/CONTRIBUTING.md)
1371
+ for development and [the verified implementation report](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/architecture/2026-09-07-implementation/REPORT.md)
1372
+ for complete Windows source and Linux wheel results, focused benchmarks and their limits.
1333
1373
 
1334
1374
  ## License
1335
1375