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
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,57 @@
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
+ export interface FrontmatterParseResult {
34
+ rules: FrontmatterRule[];
35
+ /** Frontmatter that looks like a vigiles block but failed to parse. */
36
+ errors: {
37
+ line: number;
38
+ message: string;
39
+ }[];
40
+ }
41
+ /**
42
+ * Parse `vigiles.enforce` rules out of a markdown file's YAML frontmatter.
43
+ * Does not touch the filesystem and does not verify rules against any
44
+ * linter — callers feed the returned rules into `checkLinterRule`.
45
+ *
46
+ * A file with no frontmatter, or frontmatter with no `vigiles` key, yields
47
+ * empty results with no errors. Malformed YAML or a malformed `vigiles`
48
+ * block yields error findings (never throws).
49
+ */
50
+ export declare function parseFrontmatterRules(content: string): FrontmatterParseResult;
51
+ /**
52
+ * True if the content has at least one parseable `vigiles.enforce` rule in
53
+ * its frontmatter. Used by `require-spec` validation to treat frontmatter
54
+ * mode as spec-equivalent, mirroring `hasInlineRules`.
55
+ */
56
+ export declare function hasFrontmatterRules(content: string): boolean;
57
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1,190 @@
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.enforce` list.
60
+ * Returns "none" when there's nothing for vigiles to check, "error" when a
61
+ * `vigiles`/`enforce` key is present but the wrong shape, or the list.
62
+ */
63
+ function lookupEnforce(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
+ const enforce = vigiles.enforce;
82
+ if (enforce === undefined)
83
+ return { kind: "none" };
84
+ const enforceLine = findLine(lines, "enforce:", startLine - 1);
85
+ if (!Array.isArray(enforce)) {
86
+ return {
87
+ kind: "error",
88
+ error: {
89
+ line: enforceLine,
90
+ message: "`vigiles.enforce` must be a list of { rule, why } entries.",
91
+ },
92
+ };
93
+ }
94
+ return { kind: "list", enforce, enforceLine };
95
+ }
96
+ /** Parse one `vigiles.enforce` entry into a rule or an error finding. */
97
+ function parseEntry(entry, index, ctx) {
98
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
99
+ return {
100
+ error: {
101
+ line: ctx.enforceLine,
102
+ message: `vigiles.enforce[${String(index)}] must be a mapping with \`rule\` and \`why\`.`,
103
+ },
104
+ nextCursor: ctx.cursor,
105
+ };
106
+ }
107
+ const { rule, why } = entry;
108
+ if (typeof rule !== "string" || rule.trim() === "") {
109
+ return {
110
+ error: {
111
+ line: ctx.enforceLine,
112
+ message: `vigiles.enforce[${String(index)}] is missing a string \`rule\`.`,
113
+ },
114
+ nextCursor: ctx.cursor,
115
+ };
116
+ }
117
+ const line = findLine(ctx.lines, rule, ctx.cursor);
118
+ if (typeof why !== "string" || why.trim() === "") {
119
+ return {
120
+ error: {
121
+ line,
122
+ message: `vigiles.enforce[${String(index)}] (rule "${rule}") is missing a string \`why\`.`,
123
+ },
124
+ nextCursor: line,
125
+ };
126
+ }
127
+ return { rule: { linterRule: rule, why, line }, nextCursor: line };
128
+ }
129
+ /**
130
+ * Parse `vigiles.enforce` rules out of a markdown file's YAML frontmatter.
131
+ * Does not touch the filesystem and does not verify rules against any
132
+ * linter — callers feed the returned rules into `checkLinterRule`.
133
+ *
134
+ * A file with no frontmatter, or frontmatter with no `vigiles` key, yields
135
+ * empty results with no errors. Malformed YAML or a malformed `vigiles`
136
+ * block yields error findings (never throws).
137
+ */
138
+ function parseFrontmatterRules(content) {
139
+ const fm = extractFrontmatter(content);
140
+ if (!fm)
141
+ return { rules: [], errors: [] };
142
+ const lines = content.split("\n");
143
+ let doc;
144
+ try {
145
+ doc = (0, js_yaml_1.load)(fm.yaml);
146
+ }
147
+ catch (e) {
148
+ const err = e;
149
+ const line = (err.mark?.line ?? 0) + fm.startLine;
150
+ return {
151
+ rules: [],
152
+ errors: [
153
+ {
154
+ line,
155
+ message: `Malformed YAML frontmatter: ${err.reason ?? err.message}`,
156
+ },
157
+ ],
158
+ };
159
+ }
160
+ const lookup = lookupEnforce(doc, lines, fm.startLine);
161
+ if (lookup.kind === "none")
162
+ return { rules: [], errors: [] };
163
+ if (lookup.kind === "error")
164
+ return { rules: [], errors: [lookup.error] };
165
+ const rules = [];
166
+ const errors = [];
167
+ let cursor = lookup.enforceLine; // search start: line after `enforce:`
168
+ for (let i = 0; i < lookup.enforce.length; i++) {
169
+ const r = parseEntry(lookup.enforce[i], i, {
170
+ lines,
171
+ enforceLine: lookup.enforceLine,
172
+ cursor,
173
+ });
174
+ cursor = r.nextCursor;
175
+ if (r.rule)
176
+ rules.push(r.rule);
177
+ if (r.error)
178
+ errors.push(r.error);
179
+ }
180
+ return { rules, errors };
181
+ }
182
+ /**
183
+ * True if the content has at least one parseable `vigiles.enforce` rule in
184
+ * its frontmatter. Used by `require-spec` validation to treat frontmatter
185
+ * mode as spec-equivalent, mirroring `hasInlineRules`.
186
+ */
187
+ function hasFrontmatterRules(content) {
188
+ return parseFrontmatterRules(content).rules.length > 0;
189
+ }
190
+ //# 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
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * vigiles generate-schema — emit a JSON Schema for `vigiles:` frontmatter.
4
+ *
5
+ * Sibling of `generate-types` (which emits a `.d.ts` for the spec.ts path).
6
+ * This emits `.vigiles/schema.json` so a YAML LSP can autocomplete rule
7
+ * names and squiggle typos in markdown frontmatter:
8
+ *
9
+ * ---
10
+ * # yaml-language-server: $schema=./.vigiles/schema.json
11
+ * vigiles:
12
+ * enforce:
13
+ * - rule: eslint/no-consolee # red squiggle in VS Code
14
+ * ---
15
+ *
16
+ * The `rule` enum is populated from the project's ACTUAL enabled linter
17
+ * rules — the same discovery `generate-types` runs — so the schema reflects
18
+ * what is really enforceable, not a static catalog.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.generateSchema = generateSchema;
22
+ const node_fs_1 = require("node:fs");
23
+ const node_path_1 = require("node:path");
24
+ const glob_1 = require("glob");
25
+ const generate_types_js_1 = require("./generate-types.js");
26
+ /**
27
+ * Rule references for config-declared custom linters. Mirrors
28
+ * `checkLinterRule`'s rulesDir lookup (any file `<rule>.*` is a rule) so the
29
+ * enum matches what `vigiles audit` accepts for these linters.
30
+ */
31
+ function customRuleRefs(basePath, linters) {
32
+ const refs = [];
33
+ for (const [name, cfg] of Object.entries(linters ?? {})) {
34
+ const dirs = Array.isArray(cfg.rulesDir) ? cfg.rulesDir : [cfg.rulesDir];
35
+ for (const dir of dirs) {
36
+ if (!dir)
37
+ continue;
38
+ const full = (0, node_path_1.resolve)(basePath, dir);
39
+ if (!(0, node_fs_1.existsSync)(full))
40
+ continue;
41
+ for (const f of (0, glob_1.globSync)("*.*", { cwd: full, nodir: true })) {
42
+ refs.push(`${name}/${f.replace(/\.[^.]+$/, "")}`);
43
+ }
44
+ }
45
+ }
46
+ return refs;
47
+ }
48
+ /**
49
+ * Build a JSON Schema for the `vigiles:` frontmatter block from the
50
+ * project's discovered linter rules. When no rules are discoverable the
51
+ * `rule` field falls back to a freeform string so the schema never
52
+ * false-flags a valid reference.
53
+ */
54
+ function generateSchema(options = {}) {
55
+ const basePath = options.basePath ?? process.cwd();
56
+ const { linters } = (0, generate_types_js_1.generateTypes)({ basePath });
57
+ const ruleNames = new Set();
58
+ for (const { linter, rules } of linters) {
59
+ for (const rule of rules) {
60
+ ruleNames.add(`${linter}/${rule}`);
61
+ // ESLint also accepts scoped-plugin rules in bare form (the scope is
62
+ // treated as the linter, e.g. "@typescript-eslint/no-explicit-any").
63
+ // Emit both so neither form produces a false squiggle.
64
+ if (linter === "eslint" && rule.includes("/")) {
65
+ ruleNames.add(rule);
66
+ }
67
+ }
68
+ }
69
+ for (const ref of customRuleRefs(basePath, options.linters)) {
70
+ ruleNames.add(ref);
71
+ }
72
+ const sorted = [...ruleNames].sort();
73
+ const ruleSchema = sorted.length > 0 ? { enum: sorted } : { type: "string" };
74
+ const schema = {
75
+ $schema: "http://json-schema.org/draft-07/schema#",
76
+ $id: "https://vigiles.dev/frontmatter.schema.json",
77
+ title: "vigiles frontmatter",
78
+ description: "vigiles enforce rules declared in markdown YAML frontmatter (Level 1).",
79
+ type: "object",
80
+ properties: {
81
+ vigiles: {
82
+ type: "object",
83
+ additionalProperties: false,
84
+ properties: {
85
+ enforce: {
86
+ type: "array",
87
+ description: "Linter rules to enforce, verified by `vigiles audit`.",
88
+ items: {
89
+ type: "object",
90
+ additionalProperties: false,
91
+ required: ["rule", "why"],
92
+ properties: {
93
+ rule: {
94
+ description: "Linter rule reference, e.g. eslint/no-console. Verified to exist AND be enabled.",
95
+ ...ruleSchema,
96
+ },
97
+ why: {
98
+ type: "string",
99
+ description: "Why this rule is enforced — shown to the agent as context.",
100
+ },
101
+ },
102
+ },
103
+ },
104
+ },
105
+ },
106
+ },
107
+ };
108
+ return {
109
+ schema,
110
+ json: JSON.stringify(schema, null, 2) + "\n",
111
+ ruleNames: sorted,
112
+ linters: linters.map((l) => ({ linter: l.linter, count: l.rules.length })),
113
+ };
114
+ }
115
+ //# sourceMappingURL=generate-schema.js.map