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
package/dist/evolve.js CHANGED
@@ -13,6 +13,7 @@ exports.applyMutation = applyMutation;
13
13
  exports.runProofSuite = runProofSuite;
14
14
  const proofs_js_1 = require("./proofs.js");
15
15
  const compile_js_1 = require("./compile.js");
16
+ const hash_js_1 = require("./hash.js");
16
17
  /**
17
18
  * Shallow-clone a Rule. Rules are simple value types (primitive fields only),
18
19
  * so a spread is sufficient to decouple engine state from the caller's
@@ -119,7 +120,7 @@ function applyMutation(rules, mutation) {
119
120
  rules,
120
121
  error: {
121
122
  mutation,
122
- reason: `Rule "${mutation.ruleId}" is already at maximum strength (enforce)`,
123
+ reason: `Rule "${mutation.ruleId}" is already at maximum strength (${rule._kind})`,
123
124
  },
124
125
  };
125
126
  }
@@ -142,6 +143,12 @@ function applyMutation(rules, mutation) {
142
143
  text: rule.why,
143
144
  };
144
145
  }
146
+ else if (rule._kind === "guard") {
147
+ next[mutation.ruleId] = {
148
+ _kind: "guidance",
149
+ text: rule.description,
150
+ };
151
+ }
145
152
  else {
146
153
  return {
147
154
  rules,
@@ -230,8 +237,13 @@ function applyMutation(rules, mutation) {
230
237
  else if (rule._kind === "enforce") {
231
238
  next[mutation.ruleId] = { ...rule, why: mutation.newText };
232
239
  }
240
+ else if (rule._kind === "guard") {
241
+ next[mutation.ruleId] = { ...rule, description: mutation.newText };
242
+ }
233
243
  return { rules: next };
234
244
  }
245
+ default:
246
+ return (0, hash_js_1.assertNever)(mutation);
235
247
  }
236
248
  }
237
249
  /**
@@ -258,107 +270,61 @@ function runProofSuite(before, after, options = {}) {
258
270
  });
259
271
  // 2. NCD deduplication. Only fail on pairs NEWLY INTRODUCED by the
260
272
  // candidate change — a repo with historical duplication must not
261
- // block every unrelated mutation. Compute similar pairs on `after`
262
- // and `before`, then subtract.
263
- //
264
- // Wrapped in try/catch so an unknown rule kind (legacy data / JS
265
- // caller) surfaces as a clean proof failure rather than crashing
266
- // the whole audit pipeline.
267
- try {
268
- const beforePairs = new Set((0, proofs_js_1.findSimilarRules)(before, ncdThreshold).map((p) => [p.idA, p.idB].sort().join("|")));
269
- const similar = (0, proofs_js_1.findSimilarRules)(after, ncdThreshold).filter((p) => !beforePairs.has([p.idA, p.idB].sort().join("|")));
270
- const ncdPassed = similar.length === 0;
271
- receipts.push({
272
- name: "ncd-dedup",
273
- passed: ncdPassed,
274
- detail: ncdPassed
275
- ? "No new near-duplicate rules"
276
- : `${similar.length} new near-duplicate pairs: ${similar.map((p) => `${p.idA}↔${p.idB} (${p.distance.toFixed(3)})`).join(", ")}`,
277
- });
278
- }
279
- catch (e) {
280
- receipts.push({
281
- name: "ncd-dedup",
282
- passed: false,
283
- detail: `Similarity check failed: ${e instanceof Error ? e.message : String(e)}`,
284
- });
285
- }
273
+ // block every unrelated mutation.
274
+ const beforePairs = new Set((0, proofs_js_1.findSimilarRules)(before, ncdThreshold).map((p) => [p.idA, p.idB].sort().join("|")));
275
+ const similar = (0, proofs_js_1.findSimilarRules)(after, ncdThreshold).filter((p) => !beforePairs.has([p.idA, p.idB].sort().join("|")));
276
+ const ncdPassed = similar.length === 0;
277
+ receipts.push({
278
+ name: "ncd-dedup",
279
+ passed: ncdPassed,
280
+ detail: ncdPassed
281
+ ? "No new near-duplicate rules"
282
+ : `${similar.length} new near-duplicate pairs: ${similar.map((p) => `${p.idA}↔${p.idB} (${p.distance.toFixed(3)})`).join(", ")}`,
283
+ });
286
284
  // 3. Bloom filter cross-check (fast sanity check for token overlap).
287
285
  // Baseline is built from rules that still exist in `after` — any
288
286
  // rule removed by the candidate mutation (e.g. the two sources of
289
287
  // a merge) must be excluded, otherwise the newly introduced merge
290
288
  // rule would collide against its own sources and the merge would
291
289
  // be rejected for the very similarity it was meant to deduplicate.
292
- //
293
- // Wrapped for the same reason as NCD: ruleToBloomFilter → ruleToText
294
- // throws on unknown rule kinds.
295
- try {
296
- let bloomPassed = true;
297
- const newRuleIds = Object.keys(after).filter((id) => !(id in before));
298
- const existingFilters = new Map();
299
- for (const [id, rule] of Object.entries(before)) {
300
- if (!(id in after))
301
- continue; // removed skip
302
- existingFilters.set(id, (0, proofs_js_1.ruleToBloomFilter)(rule));
303
- }
304
- const bloomOverlaps = [];
305
- for (const newId of newRuleIds) {
306
- const newFilter = (0, proofs_js_1.ruleToBloomFilter)(after[newId]);
307
- for (const [existingId, existingFilter] of existingFilters) {
308
- if (existingId === newId)
309
- continue;
310
- try {
311
- const similarity = proofs_js_1.BloomFilter.jaccardSimilarity(newFilter, existingFilter);
312
- if (similarity > 0.7) {
313
- bloomOverlaps.push(`${newId}↔${existingId} (jaccard=${similarity.toFixed(3)})`);
314
- bloomPassed = false;
315
- }
316
- }
317
- catch {
318
- // Different filter sizes — skip comparison
290
+ let bloomPassed = true;
291
+ const newRuleIds = Object.keys(after).filter((id) => !(id in before));
292
+ const existingFilters = new Map();
293
+ for (const [id, rule] of Object.entries(before)) {
294
+ if (!(id in after))
295
+ continue;
296
+ existingFilters.set(id, (0, proofs_js_1.ruleToBloomFilter)(rule));
297
+ }
298
+ const bloomOverlaps = [];
299
+ for (const newId of newRuleIds) {
300
+ const newFilter = (0, proofs_js_1.ruleToBloomFilter)(after[newId]);
301
+ for (const [existingId, existingFilter] of existingFilters) {
302
+ if (existingId === newId)
303
+ continue;
304
+ try {
305
+ const similarity = proofs_js_1.BloomFilter.jaccardSimilarity(newFilter, existingFilter);
306
+ if (similarity > 0.7) {
307
+ bloomOverlaps.push(`${newId}↔${existingId} (jaccard=${similarity.toFixed(3)})`);
308
+ bloomPassed = false;
319
309
  }
320
310
  }
311
+ catch {
312
+ // Different filter sizes — skip comparison
313
+ }
321
314
  }
322
- receipts.push({
323
- name: "bloom-overlap",
324
- passed: bloomPassed,
325
- detail: bloomPassed
326
- ? "No suspicious token overlap"
327
- : `High overlap: ${bloomOverlaps.join(", ")}`,
328
- });
329
- }
330
- catch (e) {
331
- receipts.push({
332
- name: "bloom-overlap",
333
- passed: false,
334
- detail: `Bloom filter check failed: ${e instanceof Error ? e.message : String(e)}`,
335
- });
336
315
  }
337
- // Compute fitness of the new spec. Guard against bad runtime data: fitness
338
- // recomputes similarity internally and would otherwise crash on unknown
339
- // rule kinds (legacy `check`, JS caller), propagating the throw up through
340
- // the engine. Fall back to a neutral fitness and mark the proof failed.
316
+ receipts.push({
317
+ name: "bloom-overlap",
318
+ passed: bloomPassed,
319
+ detail: bloomPassed
320
+ ? "No suspicious token overlap"
321
+ : `High overlap: ${bloomOverlaps.join(", ")}`,
322
+ });
341
323
  const specForFitness = { rules: after };
342
- let fitnessResult;
343
- try {
344
- fitnessResult = (0, proofs_js_1.fitness)(specForFitness, {
345
- maxTokens: options.maxTokens,
346
- ncdThreshold,
347
- });
348
- }
349
- catch (e) {
350
- receipts.push({
351
- name: "fitness",
352
- passed: false,
353
- detail: `Fitness computation failed: ${e instanceof Error ? e.message : String(e)}`,
354
- });
355
- fitnessResult = {
356
- score: 0,
357
- coverage: 0,
358
- redundancy: 0,
359
- budgetPressure: 0,
360
- };
361
- }
324
+ const fitnessResult = (0, proofs_js_1.fitness)(specForFitness, {
325
+ maxTokens: options.maxTokens,
326
+ ncdThreshold,
327
+ });
362
328
  const allPassed = receipts.every((r) => r.passed);
363
329
  return { passed: allPassed, receipts, fitness: fitnessResult };
364
330
  }
@@ -454,40 +420,7 @@ class EvolutionEngine {
454
420
  * Returns a detailed result including proof receipts and fitness comparison.
455
421
  */
456
422
  propose(mutation) {
457
- // Guard baseline fitness: if engine state contains a malformed rule
458
- // (legacy _kind, JS caller, cast bypass), fitness() reaches ruleToText
459
- // and throws BEFORE we enter runProofSuite's own try/catch. Return a
460
- // clean rejection with a neutral fitness instead of crashing the flow.
461
- let beforeFitness;
462
- try {
463
- beforeFitness = this.getFitness();
464
- }
465
- catch (e) {
466
- const neutral = {
467
- score: 0,
468
- coverage: 0,
469
- redundancy: 0,
470
- budgetPressure: 0,
471
- };
472
- return {
473
- accepted: false,
474
- mutation,
475
- proofs: {
476
- passed: false,
477
- receipts: [
478
- {
479
- name: "baseline-fitness",
480
- passed: false,
481
- detail: `Baseline fitness failed: ${e instanceof Error ? e.message : String(e)}`,
482
- },
483
- ],
484
- fitness: neutral,
485
- },
486
- beforeFitness: neutral,
487
- afterFitness: neutral,
488
- error: `Baseline fitness failed: ${e instanceof Error ? e.message : String(e)}`,
489
- };
490
- }
423
+ const beforeFitness = this.getFitness();
491
424
  // Apply the mutation
492
425
  const { rules: candidateRules, error } = applyMutation(this.rules, mutation);
493
426
  if (error) {
@@ -594,6 +527,8 @@ function describeMutation(mutation) {
594
527
  return `Merge "${mutation.sourceIds[0]}" + "${mutation.sourceIds[1]}" → "${mutation.mergedId}"`;
595
528
  case "reword":
596
529
  return `Reword rule "${mutation.ruleId}"`;
530
+ default:
531
+ return (0, hash_js_1.assertNever)(mutation);
597
532
  }
598
533
  }
599
534
  //# sourceMappingURL=evolve.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * vigiles — YAML frontmatter rule mode (Level 1 adoption).
3
+ *
4
+ * Parses a `vigiles.enforce` block out of a markdown file's YAML
5
+ * frontmatter, so a project can declare enforce rules in structured YAML
6
+ * instead of `<!-- vigiles:enforce ... -->` HTML comments (Level 0) or a
7
+ * typed `.spec.ts` (Level 2). Every frontmatter rule goes through the same
8
+ * `checkLinterRule` verification as inline and spec rules.
9
+ *
10
+ * Shape (verbose — chosen so a JSON Schema can give `rule` an enum that
11
+ * YAML LSP autocompletes and squiggles on typo):
12
+ *
13
+ * ---
14
+ * # yaml-language-server: $schema=./.vigiles/schema.json
15
+ * vigiles:
16
+ * enforce:
17
+ * - rule: "@typescript-eslint/no-explicit-any"
18
+ * why: "Use unknown"
19
+ * ---
20
+ *
21
+ * Like inline mode, only `enforce` is supported — the prose body of the
22
+ * file is the guidance. Malformed YAML is reported, never thrown: a broken
23
+ * frontmatter block produces an error finding, not a crash.
24
+ */
25
+ export interface FrontmatterRule {
26
+ /** Linter rule reference, e.g. "eslint/no-console". */
27
+ linterRule: string;
28
+ /** Why this rule is enforced (human-readable, shown in agent context). */
29
+ why: string;
30
+ /** 1-based line number of the rule in the source file (best-effort). */
31
+ line: number;
32
+ }
33
+ /** A `vigiles.files` entry (verified to exist). */
34
+ export interface FrontmatterFileRef {
35
+ /** Project-relative path to verify exists. */
36
+ path: string;
37
+ /** 1-based line number of the entry in the source file (best-effort). */
38
+ line: number;
39
+ }
40
+ /** A `vigiles.commands` entry (npm scripts verified against package.json). */
41
+ export interface FrontmatterCmdRef {
42
+ /** Command to verify. */
43
+ command: string;
44
+ /** 1-based line number of the entry in the source file (best-effort). */
45
+ line: number;
46
+ }
47
+ export interface FrontmatterParseResult {
48
+ rules: FrontmatterRule[];
49
+ files: FrontmatterFileRef[];
50
+ commands: FrontmatterCmdRef[];
51
+ /** Frontmatter that looks like a vigiles block but failed to parse. */
52
+ errors: {
53
+ line: number;
54
+ message: string;
55
+ }[];
56
+ }
57
+ /**
58
+ * Parse `vigiles.enforce` rules, `vigiles.files`, and `vigiles.commands` out
59
+ * of a markdown file's YAML frontmatter. Does not touch the filesystem and
60
+ * does not verify references — callers feed rules into `checkLinterRule` and
61
+ * file/command refs into `validateFileRef` / `validateCommandRef`.
62
+ *
63
+ * A file with no frontmatter, or frontmatter with no `vigiles` key, yields
64
+ * empty results with no errors. Malformed YAML or a malformed `vigiles`
65
+ * block yields error findings (never throws).
66
+ */
67
+ export declare function parseFrontmatterRules(content: string): FrontmatterParseResult;
68
+ /**
69
+ * True if the content has at least one parseable `vigiles` reference in its
70
+ * frontmatter — an `enforce` rule, a `files` entry, or a `commands` entry.
71
+ * Used by `require-spec` validation to treat frontmatter mode as
72
+ * spec-equivalent, mirroring `hasInlineRules`.
73
+ */
74
+ export declare function hasFrontmatterRules(content: string): boolean;
75
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ /**
3
+ * vigiles — YAML frontmatter rule mode (Level 1 adoption).
4
+ *
5
+ * Parses a `vigiles.enforce` block out of a markdown file's YAML
6
+ * frontmatter, so a project can declare enforce rules in structured YAML
7
+ * instead of `<!-- vigiles:enforce ... -->` HTML comments (Level 0) or a
8
+ * typed `.spec.ts` (Level 2). Every frontmatter rule goes through the same
9
+ * `checkLinterRule` verification as inline and spec rules.
10
+ *
11
+ * Shape (verbose — chosen so a JSON Schema can give `rule` an enum that
12
+ * YAML LSP autocompletes and squiggles on typo):
13
+ *
14
+ * ---
15
+ * # yaml-language-server: $schema=./.vigiles/schema.json
16
+ * vigiles:
17
+ * enforce:
18
+ * - rule: "@typescript-eslint/no-explicit-any"
19
+ * why: "Use unknown"
20
+ * ---
21
+ *
22
+ * Like inline mode, only `enforce` is supported — the prose body of the
23
+ * file is the guidance. Malformed YAML is reported, never thrown: a broken
24
+ * frontmatter block produces an error finding, not a crash.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.parseFrontmatterRules = parseFrontmatterRules;
28
+ exports.hasFrontmatterRules = hasFrontmatterRules;
29
+ const js_yaml_1 = require("js-yaml");
30
+ const BOM = "";
31
+ const OPEN_RE = /^---[ \t]*\r?\n/;
32
+ const CLOSE_RE = /\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/;
33
+ /**
34
+ * Extract the YAML frontmatter block. Frontmatter must be the very first
35
+ * thing in the file (after an optional BOM): a `---` line, the YAML body,
36
+ * then a closing `---` or `...` line. Returns null when no block is present.
37
+ */
38
+ function extractFrontmatter(content) {
39
+ const text = content.startsWith(BOM) ? content.slice(BOM.length) : content;
40
+ const open = OPEN_RE.exec(text);
41
+ if (!open)
42
+ return null;
43
+ const rest = text.slice(open[0].length);
44
+ const close = CLOSE_RE.exec(rest);
45
+ if (!close)
46
+ return null;
47
+ // Line 1 is the opening `---`; YAML content starts on line 2.
48
+ return { yaml: rest.slice(0, close.index), startLine: 2 };
49
+ }
50
+ /** First 1-based line at or after `fromIndex` whose text contains `needle`. */
51
+ function findLine(lines, needle, fromIndex) {
52
+ for (let i = fromIndex; i < lines.length; i++) {
53
+ if (lines[i].includes(needle))
54
+ return i + 1;
55
+ }
56
+ return fromIndex + 1;
57
+ }
58
+ /**
59
+ * Navigate a parsed frontmatter document to its `vigiles` mapping. Returns
60
+ * "none" when there's nothing for vigiles to check, "error" when the
61
+ * `vigiles` key is present but not a mapping, or the mapping itself.
62
+ */
63
+ function getVigiles(doc, lines, startLine) {
64
+ if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
65
+ return { kind: "none" };
66
+ }
67
+ const vigiles = doc.vigiles;
68
+ if (vigiles === undefined)
69
+ return { kind: "none" };
70
+ if (vigiles === null ||
71
+ typeof vigiles !== "object" ||
72
+ Array.isArray(vigiles)) {
73
+ return {
74
+ kind: "error",
75
+ error: {
76
+ line: findLine(lines, "vigiles:", startLine - 1),
77
+ message: "`vigiles` frontmatter key must be a mapping.",
78
+ },
79
+ };
80
+ }
81
+ return { kind: "map", vigiles: vigiles };
82
+ }
83
+ /**
84
+ * Locate the `vigiles.enforce` list. Returns "none" when absent, "error" when
85
+ * present but not a list, or the list with its source line.
86
+ */
87
+ function lookupEnforce(vigiles, lines, startLine) {
88
+ const enforce = vigiles.enforce;
89
+ if (enforce === undefined)
90
+ return { kind: "none" };
91
+ const enforceLine = findLine(lines, "enforce:", startLine - 1);
92
+ if (!Array.isArray(enforce)) {
93
+ return {
94
+ kind: "error",
95
+ error: {
96
+ line: enforceLine,
97
+ message: "`vigiles.enforce` must be a list of { rule, why } entries.",
98
+ },
99
+ };
100
+ }
101
+ return { kind: "list", enforce, enforceLine };
102
+ }
103
+ /**
104
+ * Parse a `vigiles.<key>` list of plain strings (used for `files` and
105
+ * `commands`). Returns located items plus error findings for the wrong shape
106
+ * or non-string entries; an absent key yields empty results.
107
+ */
108
+ function parseStringList(vigiles, key, lines, startLine) {
109
+ const raw = vigiles[key];
110
+ if (raw === undefined)
111
+ return { items: [], errors: [] };
112
+ const keyLine = findLine(lines, `${key}:`, startLine - 1);
113
+ if (!Array.isArray(raw)) {
114
+ return {
115
+ items: [],
116
+ errors: [
117
+ {
118
+ line: keyLine,
119
+ message: `\`vigiles.${key}\` must be a list of strings.`,
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ const items = [];
125
+ const errors = [];
126
+ let cursor = keyLine;
127
+ for (let i = 0; i < raw.length; i++) {
128
+ const v = raw[i];
129
+ if (typeof v !== "string" || v.trim() === "") {
130
+ errors.push({
131
+ line: keyLine,
132
+ message: `vigiles.${key}[${String(i)}] must be a non-empty string.`,
133
+ });
134
+ continue;
135
+ }
136
+ const line = findLine(lines, v, cursor);
137
+ cursor = line;
138
+ items.push({ value: v, line });
139
+ }
140
+ return { items, errors };
141
+ }
142
+ /** Parse one `vigiles.enforce` entry into a rule or an error finding. */
143
+ function parseEntry(entry, index, ctx) {
144
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
145
+ return {
146
+ error: {
147
+ line: ctx.enforceLine,
148
+ message: `vigiles.enforce[${String(index)}] must be a mapping with \`rule\` and \`why\`.`,
149
+ },
150
+ nextCursor: ctx.cursor,
151
+ };
152
+ }
153
+ const { rule, why } = entry;
154
+ if (typeof rule !== "string" || rule.trim() === "") {
155
+ return {
156
+ error: {
157
+ line: ctx.enforceLine,
158
+ message: `vigiles.enforce[${String(index)}] is missing a string \`rule\`.`,
159
+ },
160
+ nextCursor: ctx.cursor,
161
+ };
162
+ }
163
+ const line = findLine(ctx.lines, rule, ctx.cursor);
164
+ if (typeof why !== "string" || why.trim() === "") {
165
+ return {
166
+ error: {
167
+ line,
168
+ message: `vigiles.enforce[${String(index)}] (rule "${rule}") is missing a string \`why\`.`,
169
+ },
170
+ nextCursor: line,
171
+ };
172
+ }
173
+ return { rule: { linterRule: rule, why, line }, nextCursor: line };
174
+ }
175
+ /** Fresh empty result — callers may push into the arrays, so never shared. */
176
+ function emptyResult() {
177
+ return { rules: [], files: [], commands: [], errors: [] };
178
+ }
179
+ /**
180
+ * Parse `vigiles.enforce` rules, `vigiles.files`, and `vigiles.commands` out
181
+ * of a markdown file's YAML frontmatter. Does not touch the filesystem and
182
+ * does not verify references — callers feed rules into `checkLinterRule` and
183
+ * file/command refs into `validateFileRef` / `validateCommandRef`.
184
+ *
185
+ * A file with no frontmatter, or frontmatter with no `vigiles` key, yields
186
+ * empty results with no errors. Malformed YAML or a malformed `vigiles`
187
+ * block yields error findings (never throws).
188
+ */
189
+ function parseFrontmatterRules(content) {
190
+ const fm = extractFrontmatter(content);
191
+ if (!fm)
192
+ return emptyResult();
193
+ const lines = content.split("\n");
194
+ let doc;
195
+ try {
196
+ doc = (0, js_yaml_1.load)(fm.yaml);
197
+ }
198
+ catch (e) {
199
+ const err = e;
200
+ const line = (err.mark?.line ?? 0) + fm.startLine;
201
+ return {
202
+ rules: [],
203
+ files: [],
204
+ commands: [],
205
+ errors: [
206
+ {
207
+ line,
208
+ message: `Malformed YAML frontmatter: ${err.reason ?? err.message}`,
209
+ },
210
+ ],
211
+ };
212
+ }
213
+ const vig = getVigiles(doc, lines, fm.startLine);
214
+ if (vig.kind === "none")
215
+ return emptyResult();
216
+ if (vig.kind === "error")
217
+ return { rules: [], files: [], commands: [], errors: [vig.error] };
218
+ const rules = [];
219
+ const errors = [];
220
+ const enforceLookup = lookupEnforce(vig.vigiles, lines, fm.startLine);
221
+ if (enforceLookup.kind === "error") {
222
+ errors.push(enforceLookup.error);
223
+ }
224
+ else if (enforceLookup.kind === "list") {
225
+ let cursor = enforceLookup.enforceLine; // search start: line after `enforce:`
226
+ for (let i = 0; i < enforceLookup.enforce.length; i++) {
227
+ const r = parseEntry(enforceLookup.enforce[i], i, {
228
+ lines,
229
+ enforceLine: enforceLookup.enforceLine,
230
+ cursor,
231
+ });
232
+ cursor = r.nextCursor;
233
+ if (r.rule)
234
+ rules.push(r.rule);
235
+ if (r.error)
236
+ errors.push(r.error);
237
+ }
238
+ }
239
+ const fileList = parseStringList(vig.vigiles, "files", lines, fm.startLine);
240
+ errors.push(...fileList.errors);
241
+ const files = fileList.items.map((it) => ({
242
+ path: it.value,
243
+ line: it.line,
244
+ }));
245
+ const cmdList = parseStringList(vig.vigiles, "commands", lines, fm.startLine);
246
+ errors.push(...cmdList.errors);
247
+ const commands = cmdList.items.map((it) => ({
248
+ command: it.value,
249
+ line: it.line,
250
+ }));
251
+ return { rules, files, commands, errors };
252
+ }
253
+ /**
254
+ * True if the content has at least one parseable `vigiles` reference in its
255
+ * frontmatter — an `enforce` rule, a `files` entry, or a `commands` entry.
256
+ * Used by `require-spec` validation to treat frontmatter mode as
257
+ * spec-equivalent, mirroring `hasInlineRules`.
258
+ */
259
+ function hasFrontmatterRules(content) {
260
+ const r = parseFrontmatterRules(content);
261
+ return r.rules.length + r.files.length + r.commands.length > 0;
262
+ }
263
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * vigiles generate-schema — emit a JSON Schema for `vigiles:` frontmatter.
3
+ *
4
+ * Sibling of `generate-types` (which emits a `.d.ts` for the spec.ts path).
5
+ * This emits `.vigiles/schema.json` so a YAML LSP can autocomplete rule
6
+ * names and squiggle typos in markdown frontmatter:
7
+ *
8
+ * ---
9
+ * # yaml-language-server: $schema=./.vigiles/schema.json
10
+ * vigiles:
11
+ * enforce:
12
+ * - rule: eslint/no-consolee # red squiggle in VS Code
13
+ * ---
14
+ *
15
+ * The `rule` enum is populated from the project's ACTUAL enabled linter
16
+ * rules — the same discovery `generate-types` runs — so the schema reflects
17
+ * what is really enforceable, not a static catalog.
18
+ */
19
+ export interface GenerateSchemaOptions {
20
+ basePath?: string;
21
+ /**
22
+ * Custom linters from `.vigilesrc.json` (`rulesDir`-backed). These aren't
23
+ * auto-discovered by `generate-types`, but `vigiles audit` resolves their
24
+ * rules via `checkLinterRule`, so the schema enum must include them too —
25
+ * otherwise the YAML LSP false-flags a rule that CI accepts.
26
+ */
27
+ linters?: Record<string, {
28
+ rulesDir?: string | string[];
29
+ }>;
30
+ }
31
+ export interface GenerateSchemaResult {
32
+ /** The JSON Schema document. */
33
+ schema: Record<string, unknown>;
34
+ /** Pretty-printed JSON, newline-terminated. */
35
+ json: string;
36
+ /** Rule names included in the enum (sorted, deduped). */
37
+ ruleNames: string[];
38
+ /** Linters discovered, with rule counts. */
39
+ linters: {
40
+ linter: string;
41
+ count: number;
42
+ }[];
43
+ }
44
+ /**
45
+ * Build a JSON Schema for the `vigiles:` frontmatter block from the
46
+ * project's discovered linter rules. When no rules are discoverable the
47
+ * `rule` field falls back to a freeform string so the schema never
48
+ * false-flags a valid reference.
49
+ */
50
+ export declare function generateSchema(options?: GenerateSchemaOptions): GenerateSchemaResult;
51
+ //# sourceMappingURL=generate-schema.d.ts.map