swarf 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +18 -0
- data/CHANGELOG.md +10 -0
- data/CONTEXT.md +2 -2
- data/README.md +51 -76
- data/Rakefile +0 -2
- data/exe/swarf +5 -1
- data/lib/swarf/cli.rb +6 -7
- data/lib/swarf/init.rb +39 -0
- data/lib/swarf/report.rb +0 -3
- data/lib/swarf/version.rb +1 -1
- data/lib/swarf.rb +1 -5
- data/skills/swarf/SKILL.md +77 -0
- data/swarf.gemspec +1 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07d2bf8d0f9950fa5da4bc574daa87e441966ece6477988a67c26bebe50c1df5
|
|
4
|
+
data.tar.gz: dfa6942db8ea5ca53a7859704052f325f72fe006b485a047a409ee4630881bb2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a7bd9e64ef347c33bf744eeff23d3923e0a0f846ae12144438390ce27d46f24feb4e95f5f78854d627458184862c7a2bf9219632bdccf0ac115402c4df0210e
|
|
7
|
+
data.tar.gz: b328a085e7e4d7a88c58a9d48436ee37a561b83ff0a3c7596b4880780c1b0ab7d37f6b4fd6ab3470214e20b52a7b5f3bd8e14abad8ee57adf97abad5c5dd26c3
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Test
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: ruby/setup-ruby@v1
|
|
15
|
+
with:
|
|
16
|
+
ruby-version: "3.4"
|
|
17
|
+
bundler-cache: true
|
|
18
|
+
- run: bundle exec rake
|
data/CHANGELOG.md
ADDED
data/CONTEXT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Domain language
|
|
2
2
|
|
|
3
|
-
Terms used in swarf's code, tests and commit messages. The README splits swarf into
|
|
3
|
+
Terms used in swarf's code, tests and commit messages. The README splits swarf into a **probe** and a **runner**. The terms below name the parts of the runner.
|
|
4
4
|
|
|
5
5
|
## Scan
|
|
6
6
|
|
|
@@ -12,4 +12,4 @@ A scan owns no I/O policy — it is given its paths, its ignore patterns, its st
|
|
|
12
12
|
|
|
13
13
|
What test runs recorded about one file: line hits, branch outcomes, method call counts, and the SHA-256 of the bytes those numbers were measured against.
|
|
14
14
|
|
|
15
|
-
A measurement knows whether it still describes the file on disk. It answers that question the first time it is asked and remembers the answer — asked before the source is read, it would report bytes the scan never saw, which is the confidently-wrong staleness
|
|
15
|
+
A measurement knows whether it still describes the file on disk. It answers that question the first time it is asked and remembers the answer — asked before the source is read, it would report bytes the scan never saw, which is the confidently-wrong staleness the SHA-256 check exists to prevent.
|
data/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# swarf
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ask an agent to improve a codebase and it will keep going. There is always another name to argue with, another method to rearrange, and nothing in the loop that says when the returns have run out. We bikeshed the same way; the difference is that the agent bills you for it.
|
|
4
|
+
|
|
5
|
+
The CRAP score is one way out. It folds test coverage into complexity instead of measuring complexity alone: complexity sets the floor, and coverage is the only thing that brings the number down. So a hairy method the tests cover well scores as low as it can and drops off the list, while an untested one stays at the top. That ordering is the signal — it says which code is actually worth spending tokens on.
|
|
6
|
+
|
|
7
|
+
swarf scores every method in a Ruby or Rails codebase this way, worst first.
|
|
4
8
|
|
|
5
9
|
```
|
|
6
10
|
$ swarf lib/
|
|
@@ -26,11 +30,9 @@ Complexity is squared; the _uncovered_ fraction is cubed. Two identities explain
|
|
|
26
30
|
| 100% | `CC` | fully tested code is only as risky as it is complex |
|
|
27
31
|
| 0% | `CC² + CC` | untested complexity grows quadratically |
|
|
28
32
|
|
|
29
|
-
The curve is nearly flat near full coverage and
|
|
30
|
-
point: simple code and small gaps stay quiet, complex code nobody has run scores loudly.
|
|
33
|
+
The curve is nearly flat near full coverage and steep near zero: simple code and small gaps stay quiet, complex code nobody has run scores loudly.
|
|
31
34
|
|
|
32
|
-
From Alberto Savoia and Bob Evans (2007), where it stood for _Change Risk Analysis and
|
|
33
|
-
Prediction_; Robert C. Martin's ports expand it as _Change Risk Anti-Pattern_. Same formula.
|
|
35
|
+
From Alberto Savoia and Bob Evans (2007), where it stood for _Change Risk Analysis and Prediction_; Robert C. Martin's ports expand it as _Change Risk Anti-Pattern_. Same formula.
|
|
34
36
|
|
|
35
37
|
## Install
|
|
36
38
|
|
|
@@ -43,6 +45,16 @@ gem "swarf", group: :development
|
|
|
43
45
|
$ bundle install
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
Working with Claude Code? Install the skill that teaches it the loop below, so it scores
|
|
49
|
+
the files it touched and writes the test the top row asks for:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
$ bundle exec swarf init
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That writes `.claude/skills/swarf/` and adds `.swarf/` to `.gitignore`. Commit both.
|
|
56
|
+
Run it again after upgrading the gem to refresh the skill.
|
|
57
|
+
|
|
46
58
|
## Getting started
|
|
47
59
|
|
|
48
60
|
### 1. Score complexity — no setup at all
|
|
@@ -62,9 +74,7 @@ Cart#subtotal 1 — 2.00 no data lib/cart.rb:11
|
|
|
62
74
|
No coverage recorded for every file — run your suite with swarf/probe loaded.
|
|
63
75
|
```
|
|
64
76
|
|
|
65
|
-
`no data` means no test run has been recorded yet, so every method reports `CC² + CC`.
|
|
66
|
-
That is not a placeholder — it is the correct score for code nothing has run. Adding
|
|
67
|
-
coverage can only ever pull a number _down_, toward `CC`.
|
|
77
|
+
`no data` means no test run has been recorded yet, so every method reports `CC² + CC`. That is the correct score for code nothing has run. Adding coverage can only ever pull a number _down_, toward `CC`.
|
|
68
78
|
|
|
69
79
|
### 2. Record coverage — one line
|
|
70
80
|
|
|
@@ -90,8 +100,7 @@ Minitest::TestTask.create do |t|
|
|
|
90
100
|
end
|
|
91
101
|
```
|
|
92
102
|
|
|
93
|
-
**Rails** — in `test/test_helper.rb`, above `config/environment`, or the whole app loads
|
|
94
|
-
before the probe does and none of it is measured:
|
|
103
|
+
**Rails** — in `test/test_helper.rb`, above `config/environment`, or the whole app loads before the probe does and none of it is measured:
|
|
95
104
|
|
|
96
105
|
```ruby
|
|
97
106
|
ENV["RAILS_ENV"] ||= "test"
|
|
@@ -102,9 +111,7 @@ require "swarf/probe"
|
|
|
102
111
|
require_relative "../config/environment"
|
|
103
112
|
```
|
|
104
113
|
|
|
105
|
-
`bundler/setup` is only what `config/boot` would do anyway; the probe needs it to find the
|
|
106
|
-
gem this early. Rails parallelises tests by forking, which swarf handles — every worker
|
|
107
|
-
merges into the store under a lock.
|
|
114
|
+
`bundler/setup` is only what `config/boot` would do anyway; the probe needs it to find the gem this early. Rails parallelises tests by forking, which swarf handles — every worker merges into the store under a lock.
|
|
108
115
|
|
|
109
116
|
**Anything else** — set it on the command line, no files to edit:
|
|
110
117
|
|
|
@@ -112,8 +119,7 @@ merges into the store under a lock.
|
|
|
112
119
|
$ RUBYOPT="-rswarf/probe" bundle exec rake test
|
|
113
120
|
```
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
starts. Put the probe under your application and the application is invisible to it.
|
|
122
|
+
`Coverage` measures only files loaded _after_ it starts. Put the probe under your application and the application is invisible to it.
|
|
117
123
|
|
|
118
124
|
Then ignore the store:
|
|
119
125
|
|
|
@@ -136,8 +142,7 @@ Cart#shipping 3 66.7% 3.33 2/3 br lib/cart.rb:15
|
|
|
136
142
|
Cart#subtotal 1 100.0% 1.00 1/1 ln lib/cart.rb:11
|
|
137
143
|
```
|
|
138
144
|
|
|
139
|
-
Every run merges into `.swarf/coverage.json`, so partial runs are fine — running one spec
|
|
140
|
-
file does not erase what another proved. Your runs, CI's runs and a colleague's all add up.
|
|
145
|
+
Every run merges into `.swarf/coverage.json`, so partial runs are fine — running one spec file does not erase what another proved. Your runs, CI's runs and a colleague's all add up.
|
|
141
146
|
|
|
142
147
|
## Reading the report
|
|
143
148
|
|
|
@@ -148,7 +153,7 @@ file does not erase what another proved. Your runs, CI's runs and a colleague's
|
|
|
148
153
|
| `CRAP` | the score; worst first |
|
|
149
154
|
| `Evidence` | where the coverage number came from, so you know what to do next |
|
|
150
155
|
|
|
151
|
-
The evidence column
|
|
156
|
+
The evidence column tells you what to do next:
|
|
152
157
|
|
|
153
158
|
| evidence | what it means | what to do |
|
|
154
159
|
| -------------- | ------------------------------------------- | ------------------------------------ |
|
|
@@ -160,8 +165,7 @@ The evidence column is the actionable half:
|
|
|
160
165
|
|
|
161
166
|
Both `no data` and `stale` fall back to the `CRAP = CC² + CC` floor rather than guessing.
|
|
162
167
|
|
|
163
|
-
Those two also print under the table, counted by file, because the fix is per file rather
|
|
164
|
-
than per method:
|
|
168
|
+
Those two also print under the table, counted by file, because the fix is per file rather than per method:
|
|
165
169
|
|
|
166
170
|
```
|
|
167
171
|
… 45 more (--limit 0 for all)
|
|
@@ -174,20 +178,19 @@ A fully measured project prints neither line.
|
|
|
174
178
|
## Command line
|
|
175
179
|
|
|
176
180
|
```
|
|
177
|
-
$ swarf
|
|
178
|
-
$ swarf lib/ app/
|
|
179
|
-
$ swarf lib/app/cart.rb
|
|
180
|
-
$ swarf --limit 50
|
|
181
|
-
$ swarf --limit 0
|
|
182
|
-
$ swarf --ignore "app/legacy/**"
|
|
183
|
-
$ swarf --all
|
|
181
|
+
$ swarf # the whole project
|
|
182
|
+
$ swarf lib/ app/ # directories, recursively
|
|
183
|
+
$ swarf lib/app/cart.rb # a single file
|
|
184
|
+
$ swarf --limit 50 # show 50 rows instead of 20
|
|
185
|
+
$ swarf --limit 0 # show everything
|
|
186
|
+
$ swarf --ignore "app/legacy/**" # skip a path (repeatable)
|
|
187
|
+
$ swarf --all # score everything, ignoring nothing
|
|
188
|
+
$ swarf init # install the Claude Code skill, ignore .swarf/
|
|
184
189
|
$ swarf --version
|
|
185
190
|
$ swarf --help
|
|
186
191
|
```
|
|
187
192
|
|
|
188
|
-
Only the worst 20 rows print by default, with a count of what was held back. A 245-file
|
|
189
|
-
project reports 344 methods, and the tail of that list is all `CRAP 1.00` — noise that
|
|
190
|
-
buries the handful of rows worth acting on.
|
|
193
|
+
Only the worst 20 rows print by default, with a count of what was held back. A 245-file project reports 344 methods, and the tail of that list is all `CRAP 1.00` — noise that buries the handful of rows worth acting on.
|
|
191
194
|
|
|
192
195
|
## What gets skipped
|
|
193
196
|
|
|
@@ -197,9 +200,7 @@ db/** migrations and schema
|
|
|
197
200
|
**/vendor/** **/tmp/** **/log/** **/node_modules/**
|
|
198
201
|
```
|
|
199
202
|
|
|
200
|
-
Migrations matter more than they look. They are generated, run once and never tested, so
|
|
201
|
-
on a well-tested codebase they are the only untested code left and they take over the top
|
|
202
|
-
of the report — on a 20-file Rails app they ranked 3rd, 4th and 5th.
|
|
203
|
+
Migrations matter more than they look. They are generated, run once and never tested, so on a well-tested codebase they are the only untested code left and they take over the top of the report — on a 20-file Rails app they ranked 3rd, 4th and 5th.
|
|
203
204
|
|
|
204
205
|
Add your own in `.swarfignore` at the project root, one glob per line:
|
|
205
206
|
|
|
@@ -209,12 +210,9 @@ lib/api/generated_client.rb
|
|
|
209
210
|
app/legacy/**
|
|
210
211
|
```
|
|
211
212
|
|
|
212
|
-
Patterns match against each file's path relative to the directory being scanned, and both
|
|
213
|
-
`*` and `**` cross directories. Naming a path on the command line always wins, so
|
|
214
|
-
`swarf db/migrate` scores migrations even though `db/**` is a default.
|
|
213
|
+
Patterns match against each file's path relative to the directory being scanned, and both `*` and `**` cross directories. Naming a path on the command line always wins, so `swarf db/migrate` scores migrations even though `db/**` is a default.
|
|
215
214
|
|
|
216
|
-
Directories are searched for `**/*.rb`, skipping `test/`, `spec/`, `vendor/`, `tmp/` and
|
|
217
|
-
`node_modules/`. Naming one of those directly still scores it.
|
|
215
|
+
Directories are searched for `**/*.rb`, skipping `test/`, `spec/`, `vendor/`, `tmp/` and `node_modules/`. Naming one of those directly still scores it.
|
|
218
216
|
|
|
219
217
|
`SWARF_DIR` moves the coverage store, which both the probe and the runner must agree on:
|
|
220
218
|
|
|
@@ -241,54 +239,30 @@ flowchart LR
|
|
|
241
239
|
Score --> Report["report, worst first"]
|
|
242
240
|
```
|
|
243
241
|
|
|
244
|
-
The probe is
|
|
245
|
-
The runner never loads your application — it parses text.
|
|
242
|
+
The probe is twenty lines: `Coverage.start` plus an `at_exit` that dumps the result. The runner never loads your application — it parses text.
|
|
246
243
|
|
|
247
244
|
## Things worth knowing
|
|
248
245
|
|
|
249
|
-
**
|
|
250
|
-
executed. Any run counts, not just specs — a rake task, booting the app, a script.
|
|
251
|
-
|
|
252
|
-
**The probe must load first.** `Coverage` only measures files loaded after it starts. Put
|
|
253
|
-
it ahead of your application, or the application is invisible to it.
|
|
246
|
+
**Any run counts, not just specs.** A rake task, booting the app or a script all record coverage; nothing static can tell you whether a line executed.
|
|
254
247
|
|
|
255
|
-
**swarf and SimpleCov cannot both run.** Ruby permits one `Coverage.start` per process. If
|
|
256
|
-
SimpleCov gets there first, swarf warns and records nothing rather than killing your suite.
|
|
248
|
+
**swarf and SimpleCov cannot both run.** Ruby permits one `Coverage.start` per process. If SimpleCov gets there first, swarf warns and records nothing rather than killing your suite.
|
|
257
249
|
|
|
258
|
-
**Branch coverage is preferred, with a line fallback.** Ruby puts a decision on a line that
|
|
259
|
-
runs whichever way the decision goes, so `return 0 if x.negative?` reads 100% by line even
|
|
260
|
-
when the guard never fires — maximally wrong exactly where risk collects. Methods with no
|
|
261
|
-
branches fall back to lines, where "did it run" is the whole truth.
|
|
250
|
+
**Branch coverage is preferred, with a line fallback.** Ruby puts a decision on a line that runs whichever way the decision goes, so `return 0 if x.negative?` reads 100% by line even when the guard never fires — wrong exactly where risk collects. Methods with no branches fall back to lines, where "did it run" is the whole truth.
|
|
262
251
|
|
|
263
|
-
**`never called`
|
|
264
|
-
tells you to _write_ a test; `3/6 br` tells you to _extend_ one. A bare `0.0%` tells you
|
|
265
|
-
neither.
|
|
252
|
+
**`never called` comes from a VM-level call count.** It tells you to _write_ a test; `3/6 br` tells you to _extend_ one. A bare `0.0%` tells you neither.
|
|
266
253
|
|
|
267
|
-
**Edited files report `no coverage`, not stale numbers.** Coverage is indexed by line
|
|
268
|
-
number, so inserting a method at the top of a file shifts every line below it while the
|
|
269
|
-
counters stay put. swarf stores a SHA-256 per measured file and drops entries whose bytes
|
|
270
|
-
changed, because stale coverage is worse than none — it is confidently wrong.
|
|
254
|
+
**Edited files report `no coverage`, not stale numbers.** Coverage is indexed by line number, so inserting a method at the top of a file shifts every line below it while the counters stay put. swarf stores a SHA-256 per measured file and drops entries whose bytes changed, because stale coverage is confidently wrong.
|
|
271
255
|
|
|
272
|
-
**swarf's CC will not match RuboCop's.** It counts `if`, `unless`, `while`, `until`, `for`,
|
|
273
|
-
each `when`, each `in`, each `rescue`, `&&`, `||` and `&.` — **not blocks**. Six chained
|
|
274
|
-
`add_option` blocks are not six decisions. The trade-off is that CC largely ignores
|
|
275
|
-
iteration, since Ruby iterates with blocks.
|
|
256
|
+
**swarf's CC will not match RuboCop's.** It counts `if`, `unless`, `while`, `until`, `for`, each `when`, each `in`, each `rescue`, `&&`, `||` and `&.` — **not blocks**. Six chained `add_option` blocks are not six decisions. The trade-off is that CC largely ignores iteration, since Ruby iterates with blocks.
|
|
276
257
|
|
|
277
|
-
**There is no threshold and nothing fails.** swarf sorts worst-first and prints. Because
|
|
278
|
-
`CRAP = CC` at full coverage, a fixed threshold is a complexity cap in disguise — a method
|
|
279
|
-
at CC 9 can never score under 9 however well you test it, and the only remaining move is to
|
|
280
|
-
split it. Ranking is enough; capping complexity is RuboCop's job.
|
|
258
|
+
**There is no threshold and nothing fails.** swarf sorts worst-first and prints. Because `CRAP = CC` at full coverage, a fixed threshold is a complexity cap in disguise — a method at CC 9 can never score under 9 however well you test it, and the only remaining move is to split it. Ranking is enough; capping complexity is RuboCop's job.
|
|
281
259
|
|
|
282
260
|
## Known limitations
|
|
283
261
|
|
|
284
|
-
- Methods defined inside a `Struct.new do ... end` block take the enclosing module's name
|
|
285
|
-
|
|
286
|
-
- `
|
|
287
|
-
|
|
288
|
-
- Nested `def`s get their own complexity, but the outer method's coverage still counts the
|
|
289
|
-
inner method's lines and branches.
|
|
290
|
-
- Methods inside `class << self` are named correctly; methods defined by `instance_eval` or
|
|
291
|
-
a reopened singleton via a variable are not.
|
|
262
|
+
- Methods defined inside a `Struct.new do ... end` block take the enclosing module's name (`Swarf#crap` rather than `Swarf::Score#crap`), because the block is not a class node.
|
|
263
|
+
- `define_method` and other dynamically defined methods are not seen at all — swarf reads `def`.
|
|
264
|
+
- Nested `def`s get their own complexity, but the outer method's coverage still counts the inner method's lines and branches.
|
|
265
|
+
- Methods inside `class << self` are named correctly; methods defined by `instance_eval` or a reopened singleton via a variable are not.
|
|
292
266
|
|
|
293
267
|
## Development
|
|
294
268
|
|
|
@@ -308,5 +282,6 @@ $ ruby -Ilib exe/swarf lib/
|
|
|
308
282
|
|
|
309
283
|
1. Update the version in `lib/swarf/version.rb`
|
|
310
284
|
2. Run `bundle install` to update the lockfile
|
|
311
|
-
3.
|
|
312
|
-
4.
|
|
285
|
+
3. Move the `Unreleased` entries in `CHANGELOG.md` under the new version and date
|
|
286
|
+
4. Commit: `git commit -am "Release vX.Y.Z"`
|
|
287
|
+
5. Run `bundle exec rake release` (builds the gem, creates the git tag, pushes to RubyGems)
|
data/Rakefile
CHANGED
|
@@ -5,8 +5,6 @@ require "minitest/test_task"
|
|
|
5
5
|
require "standard/rake"
|
|
6
6
|
|
|
7
7
|
Minitest::TestTask.create do |task|
|
|
8
|
-
# Record swarf's coverage with swarf. The prelude runs before the tests load,
|
|
9
|
-
# which is the only place `Coverage.start` can still see them.
|
|
10
8
|
task.test_prelude = 'require "swarf/probe"'
|
|
11
9
|
end
|
|
12
10
|
|
data/exe/swarf
CHANGED
data/lib/swarf/cli.rb
CHANGED
|
@@ -4,11 +4,8 @@ require "optparse"
|
|
|
4
4
|
|
|
5
5
|
module Swarf
|
|
6
6
|
class CLI
|
|
7
|
-
def self.run(argv, out: $stdout
|
|
8
|
-
new(argv).run(out)
|
|
9
|
-
rescue Error => e
|
|
10
|
-
err.puts("swarf: #{e.message}")
|
|
11
|
-
1
|
|
7
|
+
def self.run(argv, out: $stdout)
|
|
8
|
+
(argv == ["init"]) ? Init.new.run(out) : new(argv).run(out)
|
|
12
9
|
end
|
|
13
10
|
|
|
14
11
|
def initialize(argv)
|
|
@@ -21,7 +18,6 @@ module Swarf
|
|
|
21
18
|
def run(out)
|
|
22
19
|
scores = Scan.new(paths: @paths, ignore: ignore).scores
|
|
23
20
|
out.print Report.new(scores, limit: @limit).to_s
|
|
24
|
-
0
|
|
25
21
|
end
|
|
26
22
|
|
|
27
23
|
private
|
|
@@ -34,7 +30,10 @@ module Swarf
|
|
|
34
30
|
|
|
35
31
|
def parse(argv)
|
|
36
32
|
parser = OptionParser.new do |opts|
|
|
37
|
-
opts.banner =
|
|
33
|
+
opts.banner = <<~USAGE
|
|
34
|
+
Usage: swarf [options] [paths]
|
|
35
|
+
swarf init Install the Claude Code skill and ignore .swarf/
|
|
36
|
+
USAGE
|
|
38
37
|
opts.on("-n", "--limit N", Integer, "Rows to show (0 for all, default #{Report::DEFAULT_LIMIT})") do |n|
|
|
39
38
|
@limit = n
|
|
40
39
|
end
|
data/lib/swarf/init.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Swarf
|
|
6
|
+
class Init
|
|
7
|
+
SKILL_SOURCE = File.expand_path("../../skills/swarf", __dir__)
|
|
8
|
+
SKILL_TARGET = File.join(".claude", "skills", "swarf")
|
|
9
|
+
GITIGNORE_ENTRY = ".swarf/"
|
|
10
|
+
|
|
11
|
+
def initialize(root = Dir.pwd)
|
|
12
|
+
@root = root
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(out)
|
|
16
|
+
out.puts "#{install_skill} #{SKILL_TARGET}/"
|
|
17
|
+
out.puts "#{ignore_store} #{GITIGNORE_ENTRY} in .gitignore"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def install_skill
|
|
23
|
+
target = File.join(@root, SKILL_TARGET)
|
|
24
|
+
existed = File.directory?(target)
|
|
25
|
+
FileUtils.mkdir_p(target)
|
|
26
|
+
FileUtils.cp_r(File.join(SKILL_SOURCE, "."), target)
|
|
27
|
+
existed ? "updated" : "wrote"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ignore_store
|
|
31
|
+
path = File.join(@root, ".gitignore")
|
|
32
|
+
lines = File.exist?(path) ? File.readlines(path, chomp: true) : []
|
|
33
|
+
return "kept" if lines.any? { |line| line.strip.delete_suffix("/") == GITIGNORE_ENTRY.delete_suffix("/") }
|
|
34
|
+
|
|
35
|
+
File.write(path, [*lines, GITIGNORE_ENTRY].join("\n") + "\n")
|
|
36
|
+
"added"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/swarf/report.rb
CHANGED
|
@@ -24,9 +24,6 @@ module Swarf
|
|
|
24
24
|
|
|
25
25
|
def held_back = @scores.size - @shown.size
|
|
26
26
|
|
|
27
|
-
# Notes the reader can act on, each named for the move it asks for. Kept out of the
|
|
28
|
-
# rows because the action is per file, not per method: a project nothing has run is
|
|
29
|
-
# one sentence, not one row per method.
|
|
30
27
|
def footer
|
|
31
28
|
notes = []
|
|
32
29
|
notes << "… #{held_back} more (--limit 0 for all)" if held_back.positive?
|
data/lib/swarf/version.rb
CHANGED
data/lib/swarf.rb
CHANGED
|
@@ -9,17 +9,13 @@ require_relative "swarf/score"
|
|
|
9
9
|
require_relative "swarf/report"
|
|
10
10
|
require_relative "swarf/sources"
|
|
11
11
|
require_relative "swarf/scan"
|
|
12
|
+
require_relative "swarf/init"
|
|
12
13
|
require_relative "swarf/cli"
|
|
13
14
|
|
|
14
15
|
module Swarf
|
|
15
16
|
class Error < StandardError; end
|
|
16
17
|
|
|
17
18
|
# CRAP(m) = CC(m)^2 * (1 - coverage(m))^3 + CC(m)
|
|
18
|
-
#
|
|
19
|
-
# Complexity is squared; the *uncovered* fraction is cubed. The score is
|
|
20
|
-
# nearly flat near full coverage and violently steep near zero, so simple
|
|
21
|
-
# code and small gaps stay quiet while complex code nothing has run scores
|
|
22
|
-
# loudly.
|
|
23
19
|
def self.crap(complexity, coverage)
|
|
24
20
|
(complexity**2) * ((1.0 - coverage)**3) + complexity
|
|
25
21
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: swarf
|
|
3
|
+
description: Score Ruby methods by complexity against test coverage with swarf, and turn the worst rows into the next test to write. Use after writing or changing Ruby code and before calling a change done.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# swarf
|
|
7
|
+
|
|
8
|
+
swarf scores every Ruby method with `CRAP = CC² · (1 − coverage)³ + CC` and lists them worst first. Complexity is parsed from source. Coverage is recorded by a probe loaded into the test run and stored in `.swarf/coverage.json`, so scoring works without setup but coverage needs a test run.
|
|
9
|
+
|
|
10
|
+
## The loop
|
|
11
|
+
|
|
12
|
+
1. Run the suite with the probe loaded (see Setup if unsure):
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
bundle exec rspec # or: bundle exec rake test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. Score the files you touched, not the whole project:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
git diff --name-only --diff-filter=d main -- '*.rb' | xargs bundle exec swarf
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Named paths are always scored, even under `test/`, `db/` or a `.swarfignore` pattern.
|
|
25
|
+
|
|
26
|
+
3. Take the top rows and act on the Evidence column (next section). Write or extend the test, re-run the suite, score again.
|
|
27
|
+
|
|
28
|
+
4. Stop when the touched methods sit at `CRAP = CC` with `Cov% 100.0`. Nothing you test pulls a score under its CC. If a score is too high at full coverage, that is complexity, which is a refactor question, not a testing one. Do not split methods to chase the number.
|
|
29
|
+
|
|
30
|
+
Before saying a change is done, run step 2 and report the top rows.
|
|
31
|
+
|
|
32
|
+
## Reading a row
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
Method CC Cov% CRAP Evidence Location
|
|
36
|
+
Cart#checkout 6 0.0% 42.00 never called lib/cart.rb:31
|
|
37
|
+
Cart#discount 4 50.0% 6.00 3/6 br lib/cart.rb:24
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Evidence | Meaning | Do |
|
|
41
|
+
| -------------- | -------------------------------------------- | ------------------------------------------------------------- |
|
|
42
|
+
| `never called` | A VM call count of zero. No test reaches it. | Write a test that calls it. |
|
|
43
|
+
| `3/6 br` | 3 of 6 branch outcomes ran. | Extend a test to the untaken outcomes. |
|
|
44
|
+
| `1/1 ln` | No branches, so lines are the whole truth. | Nothing, at 100%. |
|
|
45
|
+
| `no data` | No run has been recorded for this file. | The probe is not loaded, or the suite has not run. Fix Setup. |
|
|
46
|
+
| `stale` | The file changed after it was measured. | Re-run the suite, then score again. |
|
|
47
|
+
|
|
48
|
+
`no data` and `stale` both score at the `CC² + CC` floor rather than guess. They are also counted per file under the table, because the fix is per file.
|
|
49
|
+
|
|
50
|
+
A `never called` on a method a test clearly exercises means the test ran without the probe, or the probe loaded after the file. Check Setup before writing a duplicate test.
|
|
51
|
+
|
|
52
|
+
## Setup
|
|
53
|
+
|
|
54
|
+
The probe must load before the code under test, because `Coverage` only sees files loaded after it starts. One line, placed first:
|
|
55
|
+
|
|
56
|
+
- **RSpec**: first line of `.rspec`: `--require swarf/probe`
|
|
57
|
+
- **Minitest**: first line of `test/test_helper.rb`: `require "swarf/probe"`
|
|
58
|
+
- **Rails**: in `test/test_helper.rb`, after `require "bundler/setup"` and before `require_relative "../config/environment"`. Below the environment require, the whole app is invisible to it.
|
|
59
|
+
- **Anything**: `RUBYOPT="-rswarf/probe" bundle exec rake test`
|
|
60
|
+
|
|
61
|
+
Add `.swarf/` to `.gitignore`. Runs merge, so a single spec file adds to what earlier runs proved; nothing is erased until the file's bytes change.
|
|
62
|
+
|
|
63
|
+
Do not run swarf's probe alongside SimpleCov. Ruby allows one `Coverage.start` per process; if SimpleCov started first, swarf warns and records nothing.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
bundle exec swarf # whole project, worst 20
|
|
69
|
+
bundle exec swarf lib/ app/ # directories
|
|
70
|
+
bundle exec swarf app/models/cart.rb # one file
|
|
71
|
+
bundle exec swarf --limit 0 # every row
|
|
72
|
+
bundle exec swarf --ignore "app/legacy/**"
|
|
73
|
+
bundle exec swarf --all # ignore nothing, including db/ and .swarfignore
|
|
74
|
+
SWARF_DIR=/tmp/swarf bundle exec swarf # store elsewhere; set for the test run too
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
swarf's CC counts `if`, `unless`, `while`, `until`, `for`, `when`, `in`, `rescue`, `&&`, `||` and `&.`, not blocks, so it will not match RuboCop's. Methods made by `define_method` are not seen.
|
data/swarf.gemspec
CHANGED
|
@@ -14,6 +14,7 @@ Gem::Specification.new do |spec|
|
|
|
14
14
|
spec.required_ruby_version = ">= 3.4.0"
|
|
15
15
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
16
|
spec.metadata["source_code_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
17
18
|
|
|
18
19
|
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
|
|
19
20
|
ls.readlines("\x0", chomp: true).reject do |f|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: swarf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ariel Rzezak
|
|
@@ -17,8 +17,10 @@ executables:
|
|
|
17
17
|
extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
|
19
19
|
files:
|
|
20
|
+
- ".github/workflows/ci.yml"
|
|
20
21
|
- ".standard.yml"
|
|
21
22
|
- ".swarfignore"
|
|
23
|
+
- CHANGELOG.md
|
|
22
24
|
- CONTEXT.md
|
|
23
25
|
- README.md
|
|
24
26
|
- Rakefile
|
|
@@ -27,6 +29,7 @@ files:
|
|
|
27
29
|
- lib/swarf/cli.rb
|
|
28
30
|
- lib/swarf/complexity.rb
|
|
29
31
|
- lib/swarf/coverage_map.rb
|
|
32
|
+
- lib/swarf/init.rb
|
|
30
33
|
- lib/swarf/measurement.rb
|
|
31
34
|
- lib/swarf/probe.rb
|
|
32
35
|
- lib/swarf/report.rb
|
|
@@ -35,12 +38,14 @@ files:
|
|
|
35
38
|
- lib/swarf/sources.rb
|
|
36
39
|
- lib/swarf/store.rb
|
|
37
40
|
- lib/swarf/version.rb
|
|
41
|
+
- skills/swarf/SKILL.md
|
|
38
42
|
- swarf.gemspec
|
|
39
43
|
homepage: https://github.com/arzezak/swarf
|
|
40
44
|
licenses: []
|
|
41
45
|
metadata:
|
|
42
46
|
homepage_uri: https://github.com/arzezak/swarf
|
|
43
47
|
source_code_uri: https://github.com/arzezak/swarf
|
|
48
|
+
changelog_uri: https://github.com/arzezak/swarf/blob/main/CHANGELOG.md
|
|
44
49
|
rdoc_options: []
|
|
45
50
|
require_paths:
|
|
46
51
|
- lib
|