crapkit 0.4.14__tar.gz → 0.5.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 (76) hide show
  1. {crapkit-0.4.14/src/crapkit.egg-info → crapkit-0.5.0}/PKG-INFO +175 -71
  2. {crapkit-0.4.14 → crapkit-0.5.0}/README.md +174 -70
  3. {crapkit-0.4.14 → crapkit-0.5.0}/pyproject.toml +1 -1
  4. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/__init__.py +1 -1
  5. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/analyze.py +32 -2
  6. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/churn.py +13 -2
  7. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/__init__.py +3 -2
  8. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/_shared.py +82 -5
  9. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/admin.py +283 -28
  10. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/analyses.py +54 -17
  11. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/claude_hook.py +20 -17
  12. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/parser.py +63 -35
  13. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/queue.py +79 -31
  14. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/ratchet_cmds.py +7 -5
  15. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/reports.py +14 -17
  16. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/scoring.py +212 -61
  17. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cli/verifying.py +159 -30
  18. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/config.py +167 -14
  19. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/digest.py +41 -20
  20. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/doctor.py +33 -0
  21. crapkit-0.5.0/src/crapkit/errors.py +30 -0
  22. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/gitio.py +16 -0
  23. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/hook.py +4 -5
  24. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/lanes.py +123 -25
  25. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/lizardcognitive.py +64 -16
  26. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/mcp_server.py +213 -48
  27. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/mutate.py +26 -0
  28. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/override.py +5 -2
  29. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/ratchet.py +22 -0
  30. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/report.py +21 -9
  31. crapkit-0.5.0/src/crapkit/repotext.py +48 -0
  32. crapkit-0.5.0/src/crapkit/rootfind.py +35 -0
  33. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/scaffold.py +184 -58
  34. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/store.py +49 -15
  35. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/universe.py +40 -1
  36. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/worklist.py +81 -22
  37. {crapkit-0.4.14 → crapkit-0.5.0/src/crapkit.egg-info}/PKG-INFO +175 -71
  38. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit.egg-info/SOURCES.txt +2 -0
  39. crapkit-0.4.14/src/crapkit/errors.py +0 -18
  40. {crapkit-0.4.14 → crapkit-0.5.0}/LICENSE +0 -0
  41. {crapkit-0.4.14 → crapkit-0.5.0}/setup.cfg +0 -0
  42. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/__main__.py +0 -0
  43. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/_pygdefer.py +0 -0
  44. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/cache.py +0 -0
  45. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/churn_cache.py +0 -0
  46. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/churn_log.py +0 -0
  47. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/coupling.py +0 -0
  48. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/coupling_cache.py +0 -0
  49. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/coverage_istanbul.py +0 -0
  50. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/coverage_py.py +0 -0
  51. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/covstream.py +0 -0
  52. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/diffparse.py +0 -0
  53. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/discover.py +0 -0
  54. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/dup.py +0 -0
  55. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/invocation.py +0 -0
  56. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/junitparse.py +0 -0
  57. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/keys.py +0 -0
  58. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/lizardpowershell.py +0 -0
  59. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/lizardrust.py +0 -0
  60. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/lizardshell.py +0 -0
  61. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/merge.py +0 -0
  62. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/mutate_pool.py +0 -0
  63. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/packet.py +0 -0
  64. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/procs.py +0 -0
  65. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/ratchet_report.py +0 -0
  66. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/sarif.py +0 -0
  67. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/sarifio.py +0 -0
  68. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/score.py +0 -0
  69. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/snapshot.py +0 -0
  70. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/uncovered.py +0 -0
  71. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/verify.py +0 -0
  72. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit/watch.py +0 -0
  73. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit.egg-info/dependency_links.txt +0 -0
  74. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit.egg-info/entry_points.txt +0 -0
  75. {crapkit-0.4.14 → crapkit-0.5.0}/src/crapkit.egg-info/requires.txt +0 -0
  76. {crapkit-0.4.14 → crapkit-0.5.0}/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.14
3
+ Version: 0.5.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
@@ -39,7 +39,7 @@ Dynamic: license-file
39
39
 
40
40
  # crapkit
41
41
 
42
- <!-- mcp-name: io.github.jeanfrancoisgagne/crapkit -->
42
+ <!-- mcp-name: io.github.JeanFrancoisGagne/crapkit -->
43
43
 
44
44
  [![ci](https://github.com/JeanFrancoisGagne/crapkit/actions/workflows/ci.yml/badge.svg)](https://github.com/JeanFrancoisGagne/crapkit/actions/workflows/ci.yml)
45
45
  [![PyPI](https://img.shields.io/pypi/v/crapkit)](https://pypi.org/project/crapkit/)
@@ -98,15 +98,18 @@ crapkit ratchet seed && git add crapkit.toml crapkit-ratchet.tsv .gitignore
98
98
 
99
99
  ```
100
100
  $ crapkit coverage
101
- run 1 @ fae4db93108: 2 functions scored — 2 measured / 0 untested / 0 no-lane / 0 cc-only, 1 over target 6, CRAP load 41.0, grade F
101
+ run 1 @ fae4db93108: 2 functions scored: 2 measured, 1 over ceiling 6, CRAP load 41.0, grade F
102
+ -> next: crapkit worklist
102
103
 
103
104
  $ crapkit worklist
104
- worklist @ fae4db93108 (run 1, floor ccn>=5, churn 12mo) — 1 active, 0 dormant
105
- risk 0.0 ccn 14 ( 14 std) 1c/1a w 0.00 calc/grade.py:7 classify( score , attempts , late , bonus )
105
+ worklist @ fae4db93108 (run 1, floor ccn>=5, churn 12mo) - 1 of 1 active (worklist_top 50), 0 dormant
106
+ risk 14.0 ccn 14 crap 38.5 cov 50% 1c/1a calc/grade.py:7 classify( score , attempts , late , bonus )
106
107
  ```
107
108
 
108
- `risk 0.0` is what a one-commit repo scores, because churn needs a spread of commits to
109
- rank and ccn order stands in until then ([Risk](#risk-what-ranks-the-worklist)).
109
+ `risk 14.0` is ccn times a churn weight of one: a one-commit repo has no spread of commits
110
+ to weight, so each commit counts once and the ranking is complexity order until the
111
+ history grows ([Risk](#risk-what-ranks-the-worklist)). `crap 38.5` and `cov 50%` are the
112
+ score and the coverage behind it.
110
113
 
111
114
  `ratchet seed` signs today's debt at today's score. From then on marks only ever fall, so
112
115
  the repo can get better and never worse while you burn it down.
@@ -168,12 +171,13 @@ leaves the box
168
171
 
169
172
  ```
170
173
  $ crapkit --version
171
- crapkit 0.4.14
174
+ crapkit 0.5.0
172
175
  ```
173
176
 
174
177
  `python -m crapkit` works identically to the console script and is what to use from a
175
- source checkout. Every subcommand accepts `--repo PATH` (default: the current directory),
176
- so you never have to `cd` into the repo you are scoring; [Subcommands](#subcommands) shows
178
+ source checkout. Every subcommand accepts `--repo PATH` (default: the nearest `crapkit.toml`
179
+ at or above the current directory, so a monorepo workspace finds the root's), and with it
180
+ you never have to `cd` into the repo you are scoring; [Subcommands](#subcommands) shows
177
181
  where the flag goes.
178
182
 
179
183
  ## Upgrading from 0.4.4
@@ -360,6 +364,19 @@ EOF
360
364
  chmod +x .git/hooks/pre-commit
361
365
  ```
362
366
 
367
+ The same file from PowerShell. `Out-File` and `>` write a byte-order mark (UTF-16 on
368
+ 5.1) in front of the shebang, and git then answers every commit with `cannot spawn
369
+ .git/hooks/pre-commit` and lets it through; `Set-Content -Encoding ascii` does not. Git
370
+ runs the hook with its own `sh`, so the interpreter is spelled with forward slashes and
371
+ quoted, and no `chmod` is needed on Windows:
372
+
373
+ ```powershell
374
+ $python = (Get-Command python).Source -replace '\\', '/'
375
+ Set-Content -Path .git/hooks/pre-commit -Encoding ascii -NoNewline -Value "#!/bin/sh`nexec '$python' -m crapkit hook-precommit`n"
376
+ ```
377
+
378
+ `crapkit doctor` warns when the hook file git would spawn starts with a byte-order mark.
379
+
363
380
  ### Route 2: a committed hooks directory
364
381
 
365
382
  The whole route, from a repo that has no `githooks/` yet:
@@ -398,7 +415,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
398
415
  repos:
399
416
  - repo: https://github.com/JeanFrancoisGagne/crapkit
400
417
  # crapkit's release step rewrites this line to the tag it just cut
401
- rev: v0.4.14
418
+ rev: v0.5.0
402
419
  hooks:
403
420
  - id: crapkit-gate
404
421
  ```
@@ -444,10 +461,12 @@ git, and a shallow clone does not have that commit:
444
461
 
445
462
  ```
446
463
  $ crapkit verify --baseline-tsv crapkit-baseline.tsv
447
- crapkit: baseline commit a74260f321f is not an ancestor of HEAD (rebase or amend rewrote history) — run `crapkit coverage` for a fresh baseline
464
+ crapkit: baseline commit a74260f321f is not an ancestor of HEAD in this shallow clone, which does not hold it; set fetch-depth: 0 on the checkout or run git fetch --unshallow
448
465
  ```
449
466
 
450
467
  That is exit 4 on a `git clone --depth 1` of a repo whose baseline verifies at full depth.
468
+ On a full clone the same exit blames what it used to, a rebase or an amend that rewrote
469
+ history, and asks for a fresh baseline instead.
451
470
  Set `fetch-depth: 0` on the checkout step, which is what crapkit's own
452
471
  [.github/workflows/ci.yml](https://github.com/JeanFrancoisGagne/crapkit/blob/main/.github/workflows/ci.yml) does.
453
472
 
@@ -504,7 +523,7 @@ to a workflow, and every input has a default:
504
523
  - uses: actions/checkout@v4
505
524
  with:
506
525
  fetch-depth: 0
507
- - uses: JeanFrancoisGagne/crapkit@v0.4.14
526
+ - uses: JeanFrancoisGagne/crapkit@v0.5.0
508
527
  ```
509
528
 
510
529
  The whole job those four lines sit in:
@@ -524,7 +543,7 @@ jobs:
524
543
  with:
525
544
  python-version: "3.12" # the interpreter the install below lands in
526
545
  - run: pip install -e ".[dev]" # whatever your lanes need to run
527
- - uses: JeanFrancoisGagne/crapkit@v0.4.14
546
+ - uses: JeanFrancoisGagne/crapkit@v0.5.0
528
547
  with:
529
548
  gate: "false"
530
549
  ```
@@ -550,33 +569,55 @@ One comment per pull request, edited in place on every push. A hidden
550
569
  carries one comment and not fifteen. On a `push` event there is no pull request to carry
551
570
  it, and the same text goes to the job log instead.
552
571
 
553
- Rendered against this repository's own store, with a base commit fifteen back standing in
554
- for a pull request:
572
+ Rendered from three saved payloads: a pull request that adds an untested `route()` (ccn 8)
573
+ beside a ratchet-marked `legacy_router()`, in a repository whose `diff_uncovered_max` is 3.
574
+ The payloads are under `tests/fixtures/action_comment/`, and the unit suite pins this block
575
+ to their render:
555
576
 
556
577
  ```markdown
557
578
  <!-- crapkit-action -->
558
579
 
559
580
  ## crapkit
560
581
 
561
- 1333 functions in 61 files, 0 over target, CRAP load 3713.89, grade A+.
582
+ 4 functions in 2 files, 2 over ceiling 6, CRAP load 149.59, grade F.
562
583
 
563
- **verify passed.** Run 2 against baseline 1, 7 changed files.
584
+ **verify failed, exit 6: complexity gate.**
564
585
 
565
- ### Worklist: 39 changed files
586
+ - gate: `app/calc.py:34` `route( a , b , c , d )` ccn 8, cov 0%, crap 72.0 -> decompose
587
+ - uncovered lines in `app/calc.py`: 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45
588
+
589
+ Run 3 against baseline 1, 1 changed file: 1 gate violation, 0 ratchet regressions, 0 new test failures, 11 uncovered changed lines.
590
+
591
+ ### Worklist: 1 changed file
566
592
 
567
593
  | File | Function | ccn | risk | remedy |
568
594
  |---|---|---:|---:|---|
569
- | `src/crapkit/cli/admin.py:1026` | `_recorded_roots( recorded )` | 6 | 10.9908 | ok |
570
- | `src/crapkit/lanes.py:646` | `build_retest_command( template : str , tests : set [ str ] )` | 6 | 10.8234 | ok |
571
- | `src/crapkit/cli/admin.py:80` | `_next_step( scopes : dict , lanes : tuple )` | 5 | 9.159 | ok |
572
- | `src/crapkit/cli/admin.py:239` | `_pytest_cov_probe( command : str )` | 5 | 9.159 | ok |
573
- | `src/crapkit/cli/admin.py:505` | `_segment_problems( name : str , cwd : Path , tokens : list [ str ] )` | 5 | 9.159 | ok |
595
+ | `app/calc.py:34` | `route( a , b , c , d )` | 8 | 4.0 | decompose |
596
+ | `app/calc.py:19` | `legacy_router( a , b , c , d , e )` | 8 | 4.0 | decompose (accepted debt) |
574
597
  ```
575
598
 
599
+ The first line is the run `crapkit coverage` wrote: functions and files, how many sit over
600
+ the ceiling (`over ceiling 6`, or `over their ceilings (6; reports 12, util 4)` when scopes
601
+ set their own), CRAP load and grade, with a failed lane's first error line appended as
602
+ `; lane 'js' failed: ...`. When `coverage --json` died before a summary, the line quotes the
603
+ error object it printed instead: `` `crapkit coverage` exited 5: lane 'py' cannot import
604
+ pytest-cov; pip install pytest-cov. ``
605
+
606
+ The verdict opens with the exit code and the rule it stands for (`complexity gate`,
607
+ `ratchet regressions`, `new test failures`, `diff-coverage ceiling N`), then one bullet per
608
+ finding: each gate violation with its function, ccn, coverage, CRAP and remedy; each
609
+ ratchet regression as recorded -> fresh; each new test failure by id; and the first twenty
610
+ uncovered changed lines, one bullet per file, with a count of the rest. The counts line
611
+ closes it. A verify that passed is one line: `**verify passed.** Run 2 against baseline 1,
612
+ 7 changed files.`
613
+
576
614
  The rows are the ranked worklist for the files the pull request changed, worst first,
577
- `top` of them. `risk` is ccn times churn weight, the number `crapkit worklist` ranks on,
578
- and `remedy` is the run's own verdict for that function: `decompose`, `add-tests` or `ok`.
579
- A pull request that touches no ranked function gets the heading and no table.
615
+ `top` of them, with the rows a finding names listed first. `risk` is ccn times churn
616
+ weight, the number `crapkit worklist` ranks on, and `remedy` is the run's own verdict for
617
+ that function: `decompose`, `add-tests` or `ok`. `(accepted debt)` marks a function the
618
+ committed ratchet carries a mark for, so an untouched `legacy_router` does not read like
619
+ the pull request's own new function. A pull request that touches no ranked function gets
620
+ the heading and no table.
580
621
 
581
622
  The two file counts describe the same diff, counted twice. `39 changed files` is
582
623
  `git diff --name-only base.sha...HEAD`, the branch's own commits, and it is what the
@@ -588,8 +629,8 @@ behind the checkout to measure from.
588
629
 
589
630
  | Input | Default | What it does |
590
631
  |---|---|---|
591
- | `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 |
592
- | `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 |
632
+ | `gate` | `"false"` | `"true"` exits with `crapkit verify`'s own code, so a finding fails the check. On a pull request with `delta` on it also exits 1 when the base run was not made, because a verdict with no base run judged no changed function. Anything else exits 0 and the comment is the whole output |
633
+ | `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, and the verdict then judges no changed function |
593
634
  | `top` | `"5"` | worklist rows rendered in the table |
594
635
  | `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 |
595
636
 
@@ -614,13 +655,26 @@ The base run happens in a detached worktree under `RUNNER_TEMP`, and its store i
614
655
  over the checkout's so both runs sit in one place. The cost is **two lane runs on a pull
615
656
  request**: your suite runs once at the fork point and once on the checkout. Set `delta:
616
657
  "false"` to skip the base run, and the verdict falls back to the checkout against its own
617
- run, which reports the tree's own health and judges no changed function.
658
+ run, which reports the tree's own health and judges no changed function. The comment says
659
+ so in place of `verify passed`:
618
660
 
619
- Three things leave the base run unmade, and none of them fails the job: a shallow clone
620
- that does not hold the fork point, a fork point older than your `crapkit.toml`, and a
621
- lane that will not run against that tree. The step logs `crapkit base scoring exited N`
622
- and the verdict falls back the same way `delta: "false"` does. A `push` event never makes
623
- one, because there is no base commit and no pull request to comment on.
661
+ ```markdown
662
+ **verify judged no changed function:** the base run was not made (no base commit). Run 2 against baseline 2, 0 changed files.
663
+ ```
664
+
665
+ Three things leave the base run unmade: a shallow clone that does not hold the fork
666
+ point, a fork point older than your `crapkit.toml`, and a lane that will not run against
667
+ that tree. The step logs `crapkit base scoring exited N` and writes the reason to
668
+ `crapkit-base.reason` in the words the comment then quotes, `shallow clone does not hold
669
+ the fork point of <sha>; set fetch-depth: 0 on the checkout`, `no usable crapkit.toml at
670
+ the fork point <sha>: ...`, or `lane failed at the fork point <sha>: ...` with the lane's
671
+ first error line. The verdict falls back the same way `delta: "false"` does, and the
672
+ ratchet still runs, so exit 7 there is a finding. What differs is the job's status. With
673
+ `gate: "true"` on a pull request whose base run was attempted and not made, the exit step
674
+ exits 1 and prints the reason, because `actions/checkout`'s default depth-1 clone would
675
+ otherwise turn every pull request into a green check that judged nothing. A `push` event
676
+ and `delta: "false"` never attempt the base run, so they keep verify's own code; a `push`
677
+ has no base commit and no pull request to comment on.
624
678
 
625
679
  One requirement the base run adds: the lane has to measure the tree it runs in. A lane
626
680
  that reaches an installed copy of your package instead of the checkout will measure the
@@ -633,6 +687,21 @@ worktree is the quiet one. Point the lane at the tree, or set `delta: "false"`.
633
687
  ran the lanes moments earlier on that tree, and verify parses those artifacts rather than
634
688
  running the whole suite a second time for the same numbers.
635
689
 
690
+ Which is why `crapkit coverage` has to exit 0 for there to be a verdict. When it exits
691
+ anything else (a lane that failed, an artifact it refused), the action does not run
692
+ `verify`: on a runner that keeps its workspace between jobs (`clean: false`), a
693
+ `verify --reuse-artifacts` over a failed measurement read the artifact the dead lane had
694
+ left from an earlier run, passed over it, and made that run the trusted baseline. The
695
+ comment then carries coverage's failure in place of the verdict:
696
+
697
+ ```markdown
698
+ **no verdict: `crapkit coverage` exited 5 (lane 'py' failed: lane 'py' wrote no artifact on its last attempt; the .crapkit/cov/py.json on disk predates it); verify did not run.**
699
+ ```
700
+
701
+ The parenthesis is the first line of the lane failure the summary carries. When every
702
+ lane failed, `coverage` prints no summary at all and the lane errors are only in the job
703
+ log, and the line says so. With `gate: "true"` the job exits with coverage's code.
704
+
636
705
  The other gate that judges a delta is the portable baseline in [Route 4](#route-4-ci):
637
706
  commit `crapkit-baseline.tsv` on the default branch and run `crapkit verify --baseline-tsv
638
707
  crapkit-baseline.tsv` in a step of your own. It needs no second lane run, and it needs
@@ -645,14 +714,18 @@ on PATH fails that step. Both leave the rendered text in the job log.
645
714
 
646
715
  ## Subcommands
647
716
 
648
- Every subcommand takes `--repo PATH` (default `.`), and the flag goes **after** the
649
- subcommand. `claude-hook` is the one exception: it has no `--repo`, because it takes its
650
- root from the file named in the hook payload it reads.
717
+ Every subcommand takes `--repo PATH`, and the flag goes **after** the subcommand. Without
718
+ it the root is the nearest `crapkit.toml` at or above the current directory
719
+ ([ADR 0002](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/adr/0002-configuration-is-found-upward-nearest-wins.md)): from a
720
+ monorepo workspace `crapkit worklist` reads the root configuration that claims the
721
+ workspace, says `crapkit: using crapkit.toml at /repo` on stderr, and reads a relative path
722
+ argument from where you stand. `claude-hook` is the one exception: it has no `--repo`,
723
+ because it takes its root from the file named in the hook payload it reads.
651
724
 
652
725
  ```
653
726
  $ crapkit worklist --repo /path/to/repo --scope util --top 1
654
- worklist @ a7c5c85ac37 (run 1, floor ccn>=5, churn 12mo) — 1 active, 0 dormant
655
- risk 5.4 ccn 5 ( 5 std) 5c/1a w 1.08 util/stats.py:1 bucket( value , low , high )
727
+ worklist @ a7c5c85ac37 (run 1, floor ccn>=5, churn 12mo) - 1 of 3 active (--top 1), 0 dormant
728
+ risk 5.4 ccn 5 crap 30.0 cov 0% 5c/1a util/stats.py:1 bucket( value , low , high )
656
729
  ```
657
730
 
658
731
  Before it, argparse reads the path as the subcommand name and exits 2 without ever
@@ -672,8 +745,8 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
672
745
  | `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. |
673
746
  | `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). |
674
747
  | `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. |
675
- | `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. |
676
- | `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. |
748
+ | `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. Every row carries the function's `crap` and `cov` off the ranked run and its `ratchet_mark` when the committed marks file signs for it, and the header counts the active rows the cap hid: `50 of 3980 active (worklist_top 50)`. `--scope NAME` (repeatable) is exact, not a substring; a name no `[[scope]]` declares is a configuration error, exit 3, naming the declared scopes. `--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. |
749
+ | `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, and a name no `[[scope]]` declares is a configuration error, exit 3, naming the declared scopes. `--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. |
677
750
  | `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. |
678
751
  | `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). |
679
752
  | `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. |
@@ -683,16 +756,16 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
683
756
  | `overrides [--json]` | The override audit trail: who granted what, when, and why. |
684
757
  | `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). |
685
758
  | `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. |
686
- | `report [--out PATH]` | One self-contained HTML page written to `.crapkit/report.html` (or `--out PATH`, repo-relative, or an absolute path you name), with the path printed on stdout. It renders what `worklist --json` and `trend --json` already answer at their defaults: the ranked worklist capped at `worklist_top`, the per-scope grades off the newest run, the trend series, and a banner naming every stale lane. It measures nothing and opens no network connection. Per-function CRAP and coverage are absent because no repo-wide payload carries them; each row prints the `crapkit explain` call that does. It reads the same per-run rollups `trend` does, and writes them on the same terms. |
759
+ | `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. |
687
760
  | `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. |
688
761
  | `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. |
689
- | `mutate [--files F ...] [--max-mutants N] [--drop-pool] [--json]` | Diff-scoped mutation testing: flips comparisons, boundary shifts, boolean connectives and boolean literals on changed lines, runs `mutation_command` per mutant, lists survivors. `--files` replaces diff scope with the whole file. `--max-mutants` (default 100) caps the run and the cap warning goes to stderr only, so `mutants` in `--json` is the capped count. Shell and PowerShell files are refused by name on stderr rather than mutated: `<` and `>` are redirections there, not comparisons. With `mutation_workers > 1` the worker worktrees are kept at `.crapkit/mutate-pool/` and re-prepared per run (30.6 s to build four on a 31,459-file repo, 0.46 s to re-prepare them); `--drop-pool` removes them and exits. |
762
+ | `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. |
690
763
  | `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. |
691
764
  | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
692
765
  | `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. |
693
766
  | `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. |
694
767
  | `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. |
695
- | `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). |
768
+ | `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing ten 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). |
696
769
 
697
770
  ## Reading the output
698
771
 
@@ -731,16 +804,17 @@ outrank fifty from two years ago. The window anchors on the newest commit, never
731
804
  wall clock, so a fixed tree ranks identically forever.
732
805
 
733
806
  Age is not the input, position in the log is. A log whose commits all share one timestamp
734
- reads 0.0 everywhere, which is why a one-commit repo shows `risk 0.0` on every row and
735
- falls back to ccn order. Commits minutes apart already rank. This repo was eight commits
736
- old, all made the same day:
807
+ has no range to weight against, so each commit counts once: a one-commit repo weighs every
808
+ file 1.0, ranks by ccn, and promotes nothing under the floor, because a top 10% of equal
809
+ weights would be every file. Commits minutes apart already rank. This repo was eight
810
+ commits old, all made the same day:
737
811
 
738
812
  ```
739
813
  $ crapkit worklist --scope util
740
- worklist @ a7c5c85ac37 (run 1, floor ccn>=5, churn 12mo) — 3 active, 0 dormant
741
- risk 5.4 ccn 5 ( 5 std) 5c/1a w 1.08 util/stats.py:1 bucket( value , low , high )
742
- risk 4.5 ccn 9 ( 9 std) 1c/1a w 0.50 util/curve.py:1 curve( scores , mode , floor , ceiling , skip_none )
743
- risk 4.3 ccn 4 ( 4 std) 5c/1a w 1.08 util/stats.py:13 spread( values , cap ) ok
814
+ worklist @ a7c5c85ac37 (run 1, floor ccn>=5, churn 12mo) - 3 of 3 active (worklist_top 50), 0 dormant
815
+ risk 5.4 ccn 5 crap 30.0 cov 0% 5c/1a util/stats.py:1 bucket( value , low , high )
816
+ risk 4.5 ccn 9 crap 90.0 cov 0% 1c/1a util/curve.py:1 curve( scores , mode , floor , ceiling , skip_none )
817
+ risk 4.3 ccn 4 crap 4.2 cov 75% 5c/1a util/stats.py:13 spread( values , cap ) ok
744
818
  ```
745
819
 
746
820
  `bucket` at ccn 5 outranks `curve` at ccn 9 because five commits touched it and one
@@ -861,7 +935,7 @@ warns. [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/do
861
935
  ```
862
936
  $ crapkit init
863
937
  wrote crapkit.toml with 1 scope(s): calc
864
- detected 1 lane(s) from this repo's own files: py — next: run `crapkit coverage`
938
+ detected 1 lane(s) from this repo's own files: py - next: run `crapkit coverage`
865
939
  added to .gitignore: .crapkit/, .coverage, __pycache__/
866
940
  ```
867
941
 
@@ -888,7 +962,26 @@ paths = ["calc"]
888
962
  languages = ["python"]
889
963
 
890
964
  [exclude]
891
- globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/*.test.*", "**/*.spec.*", "**/test_*.py", "**/*_test.py", "**/conftest.py", "node_modules/**", "dist/**", "build/**", "vendor/**", "*.test.*", "*.spec.*", "test_*.py", "*_test.py", "conftest.py", "*_test.go", "**/*_test.go", "*.config.ts", "*.config.js", "*.config.mts", "**/*.config.ts", "**/*.config.js", "**/*.config.mts"]
965
+ # A leading **/ matches zero or more directories, so each glob below reaches the
966
+ # repo root and every nested copy. Test directories leave the corpus on their own.
967
+ globs = [
968
+ "**/node_modules/**",
969
+ "**/dist/**",
970
+ "**/build/**",
971
+ "**/vendor/**",
972
+ "**/generated/**",
973
+ "**/__generated__/**",
974
+ "**/*.generated.*",
975
+ "**/*.test.*",
976
+ "**/*.spec.*",
977
+ "**/test_*.py",
978
+ "**/*_test.py",
979
+ "**/conftest.py",
980
+ "**/*_test.go",
981
+ "**/*.config.ts",
982
+ "**/*.config.js",
983
+ "**/*.config.mts",
984
+ ]
892
985
 
893
986
  [[lane]]
894
987
  name = "py"
@@ -908,9 +1001,12 @@ scopes = ["calc"]
908
1001
  # scopes = ["<your-scope>"]
909
1002
 
910
1003
  # `crapkit test-scoped FILES` runs one command per scope, with {files}
911
- # replaced by that scope's files, each quoted.
1004
+ # replaced by that scope's files, each quoted; a template with no {files}
1005
+ # runs as written, which is how a scope whose tests live elsewhere runs them.
912
1006
  [crapkit.scoped_tests]
913
- calc = "python -m pytest {files} -q -p no:cacheprovider"
1007
+ # calc: no test file under calc/, so the whole suite runs, from tests/
1008
+ calc = "python -m pytest tests -q -p no:cacheprovider"
1009
+
914
1010
  ```
915
1011
 
916
1012
  The last block is the one an agent loop needs. `crapkit test-scoped` exits 3 for a file
@@ -926,6 +1022,7 @@ ok config keys all recognized
926
1022
  ok scope 'calc': 1 file
927
1023
  ok every tracked source file belongs to a scope
928
1024
  ok 1 lane(s) declared
1025
+ ok lane 'py': python -> /home/you/ledger/.venv/bin/python (pytest 8.3.3, pytest-cov 7.1.0)
929
1026
  ok lizard 1.24.0
930
1027
  doctor: no problems found
931
1028
  ```
@@ -937,16 +1034,20 @@ and exit 0.
937
1034
 
938
1035
  ```
939
1036
  $ crapkit coverage
940
- run 1 @ fae4db93108: 2 functions scored — 2 measured / 0 untested / 0 no-lane / 0 cc-only, 1 over target 6, CRAP load 41.0, grade F
1037
+ run 1 @ fae4db93108: 2 functions scored: 2 measured, 1 over ceiling 6, CRAP load 41.0, grade F
1038
+ -> next: crapkit worklist
941
1039
 
942
1040
  $ crapkit worklist
943
- worklist @ fae4db93108 (run 1, floor ccn>=5, churn 12mo) — 1 active, 0 dormant
944
- risk 0.0 ccn 14 ( 14 std) 1c/1a w 0.00 calc/grade.py:7 classify( score , attempts , late , bonus )
1041
+ worklist @ fae4db93108 (run 1, floor ccn>=5, churn 12mo) - 1 of 1 active (worklist_top 50), 0 dormant
1042
+ risk 14.0 ccn 14 crap 38.5 cov 50% 1c/1a calc/grade.py:7 classify( score , attempts , late , bonus )
945
1043
  ```
946
1044
 
947
- Columns: `risk`, `ccn` with the standard-only ccn in parentheses,
948
- `<commits>c/<authors>a` in the churn window with `w<weight>`, `path:line`, the function's
949
- long name, then a marker on rows the burn-down queue will not hand out (`ok`, `no-lane`).
1045
+ Columns: `risk`, `ccn`, the function's `crap` and `cov` off the ranked run (`-` on an
1046
+ inventory-only run), `<commits>c/<authors>a` in the churn window, `path:line`, the
1047
+ function's long name, then a marker on rows the burn-down queue will not hand out (`ok`,
1048
+ `no-lane`). The header counts the active rows against their total, so `50 of 3980 active
1049
+ (worklist_top 50)` says what the cap hid, and reads `(--top N)` when the flag set the cap.
1050
+ `--json` also carries `ccn_std`, `weight` and `ratchet_mark`.
950
1051
 
951
1052
  **`worklist` is the risk map, not a to-do list.** It ranks finished rows too, so it does
952
1053
  not empty when the burn-down does. `next-item` is the other view of that run: it drops the
@@ -956,7 +1057,7 @@ not empty when the burn-down does. `next-item` is the other view of that run: it
956
1057
 
957
1058
  ```
958
1059
  $ crapkit next-item
959
- {"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": 8, "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}
1060
+ {"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}
960
1061
  ```
961
1062
 
962
1063
  `remedy: "decompose"`, `est_splits: 3` (this needs roughly three pieces to fit under 6),
@@ -971,7 +1072,7 @@ function at its current score, and from then on nothing may get worse.
971
1072
 
972
1073
  ```
973
1074
  $ crapkit ratchet seed
974
- crapkit-ratchet.tsv: added 1, tightened 0 — 1 mark(s) vs run 1 (fae4db93108)
1075
+ crapkit-ratchet.tsv: added 1, tightened 0 - 1 mark(s) vs run 1 (fae4db93108)
975
1076
 
976
1077
  $ git add crapkit.toml crapkit-ratchet.tsv .gitignore && git commit -m "adopt crapkit"
977
1078
  ```
@@ -987,7 +1088,8 @@ $ crapkit verify
987
1088
  verify OK @ 8d10c13303d vs baseline fae4db93108 (5 changed files)
988
1089
 
989
1090
  $ crapkit coverage
990
- run 3 @ 8d10c13303d: 5 functions scored — 5 measured / 0 untested / 0 no-lane / 0 cc-only, 0 over target 6, CRAP load 19.0, grade A+
1091
+ run 3 @ 8d10c13303d: 5 functions scored: 5 measured, 0 over ceiling 6, CRAP load 19.0, grade A+
1092
+ -> next: crapkit worklist
991
1093
  ```
992
1094
 
993
1095
  CRAP load 41.0 to 19.0, grade F to A+. `verify` reruns the lanes and checks three things
@@ -1011,7 +1113,7 @@ A vitest repo with `src/grade.ts` and `test/grade.test.ts`.
1011
1113
  ```
1012
1114
  $ crapkit init
1013
1115
  wrote crapkit.toml with 1 scope(s): src
1014
- detected 1 lane(s) from this repo's own files: js — next: run `crapkit coverage`
1116
+ detected 1 lane(s) from this repo's own files: js - next: run `crapkit coverage`
1015
1117
  added to .gitignore: .crapkit/
1016
1118
  ```
1017
1119
 
@@ -1074,11 +1176,12 @@ job; either one is enough. The full block is in
1074
1176
 
1075
1177
  ```
1076
1178
  $ crapkit coverage
1077
- run 1 @ 8bfbe613fcd: 2 functions scored — 2 measured / 0 untested / 0 no-lane / 0 cc-only, 1 over target 6, CRAP load 56.68, grade F
1179
+ run 1 @ 8bfbe613fcd: 2 functions scored: 2 measured, 1 over ceiling 6, CRAP load 56.68, grade F
1180
+ -> next: crapkit worklist
1078
1181
 
1079
1182
  $ crapkit worklist
1080
- worklist @ 8bfbe613fcd (run 1, floor ccn>=5, churn 12mo) — 1 active, 0 dormant
1081
- risk 0.0 ccn 15 ( 15 std) 1c/1a w 0.00 src/grade.ts:8 classify ( row Row )
1183
+ worklist @ 8bfbe613fcd (run 1, floor ccn>=5, churn 12mo) - 1 of 1 active (worklist_top 50), 0 dormant
1184
+ risk 15.0 ccn 15 crap 52.4 cov 45% 1c/1a src/grade.ts:8 classify ( row Row )
1082
1185
  ```
1083
1186
 
1084
1187
  `classify` is ccn 15 against a ceiling of 6: one function holding the late-and-retry
@@ -1091,7 +1194,7 @@ can get worse while you burn this one down.
1091
1194
 
1092
1195
  ```
1093
1196
  $ crapkit ratchet seed
1094
- crapkit-ratchet.tsv: added 1, tightened 0 — 1 mark(s) vs run 1 (8bfbe613fcd)
1197
+ crapkit-ratchet.tsv: added 1, tightened 0 - 1 mark(s) vs run 1 (8bfbe613fcd)
1095
1198
 
1096
1199
  $ git add crapkit.toml crapkit-ratchet.tsv .gitignore && git commit -m "adopt crapkit"
1097
1200
  ```
@@ -1171,7 +1274,8 @@ $ crapkit verify
1171
1274
  verify OK @ 2af3433d979 vs baseline 8bfbe613fcd (3 changed files)
1172
1275
 
1173
1276
  $ crapkit coverage
1174
- run 3 @ 2af3433d979: 5 functions scored — 5 measured / 0 untested / 0 no-lane / 0 cc-only, 0 over target 6, CRAP load 22.0, grade A+
1277
+ run 3 @ 2af3433d979: 5 functions scored: 5 measured, 0 over ceiling 6, CRAP load 22.0, grade A+
1278
+ -> next: crapkit worklist
1175
1279
  ```
1176
1280
 
1177
1281
  CRAP load 56.68 to 22.0, grade F to A+, and the mark seeded in step 4 is gone: `verify`