crapkit 0.2.0__tar.gz → 0.4.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.
- {crapkit-0.2.0/src/crapkit.egg-info → crapkit-0.4.0}/PKG-INFO +24 -13
- {crapkit-0.2.0 → crapkit-0.4.0}/README.md +23 -12
- {crapkit-0.2.0 → crapkit-0.4.0}/pyproject.toml +1 -1
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/__init__.py +1 -1
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/_pygdefer.py +6 -3
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/analyze.py +199 -12
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/__init__.py +38 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/admin.py +70 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/analyses.py +21 -8
- crapkit-0.4.0/src/crapkit/cli/claude_hook.py +307 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/parser.py +54 -5
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/reports.py +89 -7
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/scoring.py +19 -3
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/verifying.py +54 -5
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/config.py +11 -2
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/discover.py +24 -5
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/doctor.py +55 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/hook.py +3 -26
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/lizardcognitive.py +71 -10
- crapkit-0.4.0/src/crapkit/lizardpowershell.py +288 -0
- crapkit-0.4.0/src/crapkit/lizardrust.py +137 -0
- crapkit-0.4.0/src/crapkit/lizardshell.py +389 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/mutate.py +64 -3
- crapkit-0.4.0/src/crapkit/report.py +414 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/sarif.py +15 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/scaffold.py +9 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/uncovered.py +40 -12
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/universe.py +15 -0
- {crapkit-0.2.0 → crapkit-0.4.0/src/crapkit.egg-info}/PKG-INFO +24 -13
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit.egg-info/SOURCES.txt +5 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/LICENSE +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/setup.cfg +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/__main__.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cache.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/churn.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/churn_cache.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/churn_log.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/_shared.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/queue.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/cli/ratchet_cmds.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/coupling.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/coverage_istanbul.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/coverage_py.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/covstream.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/diffparse.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/digest.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/dup.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/errors.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/gitio.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/junitparse.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/lanes.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/mcp_server.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/merge.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/mutate_pool.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/override.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/packet.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/ratchet.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/ratchet_report.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/sarifio.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/score.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/snapshot.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/store.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/verify.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/watch.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit/worklist.py +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit.egg-info/dependency_links.txt +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit.egg-info/entry_points.txt +0 -0
- {crapkit-0.2.0 → crapkit-0.4.0}/src/crapkit.egg-info/requires.txt +0 -0
- {crapkit-0.2.0 → crapkit-0.4.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.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Deterministic CRAP-score framework: per-function complexity x coverage risk, worklists, ratchets, refactor verification
|
|
5
5
|
Author: Jean-Francois Gagne
|
|
6
6
|
License: MIT
|
|
@@ -28,10 +28,14 @@ Dynamic: license-file
|
|
|
28
28
|
|
|
29
29
|
crapkit scores every function in your repo on complexity times uncovered risk, ranks the
|
|
30
30
|
worst ones by how often the file changes, and blocks commits that add more. It reads
|
|
31
|
-
TypeScript, TSX, JavaScript
|
|
31
|
+
TypeScript, TSX, JavaScript, Python, Swift, Go, Rust, shell, PowerShell, C and C++,
|
|
32
|
+
Objective-C, Vue, Java and Zig through [lizard](https://github.com/terryyin/lizard),
|
|
32
33
|
and joins per-function branch coverage from istanbul or coverage.py artifacts your own test
|
|
33
|
-
command already produces.
|
|
34
|
-
|
|
34
|
+
command already produces. Those two parsers are the whole list, so Swift, Go, Rust, shell,
|
|
35
|
+
PowerShell, C and C++, Objective-C, Java and Zig have no coverage to join: declare those
|
|
36
|
+
scopes `coverage_optional` and they score on complexity alone. Vue joins istanbul coverage
|
|
37
|
+
when your own vitest run reports on `.vue` files. Every read-side command speaks JSON with a
|
|
38
|
+
pinned schema, because half the callers are coding agents.
|
|
35
39
|
|
|
36
40
|
```
|
|
37
41
|
CRAP = ccn^2 * (1 - cov)^3 + ccn
|
|
@@ -76,7 +80,7 @@ Check the install:
|
|
|
76
80
|
|
|
77
81
|
```
|
|
78
82
|
$ crapkit --version
|
|
79
|
-
crapkit 0.
|
|
83
|
+
crapkit 0.4.0
|
|
80
84
|
```
|
|
81
85
|
|
|
82
86
|
`python -m crapkit` works identically to the `crapkit` console script, and is what to use
|
|
@@ -130,7 +134,7 @@ paths = ["calc"]
|
|
|
130
134
|
languages = ["python"]
|
|
131
135
|
|
|
132
136
|
[exclude]
|
|
133
|
-
globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/*.test.*", "**/*.spec.*", "**/test_*.py", "**/*_test.py", "**/conftest.py", "*.config.ts", "*.config.js", "*.config.mts", "**/*.config.ts", "**/*.config.js", "**/*.config.mts"]
|
|
137
|
+
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"]
|
|
134
138
|
|
|
135
139
|
[[lane]]
|
|
136
140
|
name = "py"
|
|
@@ -472,11 +476,15 @@ without touching the repo-wide cache, and refuses the commit when a staged funct
|
|
|
472
476
|
its scope ceiling. It needs no coverage data and no snapshot, so it costs the size of the
|
|
473
477
|
commit, not the size of the repo.
|
|
474
478
|
|
|
475
|
-
|
|
479
|
+
Three limits to know. The gate judges files a `[[scope]]` claims; a staged source file no
|
|
476
480
|
scope claims is not gated, and the hook says so on stderr (`N staged file(s) belong to no
|
|
477
481
|
scope and were not gated`) so the hole is visible the moment a new top-level directory
|
|
478
|
-
appears.
|
|
479
|
-
|
|
482
|
+
appears. A function the committed ratchet already carries a mark for is not gated either,
|
|
483
|
+
so touching signed debt does not refuse the commit; the hook reports the count on stderr
|
|
484
|
+
(`N staged function(s) carry a ratchet mark and were not gated`) and `crapkit verify` is
|
|
485
|
+
what fails a mark that rose. And git runs hooks outside your shell's activated venv: bare
|
|
486
|
+
`python` must resolve to an interpreter that has crapkit installed, or use the absolute
|
|
487
|
+
form
|
|
480
488
|
(`exec /path/to/venv/Scripts/python -m crapkit hook-precommit`).
|
|
481
489
|
|
|
482
490
|
### Route 1: `.git/hooks/pre-commit` (local, not committed)
|
|
@@ -725,7 +733,8 @@ advances the baseline nor tightens the ratchet, exit 9 included.
|
|
|
725
733
|
## Subcommands
|
|
726
734
|
|
|
727
735
|
Every subcommand takes `--repo PATH` (default `.`), and the flag goes **after** the
|
|
728
|
-
subcommand:
|
|
736
|
+
subcommand. `claude-hook` is the one exception: it has no `--repo`, because it takes its
|
|
737
|
+
root from the file named in the hook payload it reads.
|
|
729
738
|
|
|
730
739
|
```
|
|
731
740
|
$ crapkit worklist --repo /path/to/repo --scope util --top 1
|
|
@@ -746,7 +755,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
746
755
|
| Command | What it does |
|
|
747
756
|
|---|---|
|
|
748
757
|
| `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. |
|
|
749
|
-
| `doctor [--show-files] [--json] [--tune]` | 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, lanes writing their artifacts at the repo root instead of under `.crapkit/` (WARN), committed hooks under `core.hooksPath` that are not executable in the index (WARN), directories whose functions are all `untested` while their tests exist (WARN), and scopes a lane measures with no `[crapkit.scoped_tests]` template behind them (WARN), which is the loop's step 4 with nothing to run. `--tune` prints suggested parallelism knobs and writes nothing. See [docs/agent-json.md](docs/agent-json.md#doctor---json). |
|
|
758
|
+
| `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, lanes writing their artifacts at the repo root instead of under `.crapkit/` (WARN), committed hooks under `core.hooksPath` that are not executable in the index (WARN), directories whose functions are all `untested` while their tests exist (WARN), and scopes a lane measures with no `[crapkit.scoped_tests]` template behind them (WARN), 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, comparing `.claude-plugin/plugin.json`'s version against the running crapkit and every `--protocol` in `hooks/hooks.json` against the protocol `claude-hook` answers, one line per disagreement and silence when they agree. See [docs/agent-json.md](docs/agent-json.md#doctor---json). |
|
|
750
759
|
| `inventory [--db PATH] [--export PATH] [--json]` | Two lizard passes 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. |
|
|
751
760
|
| `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). |
|
|
752
761
|
| `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. |
|
|
@@ -761,11 +770,13 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
761
770
|
| `overrides [--json]` | The override audit trail: who granted what, when, and why. |
|
|
762
771
|
| `trend [--json]` | Totals per trusted run: functions, over-target count, CRAP load, average, per-scope rollup. |
|
|
763
772
|
| `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. |
|
|
773
|
+
| `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, opens no network connection, and carries no per-function CRAP or coverage, because no repo-wide payload has them; each row prints the `crapkit explain` call that does. |
|
|
764
774
|
| `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. |
|
|
765
775
|
| `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. |
|
|
766
|
-
| `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. |
|
|
776
|
+
| `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. |
|
|
767
777
|
| `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. |
|
|
768
778
|
| `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
|
|
779
|
+
| `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 nothing is blocked, and `hook-precommit` stays the enforcement point. Exit 2 with three lines on stderr is the only thing it ever says. 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: exit 0, silence, empty stderr. Takes no `--repo` — the root is the first `crapkit.toml` above the edited file and the upward walk stops at a `.git` entry, so a worktree never borrows its parent's config. It opens no snapshot, writes nothing, and leaves stdout empty. |
|
|
769
780
|
| `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. |
|
|
770
781
|
| `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing nine read-only tools. See [docs/agent-json.md](docs/agent-json.md#mcp-server). |
|
|
771
782
|
|
|
@@ -779,7 +790,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
779
790
|
| [docs/ratchet.md](docs/ratchet.md) | Seeding, pruning, the git merge driver, metric stamps, debt policy, overrides. |
|
|
780
791
|
| [docs/agent-json.md](docs/agent-json.md) | The machine surface: `schema`, every payload field, real captured examples. |
|
|
781
792
|
| [docs/adoption.md](docs/adoption.md) | The judgment layer over the quickstarts: scope granularity, exclude vs lane, scoped_tests wiring, the first-verify taint hazard. |
|
|
782
|
-
| [
|
|
793
|
+
| [plugin/](plugin/) | The Claude Code plugin: three skills (`crapkit`, `crapkit-recover`, `crapkit-onboard`), the read-side MCP server, and the advisory PostToolUse hook. Install with `claude plugin marketplace add JeanFrancoisGagne/crapkit` then `claude plugin install crapkit@crapkit`; other runtimes copy `plugin/skills/*` into their skills directory. |
|
|
783
794
|
|
|
784
795
|
[crapkit.schema.json](crapkit.schema.json) is the authority on the config file shape.
|
|
785
796
|
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
crapkit scores every function in your repo on complexity times uncovered risk, ranks the
|
|
4
4
|
worst ones by how often the file changes, and blocks commits that add more. It reads
|
|
5
|
-
TypeScript, TSX, JavaScript
|
|
5
|
+
TypeScript, TSX, JavaScript, Python, Swift, Go, Rust, shell, PowerShell, C and C++,
|
|
6
|
+
Objective-C, Vue, Java and Zig through [lizard](https://github.com/terryyin/lizard),
|
|
6
7
|
and joins per-function branch coverage from istanbul or coverage.py artifacts your own test
|
|
7
|
-
command already produces.
|
|
8
|
-
|
|
8
|
+
command already produces. Those two parsers are the whole list, so Swift, Go, Rust, shell,
|
|
9
|
+
PowerShell, C and C++, Objective-C, Java and Zig have no coverage to join: declare those
|
|
10
|
+
scopes `coverage_optional` and they score on complexity alone. Vue joins istanbul coverage
|
|
11
|
+
when your own vitest run reports on `.vue` files. Every read-side command speaks JSON with a
|
|
12
|
+
pinned schema, because half the callers are coding agents.
|
|
9
13
|
|
|
10
14
|
```
|
|
11
15
|
CRAP = ccn^2 * (1 - cov)^3 + ccn
|
|
@@ -50,7 +54,7 @@ Check the install:
|
|
|
50
54
|
|
|
51
55
|
```
|
|
52
56
|
$ crapkit --version
|
|
53
|
-
crapkit 0.
|
|
57
|
+
crapkit 0.4.0
|
|
54
58
|
```
|
|
55
59
|
|
|
56
60
|
`python -m crapkit` works identically to the `crapkit` console script, and is what to use
|
|
@@ -104,7 +108,7 @@ paths = ["calc"]
|
|
|
104
108
|
languages = ["python"]
|
|
105
109
|
|
|
106
110
|
[exclude]
|
|
107
|
-
globs = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/vendor/**", "**/*.test.*", "**/*.spec.*", "**/test_*.py", "**/*_test.py", "**/conftest.py", "*.config.ts", "*.config.js", "*.config.mts", "**/*.config.ts", "**/*.config.js", "**/*.config.mts"]
|
|
111
|
+
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"]
|
|
108
112
|
|
|
109
113
|
[[lane]]
|
|
110
114
|
name = "py"
|
|
@@ -446,11 +450,15 @@ without touching the repo-wide cache, and refuses the commit when a staged funct
|
|
|
446
450
|
its scope ceiling. It needs no coverage data and no snapshot, so it costs the size of the
|
|
447
451
|
commit, not the size of the repo.
|
|
448
452
|
|
|
449
|
-
|
|
453
|
+
Three limits to know. The gate judges files a `[[scope]]` claims; a staged source file no
|
|
450
454
|
scope claims is not gated, and the hook says so on stderr (`N staged file(s) belong to no
|
|
451
455
|
scope and were not gated`) so the hole is visible the moment a new top-level directory
|
|
452
|
-
appears.
|
|
453
|
-
|
|
456
|
+
appears. A function the committed ratchet already carries a mark for is not gated either,
|
|
457
|
+
so touching signed debt does not refuse the commit; the hook reports the count on stderr
|
|
458
|
+
(`N staged function(s) carry a ratchet mark and were not gated`) and `crapkit verify` is
|
|
459
|
+
what fails a mark that rose. And git runs hooks outside your shell's activated venv: bare
|
|
460
|
+
`python` must resolve to an interpreter that has crapkit installed, or use the absolute
|
|
461
|
+
form
|
|
454
462
|
(`exec /path/to/venv/Scripts/python -m crapkit hook-precommit`).
|
|
455
463
|
|
|
456
464
|
### Route 1: `.git/hooks/pre-commit` (local, not committed)
|
|
@@ -699,7 +707,8 @@ advances the baseline nor tightens the ratchet, exit 9 included.
|
|
|
699
707
|
## Subcommands
|
|
700
708
|
|
|
701
709
|
Every subcommand takes `--repo PATH` (default `.`), and the flag goes **after** the
|
|
702
|
-
subcommand:
|
|
710
|
+
subcommand. `claude-hook` is the one exception: it has no `--repo`, because it takes its
|
|
711
|
+
root from the file named in the hook payload it reads.
|
|
703
712
|
|
|
704
713
|
```
|
|
705
714
|
$ crapkit worklist --repo /path/to/repo --scope util --top 1
|
|
@@ -720,7 +729,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
720
729
|
| Command | What it does |
|
|
721
730
|
|---|---|
|
|
722
731
|
| `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. |
|
|
723
|
-
| `doctor [--show-files] [--json] [--tune]` | 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, lanes writing their artifacts at the repo root instead of under `.crapkit/` (WARN), committed hooks under `core.hooksPath` that are not executable in the index (WARN), directories whose functions are all `untested` while their tests exist (WARN), and scopes a lane measures with no `[crapkit.scoped_tests]` template behind them (WARN), which is the loop's step 4 with nothing to run. `--tune` prints suggested parallelism knobs and writes nothing. See [docs/agent-json.md](docs/agent-json.md#doctor---json). |
|
|
732
|
+
| `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, lanes writing their artifacts at the repo root instead of under `.crapkit/` (WARN), committed hooks under `core.hooksPath` that are not executable in the index (WARN), directories whose functions are all `untested` while their tests exist (WARN), and scopes a lane measures with no `[crapkit.scoped_tests]` template behind them (WARN), 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, comparing `.claude-plugin/plugin.json`'s version against the running crapkit and every `--protocol` in `hooks/hooks.json` against the protocol `claude-hook` answers, one line per disagreement and silence when they agree. See [docs/agent-json.md](docs/agent-json.md#doctor---json). |
|
|
724
733
|
| `inventory [--db PATH] [--export PATH] [--json]` | Two lizard passes 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. |
|
|
725
734
|
| `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). |
|
|
726
735
|
| `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. |
|
|
@@ -735,11 +744,13 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
735
744
|
| `overrides [--json]` | The override audit trail: who granted what, when, and why. |
|
|
736
745
|
| `trend [--json]` | Totals per trusted run: functions, over-target count, CRAP load, average, per-scope rollup. |
|
|
737
746
|
| `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. |
|
|
747
|
+
| `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, opens no network connection, and carries no per-function CRAP or coverage, because no repo-wide payload has them; each row prints the `crapkit explain` call that does. |
|
|
738
748
|
| `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. |
|
|
739
749
|
| `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. |
|
|
740
|
-
| `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. |
|
|
750
|
+
| `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. |
|
|
741
751
|
| `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. |
|
|
742
752
|
| `hook-precommit` | The cc-only gate on staged blobs. No coverage, no snapshot, no repo-wide cache. Exit 6 on a violation. |
|
|
753
|
+
| `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 nothing is blocked, and `hook-precommit` stays the enforcement point. Exit 2 with three lines on stderr is the only thing it ever says. 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: exit 0, silence, empty stderr. Takes no `--repo` — the root is the first `crapkit.toml` above the edited file and the upward walk stops at a `.git` entry, so a worktree never borrows its parent's config. It opens no snapshot, writes nothing, and leaves stdout empty. |
|
|
743
754
|
| `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. |
|
|
744
755
|
| `mcp` | A dependency-free stdio MCP server (newline JSON-RPC 2.0) exposing nine read-only tools. See [docs/agent-json.md](docs/agent-json.md#mcp-server). |
|
|
745
756
|
|
|
@@ -753,7 +764,7 @@ crapkit: error: argument command: invalid choice: '/path/to/repo' (choose from '
|
|
|
753
764
|
| [docs/ratchet.md](docs/ratchet.md) | Seeding, pruning, the git merge driver, metric stamps, debt policy, overrides. |
|
|
754
765
|
| [docs/agent-json.md](docs/agent-json.md) | The machine surface: `schema`, every payload field, real captured examples. |
|
|
755
766
|
| [docs/adoption.md](docs/adoption.md) | The judgment layer over the quickstarts: scope granularity, exclude vs lane, scoped_tests wiring, the first-verify taint hazard. |
|
|
756
|
-
| [
|
|
767
|
+
| [plugin/](plugin/) | The Claude Code plugin: three skills (`crapkit`, `crapkit-recover`, `crapkit-onboard`), the read-side MCP server, and the advisory PostToolUse hook. Install with `claude plugin marketplace add JeanFrancoisGagne/crapkit` then `claude plugin install crapkit@crapkit`; other runtimes copy `plugin/skills/*` into their skills directory. |
|
|
757
768
|
|
|
758
769
|
[crapkit.schema.json](crapkit.schema.json) is the authority on the config file shape.
|
|
759
770
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "crapkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Deterministic CRAP-score framework: per-function complexity x coverage risk, worklists, ratchets, refactor verification"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""crapkit: deterministic CRAP-score framework."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.4.0"
|
|
@@ -6,9 +6,12 @@ importlib.metadata, email, zipfile and socket. Measured on this box: 42ms with
|
|
|
6
6
|
pygments, 16ms without, paid by every process that touches the analysis stack,
|
|
7
7
|
the pre-commit hook included.
|
|
8
8
|
|
|
9
|
-
crapkit analyzes
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
crapkit analyzes fourteen languages (typescript, tsx, javascript, python, swift,
|
|
10
|
+
go, rust, shell, powershell, cpp, objectivec, vue, java, zig). Erlang is not one
|
|
11
|
+
of them and no scope can name it, which is the whole argument for stubbing its
|
|
12
|
+
reader, so a test pins this list to `config.SUPPORTED_LANGUAGES`. The readers
|
|
13
|
+
that need pygments are still SHIPPED, not removed: `deferred_pygments()` puts
|
|
14
|
+
proxies in
|
|
12
15
|
sys.modules for the duration of the lizard import, so the readers bind stand-ins
|
|
13
16
|
and the real package loads the first time anything reads or calls one. An .erl
|
|
14
17
|
file analyzed through lizard directly gets the same answer; it just pays the
|
|
@@ -5,9 +5,11 @@ nested node_modules (measured hang on the first consumer repo).
|
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import codecs
|
|
8
9
|
import hashlib
|
|
9
10
|
import json
|
|
10
11
|
import os
|
|
12
|
+
import sys
|
|
11
13
|
import time
|
|
12
14
|
from concurrent.futures import ProcessPoolExecutor
|
|
13
15
|
from pathlib import Path
|
|
@@ -17,17 +19,40 @@ from ._pygdefer import deferred_pygments
|
|
|
17
19
|
with deferred_pygments(): # lizard's Erlang reader would load pygments here
|
|
18
20
|
import lizard
|
|
19
21
|
|
|
22
|
+
from .lizardpowershell import register as _register_powershell
|
|
23
|
+
from .lizardrust import register as _register_rust
|
|
24
|
+
from .lizardshell import register as _register_shell
|
|
25
|
+
|
|
20
26
|
from .cache import partition_by_cache, updated_cache
|
|
21
27
|
from .errors import ToolError
|
|
22
28
|
from .lizardcognitive import LizardExtension as _Cognitive
|
|
23
29
|
from .merge import FunctionRecord
|
|
30
|
+
from .packet import bare_name
|
|
31
|
+
|
|
32
|
+
# lizard picks a reader by extension off a hardcoded list, and none of these is
|
|
33
|
+
# on it: `.rs` resolves to a reader that counts no `match` arm (lizard #494),
|
|
34
|
+
# and `.sh` and `.ps1` resolve to nothing at all, which lizard answers with
|
|
35
|
+
# CLikeReader rather than a failure. All three belong HERE, at the module scope
|
|
36
|
+
# of the module a ProcessPoolExecutor child imports, or spawned workers measure
|
|
37
|
+
# with the readers lizard shipped and report plausible wrong numbers.
|
|
38
|
+
#
|
|
39
|
+
# lizardshell and lizardpowershell already register themselves on import and
|
|
40
|
+
# lizardrust deliberately does not (rebinding a name in another package's
|
|
41
|
+
# namespace is not something an import should do quietly). Calling all three
|
|
42
|
+
# keeps the wiring readable in one place and costs nothing: each is idempotent.
|
|
43
|
+
_register_rust()
|
|
44
|
+
_register_shell()
|
|
45
|
+
_register_powershell()
|
|
24
46
|
|
|
25
47
|
_POOL_THRESHOLD = 16
|
|
26
48
|
|
|
27
49
|
# Bump whenever analysis semantics change (merge rules, extension set, record
|
|
28
50
|
# extraction): the fingerprint must invalidate cached records produced by older
|
|
29
51
|
# logic even when file content and tool versions are identical.
|
|
30
|
-
ANALYSIS_VERSION =
|
|
52
|
+
ANALYSIS_VERSION = 6 # 6: five more C-family extension sets and .ps1/.psm1 are
|
|
53
|
+
# admitted, a C++ rvalue reference is no longer a
|
|
54
|
+
# cognitive condition, and source bytes decode utf-8
|
|
55
|
+
# then cp1252 instead of by machine locale
|
|
31
56
|
|
|
32
57
|
# The three tokens lizard's modified rule reacts to. Membership is checked before
|
|
33
58
|
# anything else runs, so the common token pays one frozenset lookup.
|
|
@@ -69,12 +94,39 @@ class _ModifiedDelta:
|
|
|
69
94
|
yield token
|
|
70
95
|
|
|
71
96
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
def _chain(cognitive_index: int) -> list:
|
|
98
|
+
"""lizard's standard extensions with cognitive spliced in at one index.
|
|
99
|
+
|
|
100
|
+
Index 0 puts cognitive ahead of lizard's own `preprocessing`, which is where
|
|
101
|
+
it has to sit for Python: `preprocessing` strips the whitespace tokens the
|
|
102
|
+
python indent rules read, and behind it a 6-branch function scores 6 instead
|
|
103
|
+
of 10. The delta comes last either way, where the modified pass used to sit.
|
|
104
|
+
"""
|
|
105
|
+
extensions = lizard.get_extensions(["ND"])
|
|
106
|
+
extensions.insert(cognitive_index, _Cognitive())
|
|
107
|
+
return extensions + [_ModifiedDelta()]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Two chains, built once per process each, not once per file: 14k files paid 14k
|
|
111
|
+
# chain builds. Every extension keeps its state in the generator frame __call__
|
|
112
|
+
# opens, so one chain serves every file of its kind.
|
|
113
|
+
_EXTENSIONS = _chain(0)
|
|
114
|
+
_PREPROCESSED_EXTENSIONS = _chain(1)
|
|
115
|
+
|
|
116
|
+
# lizard's SwiftReplaceLabel.preprocess RETURNS a list where the other seven
|
|
117
|
+
# preprocessors YIELD: it runs list() over its input, so every extension AHEAD of
|
|
118
|
+
# `preprocessing` is drained to exhaustion before lizard has split the file into
|
|
119
|
+
# functions. An extension at index 0 counts the whole file against one
|
|
120
|
+
# placeholder FunctionInfo, and every real function comes out at 0. SwiftReader
|
|
121
|
+
# and KotlinReader are the only two of lizard's 27 readers that inherit that
|
|
122
|
+
# preprocessor, so only these suffixes take the second chain.
|
|
123
|
+
_DRAINED_READER_SUFFIXES = (".swift", ".kt", ".kts")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _extensions_for(rel_path: str) -> list:
|
|
127
|
+
if rel_path.lower().endswith(_DRAINED_READER_SUFFIXES):
|
|
128
|
+
return _PREPROCESSED_EXTENSIONS
|
|
129
|
+
return _EXTENSIONS
|
|
78
130
|
|
|
79
131
|
|
|
80
132
|
def _record(rel_path: str, fn) -> FunctionRecord:
|
|
@@ -95,11 +147,145 @@ def _record(rel_path: str, fn) -> FunctionRecord:
|
|
|
95
147
|
)
|
|
96
148
|
|
|
97
149
|
|
|
150
|
+
# How many colliding names one warning prints before it stops. A generated file
|
|
151
|
+
# can hold hundreds; the point is that the file needs looking at, not the list.
|
|
152
|
+
_NAMES_SHOWN = 5
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _colliding_names(records: list[FunctionRecord]) -> list[str]:
|
|
156
|
+
"""Names this file gives to more than one function, in first-seen order.
|
|
157
|
+
|
|
158
|
+
Anonymous functions are exempt. lizard calls every one of them
|
|
159
|
+
`(anonymous)`, so a file with two arrow callbacks collides by construction
|
|
160
|
+
and a warning would name nothing anyone could act on; `packet.handles`
|
|
161
|
+
answers that collision with the `(anonymous)#N` ordinal instead.
|
|
162
|
+
"""
|
|
163
|
+
seen: set[str] = set()
|
|
164
|
+
colliding: dict[str, None] = {}
|
|
165
|
+
for record in records:
|
|
166
|
+
if bare_name(record.long_name) and record.long_name in seen:
|
|
167
|
+
colliding[record.long_name] = None
|
|
168
|
+
seen.add(record.long_name)
|
|
169
|
+
return list(colliding)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _warn_on_collisions(rel_path: str, records: list[FunctionRecord]) -> None:
|
|
173
|
+
"""One stderr line for a file whose records cannot all reach the ratchet.
|
|
174
|
+
|
|
175
|
+
A mark is keyed on (path, long_name), and so is the row a run writes, so the
|
|
176
|
+
last function under a colliding name is the only one marked and the only one
|
|
177
|
+
gated. C makes this ordinary: both arms of an `#ifdef` fork are textually
|
|
178
|
+
present, so a platform shim defines the same function twice in one file.
|
|
179
|
+
Python makes it ordinary too, because a method's long_name carries no class.
|
|
180
|
+
Neither is fixable here — the ratchet cannot key on a span, which drifts with
|
|
181
|
+
every edit — so the loss is announced rather than silent.
|
|
182
|
+
"""
|
|
183
|
+
names = _colliding_names(records)
|
|
184
|
+
if not names:
|
|
185
|
+
return
|
|
186
|
+
print(f"crapkit: {rel_path} defines {_listed(names)} more than once; the ratchet keys "
|
|
187
|
+
f"on (path, long_name) and keeps the last, so the earlier ones are neither "
|
|
188
|
+
f"marked nor gated", file=sys.stderr)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _listed(names: list[str]) -> str:
|
|
192
|
+
if len(names) <= _NAMES_SHOWN:
|
|
193
|
+
return ", ".join(names)
|
|
194
|
+
return f"{', '.join(names[:_NAMES_SHOWN])} and {len(names) - _NAMES_SHOWN} more name(s)"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _file_records(rel_path: str, functions) -> list[FunctionRecord]:
|
|
198
|
+
records = [_record(rel_path, fn) for fn in functions]
|
|
199
|
+
_warn_on_collisions(rel_path, records)
|
|
200
|
+
return records
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# --- how a source file's bytes become text -------------------------------------
|
|
204
|
+
#
|
|
205
|
+
# lizard opens a source file with `io.open(path, 'r')` and no encoding, so the
|
|
206
|
+
# MACHINE'S LOCALE decides what a repo scores. Measured on one function,
|
|
207
|
+
# `function Write-Café`, written once as UTF-8 and once as cp1252, read on a
|
|
208
|
+
# cp1252 interpreter and on a UTF-8 one:
|
|
209
|
+
#
|
|
210
|
+
# source cp1252 reader utf-8 reader
|
|
211
|
+
# utf-8 no function at all Write-Café
|
|
212
|
+
# cp1252 Write-Café Write-Caf
|
|
213
|
+
#
|
|
214
|
+
# The empty cell is not a rounding error: `é` arrives as `Ã` plus `©`, the `©`
|
|
215
|
+
# is no word character, and the declaration stops being one. The ratchet keys on
|
|
216
|
+
# path::long_name, so those are three different rows for one commit, and a
|
|
217
|
+
# Windows developer and a Linux CI cannot see each other's baseline.
|
|
218
|
+
#
|
|
219
|
+
# utf-8 first, cp1252 second, replacement for the five bytes cp1252 leaves
|
|
220
|
+
# undefined. That is the whole rule and it is fixed rather than environmental:
|
|
221
|
+
# all four cells above read `Write-Café`. The fallback is cp1252 rather than
|
|
222
|
+
# latin-1 because Windows PowerShell 5.1 writes cp1252, and because latin-1
|
|
223
|
+
# decodes every byte and so can never say it was wrong.
|
|
224
|
+
#
|
|
225
|
+
# UTF-16 is NOT handled. `Out-File` and the ISE write it, and such a file
|
|
226
|
+
# decodes here as NUL-separated cp1252 text that reports no function; it
|
|
227
|
+
# reported none before this change either, so nothing regressed and the narrow
|
|
228
|
+
# rule stays narrow.
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _characters(raw: bytes) -> str:
|
|
232
|
+
if raw.startswith(codecs.BOM_UTF8):
|
|
233
|
+
raw = raw[len(codecs.BOM_UTF8):]
|
|
234
|
+
try:
|
|
235
|
+
return raw.decode("utf-8")
|
|
236
|
+
except UnicodeDecodeError:
|
|
237
|
+
return raw.decode("cp1252", "replace")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def decode_source(raw: bytes) -> str:
|
|
241
|
+
"""Source bytes as text, decoded by content rather than by machine locale.
|
|
242
|
+
|
|
243
|
+
The single rule for every path into lizard: a file on disk, and a staged
|
|
244
|
+
blob the pre-commit gate never writes down. Those two must agree or the
|
|
245
|
+
gate judges different content than the inventory scores.
|
|
246
|
+
|
|
247
|
+
Line endings are normalized the way `io.open(path, 'r')` normalized them,
|
|
248
|
+
because that is what lizard did and every recorded line number and NLOC in
|
|
249
|
+
every cache depends on it: a lone `\\r` left in the stream is one more
|
|
250
|
+
whitespace token, not one more line.
|
|
251
|
+
"""
|
|
252
|
+
return _characters(raw).replace("\r\n", "\n").replace("\r", "\n")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def read_source(path: str) -> str:
|
|
256
|
+
return decode_source(Path(path).read_bytes())
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _install_decoder() -> None:
|
|
260
|
+
"""Point lizard's own file read at `read_source`. Idempotent.
|
|
261
|
+
|
|
262
|
+
A rebind rather than a replacement for `FileAnalyzer.__call__`, so lizard
|
|
263
|
+
keeps the IOError branch that answers a file deleted mid-run with an empty
|
|
264
|
+
result instead of a traceback, and keeps reading each file exactly once.
|
|
265
|
+
`lizard.py` binds `auto_read` into its own module namespace at import, and
|
|
266
|
+
`FileAnalyzer.__call__` resolves it there on every call.
|
|
267
|
+
|
|
268
|
+
Raises when that name is gone, which is what a lizard release that reads
|
|
269
|
+
source some other way would look like. Loud beats an attribute nobody
|
|
270
|
+
reads and a decode that silently went back to the locale's.
|
|
271
|
+
"""
|
|
272
|
+
if not hasattr(lizard, "auto_read"):
|
|
273
|
+
raise RuntimeError(
|
|
274
|
+
f"crapkit.analyze._install_decoder() found no lizard.auto_read to "
|
|
275
|
+
f"rebind: lizard {lizard.version} reads source some other way. "
|
|
276
|
+
f"Rewrite this against the new mechanism; leaving it undone makes "
|
|
277
|
+
f"every non-ASCII file score by the machine's locale.")
|
|
278
|
+
lizard.auto_read = read_source
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
_install_decoder()
|
|
282
|
+
|
|
283
|
+
|
|
98
284
|
def analyze_one(args: tuple[str, str]) -> tuple[str, list[FunctionRecord]]:
|
|
99
285
|
abs_path, rel_path = args
|
|
100
286
|
try:
|
|
101
|
-
analysis = lizard.FileAnalyzer(
|
|
102
|
-
return rel_path,
|
|
287
|
+
analysis = lizard.FileAnalyzer(_extensions_for(rel_path))(abs_path)
|
|
288
|
+
return rel_path, _file_records(rel_path, analysis.function_list)
|
|
103
289
|
except Exception as exc: # loud, with the file named
|
|
104
290
|
raise ToolError(f"lizard failed on {rel_path}: {exc}") from exc
|
|
105
291
|
|
|
@@ -110,11 +296,12 @@ def analyze_source(rel_path: str, code: str) -> list[FunctionRecord]:
|
|
|
110
296
|
analyze_source_code is what FileAnalyzer.__call__ runs once it has read the
|
|
111
297
|
file, so nothing about the analysis depends on whether the source arrived
|
|
112
298
|
from the disk or from a git blob the caller already holds; rel_path picks
|
|
113
|
-
the language exactly as the path on disk did.
|
|
299
|
+
the language, and with it the extension chain, exactly as the path on disk did.
|
|
114
300
|
"""
|
|
115
301
|
try:
|
|
116
|
-
|
|
117
|
-
|
|
302
|
+
analyzer = lizard.FileAnalyzer(_extensions_for(rel_path))
|
|
303
|
+
analysis = analyzer.analyze_source_code(rel_path, code)
|
|
304
|
+
return _file_records(rel_path, analysis.function_list)
|
|
118
305
|
except Exception as exc: # loud, with the file named
|
|
119
306
|
raise ToolError(f"lizard failed on {rel_path}: {exc}") from exc
|
|
120
307
|
|