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/compile.js CHANGED
@@ -10,13 +10,18 @@ exports.computeHash = computeHash;
10
10
  exports.addHash = addHash;
11
11
  exports.verifyHash = verifyHash;
12
12
  exports.estimateTokens = estimateTokens;
13
+ exports.validateFileRef = validateFileRef;
14
+ exports.readPackageScripts = readPackageScripts;
15
+ exports.validateCommandRef = validateCommandRef;
16
+ exports.validateSymbolRef = validateSymbolRef;
13
17
  exports.compileClaude = compileClaude;
14
18
  exports.compileSkill = compileSkill;
15
19
  exports.checkFileHash = checkFileHash;
16
20
  exports.adoptDiff = adoptDiff;
17
- const node_crypto_1 = require("node:crypto");
18
21
  const node_fs_1 = require("node:fs");
19
22
  const node_path_1 = require("node:path");
23
+ const hash_js_1 = require("./hash.js");
24
+ const symbols_js_1 = require("./symbols.js");
20
25
  const linters_js_1 = require("./linters.js");
21
26
  // ---------------------------------------------------------------------------
22
27
  // Hash utilities
@@ -25,7 +30,7 @@ const HASH_RE = /^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?
25
30
  /** @internal Compute SHA-256 hash of content (excluding any existing hash line). */
26
31
  function computeHash(content) {
27
32
  const body = content.replace(HASH_RE, "");
28
- return (0, node_crypto_1.createHash)("sha256").update(body).digest("hex").slice(0, 16);
33
+ return (0, hash_js_1.sha256short)(body);
29
34
  }
30
35
  /** @internal Prepend a hash comment to compiled content. */
31
36
  function addHash(content, specFile) {
@@ -40,10 +45,7 @@ function verifyHash(content) {
40
45
  const expectedHash = match[1];
41
46
  const specFile = match[2];
42
47
  const body = content.replace(HASH_RE, "");
43
- const actualHash = (0, node_crypto_1.createHash)("sha256")
44
- .update(body)
45
- .digest("hex")
46
- .slice(0, 16);
48
+ const actualHash = (0, hash_js_1.sha256short)(body);
47
49
  return { valid: actualHash === expectedHash, specFile };
48
50
  }
49
51
  // ---------------------------------------------------------------------------
@@ -86,24 +88,57 @@ function readPackageScripts(basePath) {
86
88
  }
87
89
  }
88
90
  function validateCommandRef(command, basePath) {
89
- // Check "npm run <script>" or "npm <script>" against package.json
90
91
  const npmRunMatch = command.match(/^npm\s+run\s+(\S+)/);
91
92
  const npmMatch = command.match(/^npm\s+(test|start|build|pretest)\b/);
92
93
  const scriptName = npmRunMatch?.[1] ?? npmMatch?.[1];
93
- if (!scriptName)
94
- return null;
95
- const scripts = readPackageScripts(basePath);
96
- if (!scripts)
94
+ if (scriptName) {
95
+ const scripts = readPackageScripts(basePath);
96
+ if (scripts && !scripts[scriptName]) {
97
+ return {
98
+ type: "stale-command",
99
+ message: `Script "${scriptName}" not found in package.json`,
100
+ path: command,
101
+ };
102
+ }
97
103
  return null;
98
- if (!scripts[scriptName]) {
104
+ }
105
+ // Script-runner commands (python/node/bash/ruby/…): verify the referenced
106
+ // script file exists. Module forms (`python -m pkg`) are skipped — no path.
107
+ const scriptFile = command.match(/^(?:python3?|node|bash|sh|ruby|deno run)\s+([^\s-]\S*\.[A-Za-z0-9]+)/)?.[1];
108
+ if (scriptFile && !(0, node_fs_1.existsSync)((0, node_path_1.resolve)(basePath, scriptFile))) {
99
109
  return {
100
110
  type: "stale-command",
101
- message: `Script "${scriptName}" not found in package.json`,
111
+ message: `Script "${scriptFile}" not found`,
102
112
  path: command,
103
113
  };
104
114
  }
105
115
  return null;
106
116
  }
117
+ function validateSymbolRef(file, name, basePath) {
118
+ const full = (0, node_path_1.resolve)(basePath, file);
119
+ if (!(0, node_fs_1.existsSync)(full)) {
120
+ return {
121
+ type: "stale-file",
122
+ message: `File not found: "${file}"`,
123
+ path: file,
124
+ };
125
+ }
126
+ if ((0, symbols_js_1.langForFile)(file) === null) {
127
+ return {
128
+ type: "stale-ref",
129
+ message: `Unsupported language for symbol check: "${file}"`,
130
+ path: file,
131
+ };
132
+ }
133
+ if (!(0, symbols_js_1.fileDefinesSymbol)(full, name)) {
134
+ return {
135
+ type: "stale-ref",
136
+ message: `"${name}" is not defined in ${file}`,
137
+ path: `${file}#${name}`,
138
+ };
139
+ }
140
+ return null;
141
+ }
107
142
  function validateRefs(fragments, basePath) {
108
143
  const errors = [];
109
144
  for (const fragment of fragments) {
@@ -134,6 +169,12 @@ function validateRefs(fragments, basePath) {
134
169
  }
135
170
  break;
136
171
  }
172
+ case "symbol": {
173
+ const err = validateSymbolRef(r.file, r.symbol, basePath);
174
+ if (err)
175
+ errors.push(err);
176
+ break;
177
+ }
137
178
  }
138
179
  }
139
180
  return errors;
@@ -141,14 +182,17 @@ function validateRefs(fragments, basePath) {
141
182
  function renderFragment(fragment) {
142
183
  if (typeof fragment === "string")
143
184
  return fragment;
144
- const r = fragment;
145
- switch (r._ref) {
185
+ switch (fragment._ref) {
146
186
  case "file":
147
- return `\`${r.path}\``;
187
+ return `\`${fragment.path}\``;
148
188
  case "cmd":
149
- return `\`${r.command}\``;
189
+ return `\`${fragment.command}\``;
150
190
  case "skill":
151
- return `[${(0, node_path_1.basename)((0, node_path_1.dirname)(r.path))}](${r.path})`;
191
+ return `[${(0, node_path_1.basename)((0, node_path_1.dirname)(fragment.path))}](${fragment.path})`;
192
+ case "symbol":
193
+ return `\`vigiles:symbol ${fragment.file}#${fragment.symbol}\``;
194
+ default:
195
+ return (0, hash_js_1.assertNever)(fragment);
152
196
  }
153
197
  }
154
198
  // ---------------------------------------------------------------------------
@@ -168,15 +212,19 @@ function compileRule(id, rule) {
168
212
  ].join("\n");
169
213
  case "guidance":
170
214
  return [`### ${title}`, "", `**Guidance only** — ${rule.text}`].join("\n");
171
- default: {
172
- // Unknown rule kind — legacy compiled JS spec artifacts, JS caller,
173
- // or cast bypass. Fail loudly rather than silently dropping the
174
- // rule from output, which would remove constraints without any
175
- // compile error.
176
- const unknown = rule._kind;
177
- throw new Error(`Unknown rule kind "${String(unknown)}" for rule "${id}". ` +
178
- `Expected "enforce" or "guidance". Runtime data is out of sync with the Rule type.`);
215
+ case "guard": {
216
+ const patterns = Array.isArray(rule.watch)
217
+ ? rule.watch.join("`, `")
218
+ : rule.watch;
219
+ return [
220
+ `### ${title}`,
221
+ "",
222
+ `**Guard:** \`${patterns}\` \`${rule.run}\``,
223
+ `**Why:** ${rule.description}`,
224
+ ].join("\n");
179
225
  }
226
+ default:
227
+ return (0, hash_js_1.assertNever)(rule);
180
228
  }
181
229
  }
182
230
  function validateSectionContent(name, text, maxSectionLines) {
@@ -404,6 +452,166 @@ function renderBody(body) {
404
452
  return body;
405
453
  return body.map(renderFragment).join("");
406
454
  }
455
+ /** Derive the `argument-hint` frontmatter value from typed inputs. */
456
+ function renderArgumentHint(inputs) {
457
+ return inputs
458
+ .map((i) => (i.required === false ? `[<${i.name}>]` : `<${i.name}>`))
459
+ .join(" ");
460
+ }
461
+ /** Render the `## Arguments` section from typed inputs. */
462
+ function renderArguments(inputs) {
463
+ const lines = ["## Arguments", ""];
464
+ inputs.forEach((i, idx) => {
465
+ const opt = i.required === false ? " _(optional)_" : "";
466
+ lines.push(`- \`$${String(idx + 1)}\` **${i.name}**${opt} — ${i.hint}`);
467
+ });
468
+ return lines.join("\n");
469
+ }
470
+ /** The human prose + machine-readable marker for a gate. */
471
+ function renderGate(gate, retry) {
472
+ if (gate._ref === "cmd") {
473
+ const r = retry && retry > 1 ? ` retry:${String(retry)}` : "";
474
+ const proseR = retry && retry > 1 ? ` (retry up to ${String(retry)}×)` : "";
475
+ return {
476
+ prose: `**Gate** — run \`${gate.command}\`${proseR}; do not proceed until it passes.`,
477
+ marker: `<!-- vigiles:gate "${gate.command}"${r} -->`,
478
+ };
479
+ }
480
+ if (gate._ref === "role") {
481
+ const proseR = retry && retry > 1 ? ` (retry up to ${String(retry)}×)` : "";
482
+ const r = retry && retry > 1 ? ` retry:${String(retry)}` : "";
483
+ return {
484
+ prose: `**Gate** — run the project's ${gate.role} command${proseR}; do not proceed until it passes.`,
485
+ marker: `<!-- vigiles:gate role:${gate.role}${r} -->`,
486
+ };
487
+ }
488
+ return {
489
+ prose: `**Gate** — \`${gate.path}\` must exist before proceeding.`,
490
+ marker: `<!-- vigiles:gate file:${gate.path} -->`,
491
+ };
492
+ }
493
+ /** Render the `## Steps` checklist with a gate per step. */
494
+ function renderSteps(steps) {
495
+ const out = ["## Steps", ""];
496
+ steps.forEach((s, idx) => {
497
+ out.push(`### Step ${String(idx + 1)}`, "");
498
+ out.push(renderBody(s.do).trim(), "");
499
+ if (s.gate) {
500
+ const g = renderGate(s.gate, s.retry);
501
+ out.push(g.prose, "", g.marker, "");
502
+ }
503
+ });
504
+ return out.join("\n").trimEnd();
505
+ }
506
+ /** Render the `## Result` postcondition gate. */
507
+ function renderResult(result) {
508
+ let target;
509
+ let marker;
510
+ if (result._ref === "cmd") {
511
+ target = `\`${result.command}\` passes`;
512
+ marker = `<!-- vigiles:result "${result.command}" -->`;
513
+ }
514
+ else if (result._ref === "role") {
515
+ target = `the project's ${result.role} command passes`;
516
+ marker = `<!-- vigiles:result role:${result.role} -->`;
517
+ }
518
+ else {
519
+ target = `\`${result.path}\` exists`;
520
+ marker = `<!-- vigiles:result file:${result.path} -->`;
521
+ }
522
+ return [
523
+ "## Result",
524
+ "",
525
+ `This skill is complete when ${target}.`,
526
+ "",
527
+ marker,
528
+ ].join("\n");
529
+ }
530
+ /** Gather every reference a skill carries that needs author-time verification. */
531
+ function collectSkillRefs(spec) {
532
+ const refs = [];
533
+ if (Array.isArray(spec.body))
534
+ refs.push(...spec.body);
535
+ for (const s of spec.steps ?? []) {
536
+ if (Array.isArray(s.do))
537
+ refs.push(...s.do);
538
+ // Role gates resolve per host project at run time — nothing to verify here.
539
+ if (s.gate && s.gate._ref !== "role")
540
+ refs.push(s.gate);
541
+ }
542
+ if (spec.result && spec.result._ref !== "role")
543
+ refs.push(spec.result);
544
+ return refs;
545
+ }
546
+ /** Build the SKILL.md YAML frontmatter block. */
547
+ function renderSkillFrontmatter(spec) {
548
+ const fm = [
549
+ "---",
550
+ "",
551
+ `name: ${spec.name}`,
552
+ `description: ${spec.description}`,
553
+ ];
554
+ if (spec.disableModelInvocation !== undefined) {
555
+ fm.push(`disable-model-invocation: ${String(spec.disableModelInvocation)}`);
556
+ }
557
+ const argHint = spec.inputs && spec.inputs.length > 0
558
+ ? renderArgumentHint(spec.inputs)
559
+ : spec.argumentHint;
560
+ if (argHint)
561
+ fm.push(`argument-hint: ${argHint}`);
562
+ fm.push("", "---");
563
+ return fm.join("\n");
564
+ }
565
+ /**
566
+ * Compose the body: Arguments, then the knowledge body (reference prose), then
567
+ * the gated Steps, then the Result. body + steps compose — a skill can carry
568
+ * both a rich reference body and a verified procedure.
569
+ */
570
+ function renderSkillSections(spec) {
571
+ const sections = [];
572
+ if (spec.inputs && spec.inputs.length > 0) {
573
+ sections.push(renderArguments(spec.inputs));
574
+ }
575
+ if (spec.body !== undefined)
576
+ sections.push(renderBody(spec.body).trim());
577
+ if (spec.steps && spec.steps.length > 0) {
578
+ sections.push(renderSteps(spec.steps));
579
+ }
580
+ if (spec.result)
581
+ sections.push(renderResult(spec.result));
582
+ return sections.join("\n\n");
583
+ }
584
+ const DEFAULT_MAX_INLINE_CODE_LINES = 20;
585
+ /** Flag inline fenced code blocks longer than `max` lines (0 = disabled). */
586
+ function checkInlineCode(markdown, max) {
587
+ if (max <= 0)
588
+ return [];
589
+ const errs = [];
590
+ const lines = markdown.split("\n");
591
+ let start = -1;
592
+ let lang = "";
593
+ for (let i = 0; i < lines.length; i++) {
594
+ const m = /^```(\w*)/.exec(lines[i].trim());
595
+ if (!m)
596
+ continue;
597
+ if (start === -1) {
598
+ start = i;
599
+ lang = m[1];
600
+ }
601
+ else {
602
+ const len = i - start - 1;
603
+ if (len > max) {
604
+ errs.push({
605
+ type: "section-too-long",
606
+ message: `Inline ${lang || "code"} block is ${String(len)} lines (max ${String(max)}); extract it to a file and reference it with file().`,
607
+ });
608
+ }
609
+ start = -1;
610
+ lang = "";
611
+ }
612
+ }
613
+ return errs;
614
+ }
407
615
  /**
408
616
  * Compile a SkillSpec into SKILL.md markdown with YAML frontmatter.
409
617
  */
@@ -427,25 +635,11 @@ function compileSkill(spec, options = {}) {
427
635
  });
428
636
  }
429
637
  }
430
- // Validate refs in body
431
- if (Array.isArray(spec.body)) {
432
- errors.push(...validateRefs(spec.body, basePath));
433
- }
434
- // Build frontmatter (blank lines after opening/before closing --- for prettier)
435
- const fm = ["---", ""];
436
- fm.push(`name: ${spec.name}`);
437
- fm.push(`description: ${spec.description}`);
438
- if (spec.disableModelInvocation !== undefined) {
439
- fm.push(`disable-model-invocation: ${String(spec.disableModelInvocation)}`);
440
- }
441
- if (spec.argumentHint) {
442
- fm.push(`argument-hint: ${spec.argumentHint}`);
443
- }
444
- fm.push("", "---");
445
- const body = renderBody(spec.body);
446
- const content = fm.join("\n") + "\n\n" + body.trim() + "\n";
447
- const markdown = addHash(content, specFile);
448
- return { markdown, errors };
638
+ errors.push(...validateRefs(collectSkillRefs(spec), basePath));
639
+ const sections = renderSkillSections(spec);
640
+ errors.push(...checkInlineCode(sections, spec.maxInlineCodeLines ?? DEFAULT_MAX_INLINE_CODE_LINES));
641
+ const content = renderSkillFrontmatter(spec) + "\n\n" + sections.trim() + "\n";
642
+ return { markdown: addHash(content, specFile), errors };
449
643
  }
450
644
  /** Check if a generated file's hash is intact. */
451
645
  function checkFileHash(filePath) {
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Spec coverage analysis: how much of the project surface is documented in specs.
3
+ *
4
+ * Two metrics:
5
+ * - Linter rule coverage: % of enabled linter rules with enforce() declarations
6
+ * - Script coverage: % of npm scripts documented in spec commands
7
+ *
8
+ * Configurable thresholds in .vigilesrc.json trigger warnings or errors when
9
+ * coverage drops below the minimum.
10
+ */
11
+ import type { CoverageThresholds } from "./types.js";
12
+ import type { ClaudeSpec } from "./spec.js";
13
+ export interface CoverageMetric {
14
+ name: string;
15
+ total: number;
16
+ covered: number;
17
+ percent: number;
18
+ threshold: number | undefined;
19
+ passing: boolean;
20
+ /** Items that ARE covered. */
21
+ coveredItems: string[];
22
+ /** Items that are NOT covered. */
23
+ uncoveredItems: string[];
24
+ }
25
+ export interface CoverageReport {
26
+ metrics: CoverageMetric[];
27
+ passing: boolean;
28
+ }
29
+ /**
30
+ * Read npm script names from package.json. Reuses readPackageScripts
31
+ * from compile.ts and returns sorted keys.
32
+ */
33
+ export declare function readNpmScripts(basePath: string): string[];
34
+ /**
35
+ * Collect commands documented in specs by loading spec source files directly.
36
+ * Reads the structured `commands` field — no markdown parsing.
37
+ */
38
+ export declare function collectDocumentedCommands(basePath: string, specs?: ClaudeSpec[]): Set<string>;
39
+ /**
40
+ * Compute script coverage: what % of npm scripts are documented in specs.
41
+ */
42
+ export declare function computeScriptCoverage(basePath: string, threshold?: number, specs?: ClaudeSpec[]): CoverageMetric;
43
+ /**
44
+ * Compute linter rule coverage from pre-computed totals.
45
+ * The actual linter scanning is done by the existing discover() in cli.ts.
46
+ */
47
+ export declare function computeLinterRuleCoverage(enabled: number, documented: number, threshold?: number): CoverageMetric;
48
+ /**
49
+ * Check all coverage metrics against thresholds.
50
+ */
51
+ export declare function checkCoverage(basePath: string, thresholds: CoverageThresholds, linterEnabled: number, linterDocumented: number, specs?: ClaudeSpec[]): CoverageReport;
52
+ /**
53
+ * Format coverage report as human-readable text.
54
+ */
55
+ export declare function formatCoverageReport(report: CoverageReport): string;
56
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ /**
3
+ * Spec coverage analysis: how much of the project surface is documented in specs.
4
+ *
5
+ * Two metrics:
6
+ * - Linter rule coverage: % of enabled linter rules with enforce() declarations
7
+ * - Script coverage: % of npm scripts documented in spec commands
8
+ *
9
+ * Configurable thresholds in .vigilesrc.json trigger warnings or errors when
10
+ * coverage drops below the minimum.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.readNpmScripts = readNpmScripts;
14
+ exports.collectDocumentedCommands = collectDocumentedCommands;
15
+ exports.computeScriptCoverage = computeScriptCoverage;
16
+ exports.computeLinterRuleCoverage = computeLinterRuleCoverage;
17
+ exports.checkCoverage = checkCoverage;
18
+ exports.formatCoverageReport = formatCoverageReport;
19
+ const node_fs_1 = require("node:fs");
20
+ const node_path_1 = require("node:path");
21
+ const glob_1 = require("glob");
22
+ const compile_js_1 = require("./compile.js");
23
+ // ---------------------------------------------------------------------------
24
+ // Script coverage
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Read npm script names from package.json. Reuses readPackageScripts
28
+ * from compile.ts and returns sorted keys.
29
+ */
30
+ function readNpmScripts(basePath) {
31
+ const scripts = (0, compile_js_1.readPackageScripts)(basePath);
32
+ return scripts ? Object.keys(scripts).sort() : [];
33
+ }
34
+ /**
35
+ * Collect commands documented in specs by loading spec source files directly.
36
+ * Reads the structured `commands` field — no markdown parsing.
37
+ */
38
+ function collectDocumentedCommands(basePath, specs) {
39
+ const commands = new Set();
40
+ if (specs) {
41
+ for (const spec of specs) {
42
+ if (spec.commands) {
43
+ for (const cmd of Object.keys(spec.commands))
44
+ commands.add(cmd);
45
+ }
46
+ }
47
+ return commands;
48
+ }
49
+ // Fallback: scan compiled markdown for spec file references, then
50
+ // load the compiled JS spec from dist/. If that fails, try to
51
+ // extract commands from the compiled output (last resort).
52
+ const mdFiles = (0, glob_1.globSync)("**/*.md", {
53
+ ignore: ["node_modules/**", "dist/**", ".vigiles/**"],
54
+ cwd: basePath,
55
+ });
56
+ for (const mdFile of mdFiles) {
57
+ const fullPath = (0, node_path_1.resolve)(basePath, mdFile);
58
+ try {
59
+ const content = (0, node_fs_1.readFileSync)(fullPath, "utf-8");
60
+ const specMatch = content.match(/^<!-- vigiles:sha256:[a-f0-9]+ compiled from (.+) -->/);
61
+ if (!specMatch)
62
+ continue;
63
+ // Try to load the spec's compiled JS from dist/
64
+ const specFile = specMatch[1];
65
+ const jsPath = (0, node_path_1.resolve)(basePath, "dist", specFile.replace(/\.ts$/, ".js"));
66
+ if ((0, node_fs_1.existsSync)(jsPath)) {
67
+ try {
68
+ const mod = require(jsPath);
69
+ const spec = mod.default && "default" in mod.default
70
+ ? mod.default.default
71
+ : mod.default;
72
+ if (spec?.commands) {
73
+ for (const cmd of Object.keys(spec.commands))
74
+ commands.add(cmd);
75
+ }
76
+ continue;
77
+ }
78
+ catch {
79
+ // Fall through to next file
80
+ }
81
+ }
82
+ }
83
+ catch {
84
+ // Skip unreadable files
85
+ }
86
+ }
87
+ return commands;
88
+ }
89
+ /**
90
+ * Compute script coverage: what % of npm scripts are documented in specs.
91
+ */
92
+ function computeScriptCoverage(basePath, threshold, specs) {
93
+ const allScripts = readNpmScripts(basePath);
94
+ const documented = collectDocumentedCommands(basePath, specs);
95
+ const covered = [];
96
+ const uncovered = [];
97
+ for (const script of allScripts) {
98
+ // Match the same forms compile-time validation accepts in cmd():
99
+ // "npm run <script>" or "npm <script>". Bare script names are not
100
+ // executable refs — accepting them here would let stale entries
101
+ // satisfy the coverage threshold without being verifiable.
102
+ const npmRun = `npm run ${script}`;
103
+ const npmDirect = `npm ${script}`;
104
+ if (documented.has(npmRun) || documented.has(npmDirect)) {
105
+ covered.push(script);
106
+ }
107
+ else {
108
+ uncovered.push(script);
109
+ }
110
+ }
111
+ const total = allScripts.length;
112
+ const percent = total > 0 ? Math.round((covered.length / total) * 100) : 100;
113
+ const passing = threshold === undefined || percent >= threshold;
114
+ return {
115
+ name: "scripts",
116
+ total,
117
+ covered: covered.length,
118
+ percent,
119
+ threshold,
120
+ passing,
121
+ coveredItems: covered,
122
+ uncoveredItems: uncovered,
123
+ };
124
+ }
125
+ /**
126
+ * Compute linter rule coverage from pre-computed totals.
127
+ * The actual linter scanning is done by the existing discover() in cli.ts.
128
+ */
129
+ function computeLinterRuleCoverage(enabled, documented, threshold) {
130
+ const percent = enabled > 0 ? Math.round((documented / enabled) * 100) : 100;
131
+ const passing = threshold === undefined || percent >= threshold;
132
+ return {
133
+ name: "linterRules",
134
+ total: enabled,
135
+ covered: documented,
136
+ percent,
137
+ threshold,
138
+ passing,
139
+ coveredItems: [],
140
+ uncoveredItems: [],
141
+ };
142
+ }
143
+ /**
144
+ * Check all coverage metrics against thresholds.
145
+ */
146
+ function checkCoverage(basePath, thresholds, linterEnabled, linterDocumented, specs) {
147
+ const metrics = [];
148
+ // Linter rule coverage
149
+ const linterMetric = computeLinterRuleCoverage(linterEnabled, linterDocumented, thresholds.linterRules);
150
+ metrics.push(linterMetric);
151
+ // Script coverage
152
+ const scriptMetric = computeScriptCoverage(basePath, thresholds.scripts, specs);
153
+ metrics.push(scriptMetric);
154
+ const passing = metrics.every((m) => m.passing);
155
+ return { metrics, passing };
156
+ }
157
+ /**
158
+ * Format coverage report as human-readable text.
159
+ */
160
+ function formatCoverageReport(report) {
161
+ const lines = [];
162
+ for (const m of report.metrics) {
163
+ const status = m.threshold !== undefined ? (m.passing ? "✓" : "✗") : " ";
164
+ const thresholdStr = m.threshold !== undefined ? ` (threshold: ${String(m.threshold)}%)` : "";
165
+ lines.push(` ${status} ${m.name}: ${String(m.covered)}/${String(m.total)} (${String(m.percent)}%)${thresholdStr}`);
166
+ if (!m.passing && m.uncoveredItems.length > 0) {
167
+ const show = m.uncoveredItems.slice(0, 5);
168
+ for (const item of show) {
169
+ lines.push(` missing: ${item}`);
170
+ }
171
+ if (m.uncoveredItems.length > 5) {
172
+ lines.push(` ... and ${String(m.uncoveredItems.length - 5)} more`);
173
+ }
174
+ }
175
+ }
176
+ return lines.join("\n");
177
+ }
178
+ //# sourceMappingURL=coverage.js.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * vigiles — Validate vigiles-builder calls in markdown code blocks.
3
+ *
4
+ * Mirror of inline mode but inverted: inline mode skips fenced code blocks
5
+ * (so `<!-- vigiles:enforce -->` in prose doesn't accidentally match an
6
+ * example). This module enters fenced code blocks (ts/typescript/js/
7
+ * javascript) and validates the vigiles builder calls inside —
8
+ * `enforce("...")`, `file("...")`, `cmd("...")`, `ref("...")` — using the
9
+ * same engines that validate them in spec.ts.
10
+ *
11
+ * Default: validate every ref. Illustrative blocks opt out via
12
+ * `<!-- vigiles:ignore -->` immediately before the fence. Whole files
13
+ * opt out via `<!-- vigiles:ignore-file -->` anywhere in the file
14
+ * (intended for research/design docs that quote hypothetical refs).
15
+ *
16
+ * Scope: ONLY vigiles builder calls. Generic TS syntax / type checking
17
+ * in markdown is explicitly out of scope — use eslint-plugin-markdown or
18
+ * twoslash for that.
19
+ */
20
+ export type DocRefKind = "enforce" | "file" | "cmd" | "ref";
21
+ export interface DocRef {
22
+ readonly file: string;
23
+ readonly line: number;
24
+ readonly kind: DocRefKind;
25
+ readonly value: string;
26
+ }
27
+ export interface DocRefError extends DocRef {
28
+ readonly message: string;
29
+ }
30
+ export interface DocRefReport {
31
+ readonly filesScanned: number;
32
+ readonly filesIgnored: number;
33
+ readonly blocksIgnored: number;
34
+ readonly refs: readonly DocRef[];
35
+ readonly errors: readonly DocRefError[];
36
+ /** Refs that couldn't be verified because the underlying tool isn't available in this env. */
37
+ readonly unverified: number;
38
+ /** Refs that contained placeholder syntax (e.g. <linter>/<rule>) and were skipped. */
39
+ readonly placeholders: number;
40
+ }
41
+ export interface FindDocRefsOptions {
42
+ readonly basePath?: string;
43
+ readonly ignore?: readonly string[];
44
+ }
45
+ interface ExtractResult {
46
+ refs: DocRef[];
47
+ blocksIgnored: number;
48
+ }
49
+ /** @internal */ export declare function extractDocRefs(content: string, file: string): ExtractResult;
50
+ /**
51
+ * Walk every `.md` under `basePath`, extract vigiles builder calls from
52
+ * fenced TS/JS code blocks, validate against the same engines used for
53
+ * spec.ts. Honors `<!-- vigiles:ignore-file -->` (skip the whole file)
54
+ * and `<!-- vigiles:ignore -->` (skip the next code block).
55
+ */
56
+ export declare function findDocRefs(options?: FindDocRefsOptions): DocRefReport;
57
+ /** Format a DocRefReport as human-readable text. */
58
+ export declare function formatDocRefReport(report: DocRefReport): string;
59
+ export {};
60
+ //# sourceMappingURL=doc-refs.d.ts.map