crapkit 0.4.11__tar.gz → 0.4.13__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.11/src/crapkit.egg-info → crapkit-0.4.13}/PKG-INFO +23 -17
  2. {crapkit-0.4.11 → crapkit-0.4.13}/README.md +22 -16
  3. {crapkit-0.4.11 → crapkit-0.4.13}/pyproject.toml +1 -1
  4. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/__init__.py +1 -1
  5. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/__init__.py +26 -0
  6. crapkit-0.4.13/src/crapkit/cli/_shared.py +228 -0
  7. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/admin.py +269 -33
  8. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/analyses.py +7 -3
  9. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/parser.py +78 -4
  10. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/queue.py +11 -9
  11. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/ratchet_cmds.py +8 -6
  12. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/reports.py +12 -10
  13. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/scoring.py +7 -5
  14. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/verifying.py +55 -16
  15. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/config.py +56 -3
  16. crapkit-0.4.13/src/crapkit/coverage_py.py +164 -0
  17. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/covstream.py +34 -30
  18. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/doctor.py +16 -8
  19. crapkit-0.4.13/src/crapkit/invocation.py +45 -0
  20. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/lanes.py +265 -26
  21. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/mcp_server.py +100 -21
  22. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/mutate_pool.py +57 -3
  23. crapkit-0.4.13/src/crapkit/procs.py +137 -0
  24. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/ratchet.py +32 -5
  25. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/report.py +7 -2
  26. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/scaffold.py +288 -24
  27. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/uncovered.py +3 -1
  28. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/universe.py +10 -1
  29. {crapkit-0.4.11 → crapkit-0.4.13/src/crapkit.egg-info}/PKG-INFO +23 -17
  30. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit.egg-info/SOURCES.txt +1 -0
  31. crapkit-0.4.11/src/crapkit/cli/_shared.py +0 -130
  32. crapkit-0.4.11/src/crapkit/coverage_py.py +0 -87
  33. crapkit-0.4.11/src/crapkit/procs.py +0 -66
  34. {crapkit-0.4.11 → crapkit-0.4.13}/LICENSE +0 -0
  35. {crapkit-0.4.11 → crapkit-0.4.13}/setup.cfg +0 -0
  36. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/__main__.py +0 -0
  37. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/_pygdefer.py +0 -0
  38. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/analyze.py +0 -0
  39. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cache.py +0 -0
  40. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/churn.py +0 -0
  41. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/churn_cache.py +0 -0
  42. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/churn_log.py +0 -0
  43. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/cli/claude_hook.py +0 -0
  44. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/coupling.py +0 -0
  45. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/coupling_cache.py +0 -0
  46. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/coverage_istanbul.py +0 -0
  47. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/diffparse.py +0 -0
  48. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/digest.py +0 -0
  49. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/discover.py +0 -0
  50. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/dup.py +0 -0
  51. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/errors.py +0 -0
  52. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/gitio.py +0 -0
  53. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/hook.py +0 -0
  54. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/junitparse.py +0 -0
  55. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/keys.py +0 -0
  56. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/lizardcognitive.py +0 -0
  57. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/lizardpowershell.py +0 -0
  58. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/lizardrust.py +0 -0
  59. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/lizardshell.py +0 -0
  60. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/merge.py +0 -0
  61. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/mutate.py +0 -0
  62. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/override.py +0 -0
  63. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/packet.py +0 -0
  64. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/ratchet_report.py +0 -0
  65. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/sarif.py +0 -0
  66. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/sarifio.py +0 -0
  67. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/score.py +0 -0
  68. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/snapshot.py +0 -0
  69. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/store.py +0 -0
  70. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/verify.py +0 -0
  71. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/watch.py +0 -0
  72. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit/worklist.py +0 -0
  73. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit.egg-info/dependency_links.txt +0 -0
  74. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit.egg-info/entry_points.txt +0 -0
  75. {crapkit-0.4.11 → crapkit-0.4.13}/src/crapkit.egg-info/requires.txt +0 -0
  76. {crapkit-0.4.11 → crapkit-0.4.13}/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.11
3
+ Version: 0.4.13
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
@@ -43,6 +43,7 @@ Dynamic: license-file
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
45
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/JeanFrancoisGagne/crapkit/blob/main/LICENSE)
46
+ [![crapkit MCP server](https://glama.ai/mcp/servers/JeanFrancoisGagne/crapkit/badges/score.svg)](https://glama.ai/mcp/servers/JeanFrancoisGagne/crapkit)
46
47
 
47
48
  ![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
49
 
@@ -117,7 +118,9 @@ command when `pytest_cov` is missing; `pip install "crapkit[py]"` pulls the plug
117
118
  alongside crapkit when the two share a venv. On a Windows PATH holding only the `py`
118
119
  launcher it writes `py`, not a `python3` the lane could never run, and when cmd.exe cannot
119
120
  start the interpreter at all (exit 9009, the Store alias) it names that instead of guessing
120
- at pytest-cov. The two quickstarts below walk a real repo end to end.
121
+ at pytest-cov. A repo that pins no lockfile and carries its own `.venv` gets that venv's
122
+ interpreter in the lane, when that interpreter can import pytest, rather than whichever
123
+ python the shell answers with. The two quickstarts below walk a real repo end to end.
121
124
 
122
125
  **On Windows a lane command is read by cmd.exe**, the shell that will run it, not by sh.
123
126
  Double quotes are the portable quoting. A single-quoted value is refused at config load
@@ -129,7 +132,7 @@ artifact:
129
132
  command = "python -m pytest -m 'not live and not perf' --cov=calc --cov-branch --cov-report=json:.crapkit/cov/py.json"
130
133
 
131
134
  $ crapkit doctor
132
- crapkit: lane 'py': positional argument 'live' narrows a full-suite coverage run; drop it, attach it to the flag it belongs to (-n8, --numprocesses=8), or set full_suite = false deliberately (cmd.exe does not treat ' as a quote: write the value in double quotes)
135
+ crapkit: lane 'py': positional argument 'live' narrows a full-suite coverage run; drop it, attach it to the flag it belongs to (-n8, --numprocesses=8), or set full_suite = false deliberately (cmd.exe does not treat ' as a quote: write the value in double quotes); a suite whose testpaths cannot be collected in one process needs one lane per testpath, each with full_suite = false and its own artifact
133
136
  ```
134
137
 
135
138
  Write it `-m "not live and not perf"`. Carets, `&&` and `|` segments, redirections and
@@ -163,7 +166,7 @@ leaves the box
163
166
 
164
167
  ```
165
168
  $ crapkit --version
166
- crapkit 0.4.11
169
+ crapkit 0.4.13
167
170
  ```
168
171
 
169
172
  `python -m crapkit` works identically to the console script and is what to use from a
@@ -393,7 +396,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
393
396
  repos:
394
397
  - repo: https://github.com/JeanFrancoisGagne/crapkit
395
398
  # crapkit's release step rewrites this line to the tag it just cut
396
- rev: v0.4.11
399
+ rev: v0.4.13
397
400
  hooks:
398
401
  - id: crapkit-gate
399
402
  ```
@@ -499,7 +502,7 @@ to a workflow, and every input has a default:
499
502
  - uses: actions/checkout@v4
500
503
  with:
501
504
  fetch-depth: 0
502
- - uses: JeanFrancoisGagne/crapkit@v0.4.11
505
+ - uses: JeanFrancoisGagne/crapkit@v0.4.13
503
506
  ```
504
507
 
505
508
  The whole job those four lines sit in:
@@ -519,7 +522,7 @@ jobs:
519
522
  with:
520
523
  python-version: "3.12" # the interpreter the install below lands in
521
524
  - run: pip install -e ".[dev]" # whatever your lanes need to run
522
- - uses: JeanFrancoisGagne/crapkit@v0.4.11
525
+ - uses: JeanFrancoisGagne/crapkit@v0.4.13
523
526
  with:
524
527
  gate: "false"
525
528
  ```
@@ -663,7 +666,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
663
666
  | Command | What it does |
664
667
  |---|---|
665
668
  | `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. |
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). |
669
+ | `doctor [--show-files] [--json] [--tune] [--plugin-root [PATH]]` | Checks the config still describes the repo: unknown keys (with the accepted spellings), zero-file scopes, tracked source no scope claims, scopes no lane covers, lane cwds and commands that no longer resolve, lizard importable, oversized files. It reads each lane command with the shell that will run it, so a quoted interpreter path is one word and a runner after `&&` is checked too, and it FAILs a lane whose runner does not resolve or that the shell cannot start, naming the word to change; a bare name is looked for on PATH and a runner spelled as a path is looked for under the directory the lane runs in, so `.venv/bin/python` answers the same from any directory you run `doctor` in; each distinct runner is probed once, not once per lane. It WARNs on a lane writing its artifact at the repo root, a `coveragepy` or `istanbul` lane with no `results_artifact` (the crashed-worker and no-new-failures checks are off for it, whichever runner the lane spells), a committed hook under `core.hooksPath` that is not executable in the index, a directory whose functions are all `untested` while its tests exist, and a scope a lane measures with no `[crapkit.scoped_tests]` template behind it, which is the loop's step 4 with nothing to run. `--tune` prints suggested parallelism knobs and writes nothing. `--plugin-root PATH` reads no repo at all: it checks an installed [plugin](https://github.com/JeanFrancoisGagne/crapkit/tree/main/plugin) against the `crapkit` on PATH (the bare name its hooks and MCP server spawn) on both version and hook `--protocol`, and FAILs when PATH carries no `crapkit` at all, one line per disagreement and silence when they agree; PATH is the plugin root or any directory above it, `~/.claude` included (only manifests named `crapkit` count, and the newest install wins), and with no PATH it looks in Claude Code's plugin cache. A root it found rather than one you typed is named first, as `crapkit doctor: checking PATH`. See [docs/agent-json.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md#doctor---json). |
667
670
  | `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. |
668
671
  | `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). |
669
672
  | `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. |
@@ -678,7 +681,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
678
681
  | `overrides [--json]` | The override audit trail: who granted what, when, and why. |
679
682
  | `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). |
680
683
  | `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. |
681
- | `report [--out PATH]` | One self-contained HTML page written to `.crapkit/report.html` (or `--out PATH`, repo-relative), with the path printed on stdout. It renders what `worklist --json` and `trend --json` already answer at their defaults: the ranked worklist capped at `worklist_top`, the per-scope grades off the newest run, the trend series, and a banner naming every stale lane. It measures nothing and opens no network connection. Per-function CRAP and coverage are absent because no repo-wide payload carries them; each row prints the `crapkit explain` call that does. It reads the same per-run rollups `trend` does, and writes them on the same terms. |
684
+ | `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. |
682
685
  | `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. |
683
686
  | `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. |
684
687
  | `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. |
@@ -686,6 +689,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
686
689
  | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
687
690
  | `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. |
688
691
  | `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. |
692
+ | `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. |
689
693
  | `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). |
690
694
 
691
695
  ## Reading the output
@@ -882,11 +886,11 @@ paths = ["calc"]
882
886
  languages = ["python"]
883
887
 
884
888
  [exclude]
885
- globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/*.test.*", "**/*.spec.*", "**/test_*.py", "**/*_test.py", "**/conftest.py", "**/*_test.go", "*.config.ts", "*.config.js", "*.config.mts", "**/*.config.ts", "**/*.config.js", "**/*.config.mts"]
889
+ 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"]
886
890
 
887
891
  [[lane]]
888
892
  name = "py"
889
- command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json --junitxml=.crapkit/cov/junit-py.xml"
893
+ command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json --junitxml=.crapkit/cov/junit-py.xml --continue-on-collection-errors"
890
894
  artifact = ".crapkit/cov/py.json"
891
895
  results_artifact = ".crapkit/cov/junit-py.xml"
892
896
  parser = "coveragepy"
@@ -895,7 +899,7 @@ scopes = ["calc"]
895
899
  # Declare one [[lane]] per coverage command, then run `crapkit coverage`.
896
900
  # [[lane]]
897
901
  # name = "js"
898
- # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml"
902
+ # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml"
899
903
  # artifact = ".crapkit/cov/js/coverage-final.json"
900
904
  # results_artifact = ".crapkit/cov/js/junit.xml"
901
905
  # parser = "istanbul"
@@ -1010,7 +1014,7 @@ added to .gitignore: .crapkit/
1010
1014
  ```
1011
1015
 
1012
1016
  The lane `init` wrote is
1013
- `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml`.
1017
+ `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml`.
1014
1018
  It reads vitest's `json` reporter from `.crapkit/cov/js/coverage-final.json`; the
1015
1019
  `reportsDirectory` flag is what keeps that report out of your root. The junit half is the
1016
1020
  lane's `results_artifact`, which the crashed-worker and no-new-failures checks read; both
@@ -1029,7 +1033,7 @@ exit 5:
1029
1033
 
1030
1034
  ```
1031
1035
  $ crapkit coverage
1032
- crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); full log: /repo/.crapkit/lane-js.log; last output: $ npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml
1036
+ crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); full log: /repo/.crapkit/lane-js.log; last output: $ npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml
1033
1037
 
1034
1038
  MISSING DEPENDENCY Cannot find dependency '@vitest/coverage-v8'
1035
1039
 
@@ -1057,9 +1061,11 @@ The artifact crapkit wants is `coverage-final.json`, written by vitest's `json`
1057
1061
  reporter, which is on by default. If your vitest config sets `coverage.reporter`
1058
1062
  explicitly, keep `"json"` in the list.
1059
1063
 
1060
- One more vitest default worth flipping now: it writes **no coverage report at all when the
1061
- run fails**, so a single red test becomes a missing artifact and a lane failure. Set
1062
- `coverage.reportOnFailure = true`. The full config block is in
1064
+ vitest writes **no coverage report at all when the run fails**. The lane `init` wrote
1065
+ already carries `--coverage.reportOnFailure`, so a red test still produces the artifact.
1066
+ If you write the lane by hand, or you would rather keep the switch beside your other
1067
+ coverage settings, `coverage.reportOnFailure = true` in the vitest config does the same
1068
+ job; either one is enough. The full block is in
1063
1069
  [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#reportonfailure).
1064
1070
 
1065
1071
  ### 3. Score the repo
@@ -4,6 +4,7 @@
4
4
  [![PyPI](https://img.shields.io/pypi/v/crapkit)](https://pypi.org/project/crapkit/)
5
5
  [![Python](https://img.shields.io/pypi/pyversions/crapkit)](https://pypi.org/project/crapkit/)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/JeanFrancoisGagne/crapkit/blob/main/LICENSE)
7
+ [![crapkit MCP server](https://glama.ai/mcp/servers/JeanFrancoisGagne/crapkit/badges/score.svg)](https://glama.ai/mcp/servers/JeanFrancoisGagne/crapkit)
7
8
 
8
9
  ![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)
9
10
 
@@ -78,7 +79,9 @@ command when `pytest_cov` is missing; `pip install "crapkit[py]"` pulls the plug
78
79
  alongside crapkit when the two share a venv. On a Windows PATH holding only the `py`
79
80
  launcher it writes `py`, not a `python3` the lane could never run, and when cmd.exe cannot
80
81
  start the interpreter at all (exit 9009, the Store alias) it names that instead of guessing
81
- at pytest-cov. The two quickstarts below walk a real repo end to end.
82
+ at pytest-cov. A repo that pins no lockfile and carries its own `.venv` gets that venv's
83
+ interpreter in the lane, when that interpreter can import pytest, rather than whichever
84
+ python the shell answers with. The two quickstarts below walk a real repo end to end.
82
85
 
83
86
  **On Windows a lane command is read by cmd.exe**, the shell that will run it, not by sh.
84
87
  Double quotes are the portable quoting. A single-quoted value is refused at config load
@@ -90,7 +93,7 @@ artifact:
90
93
  command = "python -m pytest -m 'not live and not perf' --cov=calc --cov-branch --cov-report=json:.crapkit/cov/py.json"
91
94
 
92
95
  $ crapkit doctor
93
- crapkit: lane 'py': positional argument 'live' narrows a full-suite coverage run; drop it, attach it to the flag it belongs to (-n8, --numprocesses=8), or set full_suite = false deliberately (cmd.exe does not treat ' as a quote: write the value in double quotes)
96
+ crapkit: lane 'py': positional argument 'live' narrows a full-suite coverage run; drop it, attach it to the flag it belongs to (-n8, --numprocesses=8), or set full_suite = false deliberately (cmd.exe does not treat ' as a quote: write the value in double quotes); a suite whose testpaths cannot be collected in one process needs one lane per testpath, each with full_suite = false and its own artifact
94
97
  ```
95
98
 
96
99
  Write it `-m "not live and not perf"`. Carets, `&&` and `|` segments, redirections and
@@ -124,7 +127,7 @@ leaves the box
124
127
 
125
128
  ```
126
129
  $ crapkit --version
127
- crapkit 0.4.11
130
+ crapkit 0.4.13
128
131
  ```
129
132
 
130
133
  `python -m crapkit` works identically to the console script and is what to use from a
@@ -354,7 +357,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
354
357
  repos:
355
358
  - repo: https://github.com/JeanFrancoisGagne/crapkit
356
359
  # crapkit's release step rewrites this line to the tag it just cut
357
- rev: v0.4.11
360
+ rev: v0.4.13
358
361
  hooks:
359
362
  - id: crapkit-gate
360
363
  ```
@@ -460,7 +463,7 @@ to a workflow, and every input has a default:
460
463
  - uses: actions/checkout@v4
461
464
  with:
462
465
  fetch-depth: 0
463
- - uses: JeanFrancoisGagne/crapkit@v0.4.11
466
+ - uses: JeanFrancoisGagne/crapkit@v0.4.13
464
467
  ```
465
468
 
466
469
  The whole job those four lines sit in:
@@ -480,7 +483,7 @@ jobs:
480
483
  with:
481
484
  python-version: "3.12" # the interpreter the install below lands in
482
485
  - run: pip install -e ".[dev]" # whatever your lanes need to run
483
- - uses: JeanFrancoisGagne/crapkit@v0.4.11
486
+ - uses: JeanFrancoisGagne/crapkit@v0.4.13
484
487
  with:
485
488
  gate: "false"
486
489
  ```
@@ -624,7 +627,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
624
627
  | Command | What it does |
625
628
  |---|---|
626
629
  | `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. |
627
- | `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). |
630
+ | `doctor [--show-files] [--json] [--tune] [--plugin-root [PATH]]` | Checks the config still describes the repo: unknown keys (with the accepted spellings), zero-file scopes, tracked source no scope claims, scopes no lane covers, lane cwds and commands that no longer resolve, lizard importable, oversized files. It reads each lane command with the shell that will run it, so a quoted interpreter path is one word and a runner after `&&` is checked too, and it FAILs a lane whose runner does not resolve or that the shell cannot start, naming the word to change; a bare name is looked for on PATH and a runner spelled as a path is looked for under the directory the lane runs in, so `.venv/bin/python` answers the same from any directory you run `doctor` in; each distinct runner is probed once, not once per lane. It WARNs on a lane writing its artifact at the repo root, a `coveragepy` or `istanbul` lane with no `results_artifact` (the crashed-worker and no-new-failures checks are off for it, whichever runner the lane spells), a committed hook under `core.hooksPath` that is not executable in the index, a directory whose functions are all `untested` while its tests exist, and a scope a lane measures with no `[crapkit.scoped_tests]` template behind it, which is the loop's step 4 with nothing to run. `--tune` prints suggested parallelism knobs and writes nothing. `--plugin-root PATH` reads no repo at all: it checks an installed [plugin](https://github.com/JeanFrancoisGagne/crapkit/tree/main/plugin) against the `crapkit` on PATH (the bare name its hooks and MCP server spawn) on both version and hook `--protocol`, and FAILs when PATH carries no `crapkit` at all, one line per disagreement and silence when they agree; PATH is the plugin root or any directory above it, `~/.claude` included (only manifests named `crapkit` count, and the newest install wins), and with no PATH it looks in Claude Code's plugin cache. A root it found rather than one you typed is named first, as `crapkit doctor: checking PATH`. See [docs/agent-json.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md#doctor---json). |
628
631
  | `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. |
629
632
  | `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). |
630
633
  | `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. |
@@ -639,7 +642,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
639
642
  | `overrides [--json]` | The override audit trail: who granted what, when, and why. |
640
643
  | `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). |
641
644
  | `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. |
642
- | `report [--out PATH]` | One self-contained HTML page written to `.crapkit/report.html` (or `--out PATH`, repo-relative), with the path printed on stdout. It renders what `worklist --json` and `trend --json` already answer at their defaults: the ranked worklist capped at `worklist_top`, the per-scope grades off the newest run, the trend series, and a banner naming every stale lane. It measures nothing and opens no network connection. Per-function CRAP and coverage are absent because no repo-wide payload carries them; each row prints the `crapkit explain` call that does. It reads the same per-run rollups `trend` does, and writes them on the same terms. |
645
+ | `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. |
643
646
  | `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. |
644
647
  | `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. |
645
648
  | `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. |
@@ -647,6 +650,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
647
650
  | `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
648
651
  | `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. |
649
652
  | `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. |
653
+ | `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. |
650
654
  | `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). |
651
655
 
652
656
  ## Reading the output
@@ -843,11 +847,11 @@ paths = ["calc"]
843
847
  languages = ["python"]
844
848
 
845
849
  [exclude]
846
- globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/*.test.*", "**/*.spec.*", "**/test_*.py", "**/*_test.py", "**/conftest.py", "**/*_test.go", "*.config.ts", "*.config.js", "*.config.mts", "**/*.config.ts", "**/*.config.js", "**/*.config.mts"]
850
+ 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"]
847
851
 
848
852
  [[lane]]
849
853
  name = "py"
850
- command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json --junitxml=.crapkit/cov/junit-py.xml"
854
+ command = "python -m pytest --cov --cov-branch --cov-report=json:.crapkit/cov/py.json --junitxml=.crapkit/cov/junit-py.xml --continue-on-collection-errors"
851
855
  artifact = ".crapkit/cov/py.json"
852
856
  results_artifact = ".crapkit/cov/junit-py.xml"
853
857
  parser = "coveragepy"
@@ -856,7 +860,7 @@ scopes = ["calc"]
856
860
  # Declare one [[lane]] per coverage command, then run `crapkit coverage`.
857
861
  # [[lane]]
858
862
  # name = "js"
859
- # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml"
863
+ # command = "npx vitest run --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml"
860
864
  # artifact = ".crapkit/cov/js/coverage-final.json"
861
865
  # results_artifact = ".crapkit/cov/js/junit.xml"
862
866
  # parser = "istanbul"
@@ -971,7 +975,7 @@ added to .gitignore: .crapkit/
971
975
  ```
972
976
 
973
977
  The lane `init` wrote is
974
- `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml`.
978
+ `npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml`.
975
979
  It reads vitest's `json` reporter from `.crapkit/cov/js/coverage-final.json`; the
976
980
  `reportsDirectory` flag is what keeps that report out of your root. The junit half is the
977
981
  lane's `results_artifact`, which the crashed-worker and no-new-failures checks read; both
@@ -990,7 +994,7 @@ exit 5:
990
994
 
991
995
  ```
992
996
  $ crapkit coverage
993
- crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); full log: /repo/.crapkit/lane-js.log; last output: $ npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml
997
+ crapkit: lane 'js' FAILED: lane 'js' produced no artifact at .crapkit/cov/js/coverage-final.json (command exit 1); full log: /repo/.crapkit/lane-js.log; last output: $ npm run test -- --coverage --coverage.reportsDirectory=.crapkit/cov/js --coverage.reportOnFailure --reporter=default --reporter=junit --outputFile=.crapkit/cov/js/junit.xml
994
998
 
995
999
  MISSING DEPENDENCY Cannot find dependency '@vitest/coverage-v8'
996
1000
 
@@ -1018,9 +1022,11 @@ The artifact crapkit wants is `coverage-final.json`, written by vitest's `json`
1018
1022
  reporter, which is on by default. If your vitest config sets `coverage.reporter`
1019
1023
  explicitly, keep `"json"` in the list.
1020
1024
 
1021
- One more vitest default worth flipping now: it writes **no coverage report at all when the
1022
- run fails**, so a single red test becomes a missing artifact and a lane failure. Set
1023
- `coverage.reportOnFailure = true`. The full config block is in
1025
+ vitest writes **no coverage report at all when the run fails**. The lane `init` wrote
1026
+ already carries `--coverage.reportOnFailure`, so a red test still produces the artifact.
1027
+ If you write the lane by hand, or you would rather keep the switch beside your other
1028
+ coverage settings, `coverage.reportOnFailure = true` in the vitest config does the same
1029
+ job; either one is enough. The full block is in
1024
1030
  [docs/lanes.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/lanes.md#reportonfailure).
1025
1031
 
1026
1032
  ### 3. Score the repo
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "crapkit"
7
- version = "0.4.11"
7
+ version = "0.4.13"
8
8
  description = "Scores every function on complexity times uncovered risk, ranks the worst, and blocks commits that add more."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = { text = "MIT" }
@@ -1,2 +1,2 @@
1
1
  """crapkit: deterministic CRAP-score framework."""
2
- __version__ = "0.4.11"
2
+ __version__ = "0.4.13"
@@ -59,8 +59,10 @@ _OWNER = {
59
59
  "_claimable": "queue",
60
60
  "_claims_summary": "queue",
61
61
  "_claims_to_release": "queue",
62
+ "_clearing_spellings": "verifying",
62
63
  "_collect_lanes": "scoring",
63
64
  "_collect_mutants": "analyses",
65
+ "_committed_launcher": "admin",
64
66
  "_config": "claude_hook",
65
67
  "_contexts_for_span": "reports",
66
68
  "_coupling_pairs": "analyses",
@@ -118,16 +120,21 @@ _OWNER = {
118
120
  "_guard_ratchet_stamp": "verifying",
119
121
  "_handle": "queue",
120
122
  "_held_marks": "verifying",
123
+ "_help_topic": "parser",
124
+ "_help_topics": "parser",
121
125
  "_hook_handlers": "admin",
122
126
  "_hook_modes": "admin",
123
127
  "_hook_protocols": "admin",
128
+ "_imports_pytest": "admin",
124
129
  "_installed_crapkit_roots": "admin",
125
130
  "_interpreter": "admin",
131
+ "_is_rooted": "_shared",
126
132
  "_is_test_path": "verifying",
127
133
  "_judge": "claude_hook",
128
134
  "_junit_seconds": "admin",
129
135
  "_lane_command_problems": "admin",
130
136
  "_lane_durations": "admin",
137
+ "_lane_path": "admin",
131
138
  "_lane_problem": "admin",
132
139
  "_lane_problems": "admin",
133
140
  "_lane_report": "admin",
@@ -142,18 +149,22 @@ _OWNER = {
142
149
  "_load_ratchet_or_die": "_shared",
143
150
  "_load_repo_config": "_shared",
144
151
  "_load_sources": "_shared",
152
+ "_looks_like_a_path": "parser",
145
153
  "_manifest_field": "admin",
146
154
  "_manifest_roots": "admin",
147
155
  "_manifest_version": "admin",
148
156
  "_mark_text": "queue",
157
+ "_marker_texts": "admin",
149
158
  "_matching_rows": "queue",
150
159
  "_maybe_claim": "queue",
151
160
  "_marks_for": "claude_hook",
152
161
  "_maybe_flake_retry": "verifying",
153
162
  "_merge_stamp": "ratchet_cmds",
154
163
  "_missing_named_script": "admin",
164
+ "_missing_runner": "admin",
155
165
  "_more_skips_line": "verifying",
156
166
  "_mutation_targets": "analyses",
167
+ "_name_found_root": "admin",
157
168
  "_name_matches": "queue",
158
169
  "_name_prefix": "queue",
159
170
  "_named_baseline": "verifying",
@@ -168,6 +179,7 @@ _OWNER = {
168
179
  "_next_reasons": "queue",
169
180
  "_next_step": "admin",
170
181
  "_no_baseline": "verifying",
182
+ "_no_crapkit_on_path": "admin",
171
183
  "_no_handle_message": "queue",
172
184
  "_no_lane_debt": "queue",
173
185
  "_no_lane_gap": "queue",
@@ -185,12 +197,14 @@ _OWNER = {
185
197
  "_packet_context": "queue",
186
198
  "_packet_gate": "queue",
187
199
  "_packet_scope": "queue",
200
+ "_path_first_arg": "parser",
188
201
  "_payload": "claude_hook",
189
202
  "_pick_baseline": "verifying",
190
203
  "_pick_function": "queue",
191
204
  "_plugin_json": "admin",
192
205
  "_plugins_dir": "admin",
193
206
  "_policy_findings": "ratchet_cmds",
207
+ "_positive_top": "_shared",
194
208
  "_present_lockfiles": "admin",
195
209
  "_present_markers": "admin",
196
210
  "_present_on_disk": "scoring",
@@ -200,6 +214,7 @@ _OWNER = {
200
214
  "_print_brief_context": "queue",
201
215
  "_print_brief_neighbours": "queue",
202
216
  "_print_claims": "queue",
217
+ "_print_clear_the_reason": "verifying",
203
218
  "_print_coverage": "scoring",
204
219
  "_print_duplication": "analyses",
205
220
  "_print_finding_split": "verifying",
@@ -215,6 +230,7 @@ _OWNER = {
215
230
  "_print_trend": "reports",
216
231
  "_print_uncovered": "reports",
217
232
  "_print_verify_findings": "verifying",
233
+ "_probed_cli_version": "admin",
218
234
  "_progress": "scoring",
219
235
  "_prune_renames": "ratchet_cmds",
220
236
  "_pruned": "ratchet_cmds",
@@ -227,6 +243,7 @@ _OWNER = {
227
243
  "_ratchet_mark": "reports",
228
244
  "_ratchet_merge": "ratchet_cmds",
229
245
  "_ratchet_move": "ratchet_cmds",
246
+ "_ratchet_or_die": "_shared",
230
247
  "_ratchet_report": "ratchet_cmds",
231
248
  "_ratchet_sha256": "verifying",
232
249
  "_reconfigure_streams": "parser",
@@ -235,8 +252,12 @@ _OWNER = {
235
252
  "_records_by_scope": "scoring",
236
253
  "_refuse_empty_lane_run": "scoring",
237
254
  "_refuse_lane_less_verify": "verifying",
255
+ "_refuse_path_argument": "parser",
238
256
  "_release_claims": "verifying",
239
257
  "_release_target": "queue",
258
+ "_repo_out_path": "_shared",
259
+ "_repo_relative": "_shared",
260
+ "_repo_venv_python": "admin",
240
261
  "_report": "claude_hook",
241
262
  "_report_lanes": "reports",
242
263
  "_report_payload": "reports",
@@ -274,6 +295,7 @@ _OWNER = {
274
295
  "_sequencing": "claude_hook",
275
296
  "_settle_verify": "verifying",
276
297
  "_skip_reason": "queue",
298
+ "_spawned_cli": "admin",
277
299
  "_split_marked": "verifying",
278
300
  "_split_worklist": "queue",
279
301
  "_stale_warning": "queue",
@@ -292,12 +314,15 @@ _OWNER = {
292
314
  "_uncovered_fields": "queue",
293
315
  "_uncovered_scopes": "admin",
294
316
  "_uncovered_text": "reports",
317
+ "_under_root": "_shared",
295
318
  "_under_scope_path": "verifying",
296
319
  "_unknown_key_text": "admin",
297
320
  "_unmarked_breaches": "scoring",
298
321
  "_unknown_claude_command": "parser",
299
322
  "_untracked_of": "scoring",
300
323
  "_untrusted_reason": "verifying",
324
+ "_venv_dirs": "admin",
325
+ "_venv_launcher": "admin",
301
326
  "_verdict": "claude_hook",
302
327
  "_verify_attribution": "verifying",
303
328
  "_verify_baseline": "verifying",
@@ -335,6 +360,7 @@ _OWNER = {
335
360
  "cmd_doctor": "admin",
336
361
  "cmd_duplication": "analyses",
337
362
  "cmd_explain": "reports",
363
+ "cmd_help": "parser",
338
364
  "cmd_hook_precommit": "verifying",
339
365
  "cmd_init": "admin",
340
366
  "cmd_inventory": "scoring",