vigiles 2.0.0 → 2.1.1

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 (160) hide show
  1. package/README.md +108 -131
  2. package/dist/cli.js +303 -161
  3. package/dist/compile.d.ts +1 -0
  4. package/dist/compile.js +22 -19
  5. package/dist/coverage.d.ts +56 -0
  6. package/dist/coverage.js +178 -0
  7. package/dist/doc-refs.d.ts +60 -0
  8. package/dist/doc-refs.js +246 -0
  9. package/dist/evolve.js +60 -125
  10. package/dist/frontmatter.d.ts +57 -0
  11. package/dist/frontmatter.js +190 -0
  12. package/dist/generate-schema.d.ts +51 -0
  13. package/dist/generate-schema.js +115 -0
  14. package/dist/generate-types.js +39 -1
  15. package/dist/hash.d.ts +8 -0
  16. package/dist/hash.js +16 -0
  17. package/dist/integrity.d.ts +29 -0
  18. package/dist/integrity.js +44 -0
  19. package/dist/linters.d.ts +5 -3
  20. package/dist/linters.js +116 -4
  21. package/dist/orphans.d.ts +52 -0
  22. package/dist/orphans.js +124 -0
  23. package/dist/proofs.js +22 -16
  24. package/dist/session.d.ts +97 -0
  25. package/dist/session.js +306 -0
  26. package/dist/sidecar.d.ts +35 -0
  27. package/dist/sidecar.js +102 -0
  28. package/dist/spec.d.ts +29 -9
  29. package/dist/spec.js +22 -4
  30. package/dist/test-utils.d.ts +8 -0
  31. package/dist/test-utils.js +41 -0
  32. package/dist/types.d.ts +34 -8
  33. package/dist/types.js +19 -0
  34. package/dist/validate.js +5 -3
  35. package/package.json +14 -2
  36. package/.claude/settings.json +0 -46
  37. package/.claude/settings.local.json +0 -8
  38. package/.github/workflows/ci.yml +0 -81
  39. package/.prettierignore +0 -1
  40. package/.vigiles/generated.d.ts +0 -205
  41. package/CLAUDE.md +0 -95
  42. package/CLAUDE.md.spec.ts +0 -142
  43. package/CONTRIBUTING.md +0 -121
  44. package/dist/action.d.ts.map +0 -1
  45. package/dist/action.js.map +0 -1
  46. package/dist/cli.d.ts.map +0 -1
  47. package/dist/cli.js.map +0 -1
  48. package/dist/cli.test.d.ts +0 -2
  49. package/dist/cli.test.d.ts.map +0 -1
  50. package/dist/cli.test.js +0 -650
  51. package/dist/cli.test.js.map +0 -1
  52. package/dist/compile.d.ts.map +0 -1
  53. package/dist/compile.js.map +0 -1
  54. package/dist/evolve.d.ts.map +0 -1
  55. package/dist/evolve.js.map +0 -1
  56. package/dist/freshness.d.ts +0 -67
  57. package/dist/freshness.d.ts.map +0 -1
  58. package/dist/freshness.js +0 -244
  59. package/dist/freshness.js.map +0 -1
  60. package/dist/freshness.test.d.ts +0 -2
  61. package/dist/freshness.test.d.ts.map +0 -1
  62. package/dist/freshness.test.js +0 -356
  63. package/dist/freshness.test.js.map +0 -1
  64. package/dist/generate-types.d.ts.map +0 -1
  65. package/dist/generate-types.js.map +0 -1
  66. package/dist/inline.d.ts.map +0 -1
  67. package/dist/inline.js.map +0 -1
  68. package/dist/inline.test.d.ts +0 -5
  69. package/dist/inline.test.d.ts.map +0 -1
  70. package/dist/inline.test.js +0 -152
  71. package/dist/inline.test.js.map +0 -1
  72. package/dist/linters.d.ts.map +0 -1
  73. package/dist/linters.js.map +0 -1
  74. package/dist/proofs.d.ts.map +0 -1
  75. package/dist/proofs.js.map +0 -1
  76. package/dist/proofs.test.d.ts +0 -9
  77. package/dist/proofs.test.d.ts.map +0 -1
  78. package/dist/proofs.test.js +0 -952
  79. package/dist/proofs.test.js.map +0 -1
  80. package/dist/spec.d.ts.map +0 -1
  81. package/dist/spec.js.map +0 -1
  82. package/dist/spec.test.d.ts +0 -2
  83. package/dist/spec.test.d.ts.map +0 -1
  84. package/dist/spec.test.js +0 -1222
  85. package/dist/spec.test.js.map +0 -1
  86. package/dist/types.d.ts.map +0 -1
  87. package/dist/types.js.map +0 -1
  88. package/dist/validate.d.ts.map +0 -1
  89. package/dist/validate.js.map +0 -1
  90. package/dist/validate.test.d.ts +0 -2
  91. package/dist/validate.test.d.ts.map +0 -1
  92. package/dist/validate.test.js +0 -531
  93. package/dist/validate.test.js.map +0 -1
  94. package/docs/agent-setup.md +0 -85
  95. package/docs/agent-workflows.md +0 -103
  96. package/docs/comparison.md +0 -71
  97. package/docs/freshness.md +0 -124
  98. package/docs/inline-mode.md +0 -119
  99. package/docs/linter-support.md +0 -166
  100. package/docs/spec-format.md +0 -194
  101. package/eslint.config.mjs +0 -79
  102. package/examples/CLAUDE.md +0 -54
  103. package/examples/CLAUDE.md.spec.ts +0 -65
  104. package/examples/SKILL.md +0 -50
  105. package/examples/SKILL.md.spec.ts +0 -57
  106. package/fixtures/example-project/CLAUDE.md +0 -11
  107. package/fixtures/example-project/package.json +0 -9
  108. package/fixtures/example-project/src/index.ts +0 -3
  109. package/fixtures/example-project/src/utils.test.ts +0 -2
  110. package/fixtures/example-project/src/utils.ts +0 -3
  111. package/logo.png +0 -0
  112. package/research/adoption-strategy.md +0 -111
  113. package/research/agent-integration.md +0 -145
  114. package/research/ai-code-quality.md +0 -197
  115. package/research/code-search-for-agents.md +0 -313
  116. package/research/competitive-landscape.md +0 -163
  117. package/research/doc-freshness.md +0 -516
  118. package/research/executable-specs.md +0 -368
  119. package/research/feature-ideas.md +0 -464
  120. package/research/formal-proofs-for-agents.md +0 -338
  121. package/research/fp-for-agent-harness.md +0 -150
  122. package/research/fp-for-deterministic-ai.md +0 -131
  123. package/research/self-evolving-specs.md +0 -298
  124. package/schemas/claude-md-strict.yml +0 -18
  125. package/schemas/claude-md.yml +0 -6
  126. package/schemas/skill-strict.yml +0 -12
  127. package/schemas/skill.yml +0 -5
  128. package/skills/audit-feedback-loop/SKILL.md +0 -76
  129. package/skills/edit-spec/SKILL.md +0 -131
  130. package/skills/enforce-rules-format/SKILL.md +0 -71
  131. package/skills/generate-logo/SKILL.md +0 -102
  132. package/skills/generate-rule/SKILL.md +0 -90
  133. package/skills/linter-docs/clippy.md +0 -241
  134. package/skills/linter-docs/eslint.md +0 -384
  135. package/skills/linter-docs/pylint.md +0 -288
  136. package/skills/linter-docs/rubocop.md +0 -277
  137. package/skills/linter-docs/ruff.md +0 -187
  138. package/skills/linter-docs/stylelint.md +0 -247
  139. package/skills/migrate-to-spec/SKILL.md +0 -124
  140. package/skills/pr-to-lint-rule/SKILL.md +0 -97
  141. package/skills/strengthen/SKILL.md +0 -168
  142. package/src/action.ts +0 -214
  143. package/src/cli.test.ts +0 -914
  144. package/src/cli.ts +0 -1631
  145. package/src/compile.ts +0 -691
  146. package/src/evolve.ts +0 -781
  147. package/src/freshness.test.ts +0 -449
  148. package/src/freshness.ts +0 -299
  149. package/src/generate-types.ts +0 -448
  150. package/src/inline.test.ts +0 -206
  151. package/src/inline.ts +0 -164
  152. package/src/linters.ts +0 -739
  153. package/src/proofs.test.ts +0 -1314
  154. package/src/proofs.ts +0 -849
  155. package/src/spec.test.ts +0 -1471
  156. package/src/spec.ts +0 -427
  157. package/src/types.ts +0 -117
  158. package/src/validate.test.ts +0 -701
  159. package/src/validate.ts +0 -381
  160. 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
- });