vigiles 33.0.0 → 33.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/adapter-conformance.js +14 -10
  2. package/dist/adapter.d.ts +2 -0
  3. package/dist/adapters/claude-code/layout.js +3 -0
  4. package/dist/adapters/claude-code/output-style.d.ts +3 -0
  5. package/dist/adapters/claude-code/output-style.js +81 -0
  6. package/dist/adapters/claude-code/replies.d.ts +14 -0
  7. package/dist/adapters/claude-code/replies.js +78 -0
  8. package/dist/audit-report.d.ts +5 -0
  9. package/dist/audit-report.js +29 -20
  10. package/dist/audit-report.template.html +25 -25
  11. package/dist/audit-score.js +2 -2
  12. package/dist/check.d.ts +20 -0
  13. package/dist/check.js +87 -2
  14. package/dist/cli-main.js +25 -13
  15. package/dist/core/config-schema.d.ts +9 -0
  16. package/dist/core/config-schema.js +1 -0
  17. package/dist/core/eval-driver.d.ts +10 -0
  18. package/dist/core/frame.d.ts +1 -1
  19. package/dist/core/frontmatter-read.d.ts +31 -0
  20. package/dist/core/frontmatter-read.js +4 -2
  21. package/dist/core/harness-driver.d.ts +2 -0
  22. package/dist/core/layout.d.ts +7 -0
  23. package/dist/core/output-style.d.ts +84 -0
  24. package/dist/core/output-style.js +90 -0
  25. package/dist/core/rule-meta.d.ts +1 -1
  26. package/dist/core/rule-meta.js +8 -0
  27. package/dist/core/types.d.ts +2 -0
  28. package/dist/coverage-evidence.d.ts +42 -0
  29. package/dist/coverage-evidence.js +39 -0
  30. package/dist/eval.d.ts +26 -3
  31. package/dist/eval.js +54 -5
  32. package/dist/harness-test.d.ts +30 -1
  33. package/dist/harness-test.js +65 -15
  34. package/dist/optimize.js +1 -5
  35. package/dist/output-style-arms.d.ts +27 -0
  36. package/dist/output-style-arms.js +46 -0
  37. package/dist/scan-files.js +1 -1
  38. package/dist/scan.d.ts +6 -0
  39. package/dist/scan.js +14 -4
  40. package/dist/score-core.d.ts +3 -0
  41. package/dist/score-core.js +5 -1
  42. package/dist/setup-plan.d.ts +1 -1
  43. package/dist/setup-plan.js +1 -0
  44. package/dist/test-coverage-files.d.ts +3 -1
  45. package/dist/test-coverage-files.js +20 -6
  46. package/dist/test-coverage.d.ts +18 -1
  47. package/dist/test-coverage.js +64 -14
  48. package/dist/test.d.ts +3 -1
  49. package/dist/test.js +8 -2
  50. package/dist/vigilesrc.schema.json +134 -0
  51. package/package.json +6 -3
  52. package/packages/eslint-config/README.md +17 -0
  53. package/packages/eslint-config/index.mjs +344 -0
@@ -0,0 +1,344 @@
1
+ // @ts-check
2
+ /**
3
+ * The strict lint rules vigiles and paperlint share.
4
+ *
5
+ * `.mjs`, not `.ts`: ESLint loads a config without a build step, and paperlint
6
+ * imports this file by path from `node_modules/vigiles/packages/eslint-config/`.
7
+ * That import is deliberate and private: vigiles' `exports` has no entry for this
8
+ * file, so nobody else should depend on it.
9
+ *
10
+ * The plugins are not bundled. Each repo installs eslint-plugin-boundaries,
11
+ * eslint-plugin-functional and eslint-plugin-sonarjs itself, and the imports
12
+ * below resolve to that copy.
13
+ *
14
+ * What each repo keeps for itself: its folder layout (passed to `layers`), its
15
+ * ceiling table for old functions (passed to `ceilings`) and its
16
+ * eslint-suppressions.json.
17
+ */
18
+ import boundaries from "eslint-plugin-boundaries";
19
+ import functional from "eslint-plugin-functional";
20
+ import sonarjs from "eslint-plugin-sonarjs";
21
+
22
+ /** @typedef {import("eslint").Linter.Config} Config */
23
+ /** @typedef {import("eslint").Linter.RuleEntry} RuleEntry */
24
+ /** @typedef {{ files: string[], ignores?: string[] }} Scope */
25
+
26
+ /** One collection library: remeda. These would be a second one for the same job. */
27
+ export const ONE_COLLECTION_LIBRARY = ["lodash", "lodash-es", "ramda"].map(
28
+ (name) => ({ name, message: "Use remeda." }),
29
+ );
30
+
31
+ /** `x as unknown as T` switches the type checker off. For `no-restricted-syntax`. */
32
+ export const AS_UNKNOWN_AS = {
33
+ selector:
34
+ "TSAsExpression > TSAsExpression[typeAnnotation.type='TSUnknownKeyword']",
35
+ message:
36
+ "`as unknown as` switches the type checker off. Convert with a function, or fix the type.",
37
+ };
38
+
39
+ /** A function is one job; at thirty lines a second one starts to hide. */
40
+ const MAX_LINES = { max: 30, skipComments: true, skipBlankLines: true };
41
+
42
+ /**
43
+ * Limits on the size of a function. These rules report one finding per function
44
+ * however far over the limit it is, so a suppressed finding would let the
45
+ * function keep growing. Old functions get a ceiling instead (see `ceilings`).
46
+ */
47
+ export const SIZE_LIMITS = {
48
+ complexity: 10,
49
+ "sonarjs/cognitive-complexity": 10,
50
+ "max-depth": 3,
51
+ "max-params": 4,
52
+ "max-lines-per-function": MAX_LINES.max,
53
+ "max-nested-callbacks": 3,
54
+ };
55
+
56
+ /** @typedef {keyof typeof SIZE_LIMITS} SizeRule */
57
+
58
+ /**
59
+ * @param {string} rule
60
+ * @param {number} n
61
+ * @returns {RuleEntry}
62
+ */
63
+ const sizeRule = (rule, n) =>
64
+ rule === "max-lines-per-function"
65
+ ? ["error", { ...MAX_LINES, max: n }]
66
+ : ["error", n];
67
+
68
+ /**
69
+ * The strict rules for TypeScript source. Needs the @typescript-eslint plugin registered.
70
+ *
71
+ * @param {string[]} files
72
+ * @returns {Config}
73
+ */
74
+ export const strictTypeScript = (files) => ({
75
+ files,
76
+ plugins: { sonarjs },
77
+ rules: {
78
+ ...Object.fromEntries(
79
+ Object.entries(SIZE_LIMITS).map(([rule, n]) => [rule, sizeRule(rule, n)]),
80
+ ),
81
+ "@typescript-eslint/no-explicit-any": "error",
82
+ // `x as T` is a claim the checker takes on trust. Parse or narrow instead.
83
+ "@typescript-eslint/consistent-type-assertions": [
84
+ "error",
85
+ { assertionStyle: "never" },
86
+ ],
87
+ "@typescript-eslint/switch-exhaustiveness-check": [
88
+ "error",
89
+ { considerDefaultExhaustiveForUnions: true },
90
+ ],
91
+ // Not set here: `no-restricted-imports` and `no-restricted-syntax`. Flat config
92
+ // replaces a rule's options per file group, so a repo that sets either for its
93
+ // own reasons would silently drop ours. Merge ONE_COLLECTION_LIBRARY and
94
+ // AS_UNKNOWN_AS into the repo's own blocks instead.
95
+ },
96
+ });
97
+
98
+ /**
99
+ * A test's `describe` callback is a list of cases, not a function to split.
100
+ *
101
+ * @param {string[]} files
102
+ * @returns {Config}
103
+ */
104
+ export const testSizes = (files) => ({
105
+ files,
106
+ rules: { "max-lines-per-function": sizeRule("max-lines-per-function", 60) },
107
+ });
108
+
109
+ /**
110
+ * Per-file ceilings for functions written before the limits: each number is the
111
+ * file's measured maximum, so its functions may shrink and may not grow. Lower a
112
+ * number after a refactor; a file leaves the table once it is under the limits.
113
+ *
114
+ * @param {Record<string, Partial<Record<SizeRule, number>>>} table
115
+ * @returns {Config[]}
116
+ */
117
+ export const ceilings = (table) =>
118
+ Object.entries(table).map(([file, max]) => ({
119
+ files: [file],
120
+ rules: Object.fromEntries(
121
+ Object.entries(max).map(([rule, n]) => [rule, sizeRule(rule, n)]),
122
+ ),
123
+ }));
124
+
125
+ /**
126
+ * No `let`, no mutation, no loops, readonly parameters. Tests may mutate their fixtures.
127
+ *
128
+ * @param {Scope} scope
129
+ * @returns {Config}
130
+ */
131
+ export const functionalCode = ({ files, ignores }) => ({
132
+ files,
133
+ ignores,
134
+ plugins: { functional },
135
+ rules: {
136
+ "functional/no-let": "error",
137
+ "functional/immutable-data": "error",
138
+ "functional/no-loop-statements": "error",
139
+ "functional/prefer-immutable-types": [
140
+ "error",
141
+ { enforcement: "ReadonlyShallow", ignoreInferredTypes: true },
142
+ ],
143
+ },
144
+ });
145
+
146
+ /** Node modules that ARE effects. Only an `io` file, a test or the root may import one. */
147
+ export const IO_MODULES = [
148
+ "fs",
149
+ "fs/promises",
150
+ "child_process",
151
+ "os",
152
+ "net",
153
+ "http",
154
+ "https",
155
+ "worker_threads",
156
+ "readline",
157
+ "readline/promises",
158
+ ].flatMap((m) => [m, `node:${m}`]);
159
+
160
+ /** Libraries the domain and ports may use: types, pure path arithmetic, hashing, collections. */
161
+ export const PURE_LIBRARIES = [
162
+ "ts-essentials",
163
+ "remeda",
164
+ "path",
165
+ "node:path",
166
+ "crypto",
167
+ "node:crypto",
168
+ ];
169
+
170
+ /** What app files may use besides: Node's pure helpers. */
171
+ export const APP_LIBRARIES = [
172
+ ...PURE_LIBRARIES,
173
+ "util",
174
+ "node:util",
175
+ "url",
176
+ "node:url",
177
+ "module",
178
+ "node:module",
179
+ ];
180
+
181
+ const ORIGINS = ["external", "core"];
182
+ /** @param {string[]} source */
183
+ const libraries = (source) => ({ to: { module: { origin: ORIGINS, source } } });
184
+ /** @param {...string} types */
185
+ const elements = (...types) => ({ element: { types: { anyOf: types } } });
186
+ const APP = { file: { categories: "app" } };
187
+ const ROOT = { file: { categories: "root" } };
188
+
189
+ /** Who may import whom: the domain knows only itself, an adapter never another adapter. */
190
+ /**
191
+ * @param {string[]} domainLibraries
192
+ * @param {string[]} appLibraries
193
+ */
194
+ const knowledgePolicies = (domainLibraries, appLibraries) => [
195
+ { from: elements("domain"), allow: { to: elements("domain") } },
196
+ { from: elements("port"), allow: { to: elements("domain", "port") } },
197
+ { from: APP, allow: { to: [elements("domain", "port"), APP] } },
198
+ { from: elements("adapter"), allow: { to: elements("domain", "port") } },
199
+ { from: ROOT, allow: { to: [{ element: { type: "*" } }, APP, ROOT] } },
200
+ { from: elements("domain", "port"), allow: libraries(domainLibraries) },
201
+ { from: APP, allow: libraries(appLibraries) },
202
+ {
203
+ from: [elements("adapter"), ROOT],
204
+ allow: { to: { module: { origin: ORIGINS } } },
205
+ },
206
+ {
207
+ from: { file: { categories: "test" } },
208
+ allow: {
209
+ to: [
210
+ { element: { type: "*" } },
211
+ APP,
212
+ ROOT,
213
+ { module: { origin: ORIGINS } },
214
+ ],
215
+ },
216
+ },
217
+ ];
218
+
219
+ /**
220
+ * Effects only in io files, tests and the root. Written as "nobody, then these
221
+ * three": a file with no category has `categories: null`, and a `noneOf` query
222
+ * never matches null, so the negative form would exempt every plain file.
223
+ */
224
+ /** @param {string} ioPattern */
225
+ const effectPolicies = (ioPattern) => [
226
+ {
227
+ disallow: { to: { module: { origin: "core", source: IO_MODULES } } },
228
+ message: `{{ dependency.source }} is I/O in a file that is not ${ioPattern}. Take a port instead, and do the I/O in an adapter's io file or the root.`,
229
+ },
230
+ {
231
+ from: { file: { categories: ["io", "test", "root"] } },
232
+ allow: { to: { module: { origin: "core", source: IO_MODULES } } },
233
+ },
234
+ ];
235
+
236
+ /**
237
+ * @param {string} type
238
+ * @param {string | undefined} pattern
239
+ * @param {Record<string, unknown>} [extra]
240
+ */
241
+ const element = (type, pattern, extra = {}) =>
242
+ pattern === undefined
243
+ ? []
244
+ : [{ type, pattern, partialMatch: false, ...extra }];
245
+
246
+ /**
247
+ * Where a repo keeps each layer. Patterns are relative to `root`.
248
+ *
249
+ * @typedef {object} Layout
250
+ * @property {string[]} files the files the rules apply to
251
+ * @property {string} root the repository directory
252
+ * @property {string} domain folder of the domain, e.g. "src/core"
253
+ * @property {string} [ports] folder of the ports, if the repo has one
254
+ * @property {string} adapters adapter folders, one element each, e.g. "src/adapters/*"
255
+ * @property {string} cliRoot the composition root: the only non-test file that may wire anything to anything
256
+ * @property {string} app the flat application files, e.g. "src/*.ts"
257
+ * @property {string} tests test files
258
+ * @property {string} io files allowed to do I/O, e.g. "src/adapters/*\/*.io.ts"
259
+ * @property {string[]} [domainLibraries] what the domain and ports may import (default PURE_LIBRARIES)
260
+ * @property {string[]} [appLibraries] what app files may import (default APP_LIBRARIES)
261
+ * @property {object} [resolver] import/resolver settings (default: the TypeScript resolver)
262
+ * @property {object[]} [extraElements] more boundaries elements, e.g. a folder of plain-JS modules
263
+ * @property {object[]} [extraAllows] more allow-policies, checked before the effects rule
264
+ */
265
+
266
+ /**
267
+ * The hexagonal layers, as one eslint-plugin-boundaries block. Every linted file
268
+ * must be a declared element or category, so a new file cannot sit outside the
269
+ * rules.
270
+ *
271
+ * `root` must be the repository directory. Without `boundaries/root-path` the
272
+ * plugin matches patterns against `process.cwd()`, and lint started from any
273
+ * other directory classifies nothing and passes.
274
+ *
275
+ * @param {Layout} o
276
+ * @returns {Config}
277
+ */
278
+ export const layers = (o) => ({
279
+ files: o.files,
280
+ // @ts-expect-error The plugin's types describe an ES module whose `default` is
281
+ // the plugin; Node hands a default import of this CommonJS file the plugin
282
+ // itself (keys meta, rules, configs). Runtime is right, the declaration is not.
283
+ plugins: { boundaries },
284
+ settings: {
285
+ "boundaries/root-path": o.root,
286
+ "import/resolver": o.resolver ?? { typescript: { alwaysTryTypes: true } },
287
+ "boundaries/elements": [
288
+ ...element("port", o.ports),
289
+ ...element("domain", o.domain),
290
+ ...element("adapter", o.adapters, { capture: ["name"] }),
291
+ ...(o.extraElements ?? []),
292
+ ],
293
+ "boundaries/files": [
294
+ { category: "root", pattern: o.cliRoot, exclusive: true },
295
+ { category: "app", pattern: o.app },
296
+ { category: "test", pattern: o.tests },
297
+ { category: "io", pattern: o.io },
298
+ ],
299
+ },
300
+ rules: {
301
+ "boundaries/no-unknown-files": "error",
302
+ "boundaries/no-unknown-dependencies": "error",
303
+ "boundaries/dependencies": [
304
+ "error",
305
+ {
306
+ default: "disallow",
307
+ checkAllOrigins: true,
308
+ policies: [
309
+ ...knowledgePolicies(
310
+ o.domainLibraries ?? PURE_LIBRARIES,
311
+ o.appLibraries ?? APP_LIBRARIES,
312
+ ),
313
+ ...(o.extraAllows ?? []),
314
+ ...effectPolicies(o.io),
315
+ ],
316
+ },
317
+ ],
318
+ },
319
+ });
320
+
321
+ /**
322
+ * `process` and `fetch` are not imports, so `layers` cannot see them. Same scope as its effects rule.
323
+ *
324
+ * @param {Scope} scope
325
+ * @returns {Config}
326
+ */
327
+ export const ioGlobals = ({ files, ignores }) => ({
328
+ files,
329
+ ignores,
330
+ rules: {
331
+ "no-restricted-globals": [
332
+ "error",
333
+ {
334
+ name: "process",
335
+ message:
336
+ "The environment is an input: the root reads it and passes a value in.",
337
+ },
338
+ {
339
+ name: "fetch",
340
+ message: "Network access belongs in an adapter's io file.",
341
+ },
342
+ ],
343
+ },
344
+ });