crapkit 0.7.5__tar.gz → 0.8.0__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 (103) hide show
  1. {crapkit-0.7.5 → crapkit-0.8.0}/PKG-INFO +102 -39
  2. crapkit-0.7.5/src/crapkit.egg-info/PKG-INFO → crapkit-0.8.0/README.md +1398 -1376
  3. {crapkit-0.7.5 → crapkit-0.8.0}/pyproject.toml +11 -6
  4. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/__init__.py +1 -1
  5. crapkit-0.8.0/src/crapkit/_process_owner.py +335 -0
  6. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/_windows_job.py +2 -2
  7. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/analyze.py +177 -49
  8. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cache.py +4 -3
  9. crapkit-0.8.0/src/crapkit/churn.py +224 -0
  10. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/churn_cache.py +30 -10
  11. crapkit-0.8.0/src/crapkit/churn_commits.py +184 -0
  12. crapkit-0.8.0/src/crapkit/churn_log.py +458 -0
  13. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/_shared.py +66 -18
  14. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/admin.py +173 -244
  15. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/analyses.py +11 -5
  16. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/claude_hook.py +32 -5
  17. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/maintenance.py +11 -10
  18. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/parser.py +14 -7
  19. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/queue.py +154 -156
  20. crapkit-0.8.0/src/crapkit/cli/ratchet_cmds.py +524 -0
  21. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/reports.py +62 -18
  22. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/scoring.py +145 -30
  23. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/verifying.py +189 -108
  24. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/config.py +38 -5
  25. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/config_contract.py +24 -6
  26. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/coupling.py +8 -1
  27. crapkit-0.8.0/src/crapkit/coverage_format.py +57 -0
  28. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/coverage_istanbul.py +138 -3
  29. crapkit-0.8.0/src/crapkit/coverage_py.py +287 -0
  30. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/covstream.py +17 -220
  31. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/doctor.py +113 -18
  32. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/dup.py +143 -48
  33. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/gitio.py +58 -31
  34. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/hook.py +9 -1
  35. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/junitparse.py +50 -8
  36. crapkit-0.8.0/src/crapkit/keys.py +402 -0
  37. crapkit-0.8.0/src/crapkit/lane_changes.py +145 -0
  38. crapkit-0.8.0/src/crapkit/lane_command.py +187 -0
  39. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/lanes.py +375 -175
  40. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/lizardcognitive.py +43 -9
  41. crapkit-0.8.0/src/crapkit/lizardpython.py +389 -0
  42. crapkit-0.8.0/src/crapkit/lizardtypescript.py +299 -0
  43. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/mcp_server.py +83 -66
  44. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/merge.py +4 -0
  45. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/override.py +48 -22
  46. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/packet.py +91 -116
  47. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/procs.py +168 -257
  48. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/ratchet.py +74 -30
  49. crapkit-0.8.0/src/crapkit/ratchetfile.py +160 -0
  50. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/report.py +24 -12
  51. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/scaffold.py +14 -10
  52. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/score.py +94 -27
  53. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/snapshot.py +11 -3
  54. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/store.py +471 -140
  55. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/uncovered.py +14 -20
  56. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/universe.py +3 -3
  57. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/verify.py +49 -8
  58. crapkit-0.7.5/README.md → crapkit-0.8.0/src/crapkit.egg-info/PKG-INFO +1439 -1335
  59. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit.egg-info/SOURCES.txt +4 -1
  60. crapkit-0.7.5/src/crapkit/_process_owner.py +0 -129
  61. crapkit-0.7.5/src/crapkit/churn.py +0 -86
  62. crapkit-0.7.5/src/crapkit/churn_log.py +0 -340
  63. crapkit-0.7.5/src/crapkit/cli/ratchet_cmds.py +0 -258
  64. crapkit-0.7.5/src/crapkit/coverage_py.py +0 -119
  65. crapkit-0.7.5/src/crapkit/keys.py +0 -171
  66. crapkit-0.7.5/src/crapkit/lizardpython.py +0 -262
  67. crapkit-0.7.5/src/crapkit/lizardtypescript.py +0 -142
  68. crapkit-0.7.5/src/crapkit/ratchetfile.py +0 -84
  69. crapkit-0.7.5/src/crapkit/retention.py +0 -204
  70. {crapkit-0.7.5 → crapkit-0.8.0}/LICENSE +0 -0
  71. {crapkit-0.7.5 → crapkit-0.8.0}/setup.cfg +0 -0
  72. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/__main__.py +0 -0
  73. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/_analysis_pool.py +0 -0
  74. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/_mcp_stdio.py +0 -0
  75. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/_process_family.py +0 -0
  76. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/_pygdefer.py +0 -0
  77. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/cli/__init__.py +0 -0
  78. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/coupling_cache.py +0 -0
  79. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/diffparse.py +0 -0
  80. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/digest.py +0 -0
  81. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/errors.py +0 -0
  82. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/gitpaths.py +0 -0
  83. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/invocation.py +0 -0
  84. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/lizardpowershell.py +0 -0
  85. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/lizardrust.py +0 -0
  86. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/lizardshell.py +0 -0
  87. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/locks.py +0 -0
  88. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/logs.py +0 -0
  89. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/mutate.py +0 -0
  90. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/mutate_pool.py +0 -0
  91. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/ratchet_report.py +0 -0
  92. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/records.py +0 -0
  93. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/repotext.py +0 -0
  94. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/resources.py +0 -0
  95. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/rootfind.py +0 -0
  96. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/sarif.py +0 -0
  97. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/sarifio.py +0 -0
  98. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/watch.py +0 -0
  99. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit/worklist.py +0 -0
  100. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit.egg-info/dependency_links.txt +0 -0
  101. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit.egg-info/entry_points.txt +0 -0
  102. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit.egg-info/requires.txt +0 -0
  103. {crapkit-0.7.5 → crapkit-0.8.0}/src/crapkit.egg-info/top_level.txt +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crapkit
3
- Version: 0.7.5
3
+ Version: 0.8.0
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
7
7
  Project-URL: Homepage, https://github.com/JeanFrancoisGagne/crapkit
8
- Project-URL: Documentation, https://jeanfrancoisgagne.github.io/crapkit/handbook.html
8
+ Project-URL: Documentation, https://www.jfgagne.com/crapkit/handbook.html
9
9
  Project-URL: Changelog, https://github.com/JeanFrancoisGagne/crapkit/blob/main/CHANGELOG.md
10
10
  Project-URL: Issues, https://github.com/JeanFrancoisGagne/crapkit/issues
11
11
  Project-URL: Source, https://github.com/JeanFrancoisGagne/crapkit
@@ -111,6 +111,9 @@ crapkit ratchet seed
111
111
  git add crapkit.toml crapkit-ratchet.tsv .gitignore
112
112
  ```
113
113
 
114
+ Not a Python repo? `uvx crapkit init` runs the same commands and adds nothing to your
115
+ manifest: see [A repo that is not Python](#a-repo-that-is-not-python).
116
+
114
117
  `init` detects pytest, Vitest and Jest from the repository's own files. Review the
115
118
  generated config before running its commands. When detection leaves a commented
116
119
  lane, fill it in using the [lane recipes](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md).
@@ -182,6 +185,28 @@ pip install git+https://github.com/JeanFrancoisGagne/crapkit.git
182
185
  pip install .
183
186
  ```
184
187
 
188
+ ### A repo that is not Python
189
+
190
+ crapkit is a command-line tool, never a dependency of the code it scores. A TypeScript,
191
+ Go or Rust repo adds nothing to its own manifest. With [uv](https://docs.astral.sh/uv/)
192
+ on the machine, `uvx` fetches crapkit into a cache of its own and runs it:
193
+
194
+ ```
195
+ $ uvx crapkit init
196
+ wrote crapkit.toml with 1 scope(s): src
197
+ detected 1 lane(s) from this repo's own files: js - next: run `crapkit coverage`
198
+ added to .gitignore: .crapkit/
199
+
200
+ $ uvx crapkit coverage
201
+ $ uvx crapkit worklist
202
+ ```
203
+
204
+ The lane still runs your own test runner, so Vitest or Jest and its coverage package come
205
+ from the repo's `node_modules` as they do today. `uv tool install crapkit` or
206
+ `pipx install crapkit` puts a `crapkit` command on PATH once, which is what the
207
+ [commit gate](#the-gate) and the Claude Code plugin call. uv brings its own Python when
208
+ the machine has none.
209
+
185
210
  Requires Python 3.11 or newer and Git on PATH. The CLI has one runtime dependency,
186
211
  `lizard>=1.24.0`; a package mirror needs both distributions. Install into the environment
187
212
  you intend to use, then check `crapkit --version`. The `pip install -e ".[dev]"` under
@@ -199,7 +224,7 @@ files. Review those commands before running Crapkit in a repository you do not t
199
224
 
200
225
  ```
201
226
  $ crapkit --version
202
- crapkit 0.7.5
227
+ crapkit 0.8.0
203
228
  ```
204
229
 
205
230
  `python -m crapkit` works identically to the console script and is what to use from a
@@ -212,18 +237,22 @@ where the flag goes.
212
237
 
213
238
  Keep the CLI and plugin versions aligned, measure fresh coverage after upgrading,
214
239
  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)
240
+ analysis version 11. It renames once each Python def with a PEP 695 type parameter
241
+ list and each def nested three or more deep, and it lists a def whose body sits on
242
+ its colon line; `crapkit ratchet prune` drops the marks left under the old names.
243
+ Older JavaScript and TypeScript callback marks can require a reviewed mapping.
244
+ Follow the [upgrade guide](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/upgrading.md)
217
245
  for saved state, portable records and Windows launcher locks.
218
246
 
219
247
  ### Upgrading from 0.4.4
220
248
 
221
249
  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:
250
+ version 7 to 8. It is retained to explain the refusal, quoted as crapkit prints it
251
+ today:
223
252
 
224
253
  ```
225
254
  $ crapkit verify
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`
255
+ 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; run `crapkit coverage`, then re-baseline with `crapkit ratchet seed`
227
256
  ```
228
257
 
229
258
  That transition changed cognitive complexity, not `ccn` or the CRAP formula.
@@ -251,8 +280,10 @@ any function an edit pushed over its ceiling. Claude reaches two of the skills b
251
280
  wiring a repo up happens once and its description has no business in every turn's window.
252
281
  It adds no files to your repo, and it needs the crapkit CLI on PATH.
253
282
 
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:
283
+ A repo with no `crapkit.toml` costs a silent no-op per edit: 68 ms on Windows through the
284
+ `crapkit.exe` launcher Claude Code starts, where a bare `python -c pass` took 32 ms on the
285
+ same machine. After upgrading the CLI, refresh the marketplace before updating the
286
+ installed plugin:
256
287
 
257
288
  ```
258
289
  claude plugin marketplace update crapkit
@@ -285,8 +316,8 @@ command, matcher `Bash`:
285
316
  ```
286
317
 
287
318
  The cost is one `git rev-parse --show-toplevel` and one `git status --porcelain -z -uall`
288
- per shell call in any git repo, whether or not crapkit measures it: about 30 ms together
289
- on crapkit's own checkout, and more on a bigger tree. What comes back is the dirty or
319
+ per shell call in any git repo, whether or not crapkit measures it: about 50 ms together
320
+ on crapkit's own checkout on Windows, and more on a bigger tree. What comes back is the dirty or
290
321
  untracked `*.py` files written in the last 12 seconds, 25 at most, each judged the way an
291
322
  edit is. Python only, so a TypeScript or Go repo pays the two spawns and hears nothing.
292
323
 
@@ -371,7 +402,7 @@ full verdict. The preview and hooks differ in what their available evidence can
371
402
  | Surface | Fires | Power |
372
403
  |---|---|---|
373
404
  | `crapkit claude-hook` | after an agent's edit lands | **advisory.** Names the breach on stderr. Blocks nothing, because PostToolUse runs after the write |
374
- | `crapkit rescore FILE --gate` | when you ask, after the first coverage run | **preview.** The commit gate's verdict on demand, sub-second, before you stage. With no run behind it, exit 1 and `no snapshot` |
405
+ | `crapkit rescore FILE --gate` | when you ask, after the first coverage run | **preview.** A stricter preview of the commit gate, sub-second, before you stage: a ratchet mark pardons a function only while its CRAP is at or under the mark. With no run behind it, exit 1 and `no snapshot` |
375
406
  | `crapkit hook-precommit` | `git commit` | **blocks.** The hook exits 6; git reports 1. Staged blobs only, so it costs the size of the commit and needs no coverage |
376
407
  | `crapkit verify` | before you push, and in CI | **the verdict.** Gate, ratchet, new test failures, diff coverage, against the trusted baseline |
377
408
 
@@ -405,8 +436,9 @@ chmod +x .git/hooks/pre-commit
405
436
  ```
406
437
 
407
438
  The same file from PowerShell. `Out-File` and `>` write a byte-order mark (UTF-16 on
408
- 5.1) in front of the shebang, and git then answers every commit with `cannot spawn
409
- .git/hooks/pre-commit` and lets it through; `Set-Content -Encoding ascii` does not. Git
439
+ 5.1) in front of the shebang, and git then refuses every commit with `error: cannot spawn
440
+ .git/hooks/pre-commit: No such file or directory` (measured on git 2.43 for Windows)
441
+ without ever running the gate; `Set-Content -Encoding ascii` writes no mark. Git
410
442
  runs the hook with its own `sh`, so the interpreter is spelled with forward slashes and
411
443
  quoted, and no `chmod` is needed on Windows:
412
444
 
@@ -455,7 +487,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
455
487
  repos:
456
488
  - repo: https://github.com/JeanFrancoisGagne/crapkit
457
489
  # crapkit's release step rewrites this line to the tag it just cut
458
- rev: v0.7.5
490
+ rev: v0.8.0
459
491
  hooks:
460
492
  - id: crapkit-gate
461
493
  ```
@@ -507,6 +539,16 @@ crapkit: baseline commit a74260f321f is not an ancestor of HEAD in this shallow
507
539
  That is exit 4 on a `git clone --depth 1` of a repo whose baseline verifies at full depth.
508
540
  On a full clone the same exit blames what it used to, a rebase or an amend that rewrote
509
541
  history, and asks for a fresh baseline instead.
542
+ `verify --base` and `hook-precommit --base` look up the fork point with `git merge-base`,
543
+ and in the same clone they refuse with exit 4 and the same fix:
544
+
545
+ ```
546
+ $ crapkit hook-precommit --base c47a37b1df69c434ba42eec5979ddad03d2bf1e4
547
+ crapkit: git merge-base c47a37b1df69c434ba42eec5979ddad03d2bf1e4 HEAD failed in /home/runner/work/app/app: fatal: Not a valid commit name c47a37b1df69c434ba42eec5979ddad03d2bf1e4; this shallow clone does not hold every commit: set fetch-depth: 0 on the checkout or run git fetch --unshallow
548
+ ```
549
+
550
+ When the clone holds both commits but not the one they fork from, the line reads
551
+ `no merge base between REF and HEAD in ROOT`, followed by the same fix.
510
552
  Set `fetch-depth: 0` on the checkout step, which is what crapkit's own
511
553
  [.github/workflows/ci.yml](https://github.com/JeanFrancoisGagne/crapkit/blob/main/.github/workflows/ci.yml) does.
512
554
 
@@ -563,7 +605,7 @@ to a workflow, and every input has a default:
563
605
  - uses: actions/checkout@v4
564
606
  with:
565
607
  fetch-depth: 0
566
- - uses: JeanFrancoisGagne/crapkit@v0.7.5
608
+ - uses: JeanFrancoisGagne/crapkit@v0.8.0
567
609
  ```
568
610
 
569
611
  The whole job those four lines sit in:
@@ -583,7 +625,7 @@ jobs:
583
625
  with:
584
626
  python-version: "3.12" # the interpreter the install below lands in
585
627
  - run: pip install -e ".[dev]" # whatever your lanes need to run
586
- - uses: JeanFrancoisGagne/crapkit@v0.7.5
628
+ - uses: JeanFrancoisGagne/crapkit@v0.8.0
587
629
  with:
588
630
  gate: "false"
589
631
  ```
@@ -654,7 +696,7 @@ closes it. A verify that passed is one line: `**verify passed.** Run 2 against b
654
696
  The rows are the ranked worklist for the files the pull request changed, worst first,
655
697
  `top` of them, with the rows a finding names listed first. `risk` is ccn times churn
656
698
  weight, the number `crapkit worklist` ranks on, and `remedy` is the run's own verdict for
657
- that function: `decompose`, `add-tests` or `ok`. `(accepted debt)` marks a function the
699
+ that function: `decompose`, `split-lines`, `add-tests` or `ok`. `(accepted debt)` marks a function the
658
700
  committed ratchet carries a mark for, so an untouched `legacy_router` does not read like
659
701
  the pull request's own new function. A pull request that touches no ranked function gets
660
702
  the heading and no table.
@@ -739,8 +781,9 @@ comment then carries coverage's failure in place of the verdict:
739
781
  ```
740
782
 
741
783
  The parenthesis is the first line of the lane failure the summary carries. When every
742
- lane failed, `coverage` prints no summary at all and the lane errors are only in the job
743
- log, and the line says so. With `gate: "true"` the job exits with coverage's code.
784
+ lane failed, `coverage` prints an error object instead of a summary and the lane errors
785
+ are only in the job log, and the line says so: `(every lane failed (1 of 1); the lane
786
+ errors are in the job log)`. With `gate: "true"` the job exits with coverage's code.
744
787
 
745
788
  The other gate that judges a delta is the portable baseline in [Route 4](#route-4-ci):
746
789
  commit `crapkit-baseline.tsv` on the default branch and run `crapkit verify --baseline-tsv
@@ -754,9 +797,9 @@ on PATH fails that step. Both leave the rendered text in the job log.
754
797
 
755
798
  ## Subcommands
756
799
 
757
- `crapkit clean --dry-run --json` previews policy-based cleanup. See
800
+ `crapkit clean --dry-run --json` previews abandoned temporary mutation recovery. See
758
801
  [resource policies](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/resources.md)
759
- for shared analysis workers, process lifetime, bounded logs and retained evidence.
802
+ for shared analysis workers, process lifetime and bounded logs.
760
803
 
761
804
  Every subcommand takes `--repo PATH`, and the flag goes **after** the subcommand. Without
762
805
  it the root is the nearest `crapkit.toml` at or above the current directory
@@ -784,7 +827,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
784
827
 
785
828
  | Command | What it does |
786
829
  |---|---|
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. |
830
+ | `clean [--dry-run] [--json]` | Recovers abandoned temporary mutation worktrees. Preserves active runs and intentional mutation pools. `--dry-run` reports planned recoveries. It leaves test evidence alone: crapkit's own development runner, `tools/testing/run.py`, prunes `.crapkit/test-runs/` with `--retention-days` and `--retention-count`. `--json` keeps `test_runs` with every array empty. |
788
831
  | `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. |
789
832
  | `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). |
790
833
  | `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. |
@@ -795,8 +838,8 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
795
838
  | `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. |
796
839
  | `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). |
797
840
  | `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. |
798
- | `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. |
799
- | `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). |
841
+ | `rescore FILE ... [--gate] [--json]` | Fresh complexity for named files over the latest run's stale coverage, joined by name. A function on a line span another one shares, and a Python def whose body starts on the line its signature ends, scores untested, as the coverage run scores it. 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. |
842
+ | `ratchet seed \| prune \| merge \| move \| report [--baseline ID] [--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. `seed` and `prune` take `--baseline ID` to read a named run instead of verify's pick, refused for the reasons `verify --baseline` refuses one. See [docs/ratchet.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md). |
800
843
  | `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. |
801
844
  | `overrides [--json]` | The override audit trail: who granted what, when, and why. |
802
845
  | `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). |
@@ -806,7 +849,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
806
849
  | `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. |
807
850
  | `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. |
808
851
  | `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. |
809
- | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
852
+ | `hook-precommit [--base REF]` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. `--base REF` compares the index with the merge base of REF and HEAD, the form a CI checkout runs. |
810
853
  | `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. |
811
854
  | `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. |
812
855
  | `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. |
@@ -830,6 +873,7 @@ The coverage summary counts all four as `measured` / `untested` / `no_lane` / `c
830
873
  | Remedy | Condition | Action |
831
874
  |---|---|---|
832
875
  | `decompose` | `ccn > ceiling` | Split it. No amount of coverage clears this. |
876
+ | `split-lines` | `ccn <= ceiling`, `crap > ceiling`, and another function shares its source lines, or a Python def's body starts on the line its signature ends | Put each definition on its own lines, then measure again. Coverage cannot tell functions on one line apart, so the score stays at uncovered whatever the tests do. A one-line Python def shares its line with its `def` statement, which runs at import, so coverage.py cannot show a call. So does a body on the last line of a signature that spans several lines, or one that goes on from the colon's line inside brackets or after a backslash: move the body to its own line after the signature. |
833
877
  | `add-tests` | `ccn <= ceiling` and `crap > ceiling` | Cover the branches. |
834
878
  | `ok` | `crap <= ceiling` | Nothing. |
835
879
 
@@ -920,6 +964,11 @@ bypasses the rule, and the run history records which run the verdict used. Nothi
920
964
  touches a repo that has never run `verify`: with no failure to protect, `coverage` alone
921
965
  always advances the baseline.
922
966
 
967
+ `ratchet seed --baseline ID` and `ratchet prune --baseline ID` take the same name as
968
+ `verify --baseline ID`, admitted by the same rule. When a failed verify pins seed to a run
969
+ it cannot read or sign, that is the way out
970
+ ([docs/ratchet.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md#naming-the-run-to-seed-from)).
971
+
923
972
  **When the id you pass cannot serve.** A `--baseline ID` naming a real run that is not a
924
973
  candidate says which run it is, why, and which ones can:
925
974
 
@@ -941,7 +990,7 @@ crapkit: run 3 is an inventory run (no coverage was measured) and cannot serve a
941
990
  | 6 | Gate violation. A function the diff touched is over its ceiling and past any ratchet mark it carries: an edit that leaves a marked function at or under its mark is the debt the repo signed for and is exempt. Also `rescore --gate`, which applies the same rule, and `hook-precommit`, which exempts on the mark's existence instead. |
942
991
  | 7 | Ratchet regression the diff never touched. A marked function scores worse than its recorded high-water mark; a touched one past its mark reports 6. |
943
992
  | 8 | New test failures against the baseline run. Failures the baseline already had do not count. |
944
- | 9 | Diff-coverage ceiling breached: `diff_uncovered_max` is set and more changed lines than that never ran. |
993
+ | 9 | Diff-coverage ceiling breached: `diff_uncovered_max` is set and more changed lines than that never ran. A changed file no lane artifact mentions counts every line of its functions. |
945
994
 
946
995
  ### Exit 1 means one of three things
947
996
 
@@ -1063,6 +1112,7 @@ makes it step 4 of the burn-down loop. Every key is in
1063
1112
 
1064
1113
  ```
1065
1114
  $ crapkit doctor
1115
+ resources: up to 8 analysis worker(s) per pool, 8 shared slot(s); lane log limit 16777216 bytes per file
1066
1116
  ok config keys all recognized
1067
1117
  ok scope 'calc': 1 file
1068
1118
  ok every tracked source file belongs to a scope
@@ -1102,7 +1152,7 @@ not empty when the burn-down does. `next-item` is the other view of that run: it
1102
1152
 
1103
1153
  ```
1104
1154
  $ crapkit next-item
1105
- {"commit": "fae4db93108b4841a00959f9117430679e7250ca", "empty": false, "item": {"authors": 1, "ccn": 14, "ccn_std": 14, "cognitive": 13, "commits": 1, "cov": 0.5, "crap": 38.5, "end": 28, "est_splits": 3, "est_uncovered_paths": 7, "flag": "measured", "function": "classify( score , attempts , late , bonus )", "handle": "classify", "nesting": 3, "nloc": 22, "path": "calc/grade.py", "remedy": "decompose", "scope": "calc", "start": 7, "target": 6, "uncovered_lines": [9, 11, 15, 17, 19, 24, 25, 26, 27, 28]}, "run_id": 1, "schema": 1, "skipped_no_lane": 0, "stale": false}
1155
+ {"commit": "fae4db93108b4841a00959f9117430679e7250ca", "empty": false, "item": {"authors": 1, "ccn": 14, "ccn_std": 14, "cognitive": 13, "commits": 1, "cov": 0.5, "crap": 38.5, "end": 28, "est_splits": 3, "est_uncovered_paths": 7, "flag": "measured", "function": "classify( score , attempts , late , bonus )", "handle": "classify", "nesting": 3, "nloc": 22, "occurrence": 1, "path": "calc/grade.py", "remedy": "decompose", "scope": "calc", "start": 7, "target": 6, "uncovered_lines": [9, 11, 15, 17, 19, 24, 25, 26, 27, 28]}, "run_id": 1, "schema": 1, "skipped_no_lane": 0, "stale": false}
1106
1156
  ```
1107
1157
 
1108
1158
  `remedy: "decompose"`, `est_splits: 3` (this needs roughly three pieces to fit under 6),
@@ -1130,7 +1180,7 @@ table of cases pushed into parametrized tests. Commit the fix, then:
1130
1180
 
1131
1181
  ```
1132
1182
  $ crapkit verify
1133
- verify OK @ 8d10c13303d vs baseline fae4db93108 (5 changed files)
1183
+ verify OK @ 8d10c13303d vs baseline fae4db93108 (5 changed files) ratchet: 1 dropped, 0 tightened -> git add crapkit-ratchet.tsv
1134
1184
 
1135
1185
  $ crapkit coverage
1136
1186
  run 3 @ 8d10c13303d: 5 functions scored: 5 measured, 0 over ceiling 6, CRAP load 19.0, grade A+
@@ -1174,6 +1224,12 @@ an istanbul `coverage-final.json` works; see [docs/lanes.md](https://github.com/
1174
1224
  [one directory down](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#running-from-a-subdirectory), and a
1175
1225
  [crapkit root below the repo top](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#a-crapkit-root-below-the-repo-top).
1176
1226
 
1227
+ `init` writes the vitest line under `[crapkit.scoped_tests]` commented out, because it
1228
+ cannot see which vitest config a file-scoped run needs. Until you uncomment that line or
1229
+ write your own, `crapkit doctor` WARNs `scope 'src' has a lane but no
1230
+ [crapkit.scoped_tests] template`. The WARN leaves doctor's exit at 0; it means `crapkit
1231
+ test-scoped` exits 3 on files under `src/`.
1232
+
1177
1233
  ### 2. Install a coverage provider
1178
1234
 
1179
1235
  **This is the step that stops most TypeScript users.** vitest ships no coverage provider
@@ -1268,15 +1324,16 @@ export function classify(row: Row): string {
1268
1324
  ```
1269
1325
  $ crapkit rescore src/grade.ts --gate
1270
1326
  rescore vs run 1 @ 8bfbe613fcd (coverage STALE, complexity fresh)
1271
- ccn cov crap remedy function
1272
- 5 0% 30.0 add-tests src/grade.ts:22 band ( score )
1273
- 5 0% 30.0 add-tests src/grade.ts:38 demote ( letter , row Row )
1274
- 4 0% 20.0 add-tests src/grade.ts:8 penalty ( attempts , late )
1275
- 4 45% 6.7 add-tests src/grade.ts:48 classify ( row Row )
1276
- 4 75% 4.2 ok src/grade.ts:59 average ( scores Array )
1327
+ ccn cov crap remedy function
1328
+ 5 0% 30.0 add-tests src/grade.ts:22 band ( score )
1329
+ 5 0% 30.0 add-tests src/grade.ts:38 demote ( letter , row Row )
1330
+ 4 0% 20.0 add-tests src/grade.ts:8 penalty ( attempts , late )
1331
+ 4 45% 6.7 add-tests src/grade.ts:48 classify ( row Row )
1332
+ 4 75% 4.2 ok src/grade.ts:59 average ( scores Array )
1333
+ gate: 4 changed function(s) judged, 0 over ceiling 6
1277
1334
  ```
1278
1335
 
1279
- Exit 0: every piece is at or under 6. The `crap` column is loud because its coverage half
1336
+ Exit 0 and the `gate:` line: every changed piece is at or under 6. The `crap` column is loud because its coverage half
1280
1337
  is still run 1's, from before three of those functions existed, and `add-tests` is the
1281
1338
  literal instruction for step 6.
1282
1339
 
@@ -1316,7 +1373,7 @@ verify FAILED @ 0296156ff21 vs baseline 0e646697946 (1 changed files)
1316
1373
 
1317
1374
  ```
1318
1375
  $ crapkit verify
1319
- verify OK @ 2af3433d979 vs baseline 8bfbe613fcd (3 changed files)
1376
+ verify OK @ 2af3433d979 vs baseline 8bfbe613fcd (3 changed files) ratchet: 1 dropped, 0 tightened -> git add crapkit-ratchet.tsv
1320
1377
 
1321
1378
  $ crapkit coverage
1322
1379
  run 3 @ 2af3433d979: 5 functions scored: 5 measured, 0 over ceiling 6, CRAP load 22.0, grade A+
@@ -1340,11 +1397,11 @@ with no debt.
1340
1397
 
1341
1398
  | Page | Covers |
1342
1399
  |---|---|
1343
- | [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. |
1400
+ | [The handbook](https://www.jfgagne.com/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://www.jfgagne.com/crapkit/handbook.html), self-contained, so it opens straight from a clone. |
1344
1401
  | [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. |
1345
1402
  | [docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md) | Every `crapkit.toml` key: type, default, and what it does. |
1346
1403
  | [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. |
1404
+ | [docs/resources.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/resources.md) | Worker budgets, command cleanup, log rotation and safe cleanup. |
1348
1405
  | [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
1406
  | [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
1407
  | [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. |
@@ -1371,6 +1428,12 @@ line arms the complexity gate. See
1371
1428
  for development and [the verified implementation report](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/architecture/2026-09-07-implementation/REPORT.md)
1372
1429
  for complete Windows source and Linux wheel results, focused benchmarks and their limits.
1373
1430
 
1431
+ ## Maintainer and project background
1432
+
1433
+ crapkit is created and maintained by [Jean-François Gagné](https://www.jfgagne.com/).
1434
+ Read the [project background](https://www.jfgagne.com/projects/crapkit/) for the
1435
+ problem it addresses and how it fits into his work on software and AI.
1436
+
1374
1437
  ## License
1375
1438
 
1376
1439
  MIT. See [LICENSE](https://github.com/JeanFrancoisGagne/crapkit/blob/main/LICENSE).