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.
- package/README.md +108 -131
- package/dist/cli.js +303 -161
- package/dist/compile.d.ts +1 -0
- package/dist/compile.js +22 -19
- package/dist/coverage.d.ts +56 -0
- package/dist/coverage.js +178 -0
- package/dist/doc-refs.d.ts +60 -0
- package/dist/doc-refs.js +246 -0
- package/dist/evolve.js +60 -125
- package/dist/frontmatter.d.ts +57 -0
- package/dist/frontmatter.js +190 -0
- package/dist/generate-schema.d.ts +51 -0
- package/dist/generate-schema.js +115 -0
- package/dist/generate-types.js +39 -1
- package/dist/hash.d.ts +8 -0
- package/dist/hash.js +16 -0
- package/dist/integrity.d.ts +29 -0
- package/dist/integrity.js +44 -0
- package/dist/linters.d.ts +5 -3
- package/dist/linters.js +116 -4
- package/dist/orphans.d.ts +52 -0
- package/dist/orphans.js +124 -0
- package/dist/proofs.js +22 -16
- package/dist/session.d.ts +97 -0
- package/dist/session.js +306 -0
- package/dist/sidecar.d.ts +35 -0
- package/dist/sidecar.js +102 -0
- package/dist/spec.d.ts +29 -9
- package/dist/spec.js +22 -4
- package/dist/test-utils.d.ts +8 -0
- package/dist/test-utils.js +41 -0
- package/dist/types.d.ts +34 -8
- package/dist/types.js +19 -0
- package/dist/validate.js +5 -3
- package/package.json +14 -2
- package/.claude/settings.json +0 -46
- package/.claude/settings.local.json +0 -8
- package/.github/workflows/ci.yml +0 -81
- package/.prettierignore +0 -1
- package/.vigiles/generated.d.ts +0 -205
- package/CLAUDE.md +0 -95
- package/CLAUDE.md.spec.ts +0 -142
- package/CONTRIBUTING.md +0 -121
- package/dist/action.d.ts.map +0 -1
- package/dist/action.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/cli.test.d.ts +0 -2
- package/dist/cli.test.d.ts.map +0 -1
- package/dist/cli.test.js +0 -650
- package/dist/cli.test.js.map +0 -1
- package/dist/compile.d.ts.map +0 -1
- package/dist/compile.js.map +0 -1
- package/dist/evolve.d.ts.map +0 -1
- package/dist/evolve.js.map +0 -1
- package/dist/freshness.d.ts +0 -67
- package/dist/freshness.d.ts.map +0 -1
- package/dist/freshness.js +0 -244
- package/dist/freshness.js.map +0 -1
- package/dist/freshness.test.d.ts +0 -2
- package/dist/freshness.test.d.ts.map +0 -1
- package/dist/freshness.test.js +0 -356
- package/dist/freshness.test.js.map +0 -1
- package/dist/generate-types.d.ts.map +0 -1
- package/dist/generate-types.js.map +0 -1
- package/dist/inline.d.ts.map +0 -1
- package/dist/inline.js.map +0 -1
- package/dist/inline.test.d.ts +0 -5
- package/dist/inline.test.d.ts.map +0 -1
- package/dist/inline.test.js +0 -152
- package/dist/inline.test.js.map +0 -1
- package/dist/linters.d.ts.map +0 -1
- package/dist/linters.js.map +0 -1
- package/dist/proofs.d.ts.map +0 -1
- package/dist/proofs.js.map +0 -1
- package/dist/proofs.test.d.ts +0 -9
- package/dist/proofs.test.d.ts.map +0 -1
- package/dist/proofs.test.js +0 -952
- package/dist/proofs.test.js.map +0 -1
- package/dist/spec.d.ts.map +0 -1
- package/dist/spec.js.map +0 -1
- package/dist/spec.test.d.ts +0 -2
- package/dist/spec.test.d.ts.map +0 -1
- package/dist/spec.test.js +0 -1222
- package/dist/spec.test.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js.map +0 -1
- package/dist/validate.test.d.ts +0 -2
- package/dist/validate.test.d.ts.map +0 -1
- package/dist/validate.test.js +0 -531
- package/dist/validate.test.js.map +0 -1
- package/docs/agent-setup.md +0 -85
- package/docs/agent-workflows.md +0 -103
- package/docs/comparison.md +0 -71
- package/docs/freshness.md +0 -124
- package/docs/inline-mode.md +0 -119
- package/docs/linter-support.md +0 -166
- package/docs/spec-format.md +0 -194
- package/eslint.config.mjs +0 -79
- package/examples/CLAUDE.md +0 -54
- package/examples/CLAUDE.md.spec.ts +0 -65
- package/examples/SKILL.md +0 -50
- package/examples/SKILL.md.spec.ts +0 -57
- package/fixtures/example-project/CLAUDE.md +0 -11
- package/fixtures/example-project/package.json +0 -9
- package/fixtures/example-project/src/index.ts +0 -3
- package/fixtures/example-project/src/utils.test.ts +0 -2
- package/fixtures/example-project/src/utils.ts +0 -3
- package/logo.png +0 -0
- package/research/adoption-strategy.md +0 -111
- package/research/agent-integration.md +0 -145
- package/research/ai-code-quality.md +0 -197
- package/research/code-search-for-agents.md +0 -313
- package/research/competitive-landscape.md +0 -163
- package/research/doc-freshness.md +0 -516
- package/research/executable-specs.md +0 -368
- package/research/feature-ideas.md +0 -464
- package/research/formal-proofs-for-agents.md +0 -338
- package/research/fp-for-agent-harness.md +0 -150
- package/research/fp-for-deterministic-ai.md +0 -131
- package/research/self-evolving-specs.md +0 -298
- package/schemas/claude-md-strict.yml +0 -18
- package/schemas/claude-md.yml +0 -6
- package/schemas/skill-strict.yml +0 -12
- package/schemas/skill.yml +0 -5
- package/skills/audit-feedback-loop/SKILL.md +0 -76
- package/skills/edit-spec/SKILL.md +0 -131
- package/skills/enforce-rules-format/SKILL.md +0 -71
- package/skills/generate-logo/SKILL.md +0 -102
- package/skills/generate-rule/SKILL.md +0 -90
- package/skills/linter-docs/clippy.md +0 -241
- package/skills/linter-docs/eslint.md +0 -384
- package/skills/linter-docs/pylint.md +0 -288
- package/skills/linter-docs/rubocop.md +0 -277
- package/skills/linter-docs/ruff.md +0 -187
- package/skills/linter-docs/stylelint.md +0 -247
- package/skills/migrate-to-spec/SKILL.md +0 -124
- package/skills/pr-to-lint-rule/SKILL.md +0 -97
- package/skills/strengthen/SKILL.md +0 -168
- package/src/action.ts +0 -214
- package/src/cli.test.ts +0 -914
- package/src/cli.ts +0 -1631
- package/src/compile.ts +0 -691
- package/src/evolve.ts +0 -781
- package/src/freshness.test.ts +0 -449
- package/src/freshness.ts +0 -299
- package/src/generate-types.ts +0 -448
- package/src/inline.test.ts +0 -206
- package/src/inline.ts +0 -164
- package/src/linters.ts +0 -739
- package/src/proofs.test.ts +0 -1314
- package/src/proofs.ts +0 -849
- package/src/spec.test.ts +0 -1471
- package/src/spec.ts +0 -427
- package/src/types.ts +0 -117
- package/src/validate.test.ts +0 -701
- package/src/validate.ts +0 -381
- 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,
|
|
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,
|
|
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
|
-
|
|
145
|
-
switch (r._ref) {
|
|
142
|
+
switch (fragment._ref) {
|
|
146
143
|
case "file":
|
|
147
|
-
return `\`${
|
|
144
|
+
return `\`${fragment.path}\``;
|
|
148
145
|
case "cmd":
|
|
149
|
-
return `\`${
|
|
146
|
+
return `\`${fragment.command}\``;
|
|
150
147
|
case "skill":
|
|
151
|
-
return `[${(0, node_path_1.basename)((0, node_path_1.dirname)(
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
package/dist/coverage.js
ADDED
|
@@ -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
|
package/dist/doc-refs.js
ADDED
|
@@ -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
|