crapkit 0.5.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {crapkit-0.5.0/src/crapkit.egg-info → crapkit-0.6.0}/PKG-INFO +21 -6
  2. {crapkit-0.5.0 → crapkit-0.6.0}/README.md +20 -5
  3. {crapkit-0.5.0 → crapkit-0.6.0}/pyproject.toml +1 -1
  4. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/__init__.py +1 -1
  5. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/verifying.py +28 -4
  6. crapkit-0.6.0/src/crapkit/mcp_server.py +1951 -0
  7. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/verify.py +17 -0
  8. {crapkit-0.5.0 → crapkit-0.6.0/src/crapkit.egg-info}/PKG-INFO +21 -6
  9. crapkit-0.5.0/src/crapkit/mcp_server.py +0 -417
  10. {crapkit-0.5.0 → crapkit-0.6.0}/LICENSE +0 -0
  11. {crapkit-0.5.0 → crapkit-0.6.0}/setup.cfg +0 -0
  12. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/__main__.py +0 -0
  13. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/_pygdefer.py +0 -0
  14. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/analyze.py +0 -0
  15. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cache.py +0 -0
  16. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/churn.py +0 -0
  17. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/churn_cache.py +0 -0
  18. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/churn_log.py +0 -0
  19. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/__init__.py +0 -0
  20. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/_shared.py +0 -0
  21. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/admin.py +0 -0
  22. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/analyses.py +0 -0
  23. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/claude_hook.py +0 -0
  24. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/parser.py +0 -0
  25. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/queue.py +0 -0
  26. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/ratchet_cmds.py +0 -0
  27. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/reports.py +0 -0
  28. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/cli/scoring.py +0 -0
  29. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/config.py +0 -0
  30. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/coupling.py +0 -0
  31. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/coupling_cache.py +0 -0
  32. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/coverage_istanbul.py +0 -0
  33. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/coverage_py.py +0 -0
  34. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/covstream.py +0 -0
  35. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/diffparse.py +0 -0
  36. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/digest.py +0 -0
  37. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/discover.py +0 -0
  38. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/doctor.py +0 -0
  39. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/dup.py +0 -0
  40. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/errors.py +0 -0
  41. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/gitio.py +0 -0
  42. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/hook.py +0 -0
  43. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/invocation.py +0 -0
  44. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/junitparse.py +0 -0
  45. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/keys.py +0 -0
  46. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/lanes.py +0 -0
  47. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/lizardcognitive.py +0 -0
  48. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/lizardpowershell.py +0 -0
  49. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/lizardrust.py +0 -0
  50. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/lizardshell.py +0 -0
  51. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/merge.py +0 -0
  52. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/mutate.py +0 -0
  53. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/mutate_pool.py +0 -0
  54. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/override.py +0 -0
  55. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/packet.py +0 -0
  56. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/procs.py +0 -0
  57. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/ratchet.py +0 -0
  58. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/ratchet_report.py +0 -0
  59. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/report.py +0 -0
  60. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/repotext.py +0 -0
  61. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/rootfind.py +0 -0
  62. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/sarif.py +0 -0
  63. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/sarifio.py +0 -0
  64. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/scaffold.py +0 -0
  65. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/score.py +0 -0
  66. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/snapshot.py +0 -0
  67. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/store.py +0 -0
  68. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/uncovered.py +0 -0
  69. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/universe.py +0 -0
  70. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/watch.py +0 -0
  71. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit/worklist.py +0 -0
  72. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit.egg-info/SOURCES.txt +0 -0
  73. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit.egg-info/dependency_links.txt +0 -0
  74. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit.egg-info/entry_points.txt +0 -0
  75. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit.egg-info/requires.txt +0 -0
  76. {crapkit-0.5.0 → crapkit-0.6.0}/src/crapkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crapkit
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Scores every function on complexity times uncovered risk, ranks the worst, and blocks commits that add more.
5
5
  Author: Jean-Francois Gagne
6
6
  License: MIT
@@ -73,6 +73,16 @@ half-executed straight-line function never reads as fully covered.
73
73
  function at ccn 7 with 100% coverage still scores 7 and still fails the gate. The only
74
74
  move that clears it is splitting the function.
75
75
 
76
+ **Why 6 and not 30.** crap4j's conventional threshold of 30 is a CRAP score: it lets an
77
+ untested `ccn 5` through (25 + 5 = 30) and a fully covered `ccn 30` too. crapkit's default
78
+ is a complexity ceiling, because coverage can at best collapse CRAP to `ccn`, and a
79
+ function you cannot cover past `ccn 6` is one you decompose. Set `target = 30` in
80
+ `crapkit.toml` if you want the crap4j number. A repo with existing debt does not need to:
81
+ `ratchet seed` marks today's over-ceiling functions at today's score, the gate then judges
82
+ only the functions a change touches, and marks may only fall, so adoption never starts with
83
+ a wall of red. Next to crap4py, radon, xenon, wily and SonarQube:
84
+ [docs/comparison.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/comparison.md).
85
+
76
86
  crapkit scores **git-tracked files only**. Source you have not `git add`ed is invisible to
77
87
  it.
78
88
 
@@ -171,7 +181,7 @@ leaves the box
171
181
 
172
182
  ```
173
183
  $ crapkit --version
174
- crapkit 0.5.0
184
+ crapkit 0.6.0
175
185
  ```
176
186
 
177
187
  `python -m crapkit` works identically to the console script and is what to use from a
@@ -415,7 +425,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
415
425
  repos:
416
426
  - repo: https://github.com/JeanFrancoisGagne/crapkit
417
427
  # crapkit's release step rewrites this line to the tag it just cut
418
- rev: v0.5.0
428
+ rev: v0.6.0
419
429
  hooks:
420
430
  - id: crapkit-gate
421
431
  ```
@@ -523,7 +533,7 @@ to a workflow, and every input has a default:
523
533
  - uses: actions/checkout@v4
524
534
  with:
525
535
  fetch-depth: 0
526
- - uses: JeanFrancoisGagne/crapkit@v0.5.0
536
+ - uses: JeanFrancoisGagne/crapkit@v0.6.0
527
537
  ```
528
538
 
529
539
  The whole job those four lines sit in:
@@ -543,7 +553,7 @@ jobs:
543
553
  with:
544
554
  python-version: "3.12" # the interpreter the install below lands in
545
555
  - run: pip install -e ".[dev]" # whatever your lanes need to run
546
- - uses: JeanFrancoisGagne/crapkit@v0.5.0
556
+ - uses: JeanFrancoisGagne/crapkit@v0.6.0
547
557
  with:
548
558
  gate: "false"
549
559
  ```
@@ -765,7 +775,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
765
775
  | `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. |
766
776
  | `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. |
767
777
  | `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. |
768
- | `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing ten read-only tools. Every tool shells to the CLI's own `--json` surface, so the MCP view cannot drift from what the CLI reports. Answering from a kept in-process store was benchmarked and rejected: a packet's `source` would go stale behind the edit it describes. See [docs/agent-json.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md#mcp-server). |
778
+ | `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing twelve read-only tools named `verb_noun`, each with a title and a documented output schema. 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). |
769
779
 
770
780
  ## Reading the output
771
781
 
@@ -1285,6 +1295,11 @@ dropped it once `classify` scored under the ceiling, rewriting the tracked
1285
1295
  A verify may also print `warning: N changed line(s) have no coverage` above its verdict;
1286
1296
  that block is advisory unless `diff_uncovered_max` is set
1287
1297
  ([docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md)).
1298
+ It prints `warning: N function(s) over the ceiling carry no ratchet mark` when the tree
1299
+ holds debt `ratchet seed` never signed: the gate judges touched functions only and the
1300
+ ratchet check compares marks only, so coverage loss on such a function would pass unseen.
1301
+ The count is `unmarked_over_target` in `--json`, fires no exit code, and is zero on a repo
1302
+ with no debt.
1288
1303
 
1289
1304
  ## Documentation
1290
1305
 
@@ -34,6 +34,16 @@ half-executed straight-line function never reads as fully covered.
34
34
  function at ccn 7 with 100% coverage still scores 7 and still fails the gate. The only
35
35
  move that clears it is splitting the function.
36
36
 
37
+ **Why 6 and not 30.** crap4j's conventional threshold of 30 is a CRAP score: it lets an
38
+ untested `ccn 5` through (25 + 5 = 30) and a fully covered `ccn 30` too. crapkit's default
39
+ is a complexity ceiling, because coverage can at best collapse CRAP to `ccn`, and a
40
+ function you cannot cover past `ccn 6` is one you decompose. Set `target = 30` in
41
+ `crapkit.toml` if you want the crap4j number. A repo with existing debt does not need to:
42
+ `ratchet seed` marks today's over-ceiling functions at today's score, the gate then judges
43
+ only the functions a change touches, and marks may only fall, so adoption never starts with
44
+ a wall of red. Next to crap4py, radon, xenon, wily and SonarQube:
45
+ [docs/comparison.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/comparison.md).
46
+
37
47
  crapkit scores **git-tracked files only**. Source you have not `git add`ed is invisible to
38
48
  it.
39
49
 
@@ -132,7 +142,7 @@ leaves the box
132
142
 
133
143
  ```
134
144
  $ crapkit --version
135
- crapkit 0.5.0
145
+ crapkit 0.6.0
136
146
  ```
137
147
 
138
148
  `python -m crapkit` works identically to the console script and is what to use from a
@@ -376,7 +386,7 @@ crapkit ships a `.pre-commit-hooks.yaml` declaring `id: crapkit-gate`. In your
376
386
  repos:
377
387
  - repo: https://github.com/JeanFrancoisGagne/crapkit
378
388
  # crapkit's release step rewrites this line to the tag it just cut
379
- rev: v0.5.0
389
+ rev: v0.6.0
380
390
  hooks:
381
391
  - id: crapkit-gate
382
392
  ```
@@ -484,7 +494,7 @@ to a workflow, and every input has a default:
484
494
  - uses: actions/checkout@v4
485
495
  with:
486
496
  fetch-depth: 0
487
- - uses: JeanFrancoisGagne/crapkit@v0.5.0
497
+ - uses: JeanFrancoisGagne/crapkit@v0.6.0
488
498
  ```
489
499
 
490
500
  The whole job those four lines sit in:
@@ -504,7 +514,7 @@ jobs:
504
514
  with:
505
515
  python-version: "3.12" # the interpreter the install below lands in
506
516
  - run: pip install -e ".[dev]" # whatever your lanes need to run
507
- - uses: JeanFrancoisGagne/crapkit@v0.5.0
517
+ - uses: JeanFrancoisGagne/crapkit@v0.6.0
508
518
  with:
509
519
  gate: "false"
510
520
  ```
@@ -726,7 +736,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
726
736
  | `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. |
727
737
  | `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. |
728
738
  | `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. |
729
- | `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing ten read-only tools. Every tool shells to the CLI's own `--json` surface, so the MCP view cannot drift from what the CLI reports. Answering from a kept in-process store was benchmarked and rejected: a packet's `source` would go stale behind the edit it describes. See [docs/agent-json.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/agent-json.md#mcp-server). |
739
+ | `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing twelve read-only tools named `verb_noun`, each with a title and a documented output schema. 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). |
730
740
 
731
741
  ## Reading the output
732
742
 
@@ -1246,6 +1256,11 @@ dropped it once `classify` scored under the ceiling, rewriting the tracked
1246
1256
  A verify may also print `warning: N changed line(s) have no coverage` above its verdict;
1247
1257
  that block is advisory unless `diff_uncovered_max` is set
1248
1258
  ([docs/configuration.md](https://github.com/JeanFrancoisGagne/crapkit/blob/main/docs/configuration.md)).
1259
+ It prints `warning: N function(s) over the ceiling carry no ratchet mark` when the tree
1260
+ holds debt `ratchet seed` never signed: the gate judges touched functions only and the
1261
+ ratchet check compares marks only, so coverage loss on such a function would pass unseen.
1262
+ The count is `unmarked_over_target` in `--json`, fires no exit code, and is zero on a repo
1263
+ with no debt.
1249
1264
 
1250
1265
  ## Documentation
1251
1266
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "crapkit"
7
- version = "0.5.0"
7
+ version = "0.6.0"
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.5.0"
2
+ __version__ = "0.6.0"
@@ -483,9 +483,13 @@ def _verify_attribution(verdict) -> dict:
483
483
 
484
484
 
485
485
  def _verify_result(verdict, overridden, run_id: int, baseline: dict, commit: str, ranges,
486
- uncovered: list, diff_uncovered_max: int | None) -> dict:
486
+ uncovered: list, diff_uncovered_max: int | None,
487
+ unmarked_over_target: int) -> dict:
487
488
  """`diff_uncovered_max` travels with the count it judges: a reader of exit 9
488
- (the Action's comment) can say which ceiling the lines went over."""
489
+ (the Action's comment) can say which ceiling the lines went over.
490
+ `unmarked_over_target` is the standing debt no mark covers (see
491
+ `_warn_standing_debt`); it fires no exit code and is the one number that
492
+ says how much of the tree the ratchet is not holding."""
489
493
  return {
490
494
  "ok": verdict.ok,
491
495
  "run_id": run_id,
@@ -500,10 +504,28 @@ def _verify_result(verdict, overridden, run_id: int, baseline: dict, commit: str
500
504
  "diff_uncovered_count": len(uncovered),
501
505
  "diff_uncovered": [{"path": p, "line": ln} for p, ln in uncovered[:50]],
502
506
  "diff_uncovered_max": diff_uncovered_max,
507
+ "unmarked_over_target": unmarked_over_target,
503
508
  **_verify_attribution(verdict),
504
509
  }
505
510
 
506
511
 
512
+ def _warn_standing_debt(unmarked: list) -> None:
513
+ """One stderr line for the over-ceiling functions no mark covers.
514
+
515
+ The gate never looks at an untouched function and the ratchet check compares
516
+ marks only, so a rise on these (coverage loss included) reaches no finding;
517
+ docs/ratchet.md says "seed once, early" for exactly this, and a header-only
518
+ marks file cannot say whether seed ever ran. stderr, because
519
+ `--json` prints one object on stdout. Silent at zero, which is the state
520
+ of a repo with no debt and of one seeded in full.
521
+ """
522
+ if not unmarked:
523
+ return
524
+ print(f"warning: {len(unmarked)} function(s) over the ceiling carry no ratchet mark, so a "
525
+ f"rise on them (coverage loss included) passes unseen; record them with "
526
+ f"`{_self()} ratchet seed`", file=sys.stderr)
527
+
528
+
507
529
  def _warn_diff_uncovered(uncovered: list) -> None:
508
530
  if not uncovered:
509
531
  return
@@ -576,7 +598,7 @@ def cmd_verify(args: argparse.Namespace) -> int:
576
598
  from ..diffparse import changed_ranges
577
599
  from ..gitio import GitFacts, diff_since
578
600
  from ..uncovered import missing_by_path
579
- from ..verify import diff_uncovered, evaluate
601
+ from ..verify import diff_uncovered, evaluate, unmarked_over_ceiling
580
602
 
581
603
  root = _command_root(args.repo)
582
604
  cfg = _load_repo_config(root)
@@ -616,6 +638,8 @@ def cmd_verify(args: argparse.Namespace) -> int:
616
638
  # the whole cost of the post-commit verify on an unchanged tree.
617
639
  uncovered = diff_uncovered(ranges, missing_by_path(root, cfg)) if ranges else []
618
640
  _warn_diff_uncovered(uncovered)
641
+ unmarked = unmarked_over_ceiling(scored, ratchet, cfg.target, cfg.scope_targets)
642
+ _warn_standing_debt(unmarked)
619
643
  breach = _diff_cover_breach(cfg, uncovered)
620
644
  if breach:
621
645
  verdict = verdict._replace(ok=False) # a breached run never advances the baseline
@@ -629,7 +653,7 @@ def cmd_verify(args: argparse.Namespace) -> int:
629
653
 
630
654
  _report_verify(args.json,
631
655
  {**_verify_result(verdict, overridden, run_id, baseline, commit, ranges,
632
- uncovered, cfg.diff_uncovered_max),
656
+ uncovered, cfg.diff_uncovered_max, len(unmarked)),
633
657
  **_receipt(tool_versions, ratchet_sha256, changes)},
634
658
  verdict, overridden, cfg.ratchet_file)
635
659
  _refuse_override(verdict, args.override)