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/compile.js CHANGED
@@ -10,13 +10,14 @@ exports.computeHash = computeHash;
10
10
  exports.addHash = addHash;
11
11
  exports.verifyHash = verifyHash;
12
12
  exports.estimateTokens = estimateTokens;
13
+ exports.readPackageScripts = readPackageScripts;
13
14
  exports.compileClaude = compileClaude;
14
15
  exports.compileSkill = compileSkill;
15
16
  exports.checkFileHash = checkFileHash;
16
17
  exports.adoptDiff = adoptDiff;
17
- const node_crypto_1 = require("node:crypto");
18
18
  const node_fs_1 = require("node:fs");
19
19
  const node_path_1 = require("node:path");
20
+ const hash_js_1 = require("./hash.js");
20
21
  const linters_js_1 = require("./linters.js");
21
22
  // ---------------------------------------------------------------------------
22
23
  // Hash utilities
@@ -25,7 +26,7 @@ const HASH_RE = /^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?
25
26
  /** @internal Compute SHA-256 hash of content (excluding any existing hash line). */
26
27
  function computeHash(content) {
27
28
  const body = content.replace(HASH_RE, "");
28
- return (0, node_crypto_1.createHash)("sha256").update(body).digest("hex").slice(0, 16);
29
+ return (0, hash_js_1.sha256short)(body);
29
30
  }
30
31
  /** @internal Prepend a hash comment to compiled content. */
31
32
  function addHash(content, specFile) {
@@ -40,10 +41,7 @@ function verifyHash(content) {
40
41
  const expectedHash = match[1];
41
42
  const specFile = match[2];
42
43
  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);
44
+ const actualHash = (0, hash_js_1.sha256short)(body);
47
45
  return { valid: actualHash === expectedHash, specFile };
48
46
  }
49
47
  // ---------------------------------------------------------------------------
@@ -141,14 +139,15 @@ function validateRefs(fragments, basePath) {
141
139
  function renderFragment(fragment) {
142
140
  if (typeof fragment === "string")
143
141
  return fragment;
144
- const r = fragment;
145
- switch (r._ref) {
142
+ switch (fragment._ref) {
146
143
  case "file":
147
- return `\`${r.path}\``;
144
+ return `\`${fragment.path}\``;
148
145
  case "cmd":
149
- return `\`${r.command}\``;
146
+ return `\`${fragment.command}\``;
150
147
  case "skill":
151
- return `[${(0, node_path_1.basename)((0, node_path_1.dirname)(r.path))}](${r.path})`;
148
+ return `[${(0, node_path_1.basename)((0, node_path_1.dirname)(fragment.path))}](${fragment.path})`;
149
+ default:
150
+ return (0, hash_js_1.assertNever)(fragment);
152
151
  }
153
152
  }
154
153
  // ---------------------------------------------------------------------------
@@ -168,15 +167,19 @@ function compileRule(id, rule) {
168
167
  ].join("\n");
169
168
  case "guidance":
170
169
  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.`);
170
+ case "guard": {
171
+ const patterns = Array.isArray(rule.watch)
172
+ ? rule.watch.join("`, `")
173
+ : rule.watch;
174
+ return [
175
+ `### ${title}`,
176
+ "",
177
+ `**Guard:** \`${patterns}\` \`${rule.run}\``,
178
+ `**Why:** ${rule.description}`,
179
+ ].join("\n");
179
180
  }
181
+ default:
182
+ return (0, hash_js_1.assertNever)(rule);
180
183
  }
181
184
  }
182
185
  function validateSectionContent(name, text, maxSectionLines) {
@@ -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
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ /**
3
+ * vigiles — Validate vigiles-builder calls in markdown code blocks.
4
+ *
5
+ * Mirror of inline mode but inverted: inline mode skips fenced code blocks
6
+ * (so `<!-- vigiles:enforce -->` in prose doesn't accidentally match an
7
+ * example). This module enters fenced code blocks (ts/typescript/js/
8
+ * javascript) and validates the vigiles builder calls inside —
9
+ * `enforce("...")`, `file("...")`, `cmd("...")`, `ref("...")` — using the
10
+ * same engines that validate them in spec.ts.
11
+ *
12
+ * Default: validate every ref. Illustrative blocks opt out via
13
+ * `<!-- vigiles:ignore -->` immediately before the fence. Whole files
14
+ * opt out via `<!-- vigiles:ignore-file -->` anywhere in the file
15
+ * (intended for research/design docs that quote hypothetical refs).
16
+ *
17
+ * Scope: ONLY vigiles builder calls. Generic TS syntax / type checking
18
+ * in markdown is explicitly out of scope — use eslint-plugin-markdown or
19
+ * twoslash for that.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.extractDocRefs = extractDocRefs;
23
+ exports.findDocRefs = findDocRefs;
24
+ exports.formatDocRefReport = formatDocRefReport;
25
+ const node_fs_1 = require("node:fs");
26
+ const node_path_1 = require("node:path");
27
+ const glob_1 = require("glob");
28
+ const linters_js_1 = require("./linters.js");
29
+ const compile_js_1 = require("./compile.js");
30
+ // ---------------------------------------------------------------------------
31
+ // Constants
32
+ // ---------------------------------------------------------------------------
33
+ const DEFAULT_IGNORE = [
34
+ "node_modules/**",
35
+ "dist/**",
36
+ ".vigiles/**",
37
+ ".git/**",
38
+ ];
39
+ const FENCE_RE = /^(\s{0,3})(`{3,}|~{3,})(.*)$/;
40
+ const TS_LANGS = new Set(["ts", "typescript", "js", "javascript"]);
41
+ // Ignore markers must appear as standalone lines (whole line is the
42
+ // comment, modulo whitespace) so an inline-code mention like
43
+ // `` `<!-- vigiles:ignore-file -->` `` in prose documenting the syntax
44
+ // doesn't accidentally disable validation.
45
+ const IGNORE_BLOCK_RE = /^\s{0,3}<!--\s*vigiles:ignore\s*-->\s*$/;
46
+ const IGNORE_FILE_RE = /^\s{0,3}<!--\s*vigiles:ignore-file\s*-->\s*$/m;
47
+ const CALL_RE = /\b(enforce|file|cmd|ref)\(\s*["']([^"'\n]+)["']/g;
48
+ const PLACEHOLDER_RE = /[<>]/;
49
+ /**
50
+ * Error-message patterns that mean "tool not available in this env" rather
51
+ * than "ref is actually broken." We can't decide between valid and invalid
52
+ * when the underlying linter or CLI isn't installed, so count these
53
+ * separately from real errors.
54
+ */
55
+ const UNVERIFIABLE_PATTERNS = [
56
+ /Unknown linter:/i,
57
+ /not found on PATH/i,
58
+ /No Cedar policies found/i,
59
+ ];
60
+ /** @internal */ function extractDocRefs(content, file) {
61
+ const lines = content.split("\n");
62
+ const refs = [];
63
+ let blocksIgnored = 0;
64
+ let fenceChar = null;
65
+ let fenceLen = 0;
66
+ let fenceLang = "";
67
+ let blockLines = [];
68
+ let nextBlockIgnored = false;
69
+ for (let i = 0; i < lines.length; i++) {
70
+ const line = lines[i];
71
+ const fm = FENCE_RE.exec(line);
72
+ if (fm) {
73
+ const marker = fm[2];
74
+ const ch = marker[0];
75
+ const len = marker.length;
76
+ const info = fm[3].trim();
77
+ if (fenceChar === null) {
78
+ fenceChar = ch;
79
+ fenceLen = len;
80
+ fenceLang = info.split(/\s+/)[0].toLowerCase();
81
+ blockLines = [];
82
+ continue;
83
+ }
84
+ else if (ch === fenceChar && len >= fenceLen && info === "") {
85
+ // Closing fence
86
+ if (TS_LANGS.has(fenceLang)) {
87
+ if (nextBlockIgnored) {
88
+ blocksIgnored++;
89
+ }
90
+ else {
91
+ for (const { lineNo, text } of blockLines) {
92
+ for (const m of text.matchAll(CALL_RE)) {
93
+ refs.push({
94
+ file,
95
+ line: lineNo,
96
+ kind: m[1],
97
+ value: m[2],
98
+ });
99
+ }
100
+ }
101
+ }
102
+ }
103
+ fenceChar = null;
104
+ fenceLen = 0;
105
+ fenceLang = "";
106
+ nextBlockIgnored = false;
107
+ blockLines = [];
108
+ continue;
109
+ }
110
+ }
111
+ if (fenceChar !== null) {
112
+ blockLines.push({ lineNo: i + 1, text: line });
113
+ continue;
114
+ }
115
+ if (IGNORE_BLOCK_RE.test(line)) {
116
+ nextBlockIgnored = true;
117
+ }
118
+ }
119
+ return { refs, blocksIgnored };
120
+ }
121
+ function isUnverifiable(message) {
122
+ return UNVERIFIABLE_PATTERNS.some((p) => p.test(message));
123
+ }
124
+ function validateRefs(refs, basePath) {
125
+ const errors = [];
126
+ let unverified = 0;
127
+ let placeholders = 0;
128
+ const scripts = (0, compile_js_1.readPackageScripts)(basePath) ?? {};
129
+ for (const r of refs) {
130
+ // Skip obvious placeholders like enforce("<linter>/<rule>") that
131
+ // appear in skill format documentation. They aren't typos — they're
132
+ // syntax templates. Real refs don't contain < or >.
133
+ if (PLACEHOLDER_RE.test(r.value)) {
134
+ placeholders++;
135
+ continue;
136
+ }
137
+ switch (r.kind) {
138
+ case "enforce": {
139
+ const result = (0, linters_js_1.checkLinterRule)(r.value, basePath, {
140
+ catalogOnly: true,
141
+ });
142
+ if (!result.exists) {
143
+ const msg = result.error ?? `Rule "${r.value}" not found`;
144
+ if (isUnverifiable(msg)) {
145
+ unverified++;
146
+ }
147
+ else {
148
+ errors.push({ ...r, message: msg });
149
+ }
150
+ }
151
+ break;
152
+ }
153
+ case "file":
154
+ case "ref": {
155
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.resolve)(basePath, r.value))) {
156
+ errors.push({ ...r, message: `File not found: "${r.value}"` });
157
+ }
158
+ break;
159
+ }
160
+ case "cmd": {
161
+ const npmRun = r.value.match(/^npm\s+run\s+(\S+)/);
162
+ const npmDirect = r.value.match(/^npm\s+(test|start|build|pretest)\b/);
163
+ const scriptName = npmRun?.[1] ?? npmDirect?.[1];
164
+ if (scriptName && !scripts[scriptName]) {
165
+ errors.push({
166
+ ...r,
167
+ message: `Script "${scriptName}" not found in package.json`,
168
+ });
169
+ }
170
+ break;
171
+ }
172
+ }
173
+ }
174
+ return { errors, unverified, placeholders };
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Public API
178
+ // ---------------------------------------------------------------------------
179
+ /**
180
+ * Walk every `.md` under `basePath`, extract vigiles builder calls from
181
+ * fenced TS/JS code blocks, validate against the same engines used for
182
+ * spec.ts. Honors `<!-- vigiles:ignore-file -->` (skip the whole file)
183
+ * and `<!-- vigiles:ignore -->` (skip the next code block).
184
+ */
185
+ function findDocRefs(options = {}) {
186
+ const basePath = options.basePath ?? process.cwd();
187
+ const ignore = [...DEFAULT_IGNORE, ...(options.ignore ?? [])];
188
+ const files = (0, glob_1.globSync)("**/*.md", { cwd: basePath, ignore });
189
+ const allRefs = [];
190
+ let filesIgnored = 0;
191
+ let blocksIgnored = 0;
192
+ for (const f of files) {
193
+ let content;
194
+ try {
195
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(basePath, f), "utf-8");
196
+ }
197
+ catch {
198
+ continue;
199
+ }
200
+ if (IGNORE_FILE_RE.test(content)) {
201
+ filesIgnored++;
202
+ continue;
203
+ }
204
+ const r = extractDocRefs(content, f);
205
+ allRefs.push(...r.refs);
206
+ blocksIgnored += r.blocksIgnored;
207
+ }
208
+ const outcome = validateRefs(allRefs, basePath);
209
+ return {
210
+ filesScanned: files.length,
211
+ filesIgnored,
212
+ blocksIgnored,
213
+ refs: allRefs,
214
+ errors: outcome.errors,
215
+ unverified: outcome.unverified,
216
+ placeholders: outcome.placeholders,
217
+ };
218
+ }
219
+ /** Format a DocRefReport as human-readable text. */
220
+ function formatDocRefReport(report) {
221
+ const lines = [];
222
+ const meta = [];
223
+ if (report.filesIgnored > 0)
224
+ meta.push(`${String(report.filesIgnored)} via vigiles:ignore-file`);
225
+ if (report.blocksIgnored > 0)
226
+ meta.push(`${String(report.blocksIgnored)} blocks via vigiles:ignore`);
227
+ if (report.placeholders > 0)
228
+ meta.push(`${String(report.placeholders)} placeholders skipped`);
229
+ if (report.unverified > 0)
230
+ meta.push(`${String(report.unverified)} unverified (tool unavailable)`);
231
+ const metaStr = meta.length > 0 ? ` (${meta.join(", ")})` : "";
232
+ lines.push(`scanned ${String(report.filesScanned)} files${metaStr}`);
233
+ lines.push(`${String(report.refs.length)} vigiles refs in code blocks${report.errors.length === 0 ? " — all valid" : ""}`);
234
+ if (report.errors.length === 0)
235
+ return lines.join("\n");
236
+ lines.push(`✗ ${String(report.errors.length)} broken ref(s):`);
237
+ for (const e of report.errors.slice(0, 12)) {
238
+ const trunc = e.message.length > 80 ? `${e.message.slice(0, 80)}…` : e.message;
239
+ lines.push(` ${e.file}:${String(e.line)} ${e.kind}("${e.value}") — ${trunc}`);
240
+ }
241
+ if (report.errors.length > 12) {
242
+ lines.push(` ... +${String(report.errors.length - 12)} more`);
243
+ }
244
+ return lines.join("\n");
245
+ }
246
+ //# sourceMappingURL=doc-refs.js.map