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.
- package/README.md +182 -134
- package/dist/action-gate.d.ts +28 -0
- package/dist/action-gate.js +73 -0
- package/dist/cli.js +705 -230
- package/dist/community-skills.d.ts +22 -0
- package/dist/community-skills.js +86 -0
- package/dist/compile-generator.d.ts +48 -0
- package/dist/compile-generator.js +322 -0
- package/dist/compile.d.ts +4 -0
- package/dist/compile.js +239 -45
- 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/eval.d.ts +62 -0
- package/dist/eval.js +174 -0
- package/dist/evolve.js +60 -125
- package/dist/frontmatter.d.ts +75 -0
- package/dist/frontmatter.js +263 -0
- package/dist/generate-schema.d.ts +51 -0
- package/dist/generate-schema.js +125 -0
- package/dist/generate-types.js +39 -1
- package/dist/harness-test.d.ts +38 -0
- package/dist/harness-test.js +129 -0
- package/dist/hash.d.ts +8 -0
- package/dist/hash.js +16 -0
- package/dist/inline.d.ts +22 -4
- package/dist/inline.js +60 -13
- package/dist/integrity.d.ts +29 -0
- package/dist/integrity.js +44 -0
- package/dist/linters.d.ts +5 -3
- package/dist/linters.js +144 -4
- package/dist/mock-model.d.ts +31 -0
- package/dist/mock-model.js +189 -0
- package/dist/orphans.d.ts +52 -0
- package/dist/orphans.js +124 -0
- package/dist/proofs.js +22 -16
- package/dist/refs.d.ts +44 -0
- package/dist/refs.js +144 -0
- 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/skill-driver.d.ts +77 -0
- package/dist/skill-driver.js +76 -0
- package/dist/skill-runtime.d.ts +101 -0
- package/dist/skill-runtime.js +289 -0
- package/dist/skill-test.d.ts +47 -0
- package/dist/skill-test.js +77 -0
- package/dist/spec.d.ts +119 -13
- package/dist/spec.js +51 -4
- package/dist/symbols.d.ts +30 -0
- package/dist/symbols.js +142 -0
- 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 +26 -5
- 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,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,
|
|
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,
|
|
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 (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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 "${
|
|
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
|
-
|
|
145
|
-
switch (r._ref) {
|
|
185
|
+
switch (fragment._ref) {
|
|
146
186
|
case "file":
|
|
147
|
-
return `\`${
|
|
187
|
+
return `\`${fragment.path}\``;
|
|
148
188
|
case "cmd":
|
|
149
|
-
return `\`${
|
|
189
|
+
return `\`${fragment.command}\``;
|
|
150
190
|
case "skill":
|
|
151
|
-
return `[${(0, node_path_1.basename)((0, node_path_1.dirname)(
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
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
|