vigiles 2.0.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.
Files changed (156) hide show
  1. package/.claude/settings.json +46 -0
  2. package/.claude/settings.local.json +8 -0
  3. package/.claude-plugin/hooks/post-edit.sh +34 -0
  4. package/.claude-plugin/hooks/pre-edit.sh +40 -0
  5. package/.claude-plugin/hooks/session-start.sh +38 -0
  6. package/.claude-plugin/marketplace.json +14 -0
  7. package/.claude-plugin/plugin.json +47 -0
  8. package/.github/workflows/ci.yml +81 -0
  9. package/.prettierignore +1 -0
  10. package/.vigiles/generated.d.ts +205 -0
  11. package/CLAUDE.md +95 -0
  12. package/CLAUDE.md.spec.ts +142 -0
  13. package/CONTRIBUTING.md +121 -0
  14. package/LICENSE +21 -0
  15. package/README.md +377 -0
  16. package/action.yml +25 -0
  17. package/dist/action.d.ts +7 -0
  18. package/dist/action.d.ts.map +1 -0
  19. package/dist/action.js +180 -0
  20. package/dist/action.js.map +1 -0
  21. package/dist/cli.d.ts +12 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +1267 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/cli.test.d.ts +2 -0
  26. package/dist/cli.test.d.ts.map +1 -0
  27. package/dist/cli.test.js +650 -0
  28. package/dist/cli.test.js.map +1 -0
  29. package/dist/compile.d.ts +101 -0
  30. package/dist/compile.d.ts.map +1 -0
  31. package/dist/compile.js +503 -0
  32. package/dist/compile.js.map +1 -0
  33. package/dist/evolve.d.ts +132 -0
  34. package/dist/evolve.d.ts.map +1 -0
  35. package/dist/evolve.js +599 -0
  36. package/dist/evolve.js.map +1 -0
  37. package/dist/freshness.d.ts +67 -0
  38. package/dist/freshness.d.ts.map +1 -0
  39. package/dist/freshness.js +244 -0
  40. package/dist/freshness.js.map +1 -0
  41. package/dist/freshness.test.d.ts +2 -0
  42. package/dist/freshness.test.d.ts.map +1 -0
  43. package/dist/freshness.test.js +356 -0
  44. package/dist/freshness.test.js.map +1 -0
  45. package/dist/generate-types.d.ts +34 -0
  46. package/dist/generate-types.d.ts.map +1 -0
  47. package/dist/generate-types.js +381 -0
  48. package/dist/generate-types.js.map +1 -0
  49. package/dist/inline.d.ts +58 -0
  50. package/dist/inline.d.ts.map +1 -0
  51. package/dist/inline.js +142 -0
  52. package/dist/inline.js.map +1 -0
  53. package/dist/inline.test.d.ts +5 -0
  54. package/dist/inline.test.d.ts.map +1 -0
  55. package/dist/inline.test.js +152 -0
  56. package/dist/inline.test.js.map +1 -0
  57. package/dist/linters.d.ts +38 -0
  58. package/dist/linters.d.ts.map +1 -0
  59. package/dist/linters.js +588 -0
  60. package/dist/linters.js.map +1 -0
  61. package/dist/proofs.d.ts +272 -0
  62. package/dist/proofs.d.ts.map +1 -0
  63. package/dist/proofs.js +622 -0
  64. package/dist/proofs.js.map +1 -0
  65. package/dist/proofs.test.d.ts +9 -0
  66. package/dist/proofs.test.d.ts.map +1 -0
  67. package/dist/proofs.test.js +952 -0
  68. package/dist/proofs.test.js.map +1 -0
  69. package/dist/spec.d.ts +258 -0
  70. package/dist/spec.d.ts.map +1 -0
  71. package/dist/spec.js +113 -0
  72. package/dist/spec.js.map +1 -0
  73. package/dist/spec.test.d.ts +2 -0
  74. package/dist/spec.test.d.ts.map +1 -0
  75. package/dist/spec.test.js +1222 -0
  76. package/dist/spec.test.js.map +1 -0
  77. package/dist/types.d.ts +101 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +3 -0
  80. package/dist/types.js.map +1 -0
  81. package/dist/validate.d.ts +10 -0
  82. package/dist/validate.d.ts.map +1 -0
  83. package/dist/validate.js +286 -0
  84. package/dist/validate.js.map +1 -0
  85. package/dist/validate.test.d.ts +2 -0
  86. package/dist/validate.test.d.ts.map +1 -0
  87. package/dist/validate.test.js +531 -0
  88. package/dist/validate.test.js.map +1 -0
  89. package/docs/agent-setup.md +85 -0
  90. package/docs/agent-workflows.md +103 -0
  91. package/docs/comparison.md +71 -0
  92. package/docs/freshness.md +124 -0
  93. package/docs/inline-mode.md +119 -0
  94. package/docs/linter-support.md +166 -0
  95. package/docs/spec-format.md +194 -0
  96. package/eslint.config.mjs +79 -0
  97. package/examples/CLAUDE.md +54 -0
  98. package/examples/CLAUDE.md.spec.ts +65 -0
  99. package/examples/SKILL.md +50 -0
  100. package/examples/SKILL.md.spec.ts +57 -0
  101. package/fixtures/example-project/CLAUDE.md +11 -0
  102. package/fixtures/example-project/package.json +9 -0
  103. package/fixtures/example-project/src/index.ts +3 -0
  104. package/fixtures/example-project/src/utils.test.ts +2 -0
  105. package/fixtures/example-project/src/utils.ts +3 -0
  106. package/logo.png +0 -0
  107. package/package.json +42 -0
  108. package/research/adoption-strategy.md +111 -0
  109. package/research/agent-integration.md +145 -0
  110. package/research/ai-code-quality.md +197 -0
  111. package/research/code-search-for-agents.md +313 -0
  112. package/research/competitive-landscape.md +163 -0
  113. package/research/doc-freshness.md +516 -0
  114. package/research/executable-specs.md +368 -0
  115. package/research/feature-ideas.md +464 -0
  116. package/research/formal-proofs-for-agents.md +338 -0
  117. package/research/fp-for-agent-harness.md +150 -0
  118. package/research/fp-for-deterministic-ai.md +131 -0
  119. package/research/self-evolving-specs.md +298 -0
  120. package/schemas/claude-md-strict.yml +18 -0
  121. package/schemas/claude-md.yml +6 -0
  122. package/schemas/skill-strict.yml +12 -0
  123. package/schemas/skill.yml +5 -0
  124. package/skills/audit-feedback-loop/SKILL.md +76 -0
  125. package/skills/edit-spec/SKILL.md +131 -0
  126. package/skills/enforce-rules-format/SKILL.md +71 -0
  127. package/skills/generate-logo/SKILL.md +102 -0
  128. package/skills/generate-rule/SKILL.md +90 -0
  129. package/skills/linter-docs/clippy.md +241 -0
  130. package/skills/linter-docs/eslint.md +384 -0
  131. package/skills/linter-docs/pylint.md +288 -0
  132. package/skills/linter-docs/rubocop.md +277 -0
  133. package/skills/linter-docs/ruff.md +187 -0
  134. package/skills/linter-docs/stylelint.md +247 -0
  135. package/skills/migrate-to-spec/SKILL.md +124 -0
  136. package/skills/pr-to-lint-rule/SKILL.md +97 -0
  137. package/skills/strengthen/SKILL.md +168 -0
  138. package/src/action.ts +214 -0
  139. package/src/cli.test.ts +914 -0
  140. package/src/cli.ts +1631 -0
  141. package/src/compile.ts +691 -0
  142. package/src/evolve.ts +781 -0
  143. package/src/freshness.test.ts +449 -0
  144. package/src/freshness.ts +299 -0
  145. package/src/generate-types.ts +448 -0
  146. package/src/inline.test.ts +206 -0
  147. package/src/inline.ts +164 -0
  148. package/src/linters.ts +739 -0
  149. package/src/proofs.test.ts +1314 -0
  150. package/src/proofs.ts +849 -0
  151. package/src/spec.test.ts +1471 -0
  152. package/src/spec.ts +427 -0
  153. package/src/types.ts +117 -0
  154. package/src/validate.test.ts +701 -0
  155. package/src/validate.ts +381 -0
  156. package/tsconfig.json +23 -0
@@ -0,0 +1,277 @@
1
+ # RuboCop — Reference
2
+
3
+ Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom cops).
4
+
5
+ ## Check Existing Gems First
6
+
7
+ Before writing a custom cop, search these gems — the pattern may already be covered:
8
+
9
+ | Gem | Scope | Key cops to know |
10
+ | --------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
11
+ | `rubocop` (core) | Style, Lint, Metrics, Naming, Security | `Style/FrozenStringLiteralComment`, `Lint/UnusedMethodArgument`, `Metrics/MethodLength`, `Naming/MethodName`, `Security/Eval` |
12
+ | `rubocop-rails` | Rails conventions | `Rails/HttpPositionalArguments`, `Rails/SkipsModelValidations`, `Rails/Output`, `Rails/HasAndBelongsToMany`, `Rails/DynamicFindBy` |
13
+ | `rubocop-rspec` | RSpec test patterns | `RSpec/ExampleLength`, `RSpec/MultipleExpectations`, `RSpec/NestedGroups`, `RSpec/LetSetup`, `RSpec/MessageSpies` |
14
+ | `rubocop-minitest` | Minitest patterns | `Minitest/AssertEmptyLiteral`, `Minitest/RefuteNil`, `Minitest/AssertInDelta` |
15
+ | `rubocop-performance` | Performance anti-patterns | `Performance/StringReplacement`, `Performance/Detect`, `Performance/Count`, `Performance/FlatMap`, `Performance/CaseWhenSplat` |
16
+ | `rubocop-rake` | Rakefile patterns | `Rake/Desc`, `Rake/DuplicateTask`, `Rake/MethodDefinitionInTask` |
17
+ | `rubocop-sorbet` | Sorbet type checking | `Sorbet/ForbidSuperclassConstLiteral`, `Sorbet/ValidSigil`, `Sorbet/SignatureBuildOrder` |
18
+ | `rubocop-graphql` | GraphQL conventions | `GraphQL/FieldDescription`, `GraphQL/ObjectDescription`, `GraphQL/ArgumentDescription` |
19
+ | `rubocop-factory_bot` | FactoryBot patterns | `FactoryBot/ConsistentParenthesesStyle`, `FactoryBot/CreateList`, `FactoryBot/SyntaxMethods` |
20
+ | `rubocop-capybara` | Capybara test patterns | `Capybara/CurrentPathExpectation`, `Capybara/VisibilityMatcher`, `Capybara/SpecificMatcher` |
21
+
22
+ **Tip:** Many patterns can be handled by configuring existing cops rather than writing new ones. Check if an existing cop has configurable `AllowedMethods`, `AllowedPatterns`, or `Include`/`Exclude` options first.
23
+
24
+ ## Cop Anatomy
25
+
26
+ Every RuboCop cop inherits from `RuboCop::Cop::Base`:
27
+
28
+ ```ruby
29
+ # frozen_string_literal: true
30
+
31
+ module RuboCop
32
+ module Cop
33
+ module Custom
34
+ # Disallow direct database queries outside the repository layer.
35
+ #
36
+ # @example
37
+ # # bad
38
+ # User.where(active: true)
39
+ #
40
+ # # good
41
+ # UserRepository.active_users
42
+ class NoDirectDbQuery < Base
43
+ MSG = 'Use the repository pattern — call `%<model>sRepository` instead of `%<model>s.%<method>s`.'
44
+
45
+ RESTRICT_ON_SEND = %i[where find find_by first last all count pluck].freeze
46
+
47
+ # @!method ar_model_query?(node)
48
+ def_node_matcher :ar_model_query?, <<~PATTERN
49
+ (send (const nil? _) {:where :find :find_by :first :last :all :count :pluck} ...)
50
+ PATTERN
51
+
52
+ def on_send(node)
53
+ return unless ar_model_query?(node)
54
+
55
+ model = node.receiver.short_name
56
+ add_offense(node, message: format(MSG, model: model, method: node.method_name))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ ```
63
+
64
+ ### Key concepts
65
+
66
+ | Concept | Purpose | Notes |
67
+ | ------------------ | --------------------------------------------- | ------------------------------------------------------------------- |
68
+ | `MSG` | Error message string | Use `format` with `%<name>s` for interpolation |
69
+ | `RESTRICT_ON_SEND` | Whitelist of method names | Performance optimization — cop only runs on matching `send` nodes |
70
+ | `def_node_matcher` | AST pattern matcher | DSL for matching Ruby AST patterns — replaces manual node traversal |
71
+ | `def_node_search` | Like `def_node_matcher` but finds all matches | Returns an enumerator of matching nodes |
72
+ | `add_offense` | Report a violation | Accepts `node`, optional `message:` and `severity:` |
73
+
74
+ ### Cop departments
75
+
76
+ | Department | When to use |
77
+ | ---------- | ----------------------------------------------------- |
78
+ | `Lint` | Code that is/will be broken |
79
+ | `Style` | Code that works but violates a convention |
80
+ | `Metrics` | Complexity thresholds (method length, ABC size, etc.) |
81
+ | `Naming` | Naming conventions |
82
+ | `Security` | Security anti-patterns |
83
+ | `Custom` | Your project-specific cops |
84
+
85
+ ## Node Pattern DSL — Cheat Sheet
86
+
87
+ RuboCop's node pattern DSL is the primary way to match AST nodes:
88
+
89
+ | You want to detect | Pattern | Notes |
90
+ | --------------------- | ----------------------------------- | --------------------------------------------- |
91
+ | Method call `foo` | `(send nil? :foo ...)` | `nil?` = no receiver |
92
+ | Method call `obj.foo` | `(send _ :foo ...)` | `_` = any receiver |
93
+ | Method call `Foo.bar` | `(send (const nil? :Foo) :bar ...)` | Constant receiver |
94
+ | String literal `"x"` | `(str "x")` | |
95
+ | Symbol `:x` | `(sym :x)` | |
96
+ | Block `foo { }` | `(block (send nil? :foo) ...)` | |
97
+ | Class definition | `(class (const nil? :Name) ...)` | |
98
+ | `if` conditional | `(if _ _ _)` | Three children: condition, if-body, else-body |
99
+ | Instance variable | `(ivar :@name)` | |
100
+ | Constant assignment | `(casgn nil? :NAME _)` | |
101
+ | `require "x"` | `(send nil? :require (str "x"))` | |
102
+
103
+ **Wildcards and captures:**
104
+
105
+ - `_` — matches any single node
106
+ - `...` — matches any number of nodes (rest)
107
+ - `$_` — capture a node into a variable
108
+ - `nil?` — matches nil (no receiver for top-level calls)
109
+ - `{:foo :bar}` — matches either `:foo` or `:bar`
110
+ - `(send _ {:puts :print :p} ...)` — matches puts/print/p on any receiver
111
+
112
+ **Pro tip:** Run `ruby-parse -e 'your_code_here'` to see the AST for any Ruby expression. The `parser` gem must be installed.
113
+
114
+ ## Auto-Correct
115
+
116
+ ### `extend AutoCorrector` — opt-in auto-fix
117
+
118
+ ```ruby
119
+ class NoDirectDbQuery < Base
120
+ extend AutoCorrector
121
+
122
+ def on_send(node)
123
+ return unless ar_model_query?(node)
124
+
125
+ add_offense(node) do |corrector|
126
+ model = node.receiver.short_name
127
+ corrector.replace(node, "#{model}Repository.#{node.method_name}")
128
+ end
129
+ end
130
+ end
131
+ ```
132
+
133
+ Available corrector methods:
134
+
135
+ | Method | What it does |
136
+ | ------------------------------------- | -------------------------------- |
137
+ | `corrector.replace(node, text)` | Replace node with text |
138
+ | `corrector.insert_before(node, text)` | Insert text before node |
139
+ | `corrector.insert_after(node, text)` | Insert text after node |
140
+ | `corrector.remove(node)` | Remove node |
141
+ | `corrector.wrap(node, before, after)` | Wrap node with before/after text |
142
+
143
+ Safety rules for auto-correct:
144
+
145
+ - **Mark unsafe corrections** with `def autocorrect_enabled? = false` or use the cop's `Safe` / `SafeAutoCorrect` metadata in `.rubocop.yml`
146
+ - **Never change runtime behavior.** If the fix might break code, don't auto-correct.
147
+ - RuboCop runs `rubocop -a` (safe only) vs `rubocop -A` (all including unsafe). Default to safe.
148
+
149
+ ## Testing Cops
150
+
151
+ ```ruby
152
+ # frozen_string_literal: true
153
+
154
+ require "rubocop"
155
+ require "rubocop/rspec/expect_offense"
156
+
157
+ RSpec.describe RuboCop::Cop::Custom::NoDirectDbQuery, :config do
158
+ # Tests that valid code produces no offenses
159
+ it "accepts repository pattern calls" do
160
+ expect_no_offenses(<<~RUBY)
161
+ UserRepository.active_users
162
+ RUBY
163
+ end
164
+
165
+ # Tests that invalid code produces the right offense
166
+ it "registers offense for direct AR query" do
167
+ expect_offense(<<~RUBY)
168
+ User.where(active: true)
169
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Use the repository pattern — call `UserRepository` instead of `User.where`.
170
+ RUBY
171
+ end
172
+
173
+ # Tests auto-correction
174
+ it "auto-corrects to repository call" do
175
+ expect_offense(<<~RUBY)
176
+ User.find(1)
177
+ ^^^^^^^^^^^^ Use the repository pattern — call `UserRepository` instead of `User.find`.
178
+ RUBY
179
+
180
+ expect_correction(<<~RUBY)
181
+ UserRepository.find(1)
182
+ RUBY
183
+ end
184
+ end
185
+ ```
186
+
187
+ **Testing best practices:**
188
+
189
+ 1. **Use `expect_offense` with caret markers.** The carets (`^`) must align with the exact offense location.
190
+ 2. **Test edge cases.** Dynamic receivers, safe navigation (`&.`), chained methods, blocks.
191
+ 3. **Test configuration.** If your cop respects `AllowedMethods`, test with and without the config.
192
+ 4. **Use `:config` shared context** to get a default configuration object.
193
+
194
+ ## Registration
195
+
196
+ ### In `.rubocop.yml`
197
+
198
+ ```yaml
199
+ require:
200
+ - ./lib/rubocop/cop/custom/no_direct_db_query
201
+
202
+ Custom/NoDirectDbQuery:
203
+ Enabled: true
204
+ Include:
205
+ - "app/**/*.rb"
206
+ Exclude:
207
+ - "app/repositories/**/*.rb"
208
+ ```
209
+
210
+ ### vigiles enforce() reference
211
+
212
+ ```typescript
213
+ enforce(
214
+ "rubocop/Custom/NoDirectDbQuery",
215
+ "Use repository pattern for all DB queries.",
216
+ );
217
+ ```
218
+
219
+ vigiles checks `rubocop --show-cops Custom/NoDirectDbQuery` and verifies `Enabled: true`.
220
+
221
+ ## Edge Cases and Gotchas
222
+
223
+ ### Safe vs unsafe cops
224
+
225
+ RuboCop distinguishes `Safe: true` (default) from `Safe: false`. Unsafe cops can produce false positives. When writing a custom cop that can't guarantee correctness (e.g., it doesn't understand metaprogramming), set:
226
+
227
+ ```yaml
228
+ Custom/MyCop:
229
+ Safe: false
230
+ SafeAutoCorrect: false
231
+ ```
232
+
233
+ ### Metaprogramming blind spots
234
+
235
+ RuboCop's AST parser doesn't understand:
236
+
237
+ - `method_missing` / `respond_to_missing?`
238
+ - `define_method` with dynamic names
239
+ - `send` / `public_send` with variable method names
240
+ - ActiveRecord dynamic finders (`find_by_name` generated at runtime)
241
+
242
+ If your convention targets code that's often generated via metaprogramming, expect false negatives.
243
+
244
+ ### Node matcher vs manual traversal
245
+
246
+ Use `def_node_matcher` for simple patterns. Switch to manual `node.children`, `node.each_descendant`, etc. when you need:
247
+
248
+ - Cross-method analysis (e.g., "if method A exists, method B must also exist")
249
+ - State tracking across the file (e.g., counting total occurrences)
250
+ - Complex conditional logic that the pattern DSL can't express
251
+
252
+ ### Performance
253
+
254
+ - Always use `RESTRICT_ON_SEND` when targeting method calls — it skips the cop entirely for non-matching methods.
255
+ - Avoid `on_send` without `RESTRICT_ON_SEND` on large codebases — it fires for every method call.
256
+ - `def_node_search` iterates descendants and can be slow on deeply nested ASTs. Prefer `def_node_matcher` on specific node types.
257
+
258
+ ### Monorepo considerations
259
+
260
+ - RuboCop resolves `.rubocop.yml` from the file being linted, walking up directories. Each sub-project can have its own config.
261
+ - vigiles checks `rubocop --show-cops <CopName>` from the project `basePath`. In a monorepo, run from each gem/app root.
262
+ - Custom cops must be `require`-able from the config file's location. Use relative paths or bundle them in a gem.
263
+
264
+ ## Mapping PR Feedback to Cop Strategy
265
+
266
+ | PR comment pattern | Best approach |
267
+ | ------------------------------------------ | -------------------------------------------------------------------------------- |
268
+ | "Don't call X directly" | Check if an existing cop covers it; otherwise custom cop with `RESTRICT_ON_SEND` |
269
+ | "Use our wrapper for Y" | Custom cop — detect raw calls, suggest wrapper |
270
+ | "Method too long" | `Metrics/MethodLength` — configure `Max:` threshold |
271
+ | "Missing frozen_string_literal" | `Style/FrozenStringLiteralComment` — already exists |
272
+ | "Don't use `puts` in production" | Custom cop or `Rails/Output` if using Rails |
273
+ | "Always add description to GraphQL fields" | `rubocop-graphql` `GraphQL/FieldDescription` |
274
+ | "Test files too complex" | `RSpec/ExampleLength`, `RSpec/NestedGroups` — configure thresholds |
275
+ | "Naming convention violated" | `Naming/*` cops — highly configurable |
276
+ | "Security issue: eval" | `Security/Eval` — already exists |
277
+ | "Don't skip validations" | `Rails/SkipsModelValidations` — already exists |
@@ -0,0 +1,187 @@
1
+ # Ruff — Reference
2
+
3
+ Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom rules).
4
+
5
+ ## Check Existing Rules First
6
+
7
+ Ruff reimplements 800+ rules from flake8, pylint, isort, pyupgrade, and others. Before writing a custom rule, check if Ruff already covers it:
8
+
9
+ | Prefix | Source | Key rules |
10
+ | --------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | `E` / `W` | pycodestyle | `E501` (line-too-long), `W291` (trailing-whitespace), `E711` (none-comparison) |
12
+ | `F` | pyflakes | `F401` (unused-import), `F841` (unused-variable), `F811` (redefined-unused-name) |
13
+ | `I` | isort | `I001` (unsorted-imports), `I002` (missing-required-import) |
14
+ | `N` | pep8-naming | `N801` (invalid-class-name), `N802` (invalid-function-name), `N806` (non-lowercase-variable) |
15
+ | `UP` | pyupgrade | `UP006` (non-pep585-annotation), `UP007` (non-pep604-annotation), `UP035` (deprecated-import) |
16
+ | `S` | flake8-bandit | `S101` (assert), `S105` (hardcoded-password-string), `S301` (suspicious-pickle-usage) |
17
+ | `B` | flake8-bugbear | `B006` (mutable-argument-default), `B007` (unused-loop-control-variable), `B905` (zip-without-explicit-strict) |
18
+ | `A` | flake8-builtins | `A001` (builtin-variable-shadowing), `A002` (builtin-argument-shadowing) |
19
+ | `C4` | flake8-comprehensions | `C400` (unnecessary-generator-list), `C401` (unnecessary-generator-set), `C408` (unnecessary-collection-call) |
20
+ | `T20` | flake8-print | `T201` (print), `T203` (pprint) |
21
+ | `SIM` | flake8-simplify | `SIM102` (collapsible-if), `SIM108` (if-else-block-instead-of-if-exp), `SIM110` (reimplemented-builtin) |
22
+ | `PL` | pylint | `PLC0414` (useless-import-alias), `PLE1205` (logging-too-many-args), `PLR0913` (too-many-arguments), `PLW0602` (global-variable-not-assigned) |
23
+ | `PTH` | flake8-use-pathlib | `PTH100` (os-path-abspath), `PTH118` (os-path-join), `PTH123` (builtin-open) |
24
+ | `RUF` | ruff-specific | `RUF001` (ambiguous-unicode-character), `RUF005` (collection-literal-concatenation), `RUF013` (implicit-optional) |
25
+ | `D` | pydocstyle | `D100` (undocumented-public-module), `D103` (undocumented-public-function), `D400` (first-line-should-end-with-period) |
26
+ | `ANN` | flake8-annotations | `ANN001` (missing-type-function-argument), `ANN201` (missing-return-type-public-function) |
27
+ | `ARG` | flake8-unused-arguments | `ARG001` (unused-function-argument), `ARG002` (unused-method-argument) |
28
+ | `ERA` | eradicate | `ERA001` (commented-out-code) |
29
+ | `TCH` | flake8-type-checking | `TCH001` (typing-only-first-party-import), `TCH002` (typing-only-third-party-import) |
30
+ | `FBT` | flake8-boolean-trap | `FBT001` (boolean-typed-positional-argument), `FBT002` (boolean-default-value-positional-argument) |
31
+ | `ICN` | flake8-import-conventions | `ICN001` (unconventional-import-alias) — e.g., `import numpy as np` |
32
+ | `PIE` | flake8-pie | `PIE790` (unnecessary-placeholder), `PIE804` (no-unnecessary-dict-kwargs) |
33
+ | `RSE` | flake8-raise | `RSE102` (unnecessary-paren-on-raise-exception) |
34
+ | `RET` | flake8-return | `RET501` (unnecessary-return-none), `RET504` (unnecessary-assign) |
35
+ | `TID` | flake8-tidy-imports | `TID252` (relative-imports) — ban relative imports |
36
+ | `PERF` | perflint | `PERF101` (unnecessary-list-cast), `PERF401` (manual-list-comprehension) |
37
+ | `FURB` | refurb | `FURB105` (print-empty-string), `FURB118` (reimplemented-operator) |
38
+
39
+ **Tip:** Run `ruff rule <CODE>` to see the full description of any rule. Run `ruff linter` to see all available rule groups.
40
+
41
+ ## Rule Selection
42
+
43
+ Configure in `pyproject.toml`:
44
+
45
+ ```toml
46
+ [tool.ruff.lint]
47
+ select = [
48
+ "E", "W", # pycodestyle
49
+ "F", # pyflakes
50
+ "I", # isort
51
+ "B", # flake8-bugbear
52
+ "S", # flake8-bandit
53
+ "UP", # pyupgrade
54
+ "SIM", # flake8-simplify
55
+ "T20", # flake8-print
56
+ "RUF", # ruff-specific
57
+ ]
58
+ ignore = [
59
+ "E501", # line-too-long (handled by formatter)
60
+ ]
61
+
62
+ [tool.ruff.lint.per-file-ignores]
63
+ "tests/**" = ["S101"] # allow assert in tests
64
+ "__init__.py" = ["F401"] # allow unused imports in __init__
65
+ ```
66
+
67
+ Or in `ruff.toml` (same syntax without `[tool.ruff]` prefix):
68
+
69
+ ```toml
70
+ [lint]
71
+ select = ["E", "F", "I", "B"]
72
+ ```
73
+
74
+ ### Selecting individual rules vs groups
75
+
76
+ - `"F"` enables all pyflakes rules
77
+ - `"F401"` enables only `F401`
78
+ - `"ALL"` enables everything (noisy — use `ignore` to subtract)
79
+ - Use `extend-select` to add rules without overriding the default set
80
+
81
+ ## Custom Rules
82
+
83
+ **Ruff does not support user-defined rules.** If no existing rule covers your pattern:
84
+
85
+ 1. **Configure existing rules** — many rules accept options (e.g., `flake8-import-conventions` lets you set allowed aliases)
86
+ 2. **Use `ruff.lint.flake8-import-conventions.aliases`** — for import alias enforcement
87
+ 3. **Use `ruff.lint.flake8-tidy-imports.banned-api`** — for banning specific imports/modules
88
+ 4. **Use ast-grep** — for arbitrary AST pattern matching, reference via `enforce()` with an ast-grep rule
89
+ 5. **Write a Pylint checker** — if you need the full power of a custom rule with AST analysis, use Pylint (Ruff can coexist with Pylint in CI)
90
+
91
+ ```toml
92
+ # Ban specific APIs without a custom rule
93
+ [tool.ruff.lint.flake8-tidy-imports.banned-api]
94
+ "os.system".msg = "Use subprocess.run instead."
95
+ "typing.Dict".msg = "Use dict instead (PEP 585)."
96
+ ```
97
+
98
+ ## Auto-Fix
99
+
100
+ Ruff provides auto-fix for many rules:
101
+
102
+ ```bash
103
+ ruff check --fix # apply safe fixes only
104
+ ruff check --fix --unsafe-fixes # include unsafe fixes
105
+ ruff check --fix-only # only fix, don't report remaining violations
106
+ ```
107
+
108
+ **Safe vs unsafe fixes:**
109
+
110
+ - **Safe** — guaranteed to not change semantics (e.g., removing unused imports)
111
+ - **Unsafe** — may change semantics (e.g., `UP007` rewriting `Optional[X]` to `X | None` can break runtime type checking)
112
+
113
+ Check fixability per rule with `ruff rule <CODE>` — it shows whether the rule has a fix and if it's safe.
114
+
115
+ ### vigiles enforce() reference
116
+
117
+ ```typescript
118
+ enforce("ruff/F401", "Remove unused imports.");
119
+ enforce("ruff/T201", "Use logging module instead of print.");
120
+ enforce("ruff/S101", "Don't use assert in production code.");
121
+ ```
122
+
123
+ vigiles checks `ruff rule <CODE>` and verifies the rule exists, then parses `ruff check --show-settings` to confirm it's enabled.
124
+
125
+ ## Edge Cases and Gotchas
126
+
127
+ ### Ruff vs Pylint (PL prefix)
128
+
129
+ Ruff reimplements many Pylint rules with the `PL` prefix. The mapping is not 1:1 — some Pylint rules have no Ruff equivalent, and Ruff's implementations may differ in edge cases.
130
+
131
+ | Ruff code | Pylint equivalent | Notes |
132
+ | --------- | -------------------------------------- | ------------- |
133
+ | `PLR0913` | `R0913` (too-many-arguments) | Same behavior |
134
+ | `PLC0414` | `C0414` (useless-import-alias) | Same behavior |
135
+ | `PLW0602` | `W0602` (global-variable-not-assigned) | Same behavior |
136
+ | `PLE1205` | `E1205` (logging-too-many-args) | Same behavior |
137
+
138
+ If the project uses both Ruff and Pylint, disable Pylint rules that Ruff already covers to avoid duplicate reports.
139
+
140
+ ### Preview rules
141
+
142
+ Some rules are behind `--preview` flag. These are not stable and may change between versions. Don't use `enforce()` on preview rules unless you pin the Ruff version.
143
+
144
+ ```toml
145
+ [tool.ruff]
146
+ preview = true # enables preview rules
147
+ ```
148
+
149
+ ### Formatter vs linter conflicts
150
+
151
+ Ruff has both a linter (`ruff check`) and a formatter (`ruff format`). Some linter rules conflict with the formatter:
152
+
153
+ - `E501` (line-too-long) — the formatter handles line length; disable in the linter
154
+ - `W291`/`W292`/`W293` — whitespace rules handled by formatter
155
+ - `COM812` (missing-trailing-comma) — conflicts with formatter in some cases
156
+
157
+ The Ruff docs recommend: `ignore = ["E501", "W291", "W292", "W293"]` when using `ruff format`.
158
+
159
+ ### Monorepo config inheritance
160
+
161
+ Ruff resolves config by walking up from the file being linted. In a monorepo:
162
+
163
+ - Each package can have its own `pyproject.toml` with `[tool.ruff]`
164
+ - A root config applies to all packages unless overridden
165
+ - Use `extend` to inherit from a shared config: `extend = "../../pyproject.toml"`
166
+
167
+ vigiles discovers Ruff config at `basePath` only. In a monorepo, run vigiles from each package root.
168
+
169
+ ## Mapping PR Feedback to Rule Strategy
170
+
171
+ | PR comment pattern | Best approach |
172
+ | ----------------------------- | ------------------------------------------------------------------ |
173
+ | "Remove unused imports" | `ruff/F401` — already exists |
174
+ | "Don't use print()" | `ruff/T201` — already exists |
175
+ | "Sort your imports" | `ruff/I001` — already exists |
176
+ | "Use pathlib not os.path" | `ruff/PTH*` — enable the PTH group |
177
+ | "Don't use assert in prod" | `ruff/S101` — already exists, ignore in tests via per-file-ignores |
178
+ | "Remove commented-out code" | `ruff/ERA001` — already exists |
179
+ | "Add type annotations" | `ruff/ANN*` — enable the ANN group |
180
+ | "Use comprehensions" | `ruff/C4*` — enable the C4 group |
181
+ | "Don't shadow builtins" | `ruff/A001` / `A002` — already exists |
182
+ | "Simplify this if" | `ruff/SIM102` / `SIM108` — already exists |
183
+ | "Don't use mutable defaults" | `ruff/B006` — already exists |
184
+ | "Ban specific import" | `flake8-tidy-imports.banned-api` config — no custom rule needed |
185
+ | "Naming convention violated" | `ruff/N*` — enable the N group |
186
+ | "Don't use os.system" | `flake8-tidy-imports.banned-api` config |
187
+ | "Complex pattern not in Ruff" | Write a Pylint checker or use ast-grep |