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