crapkit 0.4.2__tar.gz → 0.4.3__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.
- {crapkit-0.4.2/src/crapkit.egg-info → crapkit-0.4.3}/PKG-INFO +25 -7
- {crapkit-0.4.2 → crapkit-0.4.3}/README.md +24 -6
- {crapkit-0.4.2 → crapkit-0.4.3}/pyproject.toml +1 -1
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/__init__.py +1 -1
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/analyze.py +20 -16
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/__init__.py +4 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/claude_hook.py +14 -4
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/parser.py +2 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/queue.py +45 -13
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/ratchet_cmds.py +37 -8
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/reports.py +16 -3
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/scoring.py +37 -5
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/verifying.py +70 -9
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/config.py +85 -10
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/doctor.py +1 -1
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/dup.py +16 -3
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/hook.py +19 -4
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/junitparse.py +53 -3
- crapkit-0.4.3/src/crapkit/keys.py +86 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/lanes.py +30 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/override.py +5 -3
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/ratchet.py +76 -18
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/store.py +25 -1
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/verify.py +19 -6
- {crapkit-0.4.2 → crapkit-0.4.3/src/crapkit.egg-info}/PKG-INFO +25 -7
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit.egg-info/SOURCES.txt +1 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/LICENSE +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/setup.cfg +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/__main__.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/_pygdefer.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cache.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/churn.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/churn_cache.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/churn_log.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/_shared.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/admin.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/cli/analyses.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/coupling.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/coverage_istanbul.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/coverage_py.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/covstream.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/diffparse.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/digest.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/discover.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/errors.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/gitio.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/lizardcognitive.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/lizardpowershell.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/lizardrust.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/lizardshell.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/mcp_server.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/merge.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/mutate.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/mutate_pool.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/packet.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/ratchet_report.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/report.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/sarif.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/sarifio.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/scaffold.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/score.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/snapshot.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/uncovered.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/universe.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/watch.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit/worklist.py +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit.egg-info/dependency_links.txt +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit.egg-info/entry_points.txt +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/src/crapkit.egg-info/requires.txt +0 -0
- {crapkit-0.4.2 → crapkit-0.4.3}/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.
|
|
3
|
+
Version: 0.4.3
|
|
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
|
|
@@ -116,7 +116,7 @@ changing crapkit.
|
|
|
116
116
|
|
|
117
117
|
```
|
|
118
118
|
$ crapkit --version
|
|
119
|
-
crapkit 0.4.
|
|
119
|
+
crapkit 0.4.3
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
`python -m crapkit` works identically to the console script and is what to use from a
|
|
@@ -124,6 +124,22 @@ source checkout. Every subcommand accepts `--repo PATH` (default: the current di
|
|
|
124
124
|
so you never have to `cd` into the repo you are scoring. The flag goes after the
|
|
125
125
|
subcommand; [Subcommands](#subcommands) shows both orders.
|
|
126
126
|
|
|
127
|
+
### Upgrading on Windows
|
|
128
|
+
|
|
129
|
+
`uv tool upgrade crapkit`, and `pip install -U` into a tool venv, fail with `os error 32`
|
|
130
|
+
("The process cannot access the file because it is being used by another process") while a
|
|
131
|
+
crapkit MCP server is live: an agent session spawns `crapkit.exe mcp`, which holds the
|
|
132
|
+
launcher, and Windows will not overwrite a running executable. The venv upgrades before
|
|
133
|
+
that copy fails, so `crapkit --version` already reports the new version and the launcher is
|
|
134
|
+
the only stale piece. Quit the agent session and rerun the upgrade, or rename the locked
|
|
135
|
+
exe aside (Windows allows renaming a running one) and copy the new one in; the `.old` file
|
|
136
|
+
goes at the next reboot.
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
mv ~/.local/bin/crapkit.exe ~/.local/bin/crapkit.exe.old
|
|
140
|
+
cp %APPDATA%/uv/tools/crapkit/Scripts/crapkit.exe ~/.local/bin/crapkit.exe
|
|
141
|
+
```
|
|
142
|
+
|
|
127
143
|
## The Claude Code plugin
|
|
128
144
|
|
|
129
145
|
```
|
|
@@ -247,7 +263,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
|
|
|
247
263
|
repos:
|
|
248
264
|
- repo: https://github.com/JeanFrancoisGagne/crapkit
|
|
249
265
|
# crapkit's release step rewrites this line to the tag it just cut
|
|
250
|
-
rev: v0.4.
|
|
266
|
+
rev: v0.4.3
|
|
251
267
|
hooks:
|
|
252
268
|
- id: crapkit-gate
|
|
253
269
|
```
|
|
@@ -321,11 +337,11 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
321
337
|
| `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 WARNs on a lane writing its artifact at the repo root, a committed hook under `core.hooksPath` that is not executable in the index, a directory whose functions are all `untested` while its tests exist, and a scope a lane measures with no `[crapkit.scoped_tests]` template behind it, which is the loop's step 4 with nothing to run. `--tune` prints suggested parallelism knobs and writes nothing. `--plugin-root PATH` reads no repo at all: it checks an installed [plugin](plugin/) against this CLI on both version and hook `--protocol`, one line per disagreement and silence when they agree. See [docs/agent-json.md](docs/agent-json.md#doctor---json). |
|
|
322
338
|
| `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. |
|
|
323
339
|
| `coverage [--lane NAME] [--reuse-artifacts] [--reuse-unchanged] [--export PATH] [--sarif PATH] [--github] [--json]` | Runs the lanes, joins branch coverage onto a fresh inventory, writes a scored run. A failed lane is recorded, not fatal: its scopes fall back to `no-lane` and the run is typed `partial`, so it can never serve as a baseline. See [docs/lanes.md](docs/lanes.md). |
|
|
324
|
-
| `verify [--baseline ID \| --base REF \| --baseline-tsv PATH] [--emit-baseline PATH] [--override REASON] [--reuse-artifacts] [--reuse-unchanged] [--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. Findings a dirty tree produced are tagged `dirty` and counted apart. |
|
|
340
|
+
| `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. |
|
|
325
341
|
| `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; the normal keys stay. |
|
|
326
342
|
| `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. |
|
|
327
343
|
| `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. |
|
|
328
|
-
| `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,
|
|
344
|
+
| `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. |
|
|
329
345
|
| `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. `--history` adds the commits that touched it (`git log -L`), each carrying its message `body`, `--tests` the tests that covered it, which needs coverage.py contexts turned on ([recipe](docs/lanes.md#test-attribution-for-explain---tests)). `--json` emits the same content as one `schema` 1 object. |
|
|
330
346
|
| `rescore FILE ... [--gate] [--json]` | Fresh complexity for named files over the latest run's stale coverage, joined by name. Advisory: it writes no run. `--gate` applies the pre-commit hook's policy to the same selection the hook uses (functions the tree changed since HEAD), minus functions a ratchet mark already covers, and exits 6. |
|
|
331
347
|
| `ratchet seed \| prune \| merge \| move \| report [--enforce] [--json]` | The mark lifecycle: seed new debt, prune gone code (a mark whose file git renamed follows it), merge as a git driver, move re-paths marks, report reads burn-down from the file's own git history. See [docs/ratchet.md](docs/ratchet.md). |
|
|
@@ -334,7 +350,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
334
350
|
| `trend [--json]` | Totals per trusted run: functions, over-target count, CRAP load, average, per-scope rollup. |
|
|
335
351
|
| `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. |
|
|
336
352
|
| `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. |
|
|
337
|
-
| `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. |
|
|
353
|
+
| `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. |
|
|
338
354
|
| `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. |
|
|
339
355
|
| `mutate [--files F ...] [--max-mutants N] [--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. |
|
|
340
356
|
| `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. |
|
|
@@ -412,7 +428,9 @@ baseline**. `crapkit runs list` marks which one that is today.
|
|
|
412
428
|
never qualifies, and neither does a `partial` run (a lane failed, so some scope fell back
|
|
413
429
|
to `no-lane`) nor a `hook` override record, which carries no scored rows at all. In `runs
|
|
414
430
|
list`, `verdict=-` marks a run that produces no verdict rather than one that failed: only
|
|
415
|
-
`verify` renders a verdict.
|
|
431
|
+
`verify` renders a verdict. Three readers ask this one question and get this one answer:
|
|
432
|
+
the baseline pick here, `ratchet seed` and `prune`, and the tighten damping that compares a
|
|
433
|
+
mark against the same commit's previous run.
|
|
416
434
|
|
|
417
435
|
**What advances it.** Any qualifying run. `coverage` writes one wherever HEAD is, so a
|
|
418
436
|
dashboard cron advances the baseline exactly as CI does. A passing `verify` advances it
|
|
@@ -83,7 +83,7 @@ changing crapkit.
|
|
|
83
83
|
|
|
84
84
|
```
|
|
85
85
|
$ crapkit --version
|
|
86
|
-
crapkit 0.4.
|
|
86
|
+
crapkit 0.4.3
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
`python -m crapkit` works identically to the console script and is what to use from a
|
|
@@ -91,6 +91,22 @@ source checkout. Every subcommand accepts `--repo PATH` (default: the current di
|
|
|
91
91
|
so you never have to `cd` into the repo you are scoring. The flag goes after the
|
|
92
92
|
subcommand; [Subcommands](#subcommands) shows both orders.
|
|
93
93
|
|
|
94
|
+
### Upgrading on Windows
|
|
95
|
+
|
|
96
|
+
`uv tool upgrade crapkit`, and `pip install -U` into a tool venv, fail with `os error 32`
|
|
97
|
+
("The process cannot access the file because it is being used by another process") while a
|
|
98
|
+
crapkit MCP server is live: an agent session spawns `crapkit.exe mcp`, which holds the
|
|
99
|
+
launcher, and Windows will not overwrite a running executable. The venv upgrades before
|
|
100
|
+
that copy fails, so `crapkit --version` already reports the new version and the launcher is
|
|
101
|
+
the only stale piece. Quit the agent session and rerun the upgrade, or rename the locked
|
|
102
|
+
exe aside (Windows allows renaming a running one) and copy the new one in; the `.old` file
|
|
103
|
+
goes at the next reboot.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
mv ~/.local/bin/crapkit.exe ~/.local/bin/crapkit.exe.old
|
|
107
|
+
cp %APPDATA%/uv/tools/crapkit/Scripts/crapkit.exe ~/.local/bin/crapkit.exe
|
|
108
|
+
```
|
|
109
|
+
|
|
94
110
|
## The Claude Code plugin
|
|
95
111
|
|
|
96
112
|
```
|
|
@@ -214,7 +230,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
|
|
|
214
230
|
repos:
|
|
215
231
|
- repo: https://github.com/JeanFrancoisGagne/crapkit
|
|
216
232
|
# crapkit's release step rewrites this line to the tag it just cut
|
|
217
|
-
rev: v0.4.
|
|
233
|
+
rev: v0.4.3
|
|
218
234
|
hooks:
|
|
219
235
|
- id: crapkit-gate
|
|
220
236
|
```
|
|
@@ -288,11 +304,11 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
288
304
|
| `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 WARNs on a lane writing its artifact at the repo root, a committed hook under `core.hooksPath` that is not executable in the index, a directory whose functions are all `untested` while its tests exist, and a scope a lane measures with no `[crapkit.scoped_tests]` template behind it, which is the loop's step 4 with nothing to run. `--tune` prints suggested parallelism knobs and writes nothing. `--plugin-root PATH` reads no repo at all: it checks an installed [plugin](plugin/) against this CLI on both version and hook `--protocol`, one line per disagreement and silence when they agree. See [docs/agent-json.md](docs/agent-json.md#doctor---json). |
|
|
289
305
|
| `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. |
|
|
290
306
|
| `coverage [--lane NAME] [--reuse-artifacts] [--reuse-unchanged] [--export PATH] [--sarif PATH] [--github] [--json]` | Runs the lanes, joins branch coverage onto a fresh inventory, writes a scored run. A failed lane is recorded, not fatal: its scopes fall back to `no-lane` and the run is typed `partial`, so it can never serve as a baseline. See [docs/lanes.md](docs/lanes.md). |
|
|
291
|
-
| `verify [--baseline ID \| --base REF \| --baseline-tsv PATH] [--emit-baseline PATH] [--override REASON] [--reuse-artifacts] [--reuse-unchanged] [--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. Findings a dirty tree produced are tagged `dirty` and counted apart. |
|
|
307
|
+
| `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. |
|
|
292
308
|
| `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; the normal keys stay. |
|
|
293
309
|
| `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. |
|
|
294
310
|
| `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. |
|
|
295
|
-
| `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,
|
|
311
|
+
| `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. |
|
|
296
312
|
| `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. `--history` adds the commits that touched it (`git log -L`), each carrying its message `body`, `--tests` the tests that covered it, which needs coverage.py contexts turned on ([recipe](docs/lanes.md#test-attribution-for-explain---tests)). `--json` emits the same content as one `schema` 1 object. |
|
|
297
313
|
| `rescore FILE ... [--gate] [--json]` | Fresh complexity for named files over the latest run's stale coverage, joined by name. Advisory: it writes no run. `--gate` applies the pre-commit hook's policy to the same selection the hook uses (functions the tree changed since HEAD), minus functions a ratchet mark already covers, and exits 6. |
|
|
298
314
|
| `ratchet seed \| prune \| merge \| move \| report [--enforce] [--json]` | The mark lifecycle: seed new debt, prune gone code (a mark whose file git renamed follows it), merge as a git driver, move re-paths marks, report reads burn-down from the file's own git history. See [docs/ratchet.md](docs/ratchet.md). |
|
|
@@ -301,7 +317,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
301
317
|
| `trend [--json]` | Totals per trusted run: functions, over-target count, CRAP load, average, per-scope rollup. |
|
|
302
318
|
| `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. |
|
|
303
319
|
| `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. |
|
|
304
|
-
| `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. |
|
|
320
|
+
| `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. |
|
|
305
321
|
| `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. |
|
|
306
322
|
| `mutate [--files F ...] [--max-mutants N] [--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. |
|
|
307
323
|
| `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. |
|
|
@@ -379,7 +395,9 @@ baseline**. `crapkit runs list` marks which one that is today.
|
|
|
379
395
|
never qualifies, and neither does a `partial` run (a lane failed, so some scope fell back
|
|
380
396
|
to `no-lane`) nor a `hook` override record, which carries no scored rows at all. In `runs
|
|
381
397
|
list`, `verdict=-` marks a run that produces no verdict rather than one that failed: only
|
|
382
|
-
`verify` renders a verdict.
|
|
398
|
+
`verify` renders a verdict. Three readers ask this one question and get this one answer:
|
|
399
|
+
the baseline pick here, `ratchet seed` and `prune`, and the tighten damping that compares a
|
|
400
|
+
mark against the same commit's previous run.
|
|
383
401
|
|
|
384
402
|
**What advances it.** Any qualifying run. `coverage` writes one wherever HEAD is, so a
|
|
385
403
|
dashboard cron advances the baseline exactly as CI does. A passing `verify` advances it
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "crapkit"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.3"
|
|
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.
|
|
2
|
+
__version__ = "0.4.3"
|
|
@@ -160,10 +160,11 @@ _NAMES_SHOWN = 5
|
|
|
160
160
|
def _colliding_names(records: list[FunctionRecord]) -> list[str]:
|
|
161
161
|
"""Names this file gives to more than one function, in first-seen order.
|
|
162
162
|
|
|
163
|
-
Anonymous functions are exempt. lizard calls every one of them
|
|
163
|
+
Anonymous functions are exempt from the line. lizard calls every one of them
|
|
164
164
|
`(anonymous)`, so a file with two arrow callbacks collides by construction
|
|
165
|
-
and
|
|
166
|
-
|
|
165
|
+
and the line would name nothing anyone could act on. They take the same
|
|
166
|
+
ordinal keys as any other twin; `packet.handles` already addresses them as
|
|
167
|
+
`(anonymous)#N`.
|
|
167
168
|
"""
|
|
168
169
|
seen: set[str] = set()
|
|
169
170
|
colliding: dict[str, None] = {}
|
|
@@ -174,23 +175,26 @@ def _colliding_names(records: list[FunctionRecord]) -> list[str]:
|
|
|
174
175
|
return list(colliding)
|
|
175
176
|
|
|
176
177
|
|
|
177
|
-
def
|
|
178
|
-
"""One stderr line for a file
|
|
178
|
+
def _note_twin_keys(rel_path: str, records: list[FunctionRecord]) -> None:
|
|
179
|
+
"""One stderr line for a file that gives one name to more than one function.
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
Information, not a warning. Until 0.4.2 it was the second: a mark keyed on
|
|
182
|
+
(path, long_name) meant one twin owned the key and the rest were neither
|
|
183
|
+
marked nor gated, which is the loss this announced. `keys` ends that by
|
|
184
|
+
giving each twin its own ordinal, so the line now says what a reader will
|
|
185
|
+
see in `crapkit-ratchet.tsv` and nothing is lost.
|
|
186
|
+
|
|
187
|
+
Still printed, because a `#2` appearing in a committed marks file is
|
|
188
|
+
otherwise unexplained. C makes the shape ordinary — both arms of an `#ifdef`
|
|
189
|
+
fork are textually present — and so does Python, whose method long_names
|
|
190
|
+
carry no class.
|
|
187
191
|
"""
|
|
188
192
|
names = _colliding_names(records)
|
|
189
193
|
if not names:
|
|
190
194
|
return
|
|
191
|
-
print(f"crapkit: {rel_path} defines {_listed(names)} more than once;
|
|
192
|
-
f"
|
|
193
|
-
f"
|
|
195
|
+
print(f"crapkit: {rel_path} defines {_listed(names)} more than once; each one takes "
|
|
196
|
+
f"its own ratchet key — the first as written, later ones suffixed #2, #3 in "
|
|
197
|
+
f"file order", file=sys.stderr)
|
|
194
198
|
|
|
195
199
|
|
|
196
200
|
def _listed(names: list[str]) -> str:
|
|
@@ -201,7 +205,7 @@ def _listed(names: list[str]) -> str:
|
|
|
201
205
|
|
|
202
206
|
def _file_records(rel_path: str, functions) -> list[FunctionRecord]:
|
|
203
207
|
records = [_record(rel_path, fn) for fn in functions]
|
|
204
|
-
|
|
208
|
+
_note_twin_keys(rel_path, records)
|
|
205
209
|
return records
|
|
206
210
|
|
|
207
211
|
|
|
@@ -109,6 +109,7 @@ _OWNER = {
|
|
|
109
109
|
"_group_files_by_scope": "verifying",
|
|
110
110
|
"_guard_ratchet_stamp": "verifying",
|
|
111
111
|
"_handle": "queue",
|
|
112
|
+
"_held_marks": "verifying",
|
|
112
113
|
"_hook_handlers": "admin",
|
|
113
114
|
"_hook_modes": "admin",
|
|
114
115
|
"_hook_protocols": "admin",
|
|
@@ -160,6 +161,8 @@ _OWNER = {
|
|
|
160
161
|
"_no_line_message": "queue",
|
|
161
162
|
"_no_match_message": "queue",
|
|
162
163
|
"_no_scopes_reason": "admin",
|
|
164
|
+
"_no_tighten_line": "verifying",
|
|
165
|
+
"_no_twin_message": "queue",
|
|
163
166
|
"_note_marked_staged": "verifying",
|
|
164
167
|
"_note_stale_staged": "verifying",
|
|
165
168
|
"_open_store": "_shared",
|
|
@@ -176,6 +179,7 @@ _OWNER = {
|
|
|
176
179
|
"_policy_findings": "ratchet_cmds",
|
|
177
180
|
"_present_markers": "admin",
|
|
178
181
|
"_present_on_disk": "scoring",
|
|
182
|
+
"_prior_crap": "verifying",
|
|
179
183
|
"_print_batches": "queue",
|
|
180
184
|
"_print_brief": "queue",
|
|
181
185
|
"_print_brief_context": "queue",
|
|
@@ -154,7 +154,7 @@ def _judge(root: Path, rel: str) -> int:
|
|
|
154
154
|
records = _records(root, rel)
|
|
155
155
|
ranges = _changed(root, rel, diff.communicate()[0])
|
|
156
156
|
breaches, ceiling = _verdict(cfg, in_scope, rel, records, ranges)
|
|
157
|
-
return _report(root, cfg, rel, breaches, ceiling)
|
|
157
|
+
return _report(root, cfg, rel, breaches, ceiling, _keys(records))
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
def _config(root: Path):
|
|
@@ -260,11 +260,21 @@ def _answerable(over: list, ranges) -> list:
|
|
|
260
260
|
return [rec for rec in over if _touches(rec, ranges)]
|
|
261
261
|
|
|
262
262
|
|
|
263
|
-
def
|
|
263
|
+
def _keys(records: list) -> dict:
|
|
264
|
+
"""The file's ratchet keys, built from every record rather than the breaching
|
|
265
|
+
ones: the ordinal counts same-named functions in file order."""
|
|
266
|
+
from ..keys import key_names
|
|
267
|
+
|
|
268
|
+
return key_names(records)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _report(root: Path, cfg, rel: str, breaches: list, ceiling: int, keys: dict) -> int:
|
|
264
272
|
"""Rung 9. stdout stays empty whatever happens: protocol 1 reserves it for a
|
|
265
273
|
future JSON channel, and Claude Code parses stdout JSON on exit 0."""
|
|
274
|
+
from ..keys import key_of
|
|
275
|
+
|
|
266
276
|
marked = _marks_for(root / cfg.ratchet_file, rel)
|
|
267
|
-
unmarked = [rec for rec in breaches if rec
|
|
277
|
+
unmarked = [rec for rec in breaches if key_of(keys, rec)[1] not in marked]
|
|
268
278
|
if not unmarked:
|
|
269
279
|
return 0
|
|
270
280
|
for line in _advisory_lines(rel, unmarked, ceiling):
|
|
@@ -273,7 +283,7 @@ def _report(root: Path, cfg, rel: str, breaches: list, ceiling: int) -> int:
|
|
|
273
283
|
|
|
274
284
|
|
|
275
285
|
def _marks_for(marks_path: Path, rel: str) -> set[str]:
|
|
276
|
-
"""The
|
|
286
|
+
"""The ratchet KEY names one file carries marks for, `#N` ordinals included.
|
|
277
287
|
|
|
278
288
|
Existence, not the numeric high-water rule `verify` applies: crap needs
|
|
279
289
|
coverage, coverage needs the store, and the store stays closed. A mark is a
|
|
@@ -286,6 +286,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
286
286
|
help="rerun only lanes whose scope files changed since their artifact; reuse the rest")
|
|
287
287
|
ver.add_argument("--override", default=None, metavar="REASON",
|
|
288
288
|
help="audited exemption for gate violations: alert + ratchet debt + snapshot record")
|
|
289
|
+
ver.add_argument("--no-tighten", action="store_true",
|
|
290
|
+
help="pass the verdict without rewriting the ratchet; marks stay where they are")
|
|
289
291
|
ver.add_argument("--sarif", default=None, metavar="PATH",
|
|
290
292
|
help="write gate/ratchet findings as SARIF 2.1.0, relative to the repo")
|
|
291
293
|
ver.add_argument("--github", action="store_true",
|
|
@@ -13,6 +13,7 @@ from .. import packet
|
|
|
13
13
|
from ..churn_cache import load_churn
|
|
14
14
|
from ..errors import ConfigError, CrapkitError
|
|
15
15
|
from ..gitio import head_commit
|
|
16
|
+
from ..keys import key_names, key_of, split_ordinal
|
|
16
17
|
from ..store import SnapshotStore
|
|
17
18
|
from ..uncovered import load_uncovered
|
|
18
19
|
from ..worklist import admission, build_worklist, sql_floor
|
|
@@ -443,31 +444,50 @@ def _no_handle_message(path: str, name: str, rows: list) -> str:
|
|
|
443
444
|
|
|
444
445
|
|
|
445
446
|
def _pick_function(path: str, rows: list, name: str):
|
|
446
|
-
"""The one row `name` names, or an error listing what the file does hold.
|
|
447
|
-
|
|
448
|
-
Twins (one long_name at two spans) are ONE candidate, not an ambiguity: the
|
|
449
|
-
worst-scoring twin is what a burn-down item means, the same rule the ratchet
|
|
450
|
-
and the verdict use. The -start term keeps verify.worst_twins' tie-break —
|
|
451
|
-
equal-scoring twins resolve to the one that appears first in the file.
|
|
452
|
-
"""
|
|
447
|
+
"""The one row `name` names, or an error listing what the file does hold."""
|
|
453
448
|
at_line = _row_at_line(path, rows, name)
|
|
454
449
|
if at_line is not None:
|
|
455
450
|
return at_line
|
|
456
451
|
by_handle = _row_by_handle(path, rows, name)
|
|
457
452
|
if by_handle is not None:
|
|
458
453
|
return by_handle
|
|
459
|
-
|
|
454
|
+
wanted, ordinal = split_ordinal(name)
|
|
455
|
+
matched = _matching_rows(rows, wanted)
|
|
460
456
|
candidates = sorted({r.long_name for r in matched})
|
|
461
457
|
if len(candidates) != 1:
|
|
462
458
|
raise CrapkitError(_no_match_message(path, name, rows, candidates))
|
|
463
|
-
return
|
|
459
|
+
return _one_of(path, matched, name, wanted, ordinal)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _one_of(path: str, matched: list, name: str, wanted: str, ordinal: int):
|
|
463
|
+
"""Which of the same-named rows NAME meant.
|
|
464
|
+
|
|
465
|
+
A bare name means the burn-down item, and that is the worst twin — the rule
|
|
466
|
+
the ratchet, the verdict and `next-item` all run. The -start term is the
|
|
467
|
+
tie-break: equal-scoring twins resolve to the one that appears first.
|
|
468
|
+
|
|
469
|
+
`name#2` means the second of them in file order, which is the function its
|
|
470
|
+
own ratchet key `name#2` is about. That is how a session addresses the twin
|
|
471
|
+
a bare name does not pick.
|
|
472
|
+
"""
|
|
473
|
+
if wanted == name:
|
|
474
|
+
return max(matched, key=lambda r: (r.crap, -r.start))
|
|
475
|
+
ordered = sorted(matched, key=lambda r: r.start)
|
|
476
|
+
if ordinal > len(ordered):
|
|
477
|
+
raise CrapkitError(_no_twin_message(path, name, wanted, len(ordered)))
|
|
478
|
+
return ordered[ordinal - 1]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _no_twin_message(path: str, name: str, wanted: str, held: int) -> str:
|
|
482
|
+
return (f"no {name} in {path} in the latest scored run"
|
|
483
|
+
f" — it holds {held} function(s) named {wanted!r}")
|
|
464
484
|
|
|
465
485
|
|
|
466
|
-
def _brief_mark(entries: list | None,
|
|
486
|
+
def _brief_mark(entries: list | None, key: tuple[str, str]) -> float | None:
|
|
467
487
|
"""The committed mark on this function, or None when the repo carries none."""
|
|
468
488
|
from ..ratchet import mark_for
|
|
469
489
|
|
|
470
|
-
return None if entries is None else mark_for(entries,
|
|
490
|
+
return None if entries is None else mark_for(entries, *key)
|
|
471
491
|
|
|
472
492
|
|
|
473
493
|
def _brief_churn(churn: dict, path: str) -> dict | None:
|
|
@@ -505,6 +525,7 @@ class _BriefLoader:
|
|
|
505
525
|
self.latest = latest
|
|
506
526
|
self._whole_repo: dict = {}
|
|
507
527
|
self._scored_files: dict = {}
|
|
528
|
+
self._file_keys: dict = {}
|
|
508
529
|
self._attempts: dict = {}
|
|
509
530
|
|
|
510
531
|
def _once(self, key: str, build):
|
|
@@ -555,9 +576,20 @@ class _BriefLoader:
|
|
|
555
576
|
self._scored_files[path] = self.store.read_scored_file(self.latest["id"], path)
|
|
556
577
|
return self._scored_files[path]
|
|
557
578
|
|
|
579
|
+
def key(self, row) -> tuple[str, str]:
|
|
580
|
+
"""The row's ratchet key, counted over the whole file it lives in.
|
|
581
|
+
|
|
582
|
+
Cached per path beside the rows it is built from: a batch of packets
|
|
583
|
+
about one file would otherwise recount its ordinals per packet.
|
|
584
|
+
"""
|
|
585
|
+
if row.path not in self._file_keys:
|
|
586
|
+
self._file_keys[row.path] = key_names(self.scored_file(row.path))
|
|
587
|
+
return key_of(self._file_keys[row.path], row)
|
|
588
|
+
|
|
558
589
|
def mark(self, row) -> float | None:
|
|
559
590
|
return _brief_mark(self._once("marks",
|
|
560
|
-
lambda: _ratchet_entries(self.root, self.cfg)),
|
|
591
|
+
lambda: _ratchet_entries(self.root, self.cfg)),
|
|
592
|
+
self.key(row))
|
|
561
593
|
|
|
562
594
|
def mark_age(self, row, mark: float | None) -> int | None:
|
|
563
595
|
"""How long the mark has stood. No mark, no history read: reading the
|
|
@@ -565,7 +597,7 @@ class _BriefLoader:
|
|
|
565
597
|
if mark is None:
|
|
566
598
|
return None
|
|
567
599
|
return packet.mark_age_days(self._once("mark_events", self._read_mark_events),
|
|
568
|
-
(row
|
|
600
|
+
self.key(row))
|
|
569
601
|
|
|
570
602
|
def _read_mark_events(self) -> list:
|
|
571
603
|
from ..gitio import file_log_patches
|
|
@@ -12,11 +12,40 @@ from ..store import SnapshotStore
|
|
|
12
12
|
from ._shared import _load_ratchet_or_die, _load_repo_config, _open_store, _print_json
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
def _is_failed_verify(run: dict) -> bool:
|
|
16
|
+
return run["kind"] == "verify" and run["verdict_ok"] is False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _skipped_failed_verifies(runs: list[dict], chosen_id: int) -> list[dict]:
|
|
20
|
+
"""The failed verifies newer than the run this seed or prune settled on."""
|
|
21
|
+
return [r for r in runs if r["id"] > chosen_id and _is_failed_verify(r)]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _latest_full_run(store: SnapshotStore) -> tuple[dict, list[dict]]:
|
|
25
|
+
"""The newest TRUSTED run to work from, and the failed verifies passed over.
|
|
26
|
+
|
|
27
|
+
`verify`'s own rule, shared: a failed verify never serves as a baseline
|
|
28
|
+
because it can carry the scores of a red tree, and the failure is visible in
|
|
29
|
+
the same run. Seeding from one signs debt at values verify itself will not
|
|
30
|
+
accept as a comparison point. Before this, seed took the newest run whose
|
|
31
|
+
kind was coverage or verify and never read the verdict (#16).
|
|
32
|
+
"""
|
|
33
|
+
from ..store import is_trusted
|
|
34
|
+
|
|
35
|
+
runs = store.list_runs()
|
|
36
|
+
trusted = [r for r in runs if is_trusted(r)]
|
|
37
|
+
if not trusted:
|
|
38
|
+
raise CrapkitError("no trusted full run to work from — run `crapkit coverage` first "
|
|
39
|
+
"(failed verifies and hook runs never serve as baselines)")
|
|
40
|
+
return trusted[-1], _skipped_failed_verifies(runs, trusted[-1]["id"])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _skip_note(skipped: list[dict]) -> str:
|
|
44
|
+
"""Why the line names an older run than the newest one in the store."""
|
|
45
|
+
if not skipped:
|
|
46
|
+
return ""
|
|
47
|
+
ids = ", ".join(str(r["id"]) for r in skipped)
|
|
48
|
+
return f", skipped failed verify {'runs' if len(skipped) > 1 else 'run'} {ids}"
|
|
20
49
|
|
|
21
50
|
|
|
22
51
|
def _merge_stamp(texts: list[str]) -> str:
|
|
@@ -108,7 +137,7 @@ def _print_ratchet_report(report: dict, violations: list, ratchet_file: str) ->
|
|
|
108
137
|
|
|
109
138
|
|
|
110
139
|
def _working_marks(root: Path, ratchet_file: str) -> dict:
|
|
111
|
-
"""The marks on disk, keyed (path,
|
|
140
|
+
"""The marks on disk, keyed (path, key name) -> crap. Ages come from the
|
|
112
141
|
file's git history, but which marks are OPEN is a question about now, and a
|
|
113
142
|
seed prints "added 1" long before anybody commits the TSV."""
|
|
114
143
|
entries = _load_ratchet_or_die(root / ratchet_file, ratchet_file)
|
|
@@ -156,7 +185,7 @@ def cmd_ratchet(args: argparse.Namespace) -> int:
|
|
|
156
185
|
if args.action == "move": # a hand-declared rename needs no run to follow
|
|
157
186
|
return _ratchet_move(root, cfg, args.files)
|
|
158
187
|
store = _open_store(root)
|
|
159
|
-
latest = _latest_full_run(store)
|
|
188
|
+
latest, skipped = _latest_full_run(store)
|
|
160
189
|
fresh = store.read_scored(latest["id"])
|
|
161
190
|
ratchet_path = root / cfg.ratchet_file
|
|
162
191
|
prior = _load_ratchet_or_die(ratchet_path, cfg.ratchet_file)
|
|
@@ -168,5 +197,5 @@ def cmd_ratchet(args: argparse.Namespace) -> int:
|
|
|
168
197
|
entries, note = _pruned(root, store, prior, fresh)
|
|
169
198
|
ratchet_path.write_text(dump_ratchet(entries), encoding="utf-8", newline="\n")
|
|
170
199
|
print(f"{cfg.ratchet_file}: {note} — {len(entries)} mark(s) vs run {latest['id']} "
|
|
171
|
-
f"({latest['commit'][:11]})")
|
|
200
|
+
f"({latest['commit'][:11]}){_skip_note(skipped)}")
|
|
172
201
|
return 0
|
|
@@ -318,6 +318,10 @@ class _ExplainCtx(NamedTuple):
|
|
|
318
318
|
uncovered: MissingLines
|
|
319
319
|
ratchet: list | None
|
|
320
320
|
contexts: dict
|
|
321
|
+
# The twin NAME selected: 1 for a bare name, 2 for `f#2`. It is the ordinal
|
|
322
|
+
# in the ratchet key, and `(anonymous)#2` reads the same way — that handle
|
|
323
|
+
# and that twin's key are the same string.
|
|
324
|
+
ordinal: int = 1
|
|
321
325
|
|
|
322
326
|
|
|
323
327
|
def cmd_explain(args: argparse.Namespace) -> int:
|
|
@@ -338,19 +342,24 @@ def cmd_explain(args: argparse.Namespace) -> int:
|
|
|
338
342
|
|
|
339
343
|
|
|
340
344
|
def _explain_ctx(root: Path, cfg, store: SnapshotStore, args) -> _ExplainCtx:
|
|
345
|
+
from ..keys import split_ordinal
|
|
346
|
+
|
|
341
347
|
runs = [r for r in store.list_runs() if r["kind"] != "hook"]
|
|
342
348
|
return _ExplainCtx(root, args.path, runs[-1]["id"] if runs else None,
|
|
343
349
|
load_uncovered(root, cfg), _ratchet_entries(root, cfg),
|
|
344
|
-
_contexts_for_path(root, cfg, args.path) if args.tests else {}
|
|
350
|
+
_contexts_for_path(root, cfg, args.path) if args.tests else {},
|
|
351
|
+
split_ordinal(args.name)[1])
|
|
345
352
|
|
|
346
353
|
|
|
347
354
|
def _explain_payload(ctx: _ExplainCtx, store: SnapshotStore, args, long_name: str) -> dict:
|
|
348
355
|
"""One function's whole packet. The span is looked up once and passed down:
|
|
349
356
|
dark lines, --history and --tests all want the same line range."""
|
|
357
|
+
from ..keys import key_name
|
|
358
|
+
|
|
350
359
|
span = _latest_span(store, ctx.run_id, ctx.path, long_name)
|
|
351
360
|
out = {"long_name": long_name,
|
|
352
361
|
"history": store.function_history(ctx.path, long_name),
|
|
353
|
-
**_mark_fields(ctx.ratchet, ctx.path, long_name),
|
|
362
|
+
**_mark_fields(ctx.ratchet, ctx.path, key_name(long_name, ctx.ordinal)),
|
|
354
363
|
**_dark_fields(ctx.uncovered, ctx.path, span)}
|
|
355
364
|
if args.history:
|
|
356
365
|
out.update(_commits_fields(ctx.root, ctx.path, span))
|
|
@@ -384,7 +393,11 @@ def _latest_span(store: SnapshotStore, run_id: int | None, path: str, long_name:
|
|
|
384
393
|
|
|
385
394
|
def _mark_fields(ratchet: list | None, path: str, long_name: str) -> dict:
|
|
386
395
|
"""null and a note when the repo carries no marks file: an unmarked function
|
|
387
|
-
and a repo with no ratchet both read as null, and they want different moves.
|
|
396
|
+
and a repo with no ratchet both read as null, and they want different moves.
|
|
397
|
+
|
|
398
|
+
`long_name` arrives as the KEY name, so `explain path f#2` reads the second
|
|
399
|
+
twin's mark and a bare `f` reads the first's.
|
|
400
|
+
"""
|
|
388
401
|
from ..ratchet import mark_for
|
|
389
402
|
|
|
390
403
|
if ratchet is None:
|