vigiles 2.0.0 → 2.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 (184) hide show
  1. package/README.md +182 -134
  2. package/dist/action-gate.d.ts +28 -0
  3. package/dist/action-gate.js +73 -0
  4. package/dist/cli.js +705 -230
  5. package/dist/community-skills.d.ts +22 -0
  6. package/dist/community-skills.js +86 -0
  7. package/dist/compile-generator.d.ts +48 -0
  8. package/dist/compile-generator.js +322 -0
  9. package/dist/compile.d.ts +4 -0
  10. package/dist/compile.js +239 -45
  11. package/dist/coverage.d.ts +56 -0
  12. package/dist/coverage.js +178 -0
  13. package/dist/doc-refs.d.ts +60 -0
  14. package/dist/doc-refs.js +246 -0
  15. package/dist/eval.d.ts +62 -0
  16. package/dist/eval.js +174 -0
  17. package/dist/evolve.js +60 -125
  18. package/dist/frontmatter.d.ts +75 -0
  19. package/dist/frontmatter.js +263 -0
  20. package/dist/generate-schema.d.ts +51 -0
  21. package/dist/generate-schema.js +125 -0
  22. package/dist/generate-types.js +39 -1
  23. package/dist/harness-test.d.ts +38 -0
  24. package/dist/harness-test.js +129 -0
  25. package/dist/hash.d.ts +8 -0
  26. package/dist/hash.js +16 -0
  27. package/dist/inline.d.ts +22 -4
  28. package/dist/inline.js +60 -13
  29. package/dist/integrity.d.ts +29 -0
  30. package/dist/integrity.js +44 -0
  31. package/dist/linters.d.ts +5 -3
  32. package/dist/linters.js +144 -4
  33. package/dist/mock-model.d.ts +31 -0
  34. package/dist/mock-model.js +189 -0
  35. package/dist/orphans.d.ts +52 -0
  36. package/dist/orphans.js +124 -0
  37. package/dist/proofs.js +22 -16
  38. package/dist/refs.d.ts +44 -0
  39. package/dist/refs.js +144 -0
  40. package/dist/session.d.ts +97 -0
  41. package/dist/session.js +306 -0
  42. package/dist/sidecar.d.ts +35 -0
  43. package/dist/sidecar.js +102 -0
  44. package/dist/skill-driver.d.ts +77 -0
  45. package/dist/skill-driver.js +76 -0
  46. package/dist/skill-runtime.d.ts +101 -0
  47. package/dist/skill-runtime.js +289 -0
  48. package/dist/skill-test.d.ts +47 -0
  49. package/dist/skill-test.js +77 -0
  50. package/dist/spec.d.ts +119 -13
  51. package/dist/spec.js +51 -4
  52. package/dist/symbols.d.ts +30 -0
  53. package/dist/symbols.js +142 -0
  54. package/dist/test-utils.d.ts +8 -0
  55. package/dist/test-utils.js +41 -0
  56. package/dist/types.d.ts +34 -8
  57. package/dist/types.js +19 -0
  58. package/dist/validate.js +5 -3
  59. package/package.json +26 -5
  60. package/.claude/settings.json +0 -46
  61. package/.claude/settings.local.json +0 -8
  62. package/.github/workflows/ci.yml +0 -81
  63. package/.prettierignore +0 -1
  64. package/.vigiles/generated.d.ts +0 -205
  65. package/CLAUDE.md +0 -95
  66. package/CLAUDE.md.spec.ts +0 -142
  67. package/CONTRIBUTING.md +0 -121
  68. package/dist/action.d.ts.map +0 -1
  69. package/dist/action.js.map +0 -1
  70. package/dist/cli.d.ts.map +0 -1
  71. package/dist/cli.js.map +0 -1
  72. package/dist/cli.test.d.ts +0 -2
  73. package/dist/cli.test.d.ts.map +0 -1
  74. package/dist/cli.test.js +0 -650
  75. package/dist/cli.test.js.map +0 -1
  76. package/dist/compile.d.ts.map +0 -1
  77. package/dist/compile.js.map +0 -1
  78. package/dist/evolve.d.ts.map +0 -1
  79. package/dist/evolve.js.map +0 -1
  80. package/dist/freshness.d.ts +0 -67
  81. package/dist/freshness.d.ts.map +0 -1
  82. package/dist/freshness.js +0 -244
  83. package/dist/freshness.js.map +0 -1
  84. package/dist/freshness.test.d.ts +0 -2
  85. package/dist/freshness.test.d.ts.map +0 -1
  86. package/dist/freshness.test.js +0 -356
  87. package/dist/freshness.test.js.map +0 -1
  88. package/dist/generate-types.d.ts.map +0 -1
  89. package/dist/generate-types.js.map +0 -1
  90. package/dist/inline.d.ts.map +0 -1
  91. package/dist/inline.js.map +0 -1
  92. package/dist/inline.test.d.ts +0 -5
  93. package/dist/inline.test.d.ts.map +0 -1
  94. package/dist/inline.test.js +0 -152
  95. package/dist/inline.test.js.map +0 -1
  96. package/dist/linters.d.ts.map +0 -1
  97. package/dist/linters.js.map +0 -1
  98. package/dist/proofs.d.ts.map +0 -1
  99. package/dist/proofs.js.map +0 -1
  100. package/dist/proofs.test.d.ts +0 -9
  101. package/dist/proofs.test.d.ts.map +0 -1
  102. package/dist/proofs.test.js +0 -952
  103. package/dist/proofs.test.js.map +0 -1
  104. package/dist/spec.d.ts.map +0 -1
  105. package/dist/spec.js.map +0 -1
  106. package/dist/spec.test.d.ts +0 -2
  107. package/dist/spec.test.d.ts.map +0 -1
  108. package/dist/spec.test.js +0 -1222
  109. package/dist/spec.test.js.map +0 -1
  110. package/dist/types.d.ts.map +0 -1
  111. package/dist/types.js.map +0 -1
  112. package/dist/validate.d.ts.map +0 -1
  113. package/dist/validate.js.map +0 -1
  114. package/dist/validate.test.d.ts +0 -2
  115. package/dist/validate.test.d.ts.map +0 -1
  116. package/dist/validate.test.js +0 -531
  117. package/dist/validate.test.js.map +0 -1
  118. package/docs/agent-setup.md +0 -85
  119. package/docs/agent-workflows.md +0 -103
  120. package/docs/comparison.md +0 -71
  121. package/docs/freshness.md +0 -124
  122. package/docs/inline-mode.md +0 -119
  123. package/docs/linter-support.md +0 -166
  124. package/docs/spec-format.md +0 -194
  125. package/eslint.config.mjs +0 -79
  126. package/examples/CLAUDE.md +0 -54
  127. package/examples/CLAUDE.md.spec.ts +0 -65
  128. package/examples/SKILL.md +0 -50
  129. package/examples/SKILL.md.spec.ts +0 -57
  130. package/fixtures/example-project/CLAUDE.md +0 -11
  131. package/fixtures/example-project/package.json +0 -9
  132. package/fixtures/example-project/src/index.ts +0 -3
  133. package/fixtures/example-project/src/utils.test.ts +0 -2
  134. package/fixtures/example-project/src/utils.ts +0 -3
  135. package/logo.png +0 -0
  136. package/research/adoption-strategy.md +0 -111
  137. package/research/agent-integration.md +0 -145
  138. package/research/ai-code-quality.md +0 -197
  139. package/research/code-search-for-agents.md +0 -313
  140. package/research/competitive-landscape.md +0 -163
  141. package/research/doc-freshness.md +0 -516
  142. package/research/executable-specs.md +0 -368
  143. package/research/feature-ideas.md +0 -464
  144. package/research/formal-proofs-for-agents.md +0 -338
  145. package/research/fp-for-agent-harness.md +0 -150
  146. package/research/fp-for-deterministic-ai.md +0 -131
  147. package/research/self-evolving-specs.md +0 -298
  148. package/schemas/claude-md-strict.yml +0 -18
  149. package/schemas/claude-md.yml +0 -6
  150. package/schemas/skill-strict.yml +0 -12
  151. package/schemas/skill.yml +0 -5
  152. package/skills/audit-feedback-loop/SKILL.md +0 -76
  153. package/skills/edit-spec/SKILL.md +0 -131
  154. package/skills/enforce-rules-format/SKILL.md +0 -71
  155. package/skills/generate-logo/SKILL.md +0 -102
  156. package/skills/generate-rule/SKILL.md +0 -90
  157. package/skills/linter-docs/clippy.md +0 -241
  158. package/skills/linter-docs/eslint.md +0 -384
  159. package/skills/linter-docs/pylint.md +0 -288
  160. package/skills/linter-docs/rubocop.md +0 -277
  161. package/skills/linter-docs/ruff.md +0 -187
  162. package/skills/linter-docs/stylelint.md +0 -247
  163. package/skills/migrate-to-spec/SKILL.md +0 -124
  164. package/skills/pr-to-lint-rule/SKILL.md +0 -97
  165. package/skills/strengthen/SKILL.md +0 -168
  166. package/src/action.ts +0 -214
  167. package/src/cli.test.ts +0 -914
  168. package/src/cli.ts +0 -1631
  169. package/src/compile.ts +0 -691
  170. package/src/evolve.ts +0 -781
  171. package/src/freshness.test.ts +0 -449
  172. package/src/freshness.ts +0 -299
  173. package/src/generate-types.ts +0 -448
  174. package/src/inline.test.ts +0 -206
  175. package/src/inline.ts +0 -164
  176. package/src/linters.ts +0 -739
  177. package/src/proofs.test.ts +0 -1314
  178. package/src/proofs.ts +0 -849
  179. package/src/spec.test.ts +0 -1471
  180. package/src/spec.ts +0 -427
  181. package/src/types.ts +0 -117
  182. package/src/validate.test.ts +0 -701
  183. package/src/validate.ts +0 -381
  184. package/tsconfig.json +0 -23
@@ -1,448 +0,0 @@
1
- /**
2
- * vigiles generate-types — emit .d.ts with type unions from actual project state.
3
- *
4
- * Scans linter configs, package.json, and filesystem to generate TypeScript
5
- * types that the compiler uses to PROVE references are valid at authoring time.
6
- *
7
- * Generated types:
8
- * - EslintRule / StylelintRule / RuffRule / ... — enabled rules per linter
9
- * - NpmScript — scripts from package.json
10
- * - ProjectFile — files in the project (scoped to src/ by default)
11
- */
12
-
13
- import { existsSync, readFileSync } from "node:fs";
14
- import { resolve } from "node:path";
15
- import { execSync } from "node:child_process";
16
- import { globSync } from "glob";
17
-
18
- // ---------------------------------------------------------------------------
19
- // Linter config detection
20
- // ---------------------------------------------------------------------------
21
-
22
- function fileContainsSection(filePath: string, section: string): boolean {
23
- if (!existsSync(filePath)) return false;
24
- try {
25
- return readFileSync(filePath, "utf-8").includes(section);
26
- } catch {
27
- return false;
28
- }
29
- }
30
-
31
- function hasRuffConfig(basePath: string): boolean {
32
- return (
33
- existsSync(resolve(basePath, "ruff.toml")) ||
34
- existsSync(resolve(basePath, ".ruff.toml")) ||
35
- fileContainsSection(resolve(basePath, "pyproject.toml"), "[tool.ruff")
36
- );
37
- }
38
-
39
- function hasPylintConfig(basePath: string): boolean {
40
- return (
41
- existsSync(resolve(basePath, ".pylintrc")) ||
42
- existsSync(resolve(basePath, "pylintrc")) ||
43
- fileContainsSection(resolve(basePath, "pyproject.toml"), "[tool.pylint") ||
44
- fileContainsSection(resolve(basePath, "setup.cfg"), "[pylint")
45
- );
46
- }
47
-
48
- function hasRubocopConfig(basePath: string): boolean {
49
- return existsSync(resolve(basePath, ".rubocop.yml"));
50
- }
51
-
52
- // ---------------------------------------------------------------------------
53
- // Linter rule discovery
54
- // ---------------------------------------------------------------------------
55
-
56
- interface DiscoveredRules {
57
- linter: string;
58
- rules: string[];
59
- via: string;
60
- }
61
-
62
- function discoverEslintRules(basePath: string): DiscoveredRules | null {
63
- try {
64
- const script = `
65
- const { loadESLint } = require("eslint");
66
- (async () => {
67
- try {
68
- const ESLint = await loadESLint();
69
- const eslint = new ESLint({ cwd: ${JSON.stringify(basePath)} });
70
- const config = await eslint.calculateConfigForFile("dummy.js");
71
- const enabled = Object.entries(config.rules || {})
72
- .filter(([, v]) => {
73
- const sev = Array.isArray(v) ? v[0] : v;
74
- return sev !== 0 && sev !== "off";
75
- })
76
- .map(([k]) => k);
77
- console.log(JSON.stringify(enabled));
78
- } catch(e) {
79
- console.log("[]");
80
- }
81
- })();
82
- `;
83
- const output = execSync(`node -e '${script.replace(/'/g, "'\\''")}'`, {
84
- encoding: "utf-8",
85
- cwd: basePath,
86
- stdio: ["pipe", "pipe", "pipe"],
87
- timeout: 15000,
88
- });
89
- const rules = JSON.parse(output.trim() || "[]") as string[];
90
- if (rules.length === 0) return null;
91
- return { linter: "eslint", rules, via: "flat config" };
92
- } catch {
93
- return null;
94
- }
95
- }
96
-
97
- function discoverStylelintRules(basePath: string): DiscoveredRules | null {
98
- try {
99
- const script = `
100
- const stylelint = require("stylelint");
101
- (async () => {
102
- try {
103
- const linter = stylelint.createLinter({});
104
- const result = await linter.getConfigForFile(${JSON.stringify(resolve(basePath, "dummy.css"))});
105
- const enabled = Object.entries(result.config.rules || {})
106
- .filter(([, v]) => v !== null && !(Array.isArray(v) && v[0] === null))
107
- .map(([k]) => k);
108
- console.log(JSON.stringify(enabled));
109
- } catch(e) {
110
- console.log("[]");
111
- }
112
- })();
113
- `;
114
- const output = execSync(`node -e '${script.replace(/'/g, "'\\''")}'`, {
115
- encoding: "utf-8",
116
- cwd: basePath,
117
- stdio: ["pipe", "pipe", "pipe"],
118
- timeout: 15000,
119
- });
120
- const rules = JSON.parse(output.trim() || "[]") as string[];
121
- if (rules.length === 0) return null;
122
- return { linter: "stylelint", rules, via: "config" };
123
- } catch {
124
- return null;
125
- }
126
- }
127
-
128
- function discoverRuffRules(basePath: string): DiscoveredRules | null {
129
- try {
130
- if (!hasRuffConfig(basePath)) return null;
131
- execSync("which ruff", { stdio: "ignore" });
132
- const dummyPath = resolve(basePath, "dummy.py");
133
- const output = execSync(`ruff check --show-settings ${dummyPath}`, {
134
- encoding: "utf-8",
135
- cwd: basePath,
136
- stdio: ["pipe", "pipe", "pipe"],
137
- timeout: 10000,
138
- });
139
- const enabledMatch = output.match(
140
- /linter\.rules\.enabled\s*=\s*\[([\s\S]*?)\]/,
141
- );
142
- const rules: string[] = [];
143
- if (enabledMatch?.[1]) {
144
- const codeRe = /\(([A-Z]+\d*)\)/g;
145
- let m: RegExpExecArray | null;
146
- while ((m = codeRe.exec(enabledMatch[1])) !== null) {
147
- rules.push(m[1]);
148
- }
149
- }
150
- if (rules.length === 0) return null;
151
- return { linter: "ruff", rules, via: "CLI" };
152
- } catch {
153
- return null;
154
- }
155
- }
156
-
157
- function discoverPylintRules(basePath: string): DiscoveredRules | null {
158
- try {
159
- if (!hasPylintConfig(basePath)) return null;
160
- execSync("which pylint", { stdio: "ignore" });
161
- const output = execSync("pylint --list-msgs-enabled", {
162
- encoding: "utf-8",
163
- cwd: basePath,
164
- stdio: ["pipe", "pipe", "pipe"],
165
- timeout: 15000,
166
- });
167
- const disabledIdx = output.indexOf("Disabled messages:");
168
- const enabledSection =
169
- disabledIdx >= 0 ? output.substring(0, disabledIdx) : output;
170
- // Extract rule IDs like (C0114), (W0611) etc.
171
- const rules: string[] = [];
172
- const idRe = /\(([A-Z]\d{4})\)/g;
173
- let m: RegExpExecArray | null;
174
- while ((m = idRe.exec(enabledSection)) !== null) {
175
- rules.push(m[1]);
176
- }
177
- // Also extract symbolic names like "missing-module-docstring"
178
- const nameRe = /^(\w[\w-]+)\s*\(/gm;
179
- while ((m = nameRe.exec(enabledSection)) !== null) {
180
- rules.push(m[1]);
181
- }
182
- if (rules.length === 0) return null;
183
- return { linter: "pylint", rules, via: "CLI" };
184
- } catch {
185
- return null;
186
- }
187
- }
188
-
189
- function discoverRubocopRules(basePath: string): DiscoveredRules | null {
190
- try {
191
- if (!hasRubocopConfig(basePath)) return null;
192
- execSync("which rubocop", { stdio: "ignore" });
193
- const output = execSync(
194
- "rubocop --list-target-files --show-cops 2>/dev/null | head -500",
195
- {
196
- encoding: "utf-8",
197
- cwd: basePath,
198
- stdio: ["pipe", "pipe", "pipe"],
199
- timeout: 15000,
200
- shell: "/bin/sh",
201
- },
202
- );
203
- const rules: string[] = [];
204
- const copRe = /^(\w+\/\w+):/gm;
205
- let m: RegExpExecArray | null;
206
- while ((m = copRe.exec(output)) !== null) {
207
- rules.push(m[1]);
208
- }
209
- if (rules.length === 0) return null;
210
- return { linter: "rubocop", rules, via: "CLI" };
211
- } catch {
212
- return null;
213
- }
214
- }
215
-
216
- function discoverClippyRules(basePath: string): DiscoveredRules | null {
217
- try {
218
- const cargoPath = resolve(basePath, "Cargo.toml");
219
- if (!existsSync(cargoPath)) return null;
220
- execSync("which cargo", { stdio: "ignore" });
221
- // Clippy doesn't have a good "list enabled lints" command.
222
- // We read Cargo.toml [lints.clippy] section for explicit config,
223
- // and include default warn/deny lints from clippy -W clippy::all
224
- const content = readFileSync(cargoPath, "utf-8");
225
- const sectionMatch = content.match(/\[lints\.clippy\]([\s\S]*?)(?=\n\[|$)/);
226
- const rules: string[] = [];
227
- if (sectionMatch?.[1]) {
228
- const ruleRe = /^(\w[\w-]+)\s*=\s*"(\w+)"/gm;
229
- let m: RegExpExecArray | null;
230
- while ((m = ruleRe.exec(sectionMatch[1])) !== null) {
231
- if (m[2] !== "allow") {
232
- rules.push(m[1]);
233
- }
234
- }
235
- }
236
- if (rules.length === 0) return null;
237
- return { linter: "clippy", rules, via: "Cargo.toml" };
238
- } catch {
239
- return null;
240
- }
241
- }
242
-
243
- // ---------------------------------------------------------------------------
244
- // NPM script discovery
245
- // ---------------------------------------------------------------------------
246
-
247
- function discoverNpmScripts(basePath: string): string[] {
248
- const pkgPath = resolve(basePath, "package.json");
249
- if (!existsSync(pkgPath)) return [];
250
- try {
251
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
252
- scripts?: Record<string, string>;
253
- };
254
- return Object.keys(pkg.scripts ?? {});
255
- } catch {
256
- return [];
257
- }
258
- }
259
-
260
- // ---------------------------------------------------------------------------
261
- // Project file discovery
262
- // ---------------------------------------------------------------------------
263
-
264
- function discoverProjectFiles(
265
- basePath: string,
266
- globs: string[] = ["src/**/*"],
267
- ): string[] {
268
- const files: string[] = [];
269
- for (const pattern of globs) {
270
- const matches = globSync(pattern, {
271
- cwd: basePath,
272
- ignore: ["node_modules/**", "dist/**", ".git/**"],
273
- nodir: true,
274
- });
275
- files.push(...matches);
276
- }
277
- return [...new Set(files)].sort();
278
- }
279
-
280
- // ---------------------------------------------------------------------------
281
- // Type generation
282
- // ---------------------------------------------------------------------------
283
-
284
- function escapeForUnion(s: string): string {
285
- return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
286
- }
287
-
288
- function formatUnion(items: string[], indent: string = " "): string {
289
- if (items.length === 0) return "never";
290
- if (items.length <= 3) {
291
- return items.map((i) => `"${escapeForUnion(i)}"`).join(" | ");
292
- }
293
- return (
294
- "\n" + items.map((i) => `${indent}| "${escapeForUnion(i)}"`).join("\n")
295
- );
296
- }
297
-
298
- export interface GenerateTypesOptions {
299
- basePath?: string;
300
- fileGlobs?: string[];
301
- }
302
-
303
- export interface GenerateTypesResult {
304
- dts: string;
305
- linters: DiscoveredRules[];
306
- scripts: string[];
307
- files: string[];
308
- }
309
-
310
- /**
311
- * Generate .d.ts content with type unions from actual project state.
312
- *
313
- * Scans all available linters, package.json scripts, and project files.
314
- */
315
- export function generateTypes(
316
- options: GenerateTypesOptions = {},
317
- ): GenerateTypesResult {
318
- const basePath = options.basePath ?? process.cwd();
319
- const fileGlobs = options.fileGlobs ?? ["src/**/*"];
320
-
321
- // Discover everything
322
- const linters: DiscoveredRules[] = [];
323
-
324
- const eslint = discoverEslintRules(basePath);
325
- if (eslint) linters.push(eslint);
326
-
327
- const stylelint = discoverStylelintRules(basePath);
328
- if (stylelint) linters.push(stylelint);
329
-
330
- const ruff = discoverRuffRules(basePath);
331
- if (ruff) linters.push(ruff);
332
-
333
- const pylint = discoverPylintRules(basePath);
334
- if (pylint) linters.push(pylint);
335
-
336
- const rubocop = discoverRubocopRules(basePath);
337
- if (rubocop) linters.push(rubocop);
338
-
339
- const clippy = discoverClippyRules(basePath);
340
- if (clippy) linters.push(clippy);
341
-
342
- const scripts = discoverNpmScripts(basePath);
343
- const files = discoverProjectFiles(basePath, fileGlobs);
344
-
345
- // Generate .d.ts
346
- const sections: string[] = [];
347
-
348
- sections.push(`/**`);
349
- sections.push(` * Auto-generated by \`vigiles generate-types\`.`);
350
- sections.push(
351
- ` * DO NOT EDIT — re-run \`vigiles generate-types\` to update.`,
352
- );
353
- sections.push(` */`);
354
- sections.push(``);
355
- // ---------------------------------------------------------------------------
356
- // vigiles/generated — standalone types for direct import
357
- // ---------------------------------------------------------------------------
358
-
359
- sections.push(`declare module "vigiles/generated" {`);
360
-
361
- // Linter rules
362
- for (const { linter, rules, via } of linters) {
363
- const typeName =
364
- linter.charAt(0).toUpperCase() +
365
- linter.slice(1).replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) +
366
- "Rule";
367
- sections.push(``);
368
- sections.push(
369
- ` /** ${String(rules.length)} enabled ${linter} rules (via ${via}). */`,
370
- );
371
- sections.push(` export type ${typeName} = ${formatUnion(rules, " ")};`);
372
- }
373
-
374
- // Combined linter rule type
375
- if (linters.length > 0) {
376
- const allNames = linters.map(
377
- (l) =>
378
- l.linter.charAt(0).toUpperCase() +
379
- l.linter
380
- .slice(1)
381
- .replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) +
382
- "Rule",
383
- );
384
- sections.push(``);
385
- sections.push(
386
- ` /** All enabled linter rules across all detected linters. */`,
387
- );
388
- sections.push(` export type LinterRule = ${allNames.join(" | ")};`);
389
- }
390
-
391
- // NPM scripts
392
- if (scripts.length > 0) {
393
- sections.push(``);
394
- sections.push(
395
- ` /** ${String(scripts.length)} npm scripts from package.json. */`,
396
- );
397
- sections.push(` export type NpmScript = ${formatUnion(scripts, " ")};`);
398
- }
399
-
400
- // Project files
401
- if (files.length > 0) {
402
- sections.push(``);
403
- sections.push(` /** ${String(files.length)} project files. */`);
404
- sections.push(` export type ProjectFile = ${formatUnion(files, " ")};`);
405
- }
406
-
407
- sections.push(`}`);
408
-
409
- // ---------------------------------------------------------------------------
410
- // vigiles/spec augmentation — populates KnownLinterRules, KnownProjectFiles,
411
- // KnownNpmScripts interfaces so enforce(), file(), cmd() narrow automatically.
412
- // ---------------------------------------------------------------------------
413
-
414
- sections.push(``);
415
- sections.push(`declare module "vigiles/spec" {`);
416
-
417
- if (linters.length > 0) {
418
- sections.push(` interface KnownLinterRules {`);
419
- for (const { linter, rules } of linters) {
420
- sections.push(
421
- ` "${escapeForUnion(linter)}": ${formatUnion(rules, " ")};`,
422
- );
423
- }
424
- sections.push(` }`);
425
- }
426
-
427
- if (files.length > 0) {
428
- sections.push(` interface KnownProjectFiles {`);
429
- sections.push(` files: ${formatUnion(files, " ")};`);
430
- sections.push(` }`);
431
- }
432
-
433
- if (scripts.length > 0) {
434
- sections.push(` interface KnownNpmScripts {`);
435
- sections.push(` scripts: ${formatUnion(scripts, " ")};`);
436
- sections.push(` }`);
437
- }
438
-
439
- sections.push(`}`);
440
- sections.push(``);
441
-
442
- return {
443
- dts: sections.join("\n"),
444
- linters,
445
- scripts,
446
- files,
447
- };
448
- }
@@ -1,206 +0,0 @@
1
- /**
2
- * Tests for the inline-rule parser.
3
- */
4
-
5
- import { describe, it } from "node:test";
6
- import assert from "node:assert/strict";
7
-
8
- import { parseInlineRules, hasInlineRules } from "./inline.js";
9
-
10
- describe("parseInlineRules", () => {
11
- it("extracts a single enforce comment", () => {
12
- const { rules, errors } = parseInlineRules(
13
- `# Project
14
-
15
- <!-- vigiles:enforce eslint/no-console "Use structured logger" -->
16
-
17
- ## Logging
18
-
19
- All output through logger.ts.
20
- `,
21
- );
22
- assert.equal(errors.length, 0);
23
- assert.equal(rules.length, 1);
24
- assert.equal(rules[0].linterRule, "eslint/no-console");
25
- assert.equal(rules[0].why, "Use structured logger");
26
- assert.equal(rules[0].line, 3);
27
- });
28
-
29
- it("extracts multiple enforce comments and tracks line numbers", () => {
30
- const content = `# Doc
31
- <!-- vigiles:enforce eslint/no-console "a" -->
32
- text
33
- <!-- vigiles:enforce ruff/F401 "b" -->
34
- `;
35
- const { rules, errors } = parseInlineRules(content);
36
- assert.equal(errors.length, 0);
37
- assert.equal(rules.length, 2);
38
- assert.equal(rules[0].linterRule, "eslint/no-console");
39
- assert.equal(rules[0].line, 2);
40
- assert.equal(rules[1].linterRule, "ruff/F401");
41
- assert.equal(rules[1].line, 4);
42
- });
43
-
44
- it("supports plugin-scoped rule names with slashes", () => {
45
- const { rules, errors } = parseInlineRules(
46
- `<!-- vigiles:enforce eslint/@typescript-eslint/no-floating-promises "Await or void" -->`,
47
- );
48
- assert.equal(errors.length, 0);
49
- assert.equal(rules.length, 1);
50
- assert.equal(
51
- rules[0].linterRule,
52
- "eslint/@typescript-eslint/no-floating-promises",
53
- );
54
- });
55
-
56
- it("reports malformed vigiles:enforce as a parse error", () => {
57
- const { rules, errors } = parseInlineRules(
58
- `<!-- vigiles:enforce eslint/no-console no quotes here -->`,
59
- );
60
- assert.equal(rules.length, 0);
61
- assert.equal(errors.length, 1);
62
- assert.match(errors[0].message, /Malformed vigiles:enforce/);
63
- });
64
-
65
- it("reports unknown vigiles markers", () => {
66
- const { rules, errors } = parseInlineRules(
67
- `<!-- vigiles:strengthen eslint/no-console -->`,
68
- );
69
- assert.equal(rules.length, 0);
70
- assert.equal(errors.length, 1);
71
- assert.match(errors[0].message, /Unknown vigiles marker "strengthen"/);
72
- });
73
-
74
- it("returns empty results for a file with no vigiles comments", () => {
75
- const { rules, errors } = parseInlineRules(
76
- `# Plain\n\nJust prose, no rules.\n`,
77
- );
78
- assert.equal(rules.length, 0);
79
- assert.equal(errors.length, 0);
80
- });
81
-
82
- it("skips the compiled-file hash marker without reporting an error", () => {
83
- const { rules, errors } = parseInlineRules(
84
- `<!-- vigiles:sha256:abc123 compiled from CLAUDE.md.spec.ts -->
85
- # Project
86
- <!-- vigiles:enforce eslint/no-console "why" -->
87
- `,
88
- );
89
- assert.equal(errors.length, 0, JSON.stringify(errors));
90
- assert.equal(rules.length, 1);
91
- });
92
-
93
- it("ignores vigiles:enforce markers inside fenced code blocks", () => {
94
- // Illustrative example in docs/inline-mode.md would otherwise get
95
- // picked up as a live rule.
96
- const { rules, errors } = parseInlineRules(
97
- `# Docs
98
-
99
- Example usage:
100
-
101
- \`\`\`md
102
- <!-- vigiles:enforce eslint/no-console "Use structured logger" -->
103
- <!-- vigiles:enforce ruff/F401 "No unused imports" -->
104
- \`\`\`
105
-
106
- And a real one outside the fence:
107
-
108
- <!-- vigiles:enforce eslint/no-eval "Never eval user input" -->
109
- `,
110
- );
111
- assert.equal(errors.length, 0, JSON.stringify(errors));
112
- assert.equal(rules.length, 1);
113
- assert.equal(rules[0].linterRule, "eslint/no-eval");
114
- });
115
-
116
- it("ignores markers inside tilde-fenced code blocks", () => {
117
- const { rules } = parseInlineRules(
118
- `~~~md
119
- <!-- vigiles:enforce eslint/no-console "inside fence" -->
120
- ~~~
121
- <!-- vigiles:enforce eslint/no-eval "outside fence" -->
122
- `,
123
- );
124
- assert.equal(rules.length, 1);
125
- assert.equal(rules[0].linterRule, "eslint/no-eval");
126
- });
127
-
128
- it("ignores markers inside inline code spans in prose", () => {
129
- // Prose that describes the syntax with a backtick-wrapped example
130
- // must not trigger "malformed vigiles:enforce".
131
- const { rules, errors } = parseInlineRules(
132
- `You add \`<!-- vigiles:enforce eslint/no-console "why" -->\` comments to adopt gradually.
133
-
134
- Real rule below:
135
-
136
- <!-- vigiles:enforce eslint/no-eval "Never eval" -->
137
- `,
138
- );
139
- assert.equal(errors.length, 0, JSON.stringify(errors));
140
- assert.equal(rules.length, 1);
141
- assert.equal(rules[0].linterRule, "eslint/no-eval");
142
- });
143
-
144
- it("ignores ellipsis placeholders inside inline code spans", () => {
145
- const { rules, errors } = parseInlineRules(
146
- `You add \`<!-- vigiles:enforce ... -->\` comments to adopt gradually.
147
- `,
148
- );
149
- assert.equal(errors.length, 0, JSON.stringify(errors));
150
- assert.equal(rules.length, 0);
151
- });
152
-
153
- it("handles nested backtick fences of different lengths", () => {
154
- // Opening with 4 backticks, closing with 3 inside should NOT close
155
- // the outer block — only a 4+ backtick close matches.
156
- const { rules } = parseInlineRules(
157
- `\`\`\`\`md
158
- \`\`\`
159
- <!-- vigiles:enforce eslint/no-console "inside outer" -->
160
- \`\`\`
161
- \`\`\`\`
162
- <!-- vigiles:enforce eslint/no-eval "outside" -->
163
- `,
164
- );
165
- assert.equal(rules.length, 1);
166
- assert.equal(rules[0].linterRule, "eslint/no-eval");
167
- });
168
- });
169
-
170
- describe("hasInlineRules", () => {
171
- it("returns true when a valid enforce comment exists", () => {
172
- assert.equal(
173
- hasInlineRules(`<!-- vigiles:enforce eslint/no-console "why" -->`),
174
- true,
175
- );
176
- });
177
-
178
- it("returns false for plain markdown", () => {
179
- assert.equal(hasInlineRules(`# Title\n\nparagraph\n`), false);
180
- });
181
-
182
- it("returns false for a malformed marker", () => {
183
- assert.equal(
184
- hasInlineRules(`<!-- vigiles:enforce eslint/no-console no-quotes -->`),
185
- false,
186
- );
187
- });
188
-
189
- it("returns false for markers trapped inside fenced code blocks", () => {
190
- assert.equal(
191
- hasInlineRules(
192
- `# Docs\n\n\`\`\`md\n<!-- vigiles:enforce eslint/no-console "x" -->\n\`\`\`\n`,
193
- ),
194
- false,
195
- );
196
- });
197
-
198
- it("returns true only for parseable rules outside fences", () => {
199
- assert.equal(
200
- hasInlineRules(
201
- `\`\`\`md\n<!-- vigiles:enforce eslint/no-console "inside" -->\n\`\`\`\n<!-- vigiles:enforce eslint/no-eval "outside" -->\n`,
202
- ),
203
- true,
204
- );
205
- });
206
- });