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/cli.js CHANGED
@@ -15,17 +15,21 @@ const node_path_1 = require("node:path");
15
15
  const glob_1 = require("glob");
16
16
  const generate_types_js_1 = require("./generate-types.js");
17
17
  const validate_js_1 = require("./validate.js");
18
+ const types_js_1 = require("./types.js");
18
19
  const compile_js_1 = require("./compile.js");
19
20
  const proofs_js_1 = require("./proofs.js");
20
21
  const inline_js_1 = require("./inline.js");
22
+ const frontmatter_js_1 = require("./frontmatter.js");
23
+ const generate_schema_js_1 = require("./generate-schema.js");
21
24
  const linters_js_1 = require("./linters.js");
22
- const freshness_js_1 = require("./freshness.js");
25
+ const integrity_js_1 = require("./integrity.js");
26
+ const coverage_js_1 = require("./coverage.js");
27
+ const orphans_js_1 = require("./orphans.js");
28
+ const doc_refs_js_1 = require("./doc-refs.js");
23
29
  // ---------------------------------------------------------------------------
24
30
  // Constants
25
31
  // ---------------------------------------------------------------------------
26
32
  const IGNORE_NODE_MODULES = ["node_modules/**"];
27
- // Config is loaded from .vigilesrc.json via validate.ts::loadConfig().
28
- // All settings (validation, compilation, freshness) are in one place.
29
33
  // ---------------------------------------------------------------------------
30
34
  // Spec loading
31
35
  // ---------------------------------------------------------------------------
@@ -110,7 +114,7 @@ async function compile(specPaths, config) {
110
114
  }
111
115
  const basePath = process.cwd();
112
116
  if (spec._specType === "claude") {
113
- const { markdown: rawMarkdown, errors, linterResults, targets, } = (0, compile_js_1.compileClaude)(spec, {
117
+ const { markdown, errors, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
114
118
  basePath,
115
119
  specFile: specPath,
116
120
  maxRules: config.maxRules,
@@ -119,13 +123,6 @@ async function compile(specPaths, config) {
119
123
  catalogOnly: config.catalogOnly,
120
124
  linters: config.linters,
121
125
  });
122
- // Embed input hash for freshness tracking
123
- let markdown = rawMarkdown;
124
- if (config.freshnessMode === "input-hash") {
125
- const inputs = (0, freshness_js_1.discoverInputs)(specPath, spec, basePath, config.freshnessInputs);
126
- const inputHash = (0, freshness_js_1.computeInputHash)(inputs.files, basePath);
127
- markdown = (0, freshness_js_1.addInputHash)(rawMarkdown, inputHash);
128
- }
129
126
  const linterCount = linterResults.filter((r) => r.exists).length;
130
127
  const primaryOutput = specPath.replace(/\.spec\.ts$/, "");
131
128
  if (errors.length === 0) {
@@ -343,17 +340,7 @@ async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
343
340
  const ruleCount = Object.keys(rules).length;
344
341
  if (ruleCount < 2)
345
342
  continue;
346
- // Guard against unknown rule kinds (legacy "check" artifacts, JS
347
- // callers) that would crash ruleToText inside findSimilarRules.
348
- // Same pattern as the try/catch in runProofSuite.
349
- let pairs;
350
- try {
351
- pairs = (0, proofs_js_1.findSimilarRules)(rules, threshold);
352
- }
353
- catch (e) {
354
- log(` ⚠ ${specPath}: similarity check failed (${e instanceof Error ? e.message : String(e)})`);
355
- continue;
356
- }
343
+ const pairs = (0, proofs_js_1.findSimilarRules)(rules, threshold);
357
344
  if (pairs.length === 0)
358
345
  continue;
359
346
  if (specsWithDuplicates === 0) {
@@ -381,19 +368,44 @@ function auditExitCode(report) {
381
368
  if (report.hashErrors > 0 ||
382
369
  report.validationErrors > 0 ||
383
370
  report.inlineErrors > 0 ||
384
- report.freshnessErrors > 0)
371
+ report.frontmatterErrors > 0 ||
372
+ report.integrityErrors > 0 ||
373
+ report.coverageErrors > 0)
385
374
  return 2;
386
- if (report.duplicatePairs > 0)
375
+ if (report.duplicatePairs > 0 ||
376
+ report.orphanCount > 0 ||
377
+ report.docRefErrors > 0)
387
378
  return 1;
388
- // Coverage gaps and guidance counts are informational, not failures
379
+ // Guidance counts are informational, not failures
389
380
  return 0;
390
381
  }
391
382
  /**
392
- * Verify inline `<!-- vigiles:enforce ... -->` comments in an instruction
393
- * file. Each comment's linter rule goes through the same verification as
394
- * spec-declared enforce rules (existence, enabled status, closest-match
395
- * suggestions on typo).
383
+ * Verify one parsed enforce rule against the linter catalog/config, logging
384
+ * and annotating on failure. Returns true when the rule is valid+enabled.
396
385
  */
386
+ function verifyOneRule(rule, filePath, silent, linterOptions) {
387
+ const log = (msg) => {
388
+ if (!silent)
389
+ console.log(msg);
390
+ };
391
+ const result = (0, linters_js_1.checkLinterRule)(rule.linterRule, process.cwd(), linterOptions);
392
+ if (!result.exists) {
393
+ const message = result.error ?? `Rule "${rule.linterRule}" not found`;
394
+ log(` ✗ line ${String(rule.line)}: ${message}`);
395
+ if (!silent)
396
+ ghAnnotate("error", message, filePath, rule.line);
397
+ return false;
398
+ }
399
+ if (result.enabled === "disabled") {
400
+ const message = `Rule "${rule.linterRule}" exists but is disabled in ${result.linter} config`;
401
+ log(` ✗ line ${String(rule.line)}: ${message}`);
402
+ if (!silent)
403
+ ghAnnotate("error", message, filePath, rule.line);
404
+ return false;
405
+ }
406
+ log(` ✓ line ${String(rule.line)}: ${rule.linterRule}`);
407
+ return true;
408
+ }
397
409
  function verifyInlineRules(filePath, silent, linterOptions) {
398
410
  const log = (msg) => {
399
411
  if (!silent)
@@ -404,11 +416,11 @@ function verifyInlineRules(filePath, silent, linterOptions) {
404
416
  content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), filePath), "utf-8");
405
417
  }
406
418
  catch {
407
- return { ok: true, errorCount: 0, ruleCount: 0 };
419
+ return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
408
420
  }
409
421
  const { rules, errors: parseErrors } = (0, inline_js_1.parseInlineRules)(content);
410
422
  if (rules.length === 0 && parseErrors.length === 0) {
411
- return { ok: true, errorCount: 0, ruleCount: 0 };
423
+ return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
412
424
  }
413
425
  let errorCount = 0;
414
426
  log(`\n${filePath} (inline mode):`);
@@ -420,28 +432,113 @@ function verifyInlineRules(filePath, silent, linterOptions) {
420
432
  }
421
433
  }
422
434
  for (const rule of rules) {
423
- const result = (0, linters_js_1.checkLinterRule)(rule.linterRule, process.cwd(), linterOptions);
424
- if (!result.exists) {
425
- const message = result.error ?? `Rule "${rule.linterRule}" not found`;
426
- log(` ✗ line ${String(rule.line)}: ${message}`);
435
+ if (!verifyOneRule(rule, filePath, silent, linterOptions))
427
436
  errorCount++;
428
- if (!silent) {
429
- ghAnnotate("error", message, filePath, rule.line);
430
- }
437
+ }
438
+ return {
439
+ ok: errorCount === 0,
440
+ errorCount,
441
+ ruleCount: rules.length,
442
+ ruleNames: rules.map((r) => r.linterRule),
443
+ };
444
+ }
445
+ /**
446
+ * Verify `vigiles.enforce` rules declared in a file's YAML frontmatter.
447
+ * Same engine as inline/spec rules. Rules whose reference already appeared
448
+ * in `exclude` (e.g. declared inline in the same file) are skipped so a
449
+ * rule present in both sources is reported once, not twice.
450
+ */
451
+ function verifyFrontmatterRules(filePath, silent, exclude, linterOptions) {
452
+ const log = (msg) => {
453
+ if (!silent)
454
+ console.log(msg);
455
+ };
456
+ let content;
457
+ try {
458
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), filePath), "utf-8");
459
+ }
460
+ catch {
461
+ return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
462
+ }
463
+ const { rules: allRules, errors: parseErrors } = (0, frontmatter_js_1.parseFrontmatterRules)(content);
464
+ const rules = allRules.filter((r) => !exclude.has(r.linterRule));
465
+ if (rules.length === 0 && parseErrors.length === 0) {
466
+ return { ok: true, errorCount: 0, ruleCount: 0, ruleNames: [] };
467
+ }
468
+ let errorCount = 0;
469
+ log(`\n${filePath} (frontmatter mode):`);
470
+ for (const err of parseErrors) {
471
+ log(` ✗ line ${String(err.line)}: ${err.message}`);
472
+ errorCount++;
473
+ if (!silent) {
474
+ ghAnnotate("error", err.message, filePath, err.line);
431
475
  }
432
- else if (result.enabled === "disabled") {
433
- const message = `Rule "${rule.linterRule}" exists but is disabled in ${result.linter} config`;
434
- log(` ✗ line ${String(rule.line)}: ${message}`);
476
+ }
477
+ for (const rule of rules) {
478
+ if (!verifyOneRule(rule, filePath, silent, linterOptions))
435
479
  errorCount++;
436
- if (!silent) {
437
- ghAnnotate("error", message, filePath, rule.line);
438
- }
480
+ }
481
+ return {
482
+ ok: errorCount === 0,
483
+ errorCount,
484
+ ruleCount: rules.length,
485
+ ruleNames: rules.map((r) => r.linterRule),
486
+ };
487
+ }
488
+ /**
489
+ * Verify inline `<!-- vigiles:enforce -->` comments and `vigiles:` YAML
490
+ * frontmatter in instruction files that aren't managed by a spec.
491
+ *
492
+ * Spec mode is the source of truth when it exists, so a literal
493
+ * `<!-- vigiles:enforce ... -->` snippet that survived into compiled
494
+ * markdown (or an example in a spec-managed file) must not trip audit. A
495
+ * file is spec-managed iff it has a sibling `<file>.spec.ts` OR its own
496
+ * `<!-- vigiles:sha256:... compiled from <spec> -->` header. A rule
497
+ * declared both inline and in frontmatter is verified once (inline wins as
498
+ * the first source). See docs/markdown-mode.md.
499
+ */
500
+ function verifyMarkdownModeRules(files, silent, config) {
501
+ const totals = {
502
+ inlineErrors: 0,
503
+ inlineRules: 0,
504
+ frontmatterErrors: 0,
505
+ frontmatterRules: 0,
506
+ };
507
+ if (!silent && files.length > 0) {
508
+ console.log("\nInline + frontmatter rule verification:");
509
+ }
510
+ const linterOptions = {
511
+ catalogOnly: config?.catalogOnly,
512
+ linters: config?.linters,
513
+ };
514
+ const compiledFromRe = /<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from .+?\s*-->/;
515
+ for (const filePath of files) {
516
+ const abs = (0, node_path_1.resolve)(process.cwd(), filePath);
517
+ if ((0, node_fs_1.existsSync)(`${abs}.spec.ts`))
518
+ continue; // managed by sibling spec
519
+ let content;
520
+ try {
521
+ content = (0, node_fs_1.readFileSync)(abs, "utf-8");
439
522
  }
440
- else {
441
- log(` ✓ line ${String(rule.line)}: ${rule.linterRule}`);
523
+ catch {
524
+ continue;
442
525
  }
443
- }
444
- return { ok: errorCount === 0, errorCount, ruleCount: rules.length };
526
+ if (compiledFromRe.test(content))
527
+ continue; // managed via hash header
528
+ const inline = verifyInlineRules(filePath, silent, linterOptions);
529
+ totals.inlineErrors += inline.errorCount;
530
+ totals.inlineRules += inline.ruleCount;
531
+ const fm = verifyFrontmatterRules(filePath, silent, new Set(inline.ruleNames), linterOptions);
532
+ totals.frontmatterErrors += fm.errorCount;
533
+ totals.frontmatterRules += fm.ruleCount;
534
+ }
535
+ if (!silent &&
536
+ files.length > 0 &&
537
+ totals.inlineRules === 0 &&
538
+ totals.frontmatterRules === 0) {
539
+ console.log(" (no inline vigiles:enforce comments or vigiles: frontmatter found)");
540
+ }
541
+ return totals;
445
542
  }
446
543
  /**
447
544
  * Unified audit command: verify hashes, report coverage gaps, detect duplicates,
@@ -468,45 +565,10 @@ async function audit(restArgs, flags, config) {
468
565
  const hashResult = files.length > 0
469
566
  ? check(files, silent)
470
567
  : { valid: true, hashErrors: 0, validationErrors: 0 };
471
- // 1b. Verify inline vigiles:enforce comments in any instruction file
472
- // that isn't already managed by a .spec.ts. Spec mode is the source of
473
- // truth when it exists, so a literal `<!-- vigiles:enforce ... -->`
474
- // snippet that survived into the compiled markdown (or an
475
- // explanatory example in a spec-managed file) must not trip audit.
476
- // A file is spec-managed iff it has a sibling `<file>.spec.ts` OR its
477
- // own `<!-- vigiles:sha256:... compiled from <spec> -->` header.
478
- // See docs/inline-mode.md.
479
- let inlineErrors = 0;
480
- let inlineRules = 0;
481
- if (!silent && files.length > 0) {
482
- console.log("\nInline rule verification:");
483
- }
484
- const compiledFromRe = /<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from .+?\s*-->/;
485
- for (const filePath of files) {
486
- const abs = (0, node_path_1.resolve)(process.cwd(), filePath);
487
- if ((0, node_fs_1.existsSync)(`${abs}.spec.ts`)) {
488
- continue; // managed by sibling spec
489
- }
490
- let content;
491
- try {
492
- content = (0, node_fs_1.readFileSync)(abs, "utf-8");
493
- }
494
- catch {
495
- continue;
496
- }
497
- if (compiledFromRe.test(content)) {
498
- continue; // managed by the spec referenced in the hash header
499
- }
500
- const result = verifyInlineRules(filePath, silent, {
501
- catalogOnly: config?.catalogOnly,
502
- linters: config?.linters,
503
- });
504
- inlineErrors += result.errorCount;
505
- inlineRules += result.ruleCount;
506
- }
507
- if (!silent && files.length > 0 && inlineRules === 0) {
508
- console.log(" (no inline vigiles:enforce comments found)");
509
- }
568
+ // 1b. Verify inline + frontmatter rules in instruction files not managed
569
+ // by a spec. See verifyMarkdownModeRules / docs/markdown-mode.md.
570
+ const md = verifyMarkdownModeRules(files, silent, config);
571
+ const { inlineErrors, inlineRules, frontmatterErrors, frontmatterRules } = md;
510
572
  // 2. Coverage gaps (discover)
511
573
  if (!silent)
512
574
  console.log("\nLinter rule coverage:\n");
@@ -519,25 +581,60 @@ async function audit(restArgs, flags, config) {
519
581
  const dups = await findDuplicateRules(0.3, silent, restArgs.length > 0 ? files : undefined);
520
582
  // 4. Guidance rule count (strengthen suggestions moved to /strengthen skill)
521
583
  const guidanceCount = await countGuidanceRules(silent);
522
- // 5. Freshness check
523
- const freshnessSeverity = config?.rules.freshness;
524
- let freshnessErrors = 0;
525
- if (freshnessSeverity) {
584
+ // 5. Integrity check (hand-edit detection via SHA-256 hash)
585
+ const integritySeverity = config?.rules.integrity ?? "warn";
586
+ let integrityErrors = 0;
587
+ if (integritySeverity) {
526
588
  if (!silent)
527
- console.log("\nFreshness check:\n");
528
- const mode = config?.freshnessMode ?? "strict";
529
- freshnessErrors = await checkFreshness(files, mode, config, freshnessSeverity, silent);
589
+ console.log("\nIntegrity check:\n");
590
+ integrityErrors = checkIntegrityForFiles(files, integritySeverity, silent);
591
+ }
592
+ // 6. Coverage thresholds (gates CI when severity is "error")
593
+ const coverageErrors = await checkCoverageThresholds(coverage, config, silent);
594
+ // 7. Orphan docs check — find .md files no other markdown references.
595
+ // Enforces the `vigiles/orphan-docs` built-in rule when declared in a
596
+ // spec. Include/exclude come from .vigilesrc.json#orphans (tsconfig-
597
+ // style globs); default include is docs/ + research/ for the
598
+ // vigiles-repo convention.
599
+ if (!silent)
600
+ console.log("\nOrphan docs check:\n");
601
+ const orphanReport = (0, orphans_js_1.findOrphanDocs)({
602
+ basePath: process.cwd(),
603
+ include: config?.orphans?.include,
604
+ exclude: config?.orphans?.exclude,
605
+ });
606
+ if (!silent) {
607
+ for (const line of (0, orphans_js_1.formatOrphanReport)(orphanReport).split("\n")) {
608
+ console.log(` ${line}`);
609
+ }
610
+ }
611
+ // 8. Validate vigiles builder calls inside markdown code blocks. Default
612
+ // is to validate every ref; illustrative blocks opt out via
613
+ // `<!-- vigiles:ignore -->` (single block) or
614
+ // `<!-- vigiles:ignore-file -->` (whole file). Same engine as spec.ts.
615
+ if (!silent)
616
+ console.log("\nMarkdown code block refs:\n");
617
+ const docRefReport = (0, doc_refs_js_1.findDocRefs)({ basePath: process.cwd() });
618
+ if (!silent) {
619
+ for (const line of (0, doc_refs_js_1.formatDocRefReport)(docRefReport).split("\n")) {
620
+ console.log(` ${line}`);
621
+ }
530
622
  }
531
623
  const report = {
532
624
  hashErrors: hashResult.hashErrors,
533
625
  validationErrors: hashResult.validationErrors,
534
626
  inlineErrors,
535
627
  inlineRules,
628
+ frontmatterErrors,
629
+ frontmatterRules,
536
630
  duplicatePairs: dups.pairCount,
537
631
  coverageEnabled: coverage.enabled,
538
632
  coverageDocumented: coverage.documented,
539
633
  strengthenSuggestions: guidanceCount,
540
- freshnessErrors,
634
+ integrityErrors,
635
+ coverageErrors,
636
+ orphanCount: orphanReport.orphans.length,
637
+ docRefErrors: docRefReport.errors.length,
541
638
  files,
542
639
  };
543
640
  if (summary) {
@@ -557,15 +654,21 @@ function printAuditSummary(report) {
557
654
  parts.push(`${String(report.validationErrors)} validation errors`);
558
655
  if (report.inlineErrors > 0)
559
656
  parts.push(`${String(report.inlineErrors)} inline errors`);
657
+ if (report.frontmatterErrors > 0)
658
+ parts.push(`${String(report.frontmatterErrors)} frontmatter errors`);
560
659
  if (report.duplicatePairs > 0)
561
660
  parts.push(`${String(report.duplicatePairs)} duplicates`);
661
+ if (report.orphanCount > 0)
662
+ parts.push(`${String(report.orphanCount)} orphan docs`);
663
+ if (report.docRefErrors > 0)
664
+ parts.push(`${String(report.docRefErrors)} broken doc refs`);
562
665
  const undocumented = report.coverageEnabled - report.coverageDocumented;
563
666
  if (undocumented > 0)
564
667
  parts.push(`${String(undocumented)} undocumented rules`);
565
668
  if (report.strengthenSuggestions > 0)
566
669
  parts.push(`${String(report.strengthenSuggestions)} guidance (run /strengthen to upgrade)`);
567
- if (report.freshnessErrors > 0)
568
- parts.push(`${String(report.freshnessErrors)} stale (run vigiles compile)`);
670
+ if (report.integrityErrors > 0)
671
+ parts.push(`${String(report.integrityErrors)} tampered (edit the .spec.ts source)`);
569
672
  if (parts.length === 0) {
570
673
  console.log("vigiles: clean");
571
674
  }
@@ -883,6 +986,15 @@ async function setup(args) {
883
986
  console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
884
987
  }
885
988
  console.log(`✓ Generated ${outPath}`);
989
+ // Also emit a JSON Schema so `vigiles:` markdown frontmatter (Level 1)
990
+ // gets rule-name autocomplete + typo squiggles from the editor's YAML LSP.
991
+ const schemaResult = (0, generate_schema_js_1.generateSchema)({
992
+ basePath: process.cwd(),
993
+ linters: (0, validate_js_1.loadConfig)().linters,
994
+ });
995
+ const schemaPath = ".vigiles/schema.json";
996
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), schemaPath), schemaResult.json);
997
+ console.log(`✓ Generated ${schemaPath} (point frontmatter at it with \`# yaml-language-server: $schema=./${schemaPath}\`)`);
886
998
  // Step 5: Compile specs
887
999
  console.log("\nCompiling specs...");
888
1000
  const specs = findSpecs();
@@ -991,6 +1103,7 @@ async function setup(args) {
991
1103
  ...targets,
992
1104
  ...specPathsList,
993
1105
  ".vigiles/generated.d.ts",
1106
+ ".vigiles/schema.json",
994
1107
  ...(shouldInstallPlugin ? [".claude/settings.json"] : []),
995
1108
  ...(strict ? [".vigilesrc.json"] : []),
996
1109
  ];
@@ -999,7 +1112,7 @@ async function setup(args) {
999
1112
  // ---------------------------------------------------------------------------
1000
1113
  // Strengthen: guidance() → enforce() suggestions
1001
1114
  // ---------------------------------------------------------------------------
1002
- async function checkFreshness(files, mode, config, severity, silent) {
1115
+ function checkIntegrityForFiles(files, severity, silent) {
1003
1116
  const log = (msg) => {
1004
1117
  if (!silent)
1005
1118
  console.log(msg);
@@ -1010,79 +1123,66 @@ async function checkFreshness(files, mode, config, severity, silent) {
1010
1123
  const abs = (0, node_path_1.resolve)(basePath, filePath);
1011
1124
  if (!(0, node_fs_1.existsSync)(abs))
1012
1125
  continue;
1013
- const content = (0, node_fs_1.readFileSync)(abs, "utf-8");
1014
- // Find the spec that compiled this file
1015
- const hashMatch = content.match(/<!--\s*vigiles:sha256:[a-f0-9]+\s+compiled from (.+?)\s*-->/);
1016
- if (!hashMatch) {
1017
- // No hash = hand-written file, skip freshness check
1018
- continue;
1019
- }
1020
- let result;
1021
- const specFile = hashMatch[1];
1022
- if (mode === "strict") {
1023
- // Recompile in memory and diff
1024
- const spec = await loadSpec(specFile);
1025
- if (!spec || spec._specType !== "claude") {
1026
- log(` ? ${filePath} — can't load spec "${specFile}", skipping`);
1027
- continue;
1028
- }
1029
- const compiled = (0, compile_js_1.compileClaude)(spec, {
1030
- basePath,
1031
- specFile,
1032
- maxRules: config?.maxRules,
1033
- maxTokens: config?.maxTokens,
1034
- maxSectionLines: config?.maxSectionLines,
1035
- catalogOnly: config?.catalogOnly,
1036
- linters: config?.linters,
1037
- });
1038
- // Compare markdown body (strip hash/input lines)
1039
- const metaRe = /^<!-- vigiles:(sha256|inputs):[^\n]+ -->\r?\n?/gm;
1040
- const existingBody = content.replace(metaRe, "").trim();
1041
- const compiledBody = compiled.markdown.replace(metaRe, "").trim();
1042
- if (existingBody === compiledBody) {
1043
- result = { fresh: true, mode: "strict" };
1044
- }
1045
- else {
1046
- result = {
1047
- fresh: false,
1048
- mode: "strict",
1049
- reason: "Output would differ if recompiled — run `vigiles compile`",
1050
- };
1051
- }
1052
- }
1053
- else if (mode === "input-hash") {
1054
- const specFile = hashMatch[1];
1055
- const spec = await loadSpec(specFile);
1056
- if (!spec || spec._specType !== "claude") {
1057
- log(` ? ${filePath} — can't load spec "${specFile}", skipping`);
1058
- continue;
1059
- }
1060
- const inputs = (0, freshness_js_1.discoverInputs)(specFile, spec, basePath, config?.freshnessInputs);
1061
- result = (0, freshness_js_1.checkInputHashFreshness)(content, inputs.files, basePath);
1062
- }
1063
- else {
1064
- // output-hash mode
1065
- result = (0, freshness_js_1.checkOutputHashFreshness)(content);
1066
- }
1067
- if (!result.fresh) {
1126
+ const result = (0, integrity_js_1.checkIntegrity)((0, node_fs_1.readFileSync)(abs, "utf-8"));
1127
+ if (!result.intact) {
1068
1128
  errorCount++;
1069
1129
  const marker = severity === "error" ? "✗" : "⚠";
1070
- log(` ${marker} ${filePath} — ${result.reason ?? "stale"}`);
1071
- if (result.changedFiles && result.changedFiles.length > 0) {
1072
- for (const f of result.changedFiles) {
1073
- log(` changed: ${f}`);
1074
- }
1075
- }
1130
+ log(` ${marker} ${filePath} — ${result.reason ?? "tampered"}`);
1076
1131
  }
1077
1132
  else if (!silent) {
1078
- log(` ✓ ${filePath} — fresh (${mode})`);
1133
+ log(` ✓ ${filePath}`);
1079
1134
  }
1080
1135
  }
1081
1136
  if (errorCount === 0) {
1082
- log(" All files fresh.");
1137
+ log(" All compiled files intact.");
1083
1138
  }
1084
1139
  return severity === "error" ? errorCount : 0;
1085
1140
  }
1141
+ /**
1142
+ * Apply the configured coverage thresholds. Returns the number of failing
1143
+ * thresholds (so the audit can fail CI when severity is "error").
1144
+ *
1145
+ * Loads specs directly via loadSpec() when the scripts threshold is set —
1146
+ * avoids depending on a pre-built `dist/` tree, which the setup-generated
1147
+ * CI step doesn't guarantee.
1148
+ */
1149
+ async function checkCoverageThresholds(coverage, config, silent) {
1150
+ const severity = (0, types_js_1.ruleSeverity)(config?.rules.coverage);
1151
+ if (!severity)
1152
+ return 0;
1153
+ const opts = (0, types_js_1.ruleOptions)(config?.rules.coverage);
1154
+ if (!opts)
1155
+ return 0;
1156
+ const log = (msg) => {
1157
+ if (!silent)
1158
+ console.log(msg);
1159
+ };
1160
+ let failing = 0;
1161
+ if (!silent)
1162
+ console.log("\nCoverage thresholds:\n");
1163
+ if (opts.linterRules !== undefined) {
1164
+ const pct = coverage.enabled > 0
1165
+ ? Math.round((coverage.documented / coverage.enabled) * 100)
1166
+ : 100;
1167
+ const ok = pct >= opts.linterRules;
1168
+ if (!ok)
1169
+ failing++;
1170
+ const marker = ok ? "✓" : severity === "error" ? "✗" : "⚠";
1171
+ log(` ${marker} linterRules: ${String(pct)}% (threshold: ${String(opts.linterRules)}%)`);
1172
+ }
1173
+ if (opts.scripts !== undefined) {
1174
+ // Load all claude specs so coverage doesn't depend on a built dist/.
1175
+ const loaded = await Promise.all(findSpecs().map(loadSpec));
1176
+ const claudeSpecs = loaded.filter((s) => s?._specType === "claude");
1177
+ const metric = (0, coverage_js_1.computeScriptCoverage)(process.cwd(), opts.scripts, claudeSpecs);
1178
+ const ok = metric.passing;
1179
+ if (!ok)
1180
+ failing++;
1181
+ const marker = ok ? "✓" : severity === "error" ? "✗" : "⚠";
1182
+ log(` ${marker} scripts: ${String(metric.percent)}% (threshold: ${String(opts.scripts)}%)`);
1183
+ }
1184
+ return severity === "error" ? failing : 0;
1185
+ }
1086
1186
  async function countGuidanceRules(silent = false) {
1087
1187
  const specs = findSpecs();
1088
1188
  if (specs.length === 0)
@@ -1165,6 +1265,43 @@ function handleGenerateTypes(args, restArgs) {
1165
1265
  (0, node_fs_1.writeFileSync)(fullOut, result.dts);
1166
1266
  console.log(`\n✓ Generated ${outPath}`);
1167
1267
  }
1268
+ function handleGenerateSchema(args, restArgs) {
1269
+ const checkOnly = args.includes("--check");
1270
+ const outPath = restArgs[0] ?? ".vigiles/schema.json";
1271
+ console.log("Scanning linters...\n");
1272
+ const result = (0, generate_schema_js_1.generateSchema)({
1273
+ basePath: process.cwd(),
1274
+ linters: (0, validate_js_1.loadConfig)().linters,
1275
+ });
1276
+ for (const l of result.linters) {
1277
+ console.log(` ${l.linter}: ${String(l.count)} rules`);
1278
+ }
1279
+ console.log(` schema enum: ${String(result.ruleNames.length)} rule names`);
1280
+ const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
1281
+ if (checkOnly) {
1282
+ if (!(0, node_fs_1.existsSync)(fullOut)) {
1283
+ console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate-schema\` to create it.`);
1284
+ process.exit(1);
1285
+ }
1286
+ const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
1287
+ if (existing.trim() === result.json.trim()) {
1288
+ console.log(`\n✓ ${outPath} is up to date`);
1289
+ }
1290
+ else {
1291
+ console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate-schema\` to update.`);
1292
+ process.exit(1);
1293
+ }
1294
+ return;
1295
+ }
1296
+ const outDir = fullOut.substring(0, fullOut.lastIndexOf("/"));
1297
+ if (outDir && !(0, node_fs_1.existsSync)(outDir)) {
1298
+ (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
1299
+ }
1300
+ (0, node_fs_1.writeFileSync)(fullOut, result.json);
1301
+ console.log(`\n✓ Generated ${outPath}`);
1302
+ console.log(" Add to your markdown frontmatter:\n" +
1303
+ ` # yaml-language-server: $schema=./${outPath}`);
1304
+ }
1168
1305
  function printUsage(command) {
1169
1306
  console.log("vigiles — compile typed specs to instruction files");
1170
1307
  console.log("");
@@ -1181,6 +1318,8 @@ function printUsage(command) {
1181
1318
  console.log("Plumbing:");
1182
1319
  console.log(" vigiles generate-types [out] Emit .d.ts from project state");
1183
1320
  console.log(" vigiles generate-types --check Verify .d.ts is up to date");
1321
+ console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
1322
+ console.log(" vigiles generate-schema --check Verify schema.json is up to date");
1184
1323
  if (command && command !== "--help") {
1185
1324
  console.log(`\nUnknown command: "${command}"`);
1186
1325
  process.exit(1);
@@ -1258,6 +1397,9 @@ async function main() {
1258
1397
  case "generate-types":
1259
1398
  handleGenerateTypes(args, restArgs);
1260
1399
  break;
1400
+ case "generate-schema":
1401
+ handleGenerateSchema(args, restArgs);
1402
+ break;
1261
1403
  default:
1262
1404
  printUsage(command);
1263
1405
  break;
package/dist/compile.d.ts CHANGED
@@ -28,6 +28,7 @@ export interface CompileError {
28
28
  message: string;
29
29
  path?: string;
30
30
  }
31
+ export declare function readPackageScripts(basePath: string): Record<string, string> | null;
31
32
  export interface CompileClaudeResult {
32
33
  markdown: string;
33
34
  errors: CompileError[];