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,701 +0,0 @@
1
- import { describe, it, before, after } from "node:test";
2
- import assert from "node:assert/strict";
3
- import {
4
- mkdtempSync,
5
- writeFileSync,
6
- rmSync,
7
- symlinkSync,
8
- mkdirSync,
9
- } from "node:fs";
10
- import { join } from "node:path";
11
- import { tmpdir } from "node:os";
12
- import {
13
- validate,
14
- parseRules,
15
- readInstructionFile,
16
- validatePaths,
17
- expandGlobs,
18
- findInstructionFiles,
19
- loadConfig,
20
- } from "./validate.js";
21
- import type { MarkerType, ParseOptions } from "./types.js";
22
-
23
- // ---------------------------------------------------------------------------
24
- // parseRules
25
- // ---------------------------------------------------------------------------
26
-
27
- describe("parseRules", () => {
28
- it("should parse enforced rules", () => {
29
- const rules = parseRules(
30
- "### Use barrel imports\n**Enforced by:** `eslint/no-restricted-imports`\n**Why:** Consistency.\n",
31
- );
32
- assert.equal(rules.length, 1);
33
- assert.equal(rules[0].title, "Use barrel imports");
34
- assert.equal(rules[0].enforcement, "enforced");
35
- assert.equal(rules[0].enforcedBy, "eslint/no-restricted-imports");
36
- });
37
-
38
- it("should parse guidance-only rules", () => {
39
- const rules = parseRules(
40
- "### Use Tailwind spacing scale\n**Guidance only** — cannot be mechanically enforced\n",
41
- );
42
- assert.equal(rules.length, 1);
43
- assert.equal(rules[0].enforcement, "guidance");
44
- });
45
-
46
- it("should parse rules missing annotations", () => {
47
- const rules = parseRules("### Some rule\n**Why:** Just because.\n");
48
- assert.equal(rules.length, 1);
49
- assert.equal(rules[0].enforcement, "missing");
50
- });
51
-
52
- it("should track line numbers", () => {
53
- const rules = parseRules(
54
- "# Header\n\nSome text\n\n### First rule\n**Enforced by:** `x`\n\n### Second rule\nNo annotation\n",
55
- );
56
- assert.equal(rules[0].line, 5);
57
- assert.equal(rules[1].line, 8);
58
- });
59
-
60
- it("should handle multiple rules in sequence", () => {
61
- const rules = parseRules(
62
- "### Rule A\n**Enforced by:** `a`\n### Rule B\n**Guidance only**\n### Rule C\nNothing here.\n",
63
- );
64
- assert.equal(rules.length, 3);
65
- assert.equal(rules[0].enforcement, "enforced");
66
- assert.equal(rules[1].enforcement, "guidance");
67
- assert.equal(rules[2].enforcement, "missing");
68
- });
69
-
70
- it("should not match deeper headings (####)", () => {
71
- const rules = parseRules(
72
- "### Real rule\n**Enforced by:** `x`\n#### Not a rule\nSome details.\n",
73
- );
74
- assert.equal(rules.length, 1);
75
- });
76
-
77
- it("should not match shallower headings (## or #)", () => {
78
- const rules = parseRules(
79
- "# Top level\n## Section\n### Actual rule\n**Enforced by:** `x`\n",
80
- );
81
- assert.equal(rules.length, 1);
82
- assert.equal(rules[0].title, "Actual rule");
83
- });
84
-
85
- it("should handle empty file", () => {
86
- const rules = parseRules("");
87
- assert.equal(rules.length, 0);
88
- });
89
-
90
- it("should handle file with no rules", () => {
91
- const rules = parseRules("# CLAUDE.md\n\nThis project uses TypeScript.\n");
92
- assert.equal(rules.length, 0);
93
- });
94
-
95
- it("should stop looking for annotation at next header", () => {
96
- const rules = parseRules(
97
- "### Rule A\nSome text.\nMore text.\n### Rule B\n**Enforced by:** `x`\n",
98
- );
99
- assert.equal(rules[0].enforcement, "missing");
100
- assert.equal(rules[1].enforcement, "enforced");
101
- });
102
-
103
- it("should parse disabled rules", () => {
104
- const rules = parseRules(
105
- "### Skipped rule\n<!-- vigiles-disable -->\n**Why:** Not relevant here.\n",
106
- );
107
- assert.equal(rules.length, 1);
108
- assert.equal(rules[0].enforcement, "disabled");
109
- });
110
-
111
- it("should handle vigiles-disable with extra whitespace", () => {
112
- const rules = parseRules("### Skipped rule\n<!-- vigiles-disable -->\n");
113
- assert.equal(rules[0].enforcement, "disabled");
114
- });
115
- });
116
-
117
- // ---------------------------------------------------------------------------
118
- // parseRules with checkboxes
119
- // ---------------------------------------------------------------------------
120
-
121
- describe("parseRules with checkboxes", () => {
122
- const opts: ParseOptions = { ruleMarkers: ["checkboxes"] };
123
- const bothOpts: ParseOptions = { ruleMarkers: ["headings", "checkboxes"] };
124
-
125
- it("should parse unchecked checkbox with enforced annotation", () => {
126
- const rules = parseRules(
127
- "- [ ] Use barrel imports\n**Enforced by:** `eslint/no-restricted-imports`\n",
128
- opts,
129
- );
130
- assert.equal(rules.length, 1);
131
- assert.equal(rules[0].title, "Use barrel imports");
132
- assert.equal(rules[0].enforcement, "enforced");
133
- assert.equal(rules[0].enforcedBy, "eslint/no-restricted-imports");
134
- });
135
-
136
- it("should parse checked checkbox (lowercase x) with guidance", () => {
137
- const rules = parseRules(
138
- "- [x] Use Tailwind spacing\n**Guidance only** — cannot be enforced\n",
139
- opts,
140
- );
141
- assert.equal(rules.length, 1);
142
- assert.equal(rules[0].enforcement, "guidance");
143
- });
144
-
145
- it("should parse checked checkbox (uppercase X) with disabled", () => {
146
- const rules = parseRules(
147
- "- [X] Skipped rule\n<!-- vigiles-disable -->\n",
148
- opts,
149
- );
150
- assert.equal(rules.length, 1);
151
- assert.equal(rules[0].enforcement, "disabled");
152
- });
153
-
154
- it("should detect checkbox rule missing annotation", () => {
155
- const rules = parseRules("- [ ] Some rule\nJust a description.\n", opts);
156
- assert.equal(rules.length, 1);
157
- assert.equal(rules[0].enforcement, "missing");
158
- });
159
-
160
- it("should handle multiple checkboxes in sequence", () => {
161
- const rules = parseRules(
162
- "- [ ] Rule A\n**Enforced by:** `a`\n- [x] Rule B\n**Guidance only**\n- [ ] Rule C\nNothing.\n",
163
- opts,
164
- );
165
- assert.equal(rules.length, 3);
166
- assert.equal(rules[0].enforcement, "enforced");
167
- assert.equal(rules[1].enforcement, "guidance");
168
- assert.equal(rules[2].enforcement, "missing");
169
- });
170
-
171
- it("should track line numbers for checkbox rules", () => {
172
- const rules = parseRules(
173
- "# Header\n\nSome text\n\n- [ ] First rule\n**Enforced by:** `x`\n\n- [ ] Second rule\nNo annotation\n",
174
- opts,
175
- );
176
- assert.equal(rules[0].line, 5);
177
- assert.equal(rules[1].line, 8);
178
- });
179
-
180
- it("should not match indented checkboxes", () => {
181
- const rules = parseRules(
182
- " - [ ] Indented item\n**Enforced by:** `x`\n",
183
- opts,
184
- );
185
- assert.equal(rules.length, 0);
186
- });
187
-
188
- it("should handle mixed headers and checkboxes with both markers", () => {
189
- const rules = parseRules(
190
- "### Heading rule\n**Enforced by:** `a`\n- [ ] Checkbox rule\n**Guidance only**\n### Another heading\n**Enforced by:** `b`\n",
191
- bothOpts,
192
- );
193
- assert.equal(rules.length, 3);
194
- assert.equal(rules[0].title, "Heading rule");
195
- assert.equal(rules[0].enforcement, "enforced");
196
- assert.equal(rules[1].title, "Checkbox rule");
197
- assert.equal(rules[1].enforcement, "guidance");
198
- assert.equal(rules[2].title, "Another heading");
199
- assert.equal(rules[2].enforcement, "enforced");
200
- });
201
-
202
- it("checkbox should flush previous heading rule", () => {
203
- const rules = parseRules(
204
- "### Rule A\nSome text\n- [ ] Rule B\n**Enforced by:** `x`\n",
205
- bothOpts,
206
- );
207
- assert.equal(rules[0].title, "Rule A");
208
- assert.equal(rules[0].enforcement, "missing");
209
- assert.equal(rules[1].title, "Rule B");
210
- assert.equal(rules[1].enforcement, "enforced");
211
- });
212
-
213
- it("heading should flush previous checkbox rule", () => {
214
- const rules = parseRules(
215
- "- [ ] Rule A\nSome text\n### Rule B\n**Enforced by:** `x`\n",
216
- bothOpts,
217
- );
218
- assert.equal(rules[0].title, "Rule A");
219
- assert.equal(rules[0].enforcement, "missing");
220
- assert.equal(rules[1].title, "Rule B");
221
- assert.equal(rules[1].enforcement, "enforced");
222
- });
223
-
224
- it("should ignore checkboxes when only headings marker is enabled", () => {
225
- const rules = parseRules(
226
- "- [ ] Checkbox rule\n**Enforced by:** `x`\n### Heading rule\n**Enforced by:** `y`\n",
227
- { ruleMarkers: ["headings"] as MarkerType[] },
228
- );
229
- assert.equal(rules.length, 1);
230
- assert.equal(rules[0].title, "Heading rule");
231
- });
232
-
233
- it("should ignore headings when only checkboxes marker is enabled", () => {
234
- const rules = parseRules(
235
- "### Heading rule\n**Enforced by:** `x`\n- [ ] Checkbox rule\n**Enforced by:** `y`\n",
236
- opts,
237
- );
238
- assert.equal(rules.length, 1);
239
- assert.equal(rules[0].title, "Checkbox rule");
240
- });
241
- });
242
-
243
- // ---------------------------------------------------------------------------
244
- // validate — core
245
- // ---------------------------------------------------------------------------
246
-
247
- describe("validate", () => {
248
- it("should count enforced, guidance, and missing rules", () => {
249
- const result = validate(
250
- "### Rule A\n**Enforced by:** `x`\n### Rule B\n**Guidance only**\n### Rule C\nNothing.\n",
251
- { rules: { "require-spec": false } },
252
- );
253
- assert.equal(result.enforced, 1);
254
- assert.equal(result.guidanceOnly, 1);
255
- assert.equal(result.missing, 1);
256
- assert.equal(result.total, 3);
257
- });
258
-
259
- it("should be valid with no errors when require-spec is off", () => {
260
- const result = validate("### Rule\n**Enforced by:** `x`\n", {
261
- rules: { "require-spec": false },
262
- });
263
- assert.equal(result.valid, true);
264
- assert.equal(result.errors.length, 0);
265
- });
266
- });
267
-
268
- // ---------------------------------------------------------------------------
269
- // require-spec rule
270
- // ---------------------------------------------------------------------------
271
-
272
- describe("require-spec", () => {
273
- let tmpDir: string;
274
-
275
- before(() => {
276
- tmpDir = mkdtempSync(join(tmpdir(), "vigiles-require-spec-"));
277
- });
278
-
279
- after(() => {
280
- rmSync(tmpDir, { recursive: true, force: true });
281
- });
282
-
283
- it("should error when no .spec.ts file exists", () => {
284
- const subDir = join(tmpDir, "no-spec");
285
- mkdirSync(subDir, { recursive: true });
286
- const mdPath = join(subDir, "CLAUDE.md");
287
- writeFileSync(mdPath, "# CLAUDE.md\n### Rule\n**Enforced by:** `x`\n");
288
-
289
- const result = validate("# CLAUDE.md\n### Rule\n**Enforced by:** `x`\n", {
290
- filePath: mdPath,
291
- rules: { "require-spec": "error" },
292
- });
293
- assert.equal(result.valid, false);
294
- assert.ok(result.errors.some((e) => e.rule === "require-spec"));
295
- assert.ok(result.errors[0].message.includes("No spec file"));
296
- });
297
-
298
- it("should pass when .spec.ts file exists", () => {
299
- const subDir = join(tmpDir, "has-spec");
300
- mkdirSync(subDir, { recursive: true });
301
- const mdPath = join(subDir, "CLAUDE.md");
302
- const specPath = join(subDir, "CLAUDE.md.spec.ts");
303
- writeFileSync(mdPath, "# Test\n");
304
- writeFileSync(specPath, "export default {};\n");
305
-
306
- const result = validate("# Test\n", {
307
- filePath: mdPath,
308
- rules: { "require-spec": "error" },
309
- });
310
- assert.ok(!result.errors.some((e) => e.rule === "require-spec"));
311
- });
312
-
313
- it("should be disabled via HTML comment", () => {
314
- const subDir = join(tmpDir, "disabled");
315
- mkdirSync(subDir, { recursive: true });
316
- const mdPath = join(subDir, "CLAUDE.md");
317
- writeFileSync(mdPath, "<!-- vigiles-disable require-spec -->\n# Test\n");
318
-
319
- const result = validate("<!-- vigiles-disable require-spec -->\n# Test\n", {
320
- filePath: mdPath,
321
- rules: { "require-spec": "error" },
322
- });
323
- assert.ok(!result.errors.some((e) => e.rule === "require-spec"));
324
- });
325
-
326
- it("should be disabled via config", () => {
327
- const subDir = join(tmpDir, "config-off");
328
- mkdirSync(subDir, { recursive: true });
329
- const mdPath = join(subDir, "CLAUDE.md");
330
- writeFileSync(mdPath, "# Test\n");
331
-
332
- const result = validate("# Test\n", {
333
- filePath: mdPath,
334
- rules: { "require-spec": false },
335
- });
336
- assert.ok(!result.errors.some((e) => e.rule === "require-spec"));
337
- });
338
-
339
- it("should not run when filePath is not provided", () => {
340
- const result = validate("# Test\n", {
341
- rules: { "require-spec": "error" },
342
- });
343
- assert.equal(result.valid, true);
344
- });
345
-
346
- it("should warn by default (not error)", () => {
347
- const subDir = join(tmpDir, "warn-default");
348
- mkdirSync(subDir, { recursive: true });
349
- const mdPath = join(subDir, "CLAUDE.md");
350
- writeFileSync(mdPath, "# Test\n");
351
-
352
- // Default is "warn" — valid stays true, warning emitted
353
- const result = validate("# Test\n", { filePath: mdPath });
354
- assert.equal(result.valid, true);
355
- assert.ok(result.warnings.some((e) => e.rule === "require-spec"));
356
- assert.equal(result.errors.length, 0);
357
- });
358
-
359
- it("should error when severity is 'error'", () => {
360
- const subDir = join(tmpDir, "error-mode");
361
- mkdirSync(subDir, { recursive: true });
362
- const mdPath = join(subDir, "CLAUDE.md");
363
- writeFileSync(mdPath, "# Test\n");
364
-
365
- const result = validate("# Test\n", {
366
- filePath: mdPath,
367
- rules: { "require-spec": "error" },
368
- });
369
- assert.equal(result.valid, false);
370
- assert.ok(result.errors.some((e) => e.rule === "require-spec"));
371
- });
372
-
373
- it("should warn for SKILL.md when require-skill-spec is warn", () => {
374
- const subDir = join(tmpDir, "skill-warn");
375
- mkdirSync(subDir, { recursive: true });
376
- const mdPath = join(subDir, "SKILL.md");
377
- writeFileSync(mdPath, "# Test\n");
378
-
379
- const result = validate("# Test\n", {
380
- filePath: mdPath,
381
- rules: { "require-skill-spec": "warn" },
382
- });
383
- assert.ok(result.warnings.some((e) => e.rule === "require-skill-spec"));
384
- assert.equal(result.valid, true);
385
- });
386
-
387
- it("should not fire require-spec on SKILL.md", () => {
388
- const subDir = join(tmpDir, "skill-no-spec");
389
- mkdirSync(subDir, { recursive: true });
390
- const mdPath = join(subDir, "SKILL.md");
391
- writeFileSync(mdPath, "# Test\n");
392
-
393
- const result = validate("# Test\n", {
394
- filePath: mdPath,
395
- rules: { "require-spec": "error" },
396
- });
397
- // require-spec only applies to CLAUDE.md/AGENTS.md
398
- assert.ok(!result.errors.some((e) => e.rule === "require-spec"));
399
- });
400
- });
401
-
402
- // ---------------------------------------------------------------------------
403
- // loadConfig
404
- // ---------------------------------------------------------------------------
405
-
406
- describe("loadConfig", () => {
407
- let tmpDir: string;
408
- let originalCwd: string;
409
-
410
- before(() => {
411
- tmpDir = mkdtempSync(join(tmpdir(), "vigiles-config-"));
412
- originalCwd = process.cwd();
413
- });
414
-
415
- after(() => {
416
- process.chdir(originalCwd);
417
- rmSync(tmpDir, { recursive: true, force: true });
418
- });
419
-
420
- it("should return defaults when no config file exists", () => {
421
- process.chdir(tmpDir);
422
- const config = loadConfig();
423
- assert.deepEqual(config.ruleMarkers, ["headings", "checkboxes"]);
424
- assert.deepEqual(config.rules, {
425
- "require-spec": "warn",
426
- "require-skill-spec": "warn",
427
- freshness: "warn",
428
- });
429
- });
430
-
431
- it("should read .vigilesrc.json", () => {
432
- const configDir = mkdtempSync(join(tmpdir(), "vigiles-config-"));
433
- writeFileSync(
434
- join(configDir, ".vigilesrc.json"),
435
- JSON.stringify({ ruleMarkers: ["headings", "checkboxes"] }),
436
- );
437
- process.chdir(configDir);
438
- const config = loadConfig();
439
- assert.deepEqual(config.ruleMarkers, ["headings", "checkboxes"]);
440
- process.chdir(originalCwd);
441
- rmSync(configDir, { recursive: true, force: true });
442
- });
443
-
444
- it("should allow disabling require-spec via config", () => {
445
- const configDir = mkdtempSync(join(tmpdir(), "vigiles-config-"));
446
- writeFileSync(
447
- join(configDir, ".vigilesrc.json"),
448
- JSON.stringify({ rules: { "require-spec": false } }),
449
- );
450
- process.chdir(configDir);
451
- const config = loadConfig();
452
- assert.equal(config.rules["require-spec"], false);
453
- process.chdir(originalCwd);
454
- rmSync(configDir, { recursive: true, force: true });
455
- });
456
-
457
- it("should fall back to defaults for invalid ruleMarkers", () => {
458
- const configDir = mkdtempSync(join(tmpdir(), "vigiles-config-"));
459
- writeFileSync(
460
- join(configDir, ".vigilesrc.json"),
461
- JSON.stringify({ ruleMarkers: ["invalid"] }),
462
- );
463
- process.chdir(configDir);
464
- const config = loadConfig();
465
- assert.deepEqual(config.ruleMarkers, ["headings", "checkboxes"]);
466
- process.chdir(originalCwd);
467
- rmSync(configDir, { recursive: true, force: true });
468
- });
469
- });
470
-
471
- // ---------------------------------------------------------------------------
472
- // readInstructionFile
473
- // ---------------------------------------------------------------------------
474
-
475
- describe("readInstructionFile", () => {
476
- let tmpDir: string;
477
-
478
- before(() => {
479
- tmpDir = mkdtempSync(join(tmpdir(), "vigiles-test-"));
480
- });
481
-
482
- after(() => {
483
- rmSync(tmpDir, { recursive: true, force: true });
484
- });
485
-
486
- it("should read a regular file", () => {
487
- const filePath = join(tmpDir, "regular.md");
488
- writeFileSync(filePath, "### Rule\n**Enforced by:** `x`\n");
489
- const { content, skipped } = readInstructionFile(filePath);
490
- assert.equal(skipped, false);
491
- assert.notEqual(content, null);
492
- assert.ok((content as string).includes("### Rule"));
493
- });
494
-
495
- it("should return error for missing file", () => {
496
- const { content, skipped, reason } = readInstructionFile(
497
- join(tmpDir, "nope.md"),
498
- );
499
- assert.equal(content, null);
500
- assert.equal(skipped, false);
501
- assert.notEqual(reason, null);
502
- assert.ok((reason as string).includes("File not found"));
503
- });
504
-
505
- it("should skip symlinks by default", () => {
506
- const realFile = join(tmpDir, "real.md");
507
- const link = join(tmpDir, "link.md");
508
- writeFileSync(realFile, "### Rule\n**Enforced by:** `x`\n");
509
- symlinkSync(realFile, link);
510
- const { content, skipped, reason } = readInstructionFile(link);
511
- assert.equal(content, null);
512
- assert.equal(skipped, true);
513
- assert.notEqual(reason, null);
514
- assert.ok((reason as string).includes("symlink"));
515
- });
516
-
517
- it("should follow symlinks when opted in", () => {
518
- const realFile = join(tmpDir, "real2.md");
519
- const link = join(tmpDir, "link2.md");
520
- writeFileSync(realFile, "### Rule\n**Enforced by:** `x`\n");
521
- symlinkSync(realFile, link);
522
- const { content, skipped } = readInstructionFile(link, {
523
- followSymlinks: true,
524
- });
525
- assert.equal(skipped, false);
526
- assert.notEqual(content, null);
527
- assert.ok((content as string).includes("### Rule"));
528
- });
529
- });
530
-
531
- // ---------------------------------------------------------------------------
532
- // validatePaths
533
- // ---------------------------------------------------------------------------
534
-
535
- describe("validatePaths", () => {
536
- let tmpDir: string;
537
-
538
- before(() => {
539
- tmpDir = mkdtempSync(join(tmpdir(), "vigiles-test-"));
540
- });
541
-
542
- after(() => {
543
- rmSync(tmpDir, { recursive: true, force: true });
544
- });
545
-
546
- it("should validate multiple files", () => {
547
- const file1 = join(tmpDir, "a.md");
548
- const file2 = join(tmpDir, "b.md");
549
- writeFileSync(file1, "### Rule A\n**Enforced by:** `x`\n");
550
- writeFileSync(file2, "### Rule B\n**Guidance only**\n");
551
-
552
- const { fileResults, valid } = validatePaths([file1, file2], {
553
- rules: { "require-spec": false },
554
- });
555
- assert.equal(valid, true);
556
- assert.equal(fileResults.length, 2);
557
- assert.notEqual(fileResults[0].result, null);
558
- assert.equal(fileResults[0].result?.enforced, 1);
559
- assert.notEqual(fileResults[1].result, null);
560
- assert.equal(fileResults[1].result?.guidanceOnly, 1);
561
- });
562
-
563
- it("should fail if any file is missing", () => {
564
- const file1 = join(tmpDir, "exists.md");
565
- writeFileSync(file1, "### Rule\n**Enforced by:** `x`\n");
566
-
567
- const { valid } = validatePaths([file1, join(tmpDir, "missing.md")], {
568
- rules: { "require-spec": false },
569
- });
570
- assert.equal(valid, false);
571
- });
572
-
573
- it("should skip symlinks by default but not fail", () => {
574
- const real = join(tmpDir, "real3.md");
575
- const link = join(tmpDir, "link3.md");
576
- writeFileSync(real, "### Rule\n**Enforced by:** `x`\n");
577
- symlinkSync(real, link);
578
-
579
- const { fileResults, valid } = validatePaths([real, link], {
580
- rules: { "require-spec": false },
581
- });
582
- assert.equal(valid, true);
583
- assert.equal(fileResults[1].skipped, true);
584
- });
585
-
586
- it("should validate symlinks when follow-symlinks is enabled", () => {
587
- const real = join(tmpDir, "real4.md");
588
- const link = join(tmpDir, "link4.md");
589
- writeFileSync(real, "### Rule\n**Enforced by:** `x`\n");
590
- symlinkSync(real, link);
591
-
592
- const { fileResults, valid } = validatePaths([link], {
593
- followSymlinks: true,
594
- rules: { "require-spec": false },
595
- });
596
- assert.equal(valid, true);
597
- assert.equal(fileResults[0].skipped, false);
598
- assert.notEqual(fileResults[0].result, null);
599
- assert.equal(fileResults[0].result?.enforced, 1);
600
- });
601
- });
602
-
603
- // ---------------------------------------------------------------------------
604
- // expandGlobs
605
- // ---------------------------------------------------------------------------
606
-
607
- describe("expandGlobs", () => {
608
- let tmpDir: string;
609
-
610
- before(() => {
611
- tmpDir = mkdtempSync(join(tmpdir(), "vigiles-glob-"));
612
- });
613
-
614
- after(() => {
615
- rmSync(tmpDir, { recursive: true, force: true });
616
- });
617
-
618
- it("should pass through plain paths unchanged", () => {
619
- const result = expandGlobs(["CLAUDE.md", "foo/AGENTS.md"]);
620
- assert.deepEqual(result, ["CLAUDE.md", "foo/AGENTS.md"]);
621
- });
622
-
623
- it("should expand glob patterns into matching files", () => {
624
- writeFileSync(join(tmpDir, "a.md"), "# A\n");
625
- writeFileSync(join(tmpDir, "b.md"), "# B\n");
626
- writeFileSync(join(tmpDir, "c.txt"), "not md\n");
627
-
628
- const result = expandGlobs([join(tmpDir, "*.md")]);
629
- assert.equal(result.length, 2);
630
- assert.ok(result.some((p) => p.endsWith("a.md")));
631
- assert.ok(result.some((p) => p.endsWith("b.md")));
632
- assert.ok(!result.some((p) => p.endsWith("c.txt")));
633
- });
634
-
635
- it("should expand recursive globs", () => {
636
- const subDir = join(tmpDir, "sub");
637
- mkdirSync(subDir, { recursive: true });
638
- writeFileSync(join(subDir, "nested.md"), "# Nested\n");
639
-
640
- const result = expandGlobs([join(tmpDir, "**/*.md")]);
641
- assert.ok(result.some((p) => p.endsWith("nested.md")));
642
- });
643
-
644
- it("should return empty for globs matching nothing", () => {
645
- const result = expandGlobs([join(tmpDir, "*.nonexistent")]);
646
- assert.deepEqual(result, []);
647
- });
648
-
649
- it("should mix plain paths and globs", () => {
650
- const result = expandGlobs(["plain.md", join(tmpDir, "*.md")]);
651
- assert.equal(result[0], "plain.md");
652
- assert.ok(result.length > 1);
653
- });
654
- });
655
-
656
- // ---------------------------------------------------------------------------
657
- // findInstructionFiles
658
- // ---------------------------------------------------------------------------
659
-
660
- describe("findInstructionFiles", () => {
661
- let tmpDir: string;
662
-
663
- before(() => {
664
- tmpDir = mkdtempSync(join(tmpdir(), "vigiles-find-"));
665
- });
666
-
667
- after(() => {
668
- rmSync(tmpDir, { recursive: true, force: true });
669
- });
670
-
671
- it("should return empty when no files exist", () => {
672
- const result = findInstructionFiles(tmpDir);
673
- assert.deepEqual(result, []);
674
- });
675
-
676
- it("should find CLAUDE.md by default", () => {
677
- writeFileSync(join(tmpDir, "CLAUDE.md"), "# Test\n");
678
- const result = findInstructionFiles(tmpDir);
679
- assert.deepEqual(result, ["CLAUDE.md"]);
680
- });
681
-
682
- it("should find custom files list", () => {
683
- writeFileSync(join(tmpDir, "AGENTS.md"), "# Test\n");
684
- const result = findInstructionFiles(tmpDir, [
685
- "CLAUDE.md",
686
- "AGENTS.md",
687
- ".cursorrules",
688
- ]);
689
- assert.ok(result.includes("CLAUDE.md"));
690
- assert.ok(result.includes("AGENTS.md"));
691
- assert.ok(!result.includes(".cursorrules"));
692
- });
693
-
694
- it("should only return files that exist", () => {
695
- const result = findInstructionFiles(tmpDir, [
696
- "CLAUDE.md",
697
- "nonexistent.md",
698
- ]);
699
- assert.deepEqual(result, ["CLAUDE.md"]);
700
- });
701
- });