rubocop-kata 0.6.0 → 0.8.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: 30d894b4455161e0ce3dac01fa4c627de974cf89e7d1372a610749ee9ba0ad26
4
- data.tar.gz: 6bfcc2c6b38d212c8ff6ac0efc16bd4b563a74b972ccc546013619b813c0f6e2
3
+ metadata.gz: da7876f7f43029f7dfb90b0fb9527987fe9c50ee40336d0ba4fffdcc2cbad83f
4
+ data.tar.gz: 902e0c01d3cc5e71553d3c90c68a707b7f3a554cc3e6ec1228f0640ae8772d6a
5
5
  SHA512:
6
- metadata.gz: f7c5bd992aa75df3b4f38c9182005dc7f346b4ea0db83e5756fb02f638acbc034b32a7146dece44e7c9421ce4d04cd064e02ce46309d6ba4513a8e5560540c5e
7
- data.tar.gz: '08f28bf480cf2fcef885db1d5df0c92d3200a53b898dcc01fc026172214276a5a83a1a2b0b6bc807e2e4aed9ac78439cac6c50b7a53643986e44c05bf7a0e388'
6
+ metadata.gz: 99fdeb3a9f112fbc7232f59da504bde048071f20d50e55ef367e5516119a307a68a4f071a371cdbbe7810248268ef526f5bbdec08c5e9068e14d17cee03661ca
7
+ data.tar.gz: 9603d8560c96c877a5cb8f614bf10cab2aabf63aa5aace3209cc0596b06dab7a0edd4372919068a5c345b48cff076c1964b453ba38caa1dd3d026a96ad6c8371
data/CHANGELOG.md CHANGED
@@ -1,6 +1,87 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.8.0] - 2026-08-20
4
+
5
+ - New `rubocop-kata plan` command: buckets the backlog into `structure`,
6
+ `naming`, `prose` and `rest`, and names the stage to take next. Kata's
7
+ structural cops create names and its naming cops charge for them, so the net
8
+ offense count moves the wrong way mid-refactor and cannot be read as progress.
9
+ - New `rubocop-kata skill` command: prints an agent skill to stdout, so
10
+ redirecting it installs the skill wherever an agent reads them from. It works
11
+ the stage `plan` names one file at a time, checks every name it introduces
12
+ against the dictionary `Kata/RealWords` reads, and reports removed and
13
+ created separately.
14
+ - `Kata/AgentNoun` no longer truncates a compound to its prefix. Truncation is
15
+ not injective — `EvidencePoller`, `EvidenceValidator` and `EvidenceWaiter`
16
+ were each told to become `Evidence` — and it lands on names that already
17
+ exist elsewhere, which a per-file cop cannot see. It now derives the agent
18
+ word and keeps the rest, so `EvidenceValidator` suggests
19
+ `EvidenceValidation`, and stays quiet when no derivation applies.
20
+ - `Kata/AgentNoun` `AllowedNames` gains `Controller`, `Mailer` and
21
+ `Serializer`: suffixes a framework resolves a class by are contracts, not
22
+ naming choices.
23
+ - `Kata/EnvDiscipline` no longer flags `ENV` as a parameter default.
24
+ `def enabled?(env = ENV)` is the injection the cop asks for: the parameter
25
+ is the seam, and the default is what the boot layer would pass anyway.
26
+ - Every cop that excluded `spec/**/*` now excludes `test/**/*` too. Fifteen
27
+ cops shipped an RSpec-only exclusion, so a Minitest suite got the
28
+ production rules.
29
+ - The boot layer that `Kata/EnvDiscipline`, `Kata/IoDiscipline` and
30
+ `Kata/ClockDiscipline` exempt now covers an application's, not only a gem's:
31
+ `config/`, `db/seeds.rb`, `db/seeds/`, `lib/tasks/`, `Rakefile` and
32
+ `*.rake`, alongside `bin/` and `exe/`.
33
+ - `Kata/NoComments` now exempts `Gemfile` as it already exempts `*.gemspec`. A
34
+ dependency manifest cannot say why a dependency is pinned in code.
35
+ - `Metrics/MethodLength` counts an array, hash, heredoc or method call that
36
+ spans lines as one line. `Max: 5` and `Layout/LineLength: 120` were
37
+ otherwise unsatisfiable together: wrapping a long line spent method budget.
38
+ - `Layout/ArgumentAlignment` now uses `with_fixed_indentation`. Its default
39
+ aligns continuation arguments under the first argument's column, a step
40
+ `Elegant/MonotonicIndents` rejects.
41
+ - `Layout/EmptyLinesAroundAttributeAccessor` is off. It requires a blank line
42
+ after `attr_reader` where `Elegant/NoEmptyLinesInBlocks` forbids one; inside
43
+ an `included do` block the two autocorrect in a loop.
44
+ - `Lint/NumberConversion` is off. Its message names a replacement that
45
+ raises: `Integer(x, 10)` is only valid when `x` is a String, and a `.to_i`
46
+ receiver is usually a Time, a BigDecimal or an Integer.
47
+ - The five bundled cops that exempt test files now also match Rails'
48
+ `foo_test.rb`, `foo_spec.rb`, and suites addressed by directory. Upstream
49
+ matched `**/*Test.rb` and `**/test_*.rb` only.
50
+ - `Elegant/ClassInModule` is off. Its offenses cannot be cleared: it reports a
51
+ class nested in a class as global, and `Elegant/NoClassInModule` forbids the
52
+ module its message asks for, so the pair admits no shape. It also reads a
53
+ top-level constant as a defect, which is how Rails resolves one. Reported
54
+ upstream as yegor256/rubocop-elegant#75.
55
+ - `Elegant/PairedBrackets` autocorrect is off. It inserts a newline beside a
56
+ bracket without taking the whitespace already there, so corrected lines drift
57
+ right and `Elegant/MonotonicIndents` then reports the line it just wrote. The
58
+ rule still reports; fix the brackets by hand. Reported upstream as
59
+ yegor256/rubocop-elegant#76.
60
+ - Development runs against giacope/rubocop-elegant#fixes, the released gem plus
61
+ five open pull requests. A gemspec cannot name a git source, so consumers who
62
+ want the fixes rather than the workarounds add the same line to their own
63
+ Gemfile; the README says how.
64
+
65
+ ## [0.7.0] - 2026-08-16
66
+
67
+ - New `rubocop-kata doctor` command: reports `.rubocop.yml` entries that
68
+ configure a cop the inherited configuration already disables, and
69
+ `rubocop:disable`/`enable` comments naming a cop that is not enabled where
70
+ the comment sits. RuboCop reports neither. Exits non-zero when it finds
71
+ something.
72
+ - `Style/DisableCopsWithinSourceCodeDirective` is now on.
73
+ - `Kata/AgentNoun` suggests a name when it can derive one: a compound minus its
74
+ agent word (`PaymentProcessor` -> `Payment`), or a regular
75
+ -ator/-ector/-isor/-izer noun the shipped dictionary confirms (`Selector` ->
76
+ `Selection`, `Synthesizer` -> `Synthesis`). It never proposes a name it would
77
+ flag in turn.
78
+ - `Kata/AgentNoun` `AllowedNames` entries now match a whole name or a trailing
79
+ segment of one, so the default `Error` also exempts `UsageError` and
80
+ `ParseError` without listing them.
81
+ - `Kata/BuilderNoun` gained `AllowedNames`, for the method whose name an
82
+ external API dictates (`get_callbacks`).
83
+ - The dictionary moved to `RuboCop::Kata::Dictionary`, shared by
84
+ `Kata/RealWords` and `Kata/AgentNoun`.
4
85
 
5
86
  ## [0.6.0] - 2026-08-15
6
87
 
data/README.md CHANGED
@@ -17,13 +17,17 @@ that dodge with a shipped dictionary instead of a word list you maintain:
17
17
  - **Fifteen house cops.** Naming, dependency discipline, and data honesty in the Elegant Objects spirit — from `Kata/AgentNoun` to `Kata/ClockDiscipline`.
18
18
 
19
19
  ```ruby
20
- class PaymentProcessor # Kata/AgentNoun: `PaymentProcessor` names a doer;
21
- end # name the class for the thing it is, not the work it does.
20
+ class PaymentProcessor # Kata/AgentNoun: `PaymentProcessor` names a doer; name the class
21
+ end # for the thing it is, not the work it does. Try `Payment`.
22
22
 
23
23
  class Payment # OK
24
24
  end
25
25
  ```
26
26
 
27
+ It suggests a name only when it can derive one: a compound minus its agent word,
28
+ or a regular `-ator`/`-ector`/`-isor`/`-izer` noun the shipped dictionary
29
+ confirms (`Selector` → `Selection`, `Synthesizer` → `Synthesis`).
30
+
27
31
  ## Getting started
28
32
 
29
33
  Add the gem to your `Gemfile`:
@@ -49,32 +53,133 @@ bundle exec rubocop
49
53
 
50
54
  That's it. The plugin loads the bundled extensions and the shared defaults, so your `.rubocop.yml` keeps only what's specific to your project. Requires Ruby >= 3.4 and RuboCop ~> 1.75.
51
55
 
56
+ ### The bundled rubocop-elegant
57
+
58
+ The released `rubocop-elegant` 0.7.1 has four defects this project reported
59
+ upstream: `NoRedundantVariable` autocorrect corrupts Ruby 3.1 shorthand
60
+ ([#74](https://github.com/yegor256/rubocop-elegant/issues/74)), `ClassInModule`
61
+ reports a class nested in a class as global
62
+ ([#75](https://github.com/yegor256/rubocop-elegant/issues/75)),
63
+ `PairedBrackets` autocorrect drifts the indent
64
+ ([#76](https://github.com/yegor256/rubocop-elegant/issues/76)), and the
65
+ test-file exclusions never match a Rails or RSpec suite
66
+ ([#77](https://github.com/yegor256/rubocop-elegant/issues/77)).
67
+
68
+ The defaults here neutralise all four. `Elegant/ClassInModule` is off, because
69
+ its offences cannot be cleared: the cop it ships beside forbids the module it
70
+ asks for. The two broken correctors are set to report only, because their
71
+ offences can be cleared, by hand. The exclusions are widened. Nothing corrupts
72
+ your code out of the box, and nothing reports a defect you cannot fix.
73
+
74
+ A gemspec cannot name a git source, so to take the fixes themselves rather than
75
+ the workarounds, add this to your own `Gemfile`:
76
+
77
+ ```ruby
78
+ gem "rubocop-elegant", github: "giacope/rubocop-elegant", branch: "fixes"
79
+ ```
80
+
81
+ That branch is the released gem plus the five pull requests, with the version
82
+ pinned so Bundler resolves it. With it in place you can turn the two correctors
83
+ back on:
84
+
85
+ ```yaml
86
+ Elegant/NoRedundantVariable:
87
+ AutoCorrect: true
88
+
89
+ Elegant/PairedBrackets:
90
+ AutoCorrect: true
91
+
92
+ Elegant/ClassInModule:
93
+ Enabled: true
94
+ ```
95
+
96
+ Drop all three once upstream releases the fixes. On a Rails codebase, consider
97
+ leaving `Elegant/ClassInModule` off for good: it wants every class inside a
98
+ module, and Zeitwerk resolves `class Account` from `app/models/account.rb` as a
99
+ top-level constant by design.
100
+
52
101
  ## The cops
53
102
 
54
103
  | Cop | Default | What it enforces |
55
104
  | --- | --- | --- |
56
- | `Kata/AgentNoun` | on | Classes named for what they are, not `-er`/`-or` doers. Allow exceptions via `AllowedNames`. |
105
+ | `Kata/AgentNoun` | on | Classes named for what they are, not `-er`/`-or` doers. Suggests the better name when it can derive one. `AllowedNames` matches a whole name or a trailing segment, so `Error` covers `UsageError`. |
57
106
  | `Kata/NoComments` | on | No prose comments; say it in the code. Magic comments, linter directives, and licence headers survive. Autocorrects. |
58
- | `Kata/IoDiscipline` | on | No bare `puts`/`warn`/`pp`/`p` outside specs/scripts; write through an injected `@io` or an explicit receiver. |
107
+ | `Kata/IoDiscipline` | on | No bare `puts`/`warn`/`pp`/`p` outside the test suite and the boot layer; write through an injected `@io` or an explicit receiver. |
59
108
  | `Kata/ProsePlacement` | off | Sentence-length strings belong in the presentation layer. Enable with an `Include`/`Exclude` matching your layering. |
60
109
  | `Kata/NoUtilName` | on | No junk-drawer names (`Util`, `Helper`, `Manager`, `Service`, …). Tune via `BannedNames`. |
61
110
  | `Kata/RealWords` | on | Every name segment is a word the shipped dictionary knows — `errorcount` (smash) and `cfg` (abbreviation) both fail, with no word list to maintain. Tune via `Terms`/`BannedWords`/`AllowedNames`. |
62
111
  | `Kata/GoodMethodName` | on | One word per method name; a second word needs a role prefix (`after_fork`), a role suffix (`file_of`), or a reviewed `Terms` entry. Tune via `MaxWords`/`Prefixes`/`Suffixes`/`Terms`/`AllowedNames`. |
63
112
  | `Kata/GoodVariableName` | on | The same rule for locals, parameters, ivars, class variables, and globals; `_name` and `@_name` stay exempt. |
64
- | `Kata/BuilderNoun` | on | Builders named for what they return: `total`, not `calculate_total`. Tune via `BannedPrefixes`. |
113
+ | `Kata/BuilderNoun` | on | Builders named for what they return: `total`, not `calculate_total`. Tune via `BannedPrefixes`/`AllowedNames`. |
65
114
  | `Kata/NoBooleanFlag` | on | No positional boolean arguments; split the method or use a keyword. |
66
115
  | `Kata/ConstructorDiscipline` | on | `initialize` assigns, raises, or freezes — never computes. |
67
116
  | `Kata/NoClassMethodLogic` | on | Class methods construct (`build`, `parse`, `of`, `from_*`); instances do the work. |
68
117
  | `Kata/NoHashAsObject` | on | A hash with `MaxKeys`+ keys (default 4) wants to be an object. Keyword-argument call sites exempt. |
69
118
  | `Kata/ClockDiscipline` | on | No bare `Time.now`/`Date.today`/`.current`; inject a clock. |
70
- | `Kata/EnvDiscipline` | on | `ENV` reads only in the boot layer (`config/`, `bin/`, `exe/`). |
119
+ | `Kata/EnvDiscipline` | on | `ENV` reads only in the boot layer (`config/`, `db/seeds*`, `lib/tasks/`, rake files, `bin/`, `exe/`) — or as a parameter default, which is the seam the cop asks for. |
120
+
121
+ ## Dead configuration
122
+
123
+ `doctor` reports configuration that no longer does anything: a `.rubocop.yml`
124
+ entry for a cop the inherited configuration disables, and a
125
+ `rubocop:disable`/`enable` comment naming a cop that is not enabled where the
126
+ comment sits. RuboCop reports neither.
127
+
128
+ ```sh
129
+ bundle exec rubocop-kata doctor # or: doctor path/to/project
130
+ ```
131
+
132
+ ```
133
+ .rubocop.yml:20: `Elegant/GoodMethodName` is disabled by the configuration this project inherits; the entry does nothing.
134
+ lib/registry.rb:44: the directive names `Elegant/GoodMethodName`, which is not enabled here; the comment does nothing.
135
+ 2 dead entries
136
+ ```
137
+
138
+ It exits non-zero when it finds something, so it can gate CI.
139
+
140
+ ## Adoption order
141
+
142
+ Kata's structural cops create names and its naming cops charge for them, so a
143
+ structural refactor removes offenses and adds more. A single total cannot tell
144
+ progress from regression. `plan` buckets the backlog into the stages that cause
145
+ each other and names the one to take next.
146
+
147
+ ```sh
148
+ bundle exec rubocop-kata plan # or: plan path/to/project
149
+ ```
150
+
151
+ ```
152
+ structure 184 Kata/ConstructorDiscipline 92, Kata/NoHashAsObject 48, Kata/EnvDiscipline 44
153
+ naming 263 Kata/RealWords 141, Kata/GoodVariableName 88, Kata/AgentNoun 34
154
+ prose 57 Kata/NoComments 57
155
+ rest 412 Elegant/PairedBrackets 300, Layout/LineLength 112
156
+ next: structure — 184 offenses in 61 files; these mint the names `naming` then prices, so take them first
157
+ densest: app/models/account.rb (14)
158
+ ```
159
+
160
+ Structure before naming, because doing naming first means renaming things the
161
+ structural pass is about to move.
162
+
163
+ The gem also ships an agent skill that runs this loop: it works the stage `plan`
164
+ names, checks every name it introduces against the same dictionary
165
+ `Kata/RealWords` reads, and reports removed and created separately instead of a
166
+ net total.
167
+
168
+ ```sh
169
+ mkdir -p .claude/skills/rubocop-kata
170
+ bundle exec rubocop-kata skill > .claude/skills/rubocop-kata/SKILL.md
171
+ ```
172
+
173
+ It writes to stdout, so the same command installs it anywhere an agent reads
174
+ skills from — a project, `~/.claude/skills/`, or a plugin.
71
175
 
72
176
  ## The defaults
73
177
 
74
178
  Double-quoted strings, `Metrics/MethodLength: 5`, `Metrics/ClassLength: 100`,
75
179
  `Metrics/ParameterLists: 4`, 120-column lines, endless methods on one line,
76
- `rescue => error`, `NewCops: enable`, and heredocs counted as one line in
77
- spec examples. See
180
+ `rescue => error`, `NewCops: enable`, heredocs counted as one line in spec
181
+ examples, and no inline `rubocop:disable` comments
182
+ (`Style/DisableCopsWithinSourceCodeDirective`). See
78
183
  [config/default.yml](config/default.yml).
79
184
 
80
185
  ## License
data/config/default.yml CHANGED
@@ -11,19 +11,31 @@ AllCops:
11
11
  - "vendor/**/*"
12
12
 
13
13
  Kata/AgentNoun:
14
- Description: "Name classes for the thing they are, not the work they do."
14
+ Description: >-
15
+ Name classes for the thing they are, not the work they do. `AllowedNames`
16
+ entries match a whole name or a trailing segment of one, so `Error` also
17
+ exempts `UsageError`. The default list holds the suffixes a framework
18
+ resolves a class by, which are contracts rather than naming choices.
15
19
  Enabled: true
16
20
  VersionAdded: "0.1"
21
+ VersionChanged: "0.8"
17
22
  AllowedNames:
18
23
  - Error
24
+ - Controller
25
+ - Mailer
26
+ - Serializer
19
27
 
20
28
  Kata/NoComments:
21
29
  Description: "Say it in the code; keep only comments the machine or the law reads."
22
30
  Enabled: true
23
31
  VersionAdded: "0.1"
32
+ VersionChanged: "0.8"
24
33
  Exclude:
25
34
  - "spec/**/*"
35
+ - "test/**/*"
26
36
  - "*.gemspec"
37
+ - "Gemfile"
38
+ - "**/Gemfile"
27
39
 
28
40
  Kata/GoodMethodName:
29
41
  Description: >-
@@ -141,11 +153,18 @@ Kata/IoDiscipline:
141
153
  Description: "Write through an injected io, not bare puts/warn/pp/p."
142
154
  Enabled: true
143
155
  VersionAdded: "0.1"
156
+ VersionChanged: "0.8"
144
157
  Exclude:
145
158
  - "spec/**/*"
159
+ - "test/**/*"
146
160
  - "bin/**/*"
147
161
  - "exe/**/*"
162
+ - "config/**/*"
163
+ - "db/seeds.rb"
164
+ - "db/seeds/**/*"
165
+ - "lib/tasks/**/*"
148
166
  - "Rakefile"
167
+ - "**/*.rake"
149
168
  - "*.gemspec"
150
169
 
151
170
  Kata/ProsePlacement:
@@ -202,12 +221,14 @@ Kata/BuilderNoun:
202
221
  Description: "Builders are named for what they return; verbs are for side effects."
203
222
  Enabled: true
204
223
  VersionAdded: "0.3"
224
+ VersionChanged: "0.7"
205
225
  BannedPrefixes:
206
226
  - get
207
227
  - calculate
208
228
  - compute
209
229
  - retrieve
210
230
  - generate
231
+ AllowedNames: []
211
232
 
212
233
  Kata/NoBooleanFlag:
213
234
  Description: "A boolean flag is two methods trapped in one."
@@ -215,6 +236,7 @@ Kata/NoBooleanFlag:
215
236
  VersionAdded: "0.3"
216
237
  Exclude:
217
238
  - "spec/**/*"
239
+ - "test/**/*"
218
240
 
219
241
  Kata/ConstructorDiscipline:
220
242
  Description: "Constructors assign; they do not compute."
@@ -222,6 +244,7 @@ Kata/ConstructorDiscipline:
222
244
  VersionAdded: "0.3"
223
245
  Exclude:
224
246
  - "spec/**/*"
247
+ - "test/**/*"
225
248
 
226
249
  Kata/NoClassMethodLogic:
227
250
  Description: "Classes construct; instances work."
@@ -229,6 +252,7 @@ Kata/NoClassMethodLogic:
229
252
  VersionAdded: "0.3"
230
253
  Exclude:
231
254
  - "spec/**/*"
255
+ - "test/**/*"
232
256
  AllowedNames:
233
257
  - build
234
258
  - parse
@@ -241,27 +265,43 @@ Kata/NoHashAsObject:
241
265
  MaxKeys: 4
242
266
  Exclude:
243
267
  - "spec/**/*"
268
+ - "test/**/*"
244
269
  - "config/**/*"
245
270
 
246
271
  Kata/ClockDiscipline:
247
272
  Description: "Read time from an injected clock, not the ambient one."
248
273
  Enabled: true
249
274
  VersionAdded: "0.3"
275
+ VersionChanged: "0.8"
250
276
  Exclude:
251
277
  - "spec/**/*"
278
+ - "test/**/*"
252
279
  - "bin/**/*"
253
280
  - "exe/**/*"
281
+ - "config/**/*"
282
+ - "db/seeds.rb"
283
+ - "db/seeds/**/*"
284
+ - "lib/tasks/**/*"
285
+ - "Rakefile"
286
+ - "**/*.rake"
287
+ - "*.gemspec"
254
288
 
255
289
  Kata/EnvDiscipline:
256
290
  Description: "ENV belongs in the boot layer; pass configuration in."
257
291
  Enabled: true
258
292
  VersionAdded: "0.3"
293
+ VersionChanged: "0.8"
259
294
  Exclude:
260
295
  - "spec/**/*"
261
- - "config/**/*"
296
+ - "test/**/*"
262
297
  - "bin/**/*"
263
298
  - "exe/**/*"
299
+ - "config/**/*"
300
+ - "db/seeds.rb"
301
+ - "db/seeds/**/*"
302
+ - "lib/tasks/**/*"
264
303
  - "Rakefile"
304
+ - "**/*.rake"
265
305
  - "*.gemspec"
266
306
 
267
307
  Style/Documentation:
@@ -308,26 +348,42 @@ Layout/LineEndStringConcatenationIndentation:
308
348
  Metrics/BlockLength:
309
349
  Exclude:
310
350
  - "spec/**/*"
351
+ - "test/**/*"
311
352
  - "*.gemspec"
312
353
 
313
354
  Metrics/ClassLength:
314
355
  Max: 100
356
+ Exclude:
357
+ - "spec/**/*"
358
+ - "test/**/*"
359
+ - "**/*_test.rb"
315
360
 
316
361
  Metrics/ModuleLength:
317
362
  Max: 100
318
363
  Exclude:
319
364
  - "spec/**/*"
365
+ - "test/**/*"
366
+ - "**/*_test.rb"
320
367
 
368
+ # A wrapped call reads as one line, so it costs one; otherwise Max and
369
+ # Layout/LineLength cannot both be satisfied.
321
370
  Metrics/MethodLength:
322
371
  Max: 5
372
+ CountAsOne:
373
+ - array
374
+ - hash
375
+ - heredoc
376
+ - method_call
323
377
  Exclude:
324
378
  - "spec/**/*"
379
+ - "test/**/*"
325
380
 
326
381
  Metrics/ParameterLists:
327
382
  Max: 4
328
383
  CountKeywordArgs: true
329
384
  Exclude:
330
385
  - "spec/**/*"
386
+ - "test/**/*"
331
387
 
332
388
  RSpec/ExampleLength:
333
389
  CountAsOne:
@@ -339,10 +395,12 @@ RSpec/VerifiedDoubles:
339
395
  Elegant/NoEmptyLinesInBlocks:
340
396
  Exclude:
341
397
  - "spec/**/*"
398
+ - "test/**/*"
342
399
 
343
400
  Elegant/NoEmptyLinesInMethods:
344
401
  Exclude:
345
402
  - "spec/**/*"
403
+ - "test/**/*"
346
404
 
347
405
  Elegant/NoComments:
348
406
  Enabled: false
@@ -364,6 +422,16 @@ Layout/ClosingParenthesisIndentation:
364
422
  Layout/FirstArgumentIndentation:
365
423
  Enabled: false
366
424
 
425
+ # Its default aligns continuation arguments under the first argument's column,
426
+ # an arbitrary step Elegant/MonotonicIndents rejects; a fixed step is +2.
427
+ Layout/ArgumentAlignment:
428
+ EnforcedStyle: with_fixed_indentation
429
+
430
+ # Requires a blank line after `attr_reader` where Elegant/NoEmptyLinesInBlocks
431
+ # forbids one; inside an `included do` block the two loop.
432
+ Layout/EmptyLinesAroundAttributeAccessor:
433
+ Enabled: false
434
+
367
435
  Layout/MultilineMethodCallBraceLayout:
368
436
  Enabled: false
369
437
 
@@ -379,21 +447,32 @@ Layout/EmptyLinesAroundAccessModifier:
379
447
  Layout/EmptyLinesAfterModuleInclusion:
380
448
  Enabled: false
381
449
 
450
+ # --- A disable comment is a note saying what the code should have said;
451
+ # `rubocop-kata doctor` finds the ones core cannot see.
452
+ Style/DisableCopsWithinSourceCodeDirective:
453
+ Enabled: true
454
+
382
455
  # --- Correctors known to rewrite code incorrectly; report only.
383
456
  # Inlines a variable past side effects (ivar resets, later statements) and
384
457
  # corrupts Ruby 3.1 keyword shorthand.
385
458
  Elegant/NoRedundantVariable:
386
459
  AutoCorrect: false
387
460
 
461
+ # Inserts a newline beside a bracket without taking the whitespace already
462
+ # there, so every corrected line drifts right and MonotonicIndents then
463
+ # reports the line it just wrote.
464
+ Elegant/PairedBrackets:
465
+ AutoCorrect: false
466
+
388
467
  # Rewrites reopened classes into modules, clashing with the primary
389
468
  # class definition at boot.
390
469
  Style/StaticClass:
391
470
  AutoCorrect: false
392
471
 
393
- # Turns lenient `.to_i` into strict Integer(x, 10), which raises on
394
- # nils/Integers call sites legitimately receive.
472
+ # Its message names a replacement that raises: Integer(x, 10) is only
473
+ # valid when x is a String, and most `.to_i` receivers are not.
395
474
  Lint/NumberConversion:
396
- AutoCorrect: false
475
+ Enabled: false
397
476
 
398
477
  # Alphabetizing requires breaks load-order-dependent boot sequences.
399
478
  Style/RequireOrder:
@@ -402,3 +481,39 @@ Style/RequireOrder:
402
481
  ThreadSafety/DirChdir:
403
482
  Exclude:
404
483
  - "spec/**/*"
484
+ - "test/**/*"
485
+
486
+ # --- Elegant's test-file exclusions never match Rails' `foo_test.rb`, nor a
487
+ # suite addressed by directory.
488
+ # Off until upstream ships: it reports a class nested in a class as global, and
489
+ # Elegant/NoClassInModule forbids the module the message asks for, so the pair
490
+ # admits no shape. It also reads a top-level constant as a defect, which is how
491
+ # Rails resolves one. https://github.com/yegor256/rubocop-elegant/issues/75
492
+ Elegant/ClassInModule:
493
+ Enabled: false
494
+ inherit_mode:
495
+ merge:
496
+ - Exclude
497
+ Exclude:
498
+ - "**/*_test.rb"
499
+ - "test/**/*"
500
+ - "spec/**/*"
501
+
502
+ Elegant/NoClassInModule:
503
+ inherit_mode:
504
+ merge:
505
+ - Exclude
506
+ Exclude:
507
+ - "**/*_test.rb"
508
+ - "test/**/*"
509
+ - "spec/**/*"
510
+
511
+ Elegant/OneClassPerFile:
512
+ inherit_mode:
513
+ merge:
514
+ - Exclude
515
+ Exclude:
516
+ - "**/*_test.rb"
517
+ - "test/**/*"
518
+ - "spec/**/*"
519
+
data/exe/rubocop-kata ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "rubocop-kata"
5
+ require "rubocop/kata/checkup"
6
+ require "rubocop/kata/plan"
7
+ require "rubocop/kata/kit"
8
+
9
+ COMMANDS = { "doctor" => RuboCop::Kata::Checkup, "plan" => RuboCop::Kata::Plan, "skill" => RuboCop::Kata::Kit }.freeze
10
+
11
+ command = COMMANDS[ARGV.first]
12
+ abort("usage: rubocop-kata #{COMMANDS.keys.join("|")} [path]") if command.nil?
13
+
14
+ exit(command.new(ARGV.fetch(1, Dir.pwd)).run)
@@ -2,8 +2,10 @@
2
2
 
3
3
  class RuboCop::Cop::Kata::AgentNoun < RuboCop::Cop::Base
4
4
  MSG = "`%s` names a doer; name the class for the thing it is, not the work it does."
5
-
5
+ HINT = "#{MSG} Try `%s`.".freeze
6
6
  SUFFIX = /(?:er|or)\z/
7
+ SEGMENT = /[A-Z]+(?=[A-Z][a-z]|\z)|[A-Z][a-z0-9]*/
8
+ DERIVATIONS = [[/ator\z/, "ation"], [/ector\z/, "ection"], [/isor\z/, "ision"], [/i[zs]er\z/, "is"]].freeze
7
9
 
8
10
  def on_class(node) = check(node)
9
11
 
@@ -15,8 +17,33 @@ class RuboCop::Cop::Kata::AgentNoun < RuboCop::Cop::Base
15
17
  name = node.identifier.short_name.to_s
16
18
  return unless SUFFIX.match?(name)
17
19
  return if allowed?(name)
18
- add_offense(node.identifier, message: format(MSG, name))
20
+ add_offense(node.identifier, message: message(name))
21
+ end
22
+
23
+ def message(name)
24
+ hint = suggestion(name)
25
+ hint ? format(HINT, name, hint) : format(MSG, name)
19
26
  end
20
27
 
21
- def allowed?(name) = Array(cop_config["AllowedNames"]).map(&:to_s).include?(name)
28
+ def suggestion(name)
29
+ head, tail = split(name)
30
+ DERIVATIONS.filter_map { proposal(head + tail.sub(it.first, it.last)) }.first
31
+ end
32
+
33
+ def split(name)
34
+ segments = name.scan(SEGMENT)
35
+ [segments[0..-2].join, segments.last.to_s]
36
+ end
37
+
38
+ def proposal(name)
39
+ return if name.empty? || SUFFIX.match?(name)
40
+ return unless known?(name)
41
+ name
42
+ end
43
+
44
+ def known?(name) = RuboCop::Kata::Dictionary::ENTRIES.include?(name.scan(SEGMENT).last.to_s.downcase)
45
+
46
+ def allowed?(name) = list.any? { name.end_with?(it) }
47
+
48
+ def list = Array(cop_config["AllowedNames"]).map(&:to_s).reject(&:empty?)
22
49
  end
@@ -12,10 +12,12 @@ class RuboCop::Cop::Kata::BuilderNoun < RuboCop::Cop::Base
12
12
  def check(node)
13
13
  name = node.method_name.to_s
14
14
  verb = prefix(name)
15
- return unless verb
15
+ return if verb.nil? || allowed?(name)
16
16
  add_offense(node.loc.name, message: format(MSG, name.delete_prefix("#{verb}_"), name))
17
17
  end
18
18
 
19
+ def allowed?(name) = Array(cop_config["AllowedNames"]).map(&:to_s).include?(name)
20
+
19
21
  def prefix(name)
20
22
  Array(cop_config["BannedPrefixes"]).map(&:to_s).find { name.start_with?("#{it}_") }
21
23
  end
@@ -6,6 +6,11 @@ class RuboCop::Cop::Kata::EnvDiscipline < RuboCop::Cop::Base
6
6
  def on_const(node)
7
7
  return unless node.short_name == :ENV
8
8
  return unless node.namespace.nil? || node.namespace.cbase_type?
9
+ return if seam?(node)
9
10
  add_offense(node, message: MSG)
10
11
  end
12
+
13
+ private
14
+
15
+ def seam?(node) = node.each_ancestor(:optarg, :kwoptarg).any?
11
16
  end
@@ -1,18 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "zlib"
4
-
5
3
  class RuboCop::Cop::Kata::RealWords < RuboCop::Cop::Base
6
4
  MSG = "%s is not in the dictionary — restore the underscore between smashed words, spell the " \
7
5
  "abbreviation out, or add it to `Terms` if it is one domain term here."
8
6
  ABBREVIATION_MSG = "%s is an abbreviation; spell the word out."
9
- WORDS = File.expand_path("../../../../data/words.txt.gz", __dir__)
10
- SOFTWARE = File.expand_path("../../../../data/software.txt.gz", __dir__)
11
- EXTRA = File.expand_path("../../../../data/supplement.txt", __dir__)
12
- DICTIONARY = Set.new(
13
- [WORDS, SOFTWARE].flat_map { Zlib.gunzip(File.binread(it)).split("\n") } +
14
- File.readlines(EXTRA, chomp: true)
15
- ).freeze
16
7
  SIGIL = /\A(@@|@|\$)/
17
8
  DERIVATIONS = [
18
9
  [/s\z/, ""], [/es\z/, ""], [/ies\z/, "y"],
@@ -63,7 +54,7 @@ class RuboCop::Cop::Kata::RealWords < RuboCop::Cop::Base
63
54
 
64
55
  def word?(segment) = segment.length < 2 || term?(segment) || (known?(segment) && !banned?(segment))
65
56
 
66
- def known?(segment) = forms(segment).any? { DICTIONARY.include?(it) }
57
+ def known?(segment) = forms(segment).any? { RuboCop::Kata::Dictionary::ENTRIES.include?(it) }
67
58
 
68
59
  def forms(segment)
69
60
  base = segment.sub(/\d+\z/, "")
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ class RuboCop::Kata::Checkup
6
+ INERT = "%s:%d: `%s` is disabled by the configuration this project inherits; the entry does nothing."
7
+ DEAD = "%s:%d: the directive names `%s`, which is not enabled here; the comment does nothing."
8
+ TALLY = "%d dead entr%s"
9
+ KEY = %r{\A([A-Z]\w*/\w+):}
10
+
11
+ def initialize(root, io: $stdout)
12
+ @root = root
13
+ @io = io
14
+ end
15
+
16
+ def run
17
+ found = inert + dead
18
+ found.each { @io.puts(it) }
19
+ @io.puts(found.empty? ? "clean" : tally(found))
20
+ found.empty? ? 0 : 1
21
+ end
22
+
23
+ private
24
+
25
+ def tally(found) = format(TALLY, found.length, found.one? ? "y" : "ies")
26
+
27
+ def inert
28
+ entries.filter_map { |key, line| format(INERT, file, line, key) unless live?(key) }
29
+ end
30
+
31
+ def entries
32
+ lines.each_with_index.filter_map { |text, index| [text[KEY, 1], index + 1] if text.match?(KEY) }
33
+ end
34
+
35
+ def lines = File.file?(file) ? File.readlines(file) : []
36
+
37
+ def file = File.join(@root, ".rubocop.yml")
38
+
39
+ def live?(key) = config.cop_enabled?(key) || off?(key)
40
+
41
+ def off?(key) = project.dig(key, "Enabled") == false
42
+
43
+ def project = @_project ||= (YAML.safe_load_file(file, aliases: true) if File.file?(file)) || {}
44
+
45
+ def dead = targets.flat_map { directives(it) }
46
+
47
+ def targets = RuboCop::TargetFinder.new(store).find([@root], :only_recognized_file_types)
48
+
49
+ def directives(path)
50
+ RuboCop::ProcessedSource.from_file(path, config.target_ruby_version).comments
51
+ .map { RuboCop::DirectiveComment.new(it) }
52
+ .filter_map { finding(path, it) }
53
+ rescue StandardError
54
+ []
55
+ end
56
+
57
+ def finding(path, directive)
58
+ return unless directive.start_with_marker? && !directive.all_cops?
59
+ stale = directive.cop_names.reject { active?(it, path) }
60
+ return if stale.empty?
61
+ format(DEAD, path, directive.line_number, stale.join("`, `"))
62
+ end
63
+
64
+ def active?(name, path)
65
+ configuration = store.for_file(path)
66
+ cop = RuboCop::Cop::Registry.global.find_by_cop_name(name)
67
+ configuration.cop_enabled?(name) && cop&.new(configuration)&.relevant_file?(path)
68
+ end
69
+
70
+ def config = @_config ||= store.for_dir(@root)
71
+
72
+ def store = @_store ||= RuboCop::ConfigStore.new
73
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module RuboCop::Kata::Dictionary
6
+ WORDS = File.expand_path("../../../data/words.txt.gz", __dir__)
7
+ SOFTWARE = File.expand_path("../../../data/software.txt.gz", __dir__)
8
+ EXTRA = File.expand_path("../../../data/supplement.txt", __dir__)
9
+ ENTRIES = Set.new(
10
+ [WORDS, SOFTWARE].flat_map { Zlib.gunzip(File.binread(it)).split("\n") } +
11
+ File.readlines(EXTRA, chomp: true)
12
+ ).freeze
13
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RuboCop::Kata::Kit
4
+ SOURCE = File.expand_path("../../../skills/rubocop-kata/SKILL.md", __dir__)
5
+ MISSING = "the gem ships no skill at %s"
6
+
7
+ def initialize(_root, io: $stdout)
8
+ @io = io
9
+ end
10
+
11
+ def run
12
+ return missing unless File.file?(SOURCE)
13
+ @io.print(File.read(SOURCE))
14
+ 0
15
+ end
16
+
17
+ private
18
+
19
+ def missing
20
+ @io.puts(format(MISSING, SOURCE))
21
+ 1
22
+ end
23
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "tempfile"
5
+
6
+ class RuboCop::Kata::Plan
7
+ STAGES = {
8
+ "structure" => %w[
9
+ Kata/ConstructorDiscipline Kata/NoClassMethodLogic Kata/NoHashAsObject
10
+ Kata/NoBooleanFlag Kata/IoDiscipline Kata/ClockDiscipline Kata/EnvDiscipline
11
+ ],
12
+ "naming" => %w[
13
+ Kata/RealWords Kata/GoodMethodName Kata/GoodVariableName Kata/AgentNoun
14
+ Kata/BuilderNoun Kata/NoUtilName
15
+ ],
16
+ "prose" => %w[Kata/NoComments Kata/ProsePlacement]
17
+ }.freeze
18
+ REST = "rest"
19
+ ROW = "%-10s %6d %s"
20
+ NEXT = "next: %s — %d offense%s in %d file%s%s"
21
+ MINTS = "; these mint the names `naming` then prices, so take them first"
22
+ DENSE = "densest: %s (%d)"
23
+ CLEAN = "clean"
24
+
25
+ def initialize(root, io: $stdout)
26
+ @root = root
27
+ @io = io
28
+ end
29
+
30
+ def run
31
+ found = offenses
32
+ return clean if found.empty?
33
+ STAGES.keys.push(REST).each { row(it, found) }
34
+ advise(found)
35
+ 1
36
+ end
37
+
38
+ private
39
+
40
+ def clean
41
+ @io.puts(CLEAN)
42
+ 0
43
+ end
44
+
45
+ def row(stage, found)
46
+ picked = within(found, stage)
47
+ return if picked.empty?
48
+ @io.puts(format(ROW, stage, picked.length, breakdown(picked)))
49
+ end
50
+
51
+ def breakdown(picked)
52
+ picked.group_by { it.fetch("cop") }
53
+ .sort_by { |_, list| -list.length }
54
+ .first(3)
55
+ .map { |cop, list| "#{cop} #{list.length}" }
56
+ .join(", ")
57
+ end
58
+
59
+ def advise(found)
60
+ picked = within(found, upcoming(found))
61
+ @io.puts(headline(upcoming(found), picked))
62
+ @io.puts(format(DENSE, *hottest(picked)))
63
+ end
64
+
65
+ def upcoming(found) = STAGES.keys.find { |stage| within(found, stage).any? } || REST
66
+
67
+ def headline(stage, picked)
68
+ files = picked.map { it.fetch("path") }.uniq
69
+ format(NEXT, stage, picked.length, plural(picked), files.length, plural(files), note(stage))
70
+ end
71
+
72
+ def note(stage) = stage == REST ? "" : MINTS
73
+
74
+ def hottest(picked) = picked.map { it.fetch("path") }.tally.max_by(&:last)
75
+
76
+ def plural(list) = list.one? ? "" : "s"
77
+
78
+ def within(found, stage) = found.select { it.fetch("stage") == stage }
79
+
80
+ def offenses = report.fetch("files", []).flat_map { rows(it) }
81
+
82
+ def rows(file)
83
+ file.fetch("offenses", []).map do
84
+ { "path" => file.fetch("path"), "cop" => it.fetch("cop_name"), "stage" => stage(it.fetch("cop_name")) }
85
+ end
86
+ end
87
+
88
+ def stage(cop) = STAGES.find { |_, cops| cops.include?(cop) }&.first || REST
89
+
90
+ def report = @_report ||= JSON.parse(inspection)
91
+
92
+ def inspection = Dir.chdir(@root) { sweep }
93
+
94
+ def sweep
95
+ Tempfile.create("rubocop-kata-plan") do |sink|
96
+ RuboCop::CLI.new.run(["--format", "json", "--out", sink.path, "."])
97
+ File.read(sink.path)
98
+ end
99
+ end
100
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Kata
5
- VERSION = "0.6.0"
5
+ VERSION = "0.8.0"
6
6
  end
7
7
  end
data/lib/rubocop-kata.rb CHANGED
@@ -27,5 +27,6 @@ require_relative "rubocop/cop/kata/no_hash_as_object"
27
27
  require_relative "rubocop/cop/kata/no_util_name"
28
28
  require_relative "rubocop/cop/kata/prose_placement"
29
29
  require_relative "rubocop/cop/kata/real_words"
30
+ require_relative "rubocop/kata/dictionary"
30
31
  require_relative "rubocop/kata/plugin"
31
32
  require_relative "rubocop/kata/version"
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: rubocop-kata
3
+ description: >-
4
+ Adopt rubocop-kata's rules on an existing codebase without the offense count going
5
+ the wrong way. Kata's structural cops (ConstructorDiscipline, NoHashAsObject,
6
+ Io/Clock/EnvDiscipline, NoClassMethodLogic, NoBooleanFlag) create names, and its naming
7
+ cops (RealWords, GoodMethodName, GoodVariableName, AgentNoun, BuilderNoun, NoUtilName)
8
+ charge for every name created — so a structural refactor removes offenses and adds more,
9
+ and a single total cannot tell progress from regression. This skill sequences the work by
10
+ stage, picks names that are already clean by checking them against the dictionary the cops
11
+ read, and reports removed and created separately. Use when adopting rubocop-kata, working
12
+ down a large kata backlog, deciding what to fix next, or when kata offenses went up after a
13
+ refactor. Triggers on "rubocop-kata", "kata offenses", "kata offences", "kata backlog",
14
+ "adopt kata".
15
+ ---
16
+
17
+ # rubocop-kata adoption
18
+
19
+ > The count goes up mid-refactor because the refactor is doing its job. Stop reading the
20
+ > total; read the two numbers underneath it.
21
+
22
+ ## The problem this solves
23
+
24
+ Move a collaborator out of a constructor and you satisfy `Kata/ConstructorDiscipline`. You
25
+ also mint a parameter, an ivar, and probably a reader — three names, each priced by
26
+ `Kata/RealWords`, `Kata/GoodVariableName`, and `Kata/BuilderNoun`. Net offenses can rise
27
+ while the code strictly improves. There is no configuration that fixes this, because the
28
+ names are real and someone has to choose them.
29
+
30
+ So choose them well the first time. Every name is checkable *before* it is written, against
31
+ the same dictionary the cops read. Do that and the naming debt is never created.
32
+
33
+ ## Procedure
34
+
35
+ ### 1. Clear dead configuration first
36
+
37
+ ```
38
+ rubocop-kata doctor
39
+ ```
40
+
41
+ It reports `.rubocop.yml` entries for cops the inherited config already disables, and
42
+ `rubocop:disable` comments naming a cop that is not enabled where the comment sits. RuboCop
43
+ reports neither. Delete what it names — it is free, and it stops you tuning knobs that are
44
+ not connected to anything.
45
+
46
+ ### 2. Read the plan
47
+
48
+ ```
49
+ rubocop-kata plan
50
+ ```
51
+
52
+ It buckets every offense into four stages and names the one to work:
53
+
54
+ - **structure** — the cops that change the shape of the code, and in doing so mint names.
55
+ - **naming** — the cops that price names.
56
+ - **prose** — `NoComments`, `ProsePlacement`.
57
+ - **rest** — bundled and core cops; mostly mechanical, mostly autocorrectable.
58
+
59
+ Work the stage `plan` names, in that order. Structure before naming is the whole point: doing
60
+ naming first means renaming things the structural pass is about to move or delete.
61
+
62
+ ### 3. Work one file at a time
63
+
64
+ Take the file `plan` calls densest. Fix its offenses for the **current stage only** — leave
65
+ the other stages' offenses alone even when they are on the line you are editing. Mixing
66
+ stages is what makes a diff unreviewable and a count unreadable.
67
+
68
+ For `rest`, prefer `rubocop -a` (safe corrections). Do not run `-A` (unsafe) across a
69
+ codebase; kata turns off the correctors known to rewrite code wrongly, but unsafe correctors
70
+ outside that list still change behaviour.
71
+
72
+ ### 4. Check every name before you write it
73
+
74
+ This is the step that keeps the count honest. Kata's naming rules, condensed:
75
+
76
+ | rule | cop |
77
+ | --- | --- |
78
+ | one word, lowercase, ≤16 chars | `GoodMethodName`, `GoodVariableName` |
79
+ | a second word only via a role prefix (`after_fork`), a role suffix (`file_of`), or a reviewed `Terms` entry | same |
80
+ | never smash the underscore out — `errorcount` fails too | `RealWords` |
81
+ | every segment is a real word; no `cfg`, `ctx`, `msg`, `tmp`, … | `RealWords` |
82
+ | classes are not `-er`/`-or` doers | `AgentNoun` |
83
+ | no `Util`, `Helper`, `Manager`, `Service`, `Common`, `Shared` | `NoUtilName` |
84
+ | a method that returns something is named for what it returns, not `get_`/`calculate_`/`compute_` | `BuilderNoun` |
85
+ | memoization ivars are `@_name` | `Naming/MemoizedInstanceVariableName` |
86
+
87
+ Check a candidate against the shipped dictionary before committing to it:
88
+
89
+ ```
90
+ ruby -rrubocop-kata -e 'puts ARGV.map { |w| "#{w}: #{RuboCop::Kata::Dictionary::ENTRIES.include?(w.downcase)}" }' clock sink cursor
91
+ ```
92
+
93
+ If a word the domain genuinely needs is not in the dictionary, that is a `Terms` entry in
94
+ `.rubocop.yml`, not a reason to pick a worse name — but add it deliberately, one at a time,
95
+ and say why in the commit.
96
+
97
+ When a name is genuinely two words and neither the prefix nor the suffix list fits, that is
98
+ usually the cop telling you an object is missing. `evidence_poller` wants to be
99
+ `Evidence#poll` or an `EvidenceValidation`. Prefer extracting the object over adding a
100
+ `Terms` entry.
101
+
102
+ ### 5. Re-run `plan` and report two numbers
103
+
104
+ ```
105
+ rubocop-kata plan
106
+ ```
107
+
108
+ Report the delta as **removed** and **created**, never as a net total:
109
+
110
+ > structure 184 → 142 (−42). naming 263 → 263 (+0 created, 21 names introduced, all clean).
111
+
112
+ A rising `naming` number after a structural pass means step 4 was skipped, not that the
113
+ refactor was wrong. Go back and fix the names you just wrote — it is cheaper now than after
114
+ they spread.
115
+
116
+ ## Rules of engagement
117
+
118
+ - **Never `rubocop --auto-gen-config` to make a stage disappear.** A todo file converts a
119
+ backlog you can see into one you cannot. If a cop is genuinely wrong for this codebase,
120
+ turn it off in `.rubocop.yml` with a comment saying why; `doctor` will tell you later if
121
+ that entry stops meaning anything.
122
+ - **Never widen `AllowedNames`/`Terms` in bulk.** One entry, one reviewed reason.
123
+ - **One stage per commit.** The reviewer needs to see a structural move as a structural move.
124
+ - **Stop and ask** when a fix requires a decision you cannot verify from the code: what a
125
+ domain word means, whether a class is part of a public API, whether a framework resolves a
126
+ class by its name. Guessing on those creates rework that lints clean.
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-kata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giacomo GK
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -112,10 +112,13 @@ description: Bundles a curated RuboCop stack (rspec, performance, elegant, packa
112
112
  and adds fifteen cops of its own — naming (GoodMethodName, GoodVariableName, dictionary-backed
113
113
  RealWords, AgentNoun, BuilderNoun, NoUtilName), discipline (IoDiscipline, ClockDiscipline,
114
114
  EnvDiscipline, ConstructorDiscipline, NoClassMethodLogic), and shape (NoComments,
115
- NoBooleanFlag, NoHashAsObject, ProsePlacement).
115
+ NoBooleanFlag, NoHashAsObject, ProsePlacement). The rubocop-kata command reports
116
+ dead configuration (doctor), buckets a backlog into the stages that cause each other
117
+ (plan), and prints an agent skill for working it (skill).
116
118
  email:
117
119
  - giaco@hey.com
118
- executables: []
120
+ executables:
121
+ - rubocop-kata
119
122
  extensions: []
120
123
  extra_rdoc_files: []
121
124
  files:
@@ -128,6 +131,7 @@ files:
128
131
  - data/software.txt.gz
129
132
  - data/supplement.txt
130
133
  - data/words.txt.gz
134
+ - exe/rubocop-kata
131
135
  - lib/rubocop-kata.rb
132
136
  - lib/rubocop/cop/kata/agent_noun.rb
133
137
  - lib/rubocop/cop/kata/builder_noun.rb
@@ -144,8 +148,13 @@ files:
144
148
  - lib/rubocop/cop/kata/no_util_name.rb
145
149
  - lib/rubocop/cop/kata/prose_placement.rb
146
150
  - lib/rubocop/cop/kata/real_words.rb
151
+ - lib/rubocop/kata/checkup.rb
152
+ - lib/rubocop/kata/dictionary.rb
153
+ - lib/rubocop/kata/kit.rb
154
+ - lib/rubocop/kata/plan.rb
147
155
  - lib/rubocop/kata/plugin.rb
148
156
  - lib/rubocop/kata/version.rb
157
+ - skills/rubocop-kata/SKILL.md
149
158
  homepage: https://github.com/giacope/rubocop-kata
150
159
  licenses:
151
160
  - MIT