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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c617f49a96d65792c3b8d3e2465effae7ad84b0c9b414770a4726cc0c8187165
4
- data.tar.gz: 689601738e13957770d0a0c1a018e37507d5d563cc46688506f6162cc67d1367
3
+ metadata.gz: 07d2bf8d0f9950fa5da4bc574daa87e441966ece6477988a67c26bebe50c1df5
4
+ data.tar.gz: dfa6942db8ea5ca53a7859704052f325f72fe006b485a047a409ee4630881bb2
5
5
  SHA512:
6
- metadata.gz: 1fdfdb2342066a65fa736103f9af0caa4472a35f444f80045ae1b88a17a1290904b1d86e610b9660cf75f2887bac4f4fbc4512c4a4ac28a09365a6fe3743545e
7
- data.tar.gz: b2ae944731ba4966ce88b5abe117b61c8fe7563659274ab03e1834c3751501c7c8a8f53b2447959b7bb86c00061ee1338de8a1097698427d31ed840456b79da7
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
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-09-06
4
+
5
+ - `swarf init` installs a Claude Code skill into `.claude/skills/swarf/` and adds
6
+ `.swarf/` to `.gitignore`.
7
+
8
+ ## 0.1.0 — 2026-09-05
9
+
10
+ Initial release.
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 two halves that never talk to each other: the **probe**, which records coverage from inside your test process, and the **runner**, which scores. The terms below name the parts of the runner.
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 DESIGN §4 exists to prevent.
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
- Scores every Ruby method by how complex it is against how well your tests actually exercise it.
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 violently steep near zero, which is the
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
- Order matters and is not negotiable: `Coverage` measures only files loaded _after_ it
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 is the actionable half:
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 # the whole project
178
- $ swarf lib/ app/ # directories, recursively
179
- $ swarf lib/app/cart.rb # a single file
180
- $ swarf --limit 50 # show 50 rows instead of 20
181
- $ swarf --limit 0 # show everything
182
- $ swarf --ignore "app/legacy/**" # skip a path (repeatable)
183
- $ swarf --all # score everything, ignoring nothing
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 about thirty lines: `Coverage.start` plus an `at_exit` that dumps the result.
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
- **Coverage needs a run; complexity does not.** Nothing static can tell you whether a line
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` is a fact, not an inference.** It comes from a VM-level call count. It
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
- (`Swarf#crap` rather than `Swarf::Score#crap`), because the block is not a class node.
286
- - `define_method` and other dynamically defined methods are not seen at all swarf reads
287
- `def`.
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. Commit: `git commit -am "Release vX.Y.Z"`
312
- 4. Run `bundle exec rake release` (builds the gem, creates the git tag, pushes to RubyGems)
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
@@ -4,4 +4,8 @@
4
4
 
5
5
  require "swarf"
6
6
 
7
- exit Swarf::CLI.run(ARGV)
7
+ begin
8
+ Swarf::CLI.run(ARGV)
9
+ rescue Swarf::Error => e
10
+ abort "swarf: #{e.message}"
11
+ end
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, err: $stderr)
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 = "Usage: swarf [options] [paths]"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Swarf
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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