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,384 @@
1
+ # ESLint — 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 Plugins First
6
+
7
+ Before writing a custom rule, search these plugins — the pattern may already be covered:
8
+
9
+ | Plugin | Scope | Key rules to know |
10
+ | --------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | `@typescript-eslint` | Type-aware TS rules | `no-floating-promises`, `no-misused-promises`, `strict-boolean-expressions`, `naming-convention`, `no-restricted-imports` (type-aware variant) |
12
+ | `eslint-plugin-import-x` | Import hygiene (flat-config native) | `no-cycle`, `no-unresolved`, `no-extraneous-dependencies`, `order`, `no-internal-modules` |
13
+ | `eslint-plugin-boundaries` | Module boundary enforcement | `element-types`, `entry-point`, `external` — define allowed dependency directions between architectural layers |
14
+ | `eslint-plugin-sonarjs` | Code smells & complexity | `cognitive-complexity`, `no-duplicate-string`, `no-identical-functions`, `no-nested-conditional` |
15
+ | `eslint-plugin-unicorn` | Modern JS idioms | `prefer-node-protocol`, `no-array-for-each`, `prefer-top-level-await`, `filename-case` |
16
+ | `eslint-plugin-react` / `eslint-plugin-react-hooks` | React patterns | `rules-of-hooks`, `exhaustive-deps`, `no-unstable-nested-components`, `jsx-no-leaked-render` |
17
+ | `eslint-plugin-jsx-a11y` | Accessibility | `alt-text`, `anchor-is-valid`, `no-autofocus`, `click-events-have-key-events` |
18
+ | `eslint-plugin-n` | Node.js-specific | `no-sync`, `no-process-exit`, `prefer-global/buffer`, `no-unsupported-features` |
19
+ | `eslint-plugin-regexp` | Regex safety | `no-super-linear-backtracking`, `no-misleading-unicode-character`, `prefer-quantifier` |
20
+ | `eslint-plugin-security` | Security anti-patterns | `detect-object-injection`, `detect-non-literal-regexp`, `detect-child-process` |
21
+ | `@eslint/json` / `@eslint/markdown` | Non-JS file linting | Lint JSON and markdown files with ESLint flat config — useful for config validation |
22
+
23
+ **Tip:** `no-restricted-syntax` with an AST selector handles many one-off patterns without a custom rule. Try it first.
24
+
25
+ ## Rule Anatomy
26
+
27
+ Every ESLint rule is an object with `meta` and `create`:
28
+
29
+ ```js
30
+ /** @type {import('eslint').Rule.RuleModule} */
31
+ module.exports = {
32
+ meta: {
33
+ type: "suggestion", // "problem" | "suggestion" | "layout"
34
+ docs: {
35
+ description: "Disallow direct database calls outside the data layer",
36
+ recommended: false,
37
+ },
38
+ messages: {
39
+ noDirectDb:
40
+ "Use the repository pattern — import from 'src/data/' instead of calling {{ name }} directly.",
41
+ },
42
+ fixable: null, // "code" if you provide a fixer
43
+ schema: [], // JSON Schema for rule options
44
+ },
45
+
46
+ create(context) {
47
+ return {
48
+ // visitor methods keyed by AST node type or selector
49
+ CallExpression(node) {
50
+ if (isDirectDbCall(node)) {
51
+ context.report({
52
+ node,
53
+ messageId: "noDirectDb",
54
+ data: { name: node.callee.name },
55
+ });
56
+ }
57
+ },
58
+ };
59
+ },
60
+ };
61
+ ```
62
+
63
+ ### meta.type — choose correctly
64
+
65
+ | Type | When to use | Example |
66
+ | -------------- | ----------------------------------------- | ------------------------------------------------- |
67
+ | `"problem"` | Code that is/will be broken | Missing `await` on a promise |
68
+ | `"suggestion"` | Code that works but violates a convention | Using `console.log` instead of the project logger |
69
+ | `"layout"` | Whitespace/formatting only | Rarely used — Prettier handles this |
70
+
71
+ ### meta.messages — be prescriptive
72
+
73
+ Bad: `"Don't do this."` — tells the developer nothing.
74
+ Good: `"Use the {{ replacement }} wrapper instead of {{ original }}."` — tells them exactly what to write.
75
+
76
+ Always include **what to do** in the message, not just what's wrong.
77
+
78
+ ## AST Node Types — Cheat Sheet
79
+
80
+ Common patterns and the AST nodes that catch them:
81
+
82
+ | You want to detect | AST node / selector | Notes |
83
+ | -------------------------- | ------------------------------------------------------------------------------- | ----------------------------------- |
84
+ | Function call `foo()` | `CallExpression[callee.name="foo"]` | Selector form — no code needed |
85
+ | Method call `obj.method()` | `CallExpression[callee.type="MemberExpression"][callee.property.name="method"]` | |
86
+ | Import `from "module"` | `ImportDeclaration[source.value="module"]` | Static imports only |
87
+ | `require("module")` | `CallExpression[callee.name="require"][arguments.0.value="module"]` | CJS |
88
+ | Variable named X | `VariableDeclarator[id.name="X"]` | |
89
+ | Class with decorator | `ClassDeclaration > Decorator` | Experimental — needs parser support |
90
+ | JSX element `<Foo>` | `JSXOpeningElement[name.name="Foo"]` | Needs JSX parser |
91
+ | Template literal | `TemplateLiteral` | Includes tagged templates |
92
+ | `throw` statement | `ThrowStatement` | |
93
+ | `new Promise()` | `NewExpression[callee.name="Promise"]` | |
94
+
95
+ **Pro tip:** Use [AST Explorer](https://astexplorer.net) (parser: `@typescript-eslint/parser`, transform: `ESLint v4`) to prototype visitor logic interactively.
96
+
97
+ ## ESTree Selectors
98
+
99
+ ESLint supports CSS-like AST selectors. These replace boilerplate visitor code:
100
+
101
+ ```js
102
+ create(context) {
103
+ return {
104
+ // Match: import from any path containing "internal"
105
+ 'ImportDeclaration[source.value=/internal/]'(node) {
106
+ context.report({ node, messageId: "noInternalImport" });
107
+ },
108
+
109
+ // Match: await inside a loop body
110
+ 'ForStatement > BlockStatement AwaitExpression'(node) {
111
+ context.report({ node, messageId: "noAwaitInLoop" });
112
+ },
113
+
114
+ // Match: console.log, console.warn, console.error
115
+ 'MemberExpression[object.name="console"]'(node) {
116
+ context.report({ node: node.parent, messageId: "useLogger" });
117
+ },
118
+ };
119
+ }
120
+ ```
121
+
122
+ Selector syntax:
123
+
124
+ - `A > B` — B is a direct child of A
125
+ - `A B` — B is a descendant of A
126
+ - `A[attr="val"]` — attribute match (string equality)
127
+ - `A[attr=/regex/]` — attribute match (regex)
128
+ - `A:exit` — fires when **leaving** the node (post-order)
129
+ - `:not(A)` — negation
130
+ - `A + B` — B immediately follows A (sibling)
131
+
132
+ ## Accessing TypeScript Type Information
133
+
134
+ For type-aware rules, use `@typescript-eslint/utils`:
135
+
136
+ ```ts
137
+ import { ESLintUtils } from "@typescript-eslint/utils";
138
+
139
+ const createRule = ESLintUtils.RuleCreator(
140
+ (name) => `https://example.com/rules/${name}`,
141
+ );
142
+
143
+ export default createRule({
144
+ name: "no-unhandled-promise",
145
+ meta: {
146
+ type: "problem",
147
+ docs: { description: "Require promises to be handled" },
148
+ messages: { unhandled: "This promise must be awaited or returned." },
149
+ schema: [],
150
+ },
151
+ defaultOptions: [],
152
+ create(context) {
153
+ const services = ESLintUtils.getParserServices(context);
154
+ const checker = services.program.getTypeChecker();
155
+
156
+ return {
157
+ ExpressionStatement(node) {
158
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node.expression);
159
+ const type = checker.getTypeAtLocation(tsNode);
160
+ if (isPromiseLike(checker, type)) {
161
+ context.report({ node, messageId: "unhandled" });
162
+ }
163
+ },
164
+ };
165
+ },
166
+ });
167
+ ```
168
+
169
+ **Requirements for type-aware rules:**
170
+
171
+ 1. The ESLint config must use `@typescript-eslint/parser` with `projectService: true` (or legacy `project` option)
172
+ 2. The rule must live in a plugin (not standalone `eslint-rules/` dir) for parser services to work reliably
173
+ 3. Type-aware rules are slower — only use when you genuinely need type information
174
+
175
+ ## Auto-Fix and Suggestions
176
+
177
+ ### `fix` — automatic, silent
178
+
179
+ Use for safe, semantics-preserving changes:
180
+
181
+ ```js
182
+ context.report({
183
+ node,
184
+ messageId: "preferNodeProtocol",
185
+ fix(fixer) {
186
+ return fixer.replaceText(node.source, `"node:${module}"`);
187
+ },
188
+ });
189
+ ```
190
+
191
+ Safety rules for `fix`:
192
+
193
+ - **Never change runtime behavior.** If the fix might break code, use `suggest` instead.
194
+ - **Never remove code** unless it's provably dead.
195
+ - One `fix` per report. Multiple changes go in a single fix call using an array.
196
+ - Test your fix: `RuleTester` will verify the fix output matches `output` in your test case.
197
+
198
+ ### `suggest` — manual, user-picks
199
+
200
+ Use for changes that might alter semantics:
201
+
202
+ ```js
203
+ context.report({
204
+ node,
205
+ messageId: "noDirectDbCall",
206
+ suggest: [
207
+ {
208
+ messageId: "wrapWithRepo",
209
+ fix(fixer) {
210
+ return fixer.replaceText(
211
+ node,
212
+ `repository.${node.callee.property.name}(${argsText})`,
213
+ );
214
+ },
215
+ },
216
+ ],
217
+ });
218
+ ```
219
+
220
+ ## Testing with RuleTester
221
+
222
+ ```js
223
+ import { RuleTester } from "eslint";
224
+ import rule from "./no-direct-db.js";
225
+
226
+ const tester = new RuleTester({
227
+ languageOptions: {
228
+ ecmaVersion: 2024,
229
+ sourceType: "module",
230
+ },
231
+ });
232
+
233
+ tester.run("no-direct-db", rule, {
234
+ valid: [
235
+ // Always include: correctly following the rule
236
+ `import { findUser } from "src/data/users";`,
237
+ // Edge case: dynamic import (intentionally allowed)
238
+ `const mod = await import("pg");`,
239
+ ],
240
+ invalid: [
241
+ {
242
+ code: `import { Pool } from "pg";`,
243
+ errors: [{ messageId: "noDirectDb" }],
244
+ },
245
+ // If the rule has a fixer, test the output
246
+ {
247
+ code: `const fs = require("fs");`,
248
+ output: `const fs = require("node:fs");`,
249
+ errors: [{ messageId: "preferNodeProtocol" }],
250
+ },
251
+ ],
252
+ });
253
+ ```
254
+
255
+ **Testing best practices:**
256
+
257
+ 1. **Test the valid cases first.** If valid cases fail, your rule is too aggressive.
258
+ 2. **Include edge cases.** Dynamic imports, re-exports, type-only imports, destructured requires.
259
+ 3. **Test error placement.** Use `errors: [{ messageId, line, column }]` to verify the squiggly appears on the right token.
260
+ 4. **Test with options.** If your rule has configurable options, test each combination.
261
+ 5. **For TypeScript rules**, use `@typescript-eslint/rule-tester` and provide `parser` + `parserOptions` with a real `tsconfig.json`.
262
+
263
+ ## Flat Config Registration
264
+
265
+ ESLint 9+ uses flat config. Register custom rules via a local plugin object:
266
+
267
+ ```js
268
+ // eslint.config.js
269
+ import noDirectDb from "./eslint-rules/no-direct-db.js";
270
+
271
+ export default [
272
+ {
273
+ plugins: {
274
+ // Namespace all custom rules under "local"
275
+ local: {
276
+ rules: {
277
+ "no-direct-db": noDirectDb,
278
+ },
279
+ },
280
+ },
281
+ rules: {
282
+ "local/no-direct-db": "error",
283
+ },
284
+ },
285
+ ];
286
+ ```
287
+
288
+ **Important:** The config key in `rules` must be `<plugin-namespace>/<rule-name>`. The `enforce()` reference in vigiles matches this: `enforce("eslint/local/no-direct-db", "...")`.
289
+
290
+ If the project still uses legacy `.eslintrc`, the rule goes in `rulesDir` instead — but flat config is the path forward.
291
+
292
+ ## Edge Cases and Gotchas
293
+
294
+ ### TypeScript AST differences
295
+
296
+ TypeScript adds AST node types that ESTree doesn't have. Common ones to watch for:
297
+
298
+ - `TSAsExpression` — `x as string` wraps the expression; your visitor might miss the inner node
299
+ - `TSNonNullExpression` — `x!` adds a wrapper node
300
+ - `TSTypeAnnotation` — `: string` on parameters creates child nodes that break naive `node.params.length` checks
301
+ - `TSImportEqualsDeclaration` — `import x = require("y")` is NOT an `ImportDeclaration`
302
+ - `TSEnumDeclaration` — enums look like variable declarations but aren't
303
+
304
+ **Rule of thumb:** If your rule targets function parameters, imports, or expressions, test it with TypeScript annotations and assertions.
305
+
306
+ ### `no-restricted-syntax` — the 80% solution
307
+
308
+ Before writing a custom rule, check if `no-restricted-syntax` covers it:
309
+
310
+ ```js
311
+ rules: {
312
+ "no-restricted-syntax": ["error",
313
+ {
314
+ selector: 'CallExpression[callee.object.name="console"]',
315
+ message: "Use the project logger from 'src/lib/logger'.",
316
+ },
317
+ {
318
+ selector: 'ImportDeclaration[source.value="moment"]',
319
+ message: "Use dayjs — we're migrating off moment.",
320
+ },
321
+ ],
322
+ }
323
+ ```
324
+
325
+ This works for any pattern you can express as an AST selector. You only need a custom rule when you need:
326
+
327
+ - Type information
328
+ - Multi-node analysis (e.g., "if X is imported, then Y must also be imported")
329
+ - Auto-fix or suggestions
330
+ - Configurable options via schema
331
+
332
+ ### `no-restricted-imports` — the import-specific shortcut
333
+
334
+ For import bans specifically, `no-restricted-imports` is more ergonomic:
335
+
336
+ ```js
337
+ rules: {
338
+ "no-restricted-imports": ["error", {
339
+ paths: [
340
+ { name: "lodash", message: "Import specific lodash functions: lodash/get" },
341
+ { name: "moment", message: "Use dayjs instead." },
342
+ ],
343
+ patterns: [
344
+ { group: ["src/internal/*"], message: "Use the public API from src/index.ts" },
345
+ ],
346
+ }],
347
+ }
348
+ ```
349
+
350
+ ### Auto-fix conflicts
351
+
352
+ If two rules try to fix the same range of code, ESLint drops both fixes. Avoid this by:
353
+
354
+ - Keeping fix ranges tight (fix only the exact tokens, not the whole statement)
355
+ - Using `suggest` instead of `fix` when the rule might overlap with Prettier or another fixer
356
+ - Testing with `--fix-dry-run` before enabling in CI
357
+
358
+ ### Performance
359
+
360
+ - Avoid `Program:exit` handlers that walk the entire AST — use specific node visitors instead.
361
+ - Selector matching has overhead. For hot paths, a manual `CallExpression` check is faster than a complex selector.
362
+ - Type-aware rules add ~2-5x overhead because they invoke the TypeScript compiler. Group them in a separate config block with `files: ["src/**/*.ts"]` so they only run on TS files.
363
+ - If your custom rules directory has more than ~10 rules, bundle them into a proper plugin package for better caching.
364
+
365
+ ### Monorepo considerations
366
+
367
+ - ESLint flat config is resolved from `cwd`, not from the file being linted. In a monorepo, set `cwd` to the package root, not the workspace root.
368
+ - `vigiles generate-types` discovers ESLint rules using `calculateConfigForFile("dummy.js")` from the project `basePath`. If your monorepo has different configs per package, run `generate-types` from each package root.
369
+ - Plugin rules must be installed in the `node_modules` visible from the config file's location. Hoisted deps in a monorepo can cause "plugin not found" errors — install them in the package's own `devDependencies`.
370
+
371
+ ## Mapping PR Feedback to Rule Strategy
372
+
373
+ | PR comment pattern | Best approach |
374
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
375
+ | "Don't import from X" | `no-restricted-imports` — zero custom code |
376
+ | "Don't call X()" | `no-restricted-syntax` with a `CallExpression` selector |
377
+ | "Use X instead of Y" | `no-restricted-syntax` if detection is simple; custom rule with `fix` if you want auto-replacement |
378
+ | "Every file must have X" | Not ESLint — use vigiles `check(every(...).has(...))` |
379
+ | "This promise isn't awaited" | `@typescript-eslint/no-floating-promises` — already exists |
380
+ | "Use our custom hook" | Custom rule checking `CallExpression` against an allow-list |
381
+ | "Wrong naming convention" | `@typescript-eslint/naming-convention` — highly configurable, rarely needs custom code |
382
+ | "Don't use `any`" | `@typescript-eslint/no-explicit-any` — already exists |
383
+ | "Wrap API calls with error handler" | Custom rule: detect unwrapped `fetch`/`axios` calls, `suggest` the wrapper |
384
+ | "Don't mutate state directly" | `eslint-plugin-react` `no-direct-mutation-state` or custom rule checking assignment targets |
@@ -0,0 +1,288 @@
1
+ # Pylint — Reference
2
+
3
+ Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom checkers).
4
+
5
+ ## Check Existing Plugins First
6
+
7
+ Before writing a custom checker, search these plugins — the pattern may already be covered:
8
+
9
+ | Plugin | Scope | Key messages to know |
10
+ | ------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | `pylint` (core) | Convention, Refactor, Warning, Error, Fatal | `C0114` (missing-module-docstring), `R0902` (too-many-instance-attributes), `W0612` (unused-variable), `E1101` (no-member), `W0611` (unused-import) |
12
+ | `pylint-django` | Django conventions | `E5110` (no-member on Django models), `W5101` (fixme in templates), `C5105` (string-used-as-django-setting) |
13
+ | `pylint-celery` | Celery task patterns | Detects common Celery anti-patterns (task retry without max_retries, etc.) |
14
+ | `pylint-pydantic` | Pydantic model checking | Suppresses false positives from Pydantic's metaclass magic |
15
+ | `pylint-pytest` | pytest conventions | `W6301` (useless-pytest-mark-decorator), `W6302` (unnecessary-pytest-mark-parametrize) |
16
+ | `pylint-flask` | Flask patterns | Suppresses false positives for Flask's app context, request globals |
17
+ | `pylint-sqlalchemy` | SQLAlchemy ORM | Suppresses false `no-member` on SQLAlchemy models |
18
+ | `pylint-protobuf` | Protobuf message checking | Type-checks protobuf field access, catches typos in field names |
19
+ | `pylint-import-modules` | Import style enforcement | `C6201` (import-modules-only) — enforce `import module` over `from module import name` |
20
+ | `pylint-secure-coding-standard` | Security patterns | Detects insecure patterns: `eval`, `exec`, hardcoded passwords, insecure hash algorithms |
21
+
22
+ **Tip:** Many patterns can be handled by Pylint's built-in `bad-names`, `good-names`, `bad-functions`, or the `--disable`/`--enable` system without a custom checker.
23
+
24
+ **Also consider Ruff.** If the project uses Ruff, it reimplements many Pylint rules (prefixed `PL`) at much higher speed. Check Ruff's rule catalog before writing a Pylint-specific checker.
25
+
26
+ ## Checker Anatomy
27
+
28
+ Every Pylint checker inherits from `BaseChecker`:
29
+
30
+ ```python
31
+ """Checker for direct database queries outside the repository layer."""
32
+
33
+ from astroid import nodes
34
+ from pylint.checkers import BaseChecker
35
+ from pylint.interfaces import HIGH
36
+
37
+
38
+ class NoDirectDbQueryChecker(BaseChecker):
39
+ name = "no-direct-db-query"
40
+
41
+ msgs = {
42
+ "C9001": (
43
+ "Use %sRepository instead of %s.%s",
44
+ "no-direct-db-query",
45
+ "Direct model queries should go through the repository layer.",
46
+ ),
47
+ }
48
+
49
+ # Optional: add configuration options
50
+ options = (
51
+ (
52
+ "allowed-models",
53
+ {
54
+ "default": (),
55
+ "type": "csv",
56
+ "metavar": "<models>",
57
+ "help": "Models that are allowed to be queried directly.",
58
+ },
59
+ ),
60
+ )
61
+
62
+ def visit_call(self, node: nodes.Call) -> None:
63
+ if not isinstance(node.func, nodes.Attribute):
64
+ return
65
+ if not isinstance(node.func.expr, nodes.Name):
66
+ return
67
+
68
+ model = node.func.expr.name
69
+ method = node.func.attrname
70
+
71
+ if method in ("objects", "filter", "get", "all", "count", "values"):
72
+ if model not in self.linter.config.allowed_models:
73
+ self.add_message(
74
+ "no-direct-db-query",
75
+ node=node,
76
+ args=(model, model, method),
77
+ confidence=HIGH,
78
+ )
79
+
80
+
81
+ def register(linter):
82
+ linter.register_checker(NoDirectDbQueryChecker(linter))
83
+ ```
84
+
85
+ ### Key concepts
86
+
87
+ | Concept | Purpose | Notes |
88
+ | ------------- | ------------------- | ---------------------------------------------------------- |
89
+ | `name` | Checker identifier | Must be unique across all checkers |
90
+ | `msgs` | Message dictionary | Key format: `{C,R,W,E,F}{4 digits}` — category + unique ID |
91
+ | `options` | Configuration | Exposed via `pylintrc` or command line |
92
+ | `visit_*` | AST visitor methods | Called for each matching node type |
93
+ | `leave_*` | Post-visit methods | Called when leaving a node (post-order) |
94
+ | `add_message` | Report a violation | Pass message ID, node, args for formatting |
95
+ | `register` | Plugin entry point | Module-level function that registers the checker |
96
+
97
+ ### Message categories
98
+
99
+ | Prefix | Category | When to use |
100
+ | ------ | ---------- | ------------------------------------------ |
101
+ | `C` | Convention | Style/naming conventions |
102
+ | `R` | Refactor | Code that works but should be restructured |
103
+ | `W` | Warning | Probable bugs or risky patterns |
104
+ | `E` | Error | Definite bugs or broken code |
105
+ | `F` | Fatal | Pylint can't process the file |
106
+
107
+ Choose a unique 4-digit code starting from `9001` to avoid collisions with core Pylint. Check existing codes with `pylint --list-msgs`.
108
+
109
+ ## AST Nodes — Cheat Sheet (astroid)
110
+
111
+ Pylint uses `astroid` for AST parsing, which adds type inference on top of Python's `ast` module:
112
+
113
+ | You want to detect | Node type / visitor | Notes |
114
+ | -------------------------- | --------------------------------------------------- | --------------------------------------------- |
115
+ | Function call `foo()` | `visit_call` + check `node.func` | `node.func` is a `Name` or `Attribute` |
116
+ | Method call `obj.method()` | `visit_call` + `isinstance(node.func, Attribute)` | `node.func.attrname` = method name |
117
+ | Import `import X` | `visit_import` | `node.names` = list of `(name, alias)` tuples |
118
+ | Import `from X import Y` | `visit_importfrom` | `node.modname` = module, `node.names` = names |
119
+ | Class definition | `visit_classdef` | `node.name`, `node.bases`, `node.body` |
120
+ | Function definition | `visit_functiondef` | `node.name`, `node.args`, `node.body` |
121
+ | Assignment `x = ...` | `visit_assign` | `node.targets`, `node.value` |
122
+ | String literal | `visit_const` + check `isinstance(node.value, str)` | |
123
+ | Decorator `@foo` | `visit_decorators` | Or check `node.decorators` on function/class |
124
+ | `raise` statement | `visit_raise` | `node.exc` = exception expression |
125
+ | `with` statement | `visit_with` | `node.items` = context manager list |
126
+ | `try`/`except` | `visit_tryexcept` | `node.handlers` = list of except clauses |
127
+
128
+ ### astroid type inference
129
+
130
+ astroid can infer types, which is more powerful than plain AST:
131
+
132
+ ```python
133
+ def visit_call(self, node: nodes.Call) -> None:
134
+ # Infer what the function resolves to
135
+ try:
136
+ for inferred in node.func.infer():
137
+ if isinstance(inferred, nodes.FunctionDef):
138
+ # We know the actual function being called
139
+ if inferred.name == "dangerous_function":
140
+ self.add_message("no-dangerous-call", node=node)
141
+ except astroid.InferenceError:
142
+ pass # Can't determine — skip
143
+ ```
144
+
145
+ **Caution:** `infer()` can raise `InferenceError` and can return `Uninferable`. Always handle both.
146
+
147
+ ## Testing Checkers
148
+
149
+ ```python
150
+ """Tests for no_direct_db_query checker."""
151
+
152
+ import astroid
153
+ import pylint.testutils
154
+
155
+ from my_checkers.no_direct_db_query import NoDirectDbQueryChecker
156
+
157
+
158
+ class TestNoDirectDbQueryChecker(pylint.testutils.UnittestLinter):
159
+ CHECKER_CLASS = NoDirectDbQueryChecker
160
+
161
+ def test_direct_query_flagged(self):
162
+ node = astroid.extract_node("""
163
+ User.objects.filter(active=True) #@
164
+ """)
165
+ with self.assertAddsMessages(
166
+ pylint.testutils.MessageTest(
167
+ msg_id="no-direct-db-query",
168
+ node=node,
169
+ args=("User", "User", "objects"),
170
+ ),
171
+ ):
172
+ self.checker.visit_call(node)
173
+
174
+ def test_repository_call_allowed(self):
175
+ node = astroid.extract_node("""
176
+ UserRepository.active_users() #@
177
+ """)
178
+ with self.assertNoMessages():
179
+ self.checker.visit_call(node)
180
+ ```
181
+
182
+ **Testing best practices:**
183
+
184
+ 1. **Use `astroid.extract_node` with `#@` marker.** The marker indicates which node to extract from the code.
185
+ 2. **Test with `assertAddsMessages` and `assertNoMessages`.** These are the canonical assertion methods.
186
+ 3. **Test configuration options.** If your checker has options, test each combination.
187
+ 4. **Test inference edge cases.** Code with dynamic attributes, metaclasses, or `__getattr__` may cause `InferenceError`.
188
+ 5. **Run with `pytest` not `unittest`.** Pylint's test utils work with both, but pytest gives better output.
189
+
190
+ ## Registration
191
+
192
+ ### As a Pylint plugin
193
+
194
+ Add to `pyproject.toml`:
195
+
196
+ ```toml
197
+ [tool.pylint.main]
198
+ load-plugins = ["my_checkers.no_direct_db_query"]
199
+ ```
200
+
201
+ Or `.pylintrc`:
202
+
203
+ ```ini
204
+ [MAIN]
205
+ load-plugins=my_checkers.no_direct_db_query
206
+ ```
207
+
208
+ The module must be importable from `PYTHONPATH`. For a local checker, put it in the project and ensure the path is set.
209
+
210
+ ### vigiles enforce() reference
211
+
212
+ ```typescript
213
+ enforce(
214
+ "pylint/no-direct-db-query",
215
+ "Use repository pattern for all DB queries.",
216
+ );
217
+ ```
218
+
219
+ vigiles checks `pylint --help-msg=no-direct-db-query` and verifies the message exists and is enabled.
220
+
221
+ ## Edge Cases and Gotchas
222
+
223
+ ### Symbolic names vs message codes
224
+
225
+ Pylint messages have both a code (`C0114`) and a symbolic name (`missing-module-docstring`). Both work in vigiles:
226
+
227
+ ```typescript
228
+ enforce("pylint/C0114", "All modules need docstrings.");
229
+ enforce("pylint/missing-module-docstring", "All modules need docstrings.");
230
+ ```
231
+
232
+ Prefer symbolic names — they're readable and stable across Pylint versions.
233
+
234
+ ### astroid inference limitations
235
+
236
+ astroid can't infer:
237
+
238
+ - Dynamically constructed classes (`type("Foo", (Base,), {...})`)
239
+ - Heavy metaprogramming (`__init_subclass__`, custom metaclasses)
240
+ - C extensions (some methods on `numpy`, `pandas` are opaque)
241
+ - Runtime-only attributes (`setattr`, `__dict__` manipulation)
242
+
243
+ If your convention targets heavily dynamic code, expect false negatives. Add a comment explaining the limitation in the checker.
244
+
245
+ ### Plugin vs Ruff
246
+
247
+ For simple pattern bans, Ruff's built-in rules or `flake8-*` plugins reimplemented in Ruff are 10-100x faster. Use a Pylint custom checker only when you need:
248
+
249
+ - Type inference (astroid's `infer()`)
250
+ - Cross-function or cross-module analysis
251
+ - Configuration options exposed in `pylintrc`
252
+ - Integration with Django/Flask/SQLAlchemy brain plugins
253
+
254
+ ### Message ID collisions
255
+
256
+ Core Pylint uses codes `C0001`-`C8999`, `W0001`-`W8999`, etc. Custom checkers should use `9000+` to avoid collisions. Check with:
257
+
258
+ ```bash
259
+ pylint --list-msgs | grep "C90\|W90\|E90"
260
+ ```
261
+
262
+ ### Performance
263
+
264
+ - Pylint is slow on large codebases. Custom checkers add overhead per-file.
265
+ - Avoid `infer()` in hot paths — each call can trigger recursive type resolution.
266
+ - Use `visit_module` + early return to skip files that can't contain violations (e.g., skip test files for production-only rules).
267
+ - Consider `--jobs=N` for parallel execution, but note that custom checkers must be pickle-safe for multiprocessing.
268
+
269
+ ### Monorepo considerations
270
+
271
+ - Pylint resolves `pylintrc` / `pyproject.toml` from `cwd`, not from the file being linted. In a monorepo, run Pylint per-package.
272
+ - vigiles checks `pylint --help-msg=<name>` from the project `basePath`. If packages have different plugins loaded, run from each package root.
273
+ - Custom checkers must be on `PYTHONPATH`. In a monorepo, use relative `load-plugins` paths or install checkers as a development package.
274
+
275
+ ## Mapping PR Feedback to Checker Strategy
276
+
277
+ | PR comment pattern | Best approach |
278
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------- |
279
+ | "Don't import X directly" | `pylint/no-name-in-module` or custom checker with `visit_importfrom` |
280
+ | "Don't call X()" | `pylint/bad-functions` config or custom checker with `visit_call` |
281
+ | "Missing docstring" | `pylint/missing-function-docstring` (C0116) — already exists |
282
+ | "Function too long" | `pylint/too-many-statements` (R0915) — configure threshold |
283
+ | "Too many arguments" | `pylint/too-many-arguments` (R0913) — configure `max-args` |
284
+ | "Use logging instead of print" | Ruff `T201` or custom checker banning `print()` |
285
+ | "Don't use global state" | Custom checker detecting `global` keyword or module-level mutation |
286
+ | "Always type-hint public methods" | `pylint/missing-function-docstring` doesn't cover this — use `mypy --strict` or custom checker |
287
+ | "Don't catch bare Exception" | `pylint/broad-exception-caught` (W0718) — already exists |
288
+ | "Don't use mutable default arguments" | `pylint/dangerous-default-value` (W0102) — already exists |