crapkit 0.4.9__tar.gz → 0.4.11__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 (72) hide show
  1. {crapkit-0.4.9 → crapkit-0.4.11}/PKG-INFO +70 -48
  2. crapkit-0.4.9/src/crapkit.egg-info/PKG-INFO → crapkit-0.4.11/README.md +1169 -1186
  3. {crapkit-0.4.9 → crapkit-0.4.11}/pyproject.toml +1 -1
  4. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/__init__.py +1 -1
  5. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/admin.py +4 -1
  6. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/scoring.py +7 -1
  7. crapkit-0.4.9/README.md → crapkit-0.4.11/src/crapkit.egg-info/PKG-INFO +1208 -1147
  8. {crapkit-0.4.9 → crapkit-0.4.11}/LICENSE +0 -0
  9. {crapkit-0.4.9 → crapkit-0.4.11}/setup.cfg +0 -0
  10. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/__main__.py +0 -0
  11. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/_pygdefer.py +0 -0
  12. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/analyze.py +0 -0
  13. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cache.py +0 -0
  14. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/churn.py +0 -0
  15. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/churn_cache.py +0 -0
  16. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/churn_log.py +0 -0
  17. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/__init__.py +0 -0
  18. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/_shared.py +0 -0
  19. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/analyses.py +0 -0
  20. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/claude_hook.py +0 -0
  21. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/parser.py +0 -0
  22. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/queue.py +0 -0
  23. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/ratchet_cmds.py +0 -0
  24. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/reports.py +0 -0
  25. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/cli/verifying.py +0 -0
  26. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/config.py +0 -0
  27. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/coupling.py +0 -0
  28. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/coupling_cache.py +0 -0
  29. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/coverage_istanbul.py +0 -0
  30. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/coverage_py.py +0 -0
  31. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/covstream.py +0 -0
  32. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/diffparse.py +0 -0
  33. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/digest.py +0 -0
  34. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/discover.py +0 -0
  35. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/doctor.py +0 -0
  36. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/dup.py +0 -0
  37. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/errors.py +0 -0
  38. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/gitio.py +0 -0
  39. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/hook.py +0 -0
  40. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/junitparse.py +0 -0
  41. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/keys.py +0 -0
  42. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/lanes.py +0 -0
  43. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/lizardcognitive.py +0 -0
  44. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/lizardpowershell.py +0 -0
  45. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/lizardrust.py +0 -0
  46. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/lizardshell.py +0 -0
  47. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/mcp_server.py +0 -0
  48. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/merge.py +0 -0
  49. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/mutate.py +0 -0
  50. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/mutate_pool.py +0 -0
  51. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/override.py +0 -0
  52. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/packet.py +0 -0
  53. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/procs.py +0 -0
  54. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/ratchet.py +0 -0
  55. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/ratchet_report.py +0 -0
  56. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/report.py +0 -0
  57. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/sarif.py +0 -0
  58. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/sarifio.py +0 -0
  59. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/scaffold.py +0 -0
  60. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/score.py +0 -0
  61. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/snapshot.py +0 -0
  62. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/store.py +0 -0
  63. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/uncovered.py +0 -0
  64. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/universe.py +0 -0
  65. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/verify.py +0 -0
  66. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/watch.py +0 -0
  67. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit/worklist.py +0 -0
  68. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit.egg-info/SOURCES.txt +0 -0
  69. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit.egg-info/dependency_links.txt +0 -0
  70. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit.egg-info/entry_points.txt +0 -0
  71. {crapkit-0.4.9 → crapkit-0.4.11}/src/crapkit.egg-info/requires.txt +0 -0
  72. {crapkit-0.4.9 → crapkit-0.4.11}/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.9
3
+ Version: 0.4.11
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
 
@@ -145,9 +156,14 @@ mirror installs fine. Requires Python 3.11 or newer. The `pip install -e ".[dev]
145
156
  [Development](#development) is a different thing: it adds the test extra, for people
146
157
  changing crapkit.
147
158
 
159
+ Scoring runs your own test command on your own machine and reads the artifact it writes.
160
+ There is no network call anywhere in crapkit, so no source, no score and no telemetry
161
+ leaves the box
162
+ ([SECURITY.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/SECURITY.md)).
163
+
148
164
  ```
149
165
  $ crapkit --version
150
- crapkit 0.4.9
166
+ crapkit 0.4.11
151
167
  ```
152
168
 
153
169
  `python -m crapkit` works identically to the console script and is what to use from a
@@ -222,9 +238,11 @@ claude plugin install crapkit@crapkit
222
238
  ```
223
239
 
224
240
  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.
241
+ ships three skills, the read-only MCP server, and one advisory PostToolUse hook that names
242
+ any function an edit pushed over its ceiling. Claude reaches two of the skills by itself,
243
+ `crapkit` and `crapkit-recover`; the third you type, as `/crapkit:crapkit-onboard`, because
244
+ wiring a repo up happens once and its description has no business in every turn's window.
245
+ It adds no files to your repo, and it needs the crapkit CLI on PATH.
228
246
 
229
247
  A repo with no `crapkit.toml` costs a silent sub-50 ms no-op per edit. Other agent
230
248
  runtimes have no marketplace: copy `plugin/skills/*` into their skills directory instead.
@@ -375,7 +393,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
375
393
  repos:
376
394
  - repo: https://github.com/JeanFrancoisGagne/crapkit
377
395
  # crapkit's release step rewrites this line to the tag it just cut
378
- rev: v0.4.9
396
+ rev: v0.4.11
379
397
  hooks:
380
398
  - id: crapkit-gate
381
399
  ```
@@ -426,7 +444,7 @@ crapkit: baseline commit a74260f321f is not an ancestor of HEAD (rebase or amend
426
444
 
427
445
  That is exit 4 on a `git clone --depth 1` of a repo whose baseline verifies at full depth.
428
446
  Set `fetch-depth: 0` on the checkout step, which is what crapkit's own
429
- [.github/workflows/ci.yml](.github/workflows/ci.yml) does.
447
+ [.github/workflows/ci.yml](https://github.com/JeanFrancoisGagne/crapkit/blob/main/.github/workflows/ci.yml) does.
430
448
 
431
449
  The whole PR job, on GitHub Actions:
432
450
 
@@ -469,11 +487,11 @@ violation and 0 otherwise. The stderr block above is the same either way.
469
487
  three-record audit: an alert line through `alert_command`, a ratchet entry staged into the
470
488
  commit, and a row in the override log. All three land or nothing does, and an unset
471
489
  `alert_command` refuses the override outright. See
472
- [docs/ratchet.md](docs/ratchet.md#overrides-and-the-audit-trail).
490
+ [docs/ratchet.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md#overrides-and-the-audit-trail).
473
491
 
474
492
  ## The GitHub Action
475
493
 
476
- [action.yml](action.yml) at this repository's root is a composite action, so a reviewer
494
+ [action.yml](https://github.com/JeanFrancoisGagne/crapkit/blob/main/action.yml) at this repository's root is a composite action, so a reviewer
477
495
  sees crapkit's numbers on the pull request without installing anything. Four lines add it
478
496
  to a workflow, and every input has a default:
479
497
 
@@ -481,7 +499,7 @@ to a workflow, and every input has a default:
481
499
  - uses: actions/checkout@v4
482
500
  with:
483
501
  fetch-depth: 0
484
- - uses: JeanFrancoisGagne/crapkit@v0.4.8
502
+ - uses: JeanFrancoisGagne/crapkit@v0.4.11
485
503
  ```
486
504
 
487
505
  The whole job those four lines sit in:
@@ -497,8 +515,11 @@ jobs:
497
515
  - uses: actions/checkout@v4
498
516
  with:
499
517
  fetch-depth: 0 # the diff, and verify's baseline commit
518
+ - uses: actions/setup-python@v5
519
+ with:
520
+ python-version: "3.12" # the interpreter the install below lands in
500
521
  - run: pip install -e ".[dev]" # whatever your lanes need to run
501
- - uses: JeanFrancoisGagne/crapkit@v0.4.8
522
+ - uses: JeanFrancoisGagne/crapkit@v0.4.11
502
523
  with:
503
524
  gate: "false"
504
525
  ```
@@ -513,8 +534,9 @@ baseline's commit. With a shallow clone the file list comes back empty and the c
513
534
  ranks the whole repository instead of the diff.
514
535
 
515
536
  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.
537
+ ref you pinned in `uses:`. So a pin left at last month's tag scores your tree with last
538
+ month's crapkit rather than with whatever released since, and pinning a tag is the whole
539
+ version policy; the snippets above name the current release.
518
540
 
519
541
  ### What the comment looks like
520
542
 
@@ -564,7 +586,7 @@ behind the checkout to measure from.
564
586
  | `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
587
  | `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
588
  | `top` | `"5"` | worklist rows rendered in the table |
567
- | `python-version` | `"3.12"` | the interpreter `actions/setup-python` installs crapkit into |
589
+ | `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
590
 
569
591
  `gate: "false"` is the default on purpose. A team adopts the action before it has decided
570
592
  which findings should stop a merge, and a check that fails on day one gets turned off on
@@ -641,17 +663,17 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
641
663
  | Command | What it does |
642
664
  |---|---|
643
665
  | `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). |
666
+ | `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](https://github.com/JeanFrancoisGagne/crapkit/tree/main/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](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md#doctor---json). |
645
667
  | `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). |
668
+ | `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
669
  | `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
670
  | `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
671
  | `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
672
  | `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
673
  | `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. |
674
+ | `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
675
  | `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). |
676
+ | `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
677
  | `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
678
  | `overrides [--json]` | The override audit trail: who granted what, when, and why. |
657
679
  | `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). |
@@ -664,7 +686,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
664
686
  | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
665
687
  | `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
688
  | `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). |
689
+ | `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
690
 
669
691
  ## Reading the output
670
692
 
@@ -826,7 +848,7 @@ pip install pytest-cov
826
848
  If your suite drives its own CLI through `subprocess.run`, add `[tool.coverage.run]
827
849
  patch = ["subprocess"]` to `pyproject.toml` and keep `coverage>=7.10.6`: pytest-cov 7.0.0
828
850
  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.
851
+ warns. [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md) has the whole rule.
830
852
 
831
853
  ### 1. Scaffold the config
832
854
 
@@ -844,11 +866,11 @@ coverage lane from what the repo already has: a pytest marker file (`pyproject.t
844
866
  `poetry.lock`, `pdm.lock` or `Pipfile.lock` makes the lane `uv run python -m pytest …` (and
845
867
  the matching `run` for the rest), because a bare `python` binds to whichever venv the shell
846
868
  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
869
+ [The interpreter a lane binds to](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#the-interpreter-a-lane-binds-to). Whatever
848
870
  it detects, it also leaves commented templates for the runners it did not find, and those
849
871
  carry the same launcher, so uncommenting one cannot hand the bare `python` back. Every lane
850
872
  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).
873
+ [Where artifacts live](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#where-artifacts-live).
852
874
 
853
875
  ```toml
854
876
  [crapkit]
@@ -886,16 +908,16 @@ calc = "python -m pytest {files} -q -p no:cacheprovider"
886
908
  ```
887
909
 
888
910
  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)
911
+ whose scope declares no template, and [AGENTS.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/AGENTS.md#4-run-the-owning-scopes-tests)
890
912
  makes it step 4 of the burn-down loop. Every key is in
891
- [docs/configuration.md](docs/configuration.md).
913
+ [docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md).
892
914
 
893
915
  ### 2. Check the config against the repo
894
916
 
895
917
  ```
896
918
  $ crapkit doctor
897
919
  ok config keys all recognized
898
- ok scope 'calc': 1 files
920
+ ok scope 'calc': 1 file
899
921
  ok every tracked source file belongs to a scope
900
922
  ok 1 lane(s) declared
901
923
  ok lizard 1.24.0
@@ -934,7 +956,7 @@ $ crapkit next-item
934
956
  `remedy: "decompose"`, `est_splits: 3` (this needs roughly three pieces to fit under 6),
935
957
  and `uncovered_lines` naming the ten lines no test walks. `handle` is the name form to
936
958
  pass back, and `stale: false` says the run still describes HEAD. Every field is in
937
- [docs/agent-json.md](docs/agent-json.md).
959
+ [docs/agent-json.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md).
938
960
 
939
961
  ### 5. Seed the ratchet
940
962
 
@@ -967,11 +989,11 @@ against the trusted baseline: every function the diff touched sits at or under i
967
989
  ceiling, no marked function got worse, and no test that passed in the baseline fails now.
968
990
  Exit 0 advances the baseline and tightens `crapkit-ratchet.tsv` in place, so the repaid
969
991
  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).
992
+ mark lifecycle is in [docs/ratchet.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md).
971
993
 
972
994
  `crapkit next-item` now comes back `empty: true` with a `reasons` object saying which
973
995
  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
996
+ [AGENTS.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/AGENTS.md#the-termination-rule) states the whole rule and reads the rest of
975
997
  `reasons`.
976
998
 
977
999
  ## Quickstart: TypeScript
@@ -994,10 +1016,10 @@ It reads vitest's `json` reporter from `.crapkit/cov/js/coverage-final.json`; th
994
1016
  lane's `results_artifact`, which the crashed-worker and no-new-failures checks read; both
995
1017
  reporters are named because `--reporter=junit` alone would replace the console output you
996
1018
  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).
1019
+ an istanbul `coverage-final.json` works; see [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md) for the
1020
+ [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
1021
+ [one directory down](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#running-from-a-subdirectory), and a
1022
+ [crapkit root below the repo top](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#a-crapkit-root-below-the-repo-top).
1001
1023
 
1002
1024
  ### 2. Install a coverage provider
1003
1025
 
@@ -1012,7 +1034,7 @@ crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/cov
1012
1034
  MISSING DEPENDENCY Cannot find dependency '@vitest/coverage-v8'
1013
1035
 
1014
1036
  (exit 1)
1015
- crapkit: every lane failed: ...
1037
+ crapkit: every lane failed (1 of 1); the errors are above
1016
1038
  ```
1017
1039
 
1018
1040
  That failure **writes no run**. Every lane failed, so `coverage` exits before it opens a
@@ -1038,7 +1060,7 @@ explicitly, keep `"json"` in the list.
1038
1060
  One more vitest default worth flipping now: it writes **no coverage report at all when the
1039
1061
  run fails**, so a single red test becomes a missing artifact and a lane failure. Set
1040
1062
  `coverage.reportOnFailure = true`. The full config block is in
1041
- [docs/lanes.md](docs/lanes.md#reportonfailure).
1063
+ [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#reportonfailure).
1042
1064
 
1043
1065
  ### 3. Score the repo
1044
1066
 
@@ -1150,22 +1172,22 @@ dropped it once `classify` scored under the ceiling, rewriting the tracked
1150
1172
 
1151
1173
  A verify may also print `warning: N changed line(s) have no coverage` above its verdict;
1152
1174
  that block is advisory unless `diff_uncovered_max` is set
1153
- ([docs/configuration.md](docs/configuration.md)).
1175
+ ([docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md)).
1154
1176
 
1155
1177
  ## Documentation
1156
1178
 
1157
1179
  | Page | Covers |
1158
1180
  |---|---|
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. |
1181
+ | [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. |
1182
+ | [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. |
1183
+ | [docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md) | Every `crapkit.toml` key: type, default, and what it does. |
1184
+ | [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. |
1185
+ | [docs/ratchet.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/ratchet.md) | Seeding, pruning, the git merge driver, metric stamps, debt policy, overrides. |
1186
+ | [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. |
1187
+ | [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. |
1188
+ | [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
1189
 
1168
- [crapkit.schema.json](crapkit.schema.json) is the authority on the config file shape.
1190
+ [crapkit.schema.json](https://github.com/JeanFrancoisGagne/crapkit/blob/main/crapkit.schema.json) is the authority on the config file shape.
1169
1191
 
1170
1192
  ## Development
1171
1193
 
@@ -1179,8 +1201,8 @@ python -m pytest -q
1179
1201
  `pytest-xdist` is not optional: `tests/fixtures/mini_repo` declares a lane that shells out
1180
1202
  to `pytest ... -n 2`, and without it that subprocess dies on an unrecognized `-n`. The
1181
1203
  `git config` line arms the complexity gate on your own commits. Same steps, with what each
1182
- one buys, in [CONTRIBUTING.md](CONTRIBUTING.md).
1204
+ one buys, in [CONTRIBUTING.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/CONTRIBUTING.md).
1183
1205
 
1184
1206
  ## License
1185
1207
 
1186
- MIT. See [LICENSE](LICENSE).
1208
+ MIT. See [LICENSE](https://github.com/JeanFrancoisGagne/crapkit/blob/main/LICENSE).