vigiles 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +108 -131
  2. package/dist/cli.js +303 -161
  3. package/dist/compile.d.ts +1 -0
  4. package/dist/compile.js +22 -19
  5. package/dist/coverage.d.ts +56 -0
  6. package/dist/coverage.js +178 -0
  7. package/dist/doc-refs.d.ts +60 -0
  8. package/dist/doc-refs.js +246 -0
  9. package/dist/evolve.js +60 -125
  10. package/dist/frontmatter.d.ts +57 -0
  11. package/dist/frontmatter.js +190 -0
  12. package/dist/generate-schema.d.ts +51 -0
  13. package/dist/generate-schema.js +115 -0
  14. package/dist/generate-types.js +39 -1
  15. package/dist/hash.d.ts +8 -0
  16. package/dist/hash.js +16 -0
  17. package/dist/integrity.d.ts +29 -0
  18. package/dist/integrity.js +44 -0
  19. package/dist/linters.d.ts +5 -3
  20. package/dist/linters.js +116 -4
  21. package/dist/orphans.d.ts +52 -0
  22. package/dist/orphans.js +124 -0
  23. package/dist/proofs.js +22 -16
  24. package/dist/session.d.ts +97 -0
  25. package/dist/session.js +306 -0
  26. package/dist/sidecar.d.ts +35 -0
  27. package/dist/sidecar.js +102 -0
  28. package/dist/spec.d.ts +29 -9
  29. package/dist/spec.js +22 -4
  30. package/dist/test-utils.d.ts +8 -0
  31. package/dist/test-utils.js +41 -0
  32. package/dist/types.d.ts +34 -8
  33. package/dist/types.js +19 -0
  34. package/dist/validate.js +5 -3
  35. package/package.json +14 -2
  36. package/.claude/settings.json +0 -46
  37. package/.claude/settings.local.json +0 -8
  38. package/.github/workflows/ci.yml +0 -81
  39. package/.prettierignore +0 -1
  40. package/.vigiles/generated.d.ts +0 -205
  41. package/CLAUDE.md +0 -95
  42. package/CLAUDE.md.spec.ts +0 -142
  43. package/CONTRIBUTING.md +0 -121
  44. package/dist/action.d.ts.map +0 -1
  45. package/dist/action.js.map +0 -1
  46. package/dist/cli.d.ts.map +0 -1
  47. package/dist/cli.js.map +0 -1
  48. package/dist/cli.test.d.ts +0 -2
  49. package/dist/cli.test.d.ts.map +0 -1
  50. package/dist/cli.test.js +0 -650
  51. package/dist/cli.test.js.map +0 -1
  52. package/dist/compile.d.ts.map +0 -1
  53. package/dist/compile.js.map +0 -1
  54. package/dist/evolve.d.ts.map +0 -1
  55. package/dist/evolve.js.map +0 -1
  56. package/dist/freshness.d.ts +0 -67
  57. package/dist/freshness.d.ts.map +0 -1
  58. package/dist/freshness.js +0 -244
  59. package/dist/freshness.js.map +0 -1
  60. package/dist/freshness.test.d.ts +0 -2
  61. package/dist/freshness.test.d.ts.map +0 -1
  62. package/dist/freshness.test.js +0 -356
  63. package/dist/freshness.test.js.map +0 -1
  64. package/dist/generate-types.d.ts.map +0 -1
  65. package/dist/generate-types.js.map +0 -1
  66. package/dist/inline.d.ts.map +0 -1
  67. package/dist/inline.js.map +0 -1
  68. package/dist/inline.test.d.ts +0 -5
  69. package/dist/inline.test.d.ts.map +0 -1
  70. package/dist/inline.test.js +0 -152
  71. package/dist/inline.test.js.map +0 -1
  72. package/dist/linters.d.ts.map +0 -1
  73. package/dist/linters.js.map +0 -1
  74. package/dist/proofs.d.ts.map +0 -1
  75. package/dist/proofs.js.map +0 -1
  76. package/dist/proofs.test.d.ts +0 -9
  77. package/dist/proofs.test.d.ts.map +0 -1
  78. package/dist/proofs.test.js +0 -952
  79. package/dist/proofs.test.js.map +0 -1
  80. package/dist/spec.d.ts.map +0 -1
  81. package/dist/spec.js.map +0 -1
  82. package/dist/spec.test.d.ts +0 -2
  83. package/dist/spec.test.d.ts.map +0 -1
  84. package/dist/spec.test.js +0 -1222
  85. package/dist/spec.test.js.map +0 -1
  86. package/dist/types.d.ts.map +0 -1
  87. package/dist/types.js.map +0 -1
  88. package/dist/validate.d.ts.map +0 -1
  89. package/dist/validate.js.map +0 -1
  90. package/dist/validate.test.d.ts +0 -2
  91. package/dist/validate.test.d.ts.map +0 -1
  92. package/dist/validate.test.js +0 -531
  93. package/dist/validate.test.js.map +0 -1
  94. package/docs/agent-setup.md +0 -85
  95. package/docs/agent-workflows.md +0 -103
  96. package/docs/comparison.md +0 -71
  97. package/docs/freshness.md +0 -124
  98. package/docs/inline-mode.md +0 -119
  99. package/docs/linter-support.md +0 -166
  100. package/docs/spec-format.md +0 -194
  101. package/eslint.config.mjs +0 -79
  102. package/examples/CLAUDE.md +0 -54
  103. package/examples/CLAUDE.md.spec.ts +0 -65
  104. package/examples/SKILL.md +0 -50
  105. package/examples/SKILL.md.spec.ts +0 -57
  106. package/fixtures/example-project/CLAUDE.md +0 -11
  107. package/fixtures/example-project/package.json +0 -9
  108. package/fixtures/example-project/src/index.ts +0 -3
  109. package/fixtures/example-project/src/utils.test.ts +0 -2
  110. package/fixtures/example-project/src/utils.ts +0 -3
  111. package/logo.png +0 -0
  112. package/research/adoption-strategy.md +0 -111
  113. package/research/agent-integration.md +0 -145
  114. package/research/ai-code-quality.md +0 -197
  115. package/research/code-search-for-agents.md +0 -313
  116. package/research/competitive-landscape.md +0 -163
  117. package/research/doc-freshness.md +0 -516
  118. package/research/executable-specs.md +0 -368
  119. package/research/feature-ideas.md +0 -464
  120. package/research/formal-proofs-for-agents.md +0 -338
  121. package/research/fp-for-agent-harness.md +0 -150
  122. package/research/fp-for-deterministic-ai.md +0 -131
  123. package/research/self-evolving-specs.md +0 -298
  124. package/schemas/claude-md-strict.yml +0 -18
  125. package/schemas/claude-md.yml +0 -6
  126. package/schemas/skill-strict.yml +0 -12
  127. package/schemas/skill.yml +0 -5
  128. package/skills/audit-feedback-loop/SKILL.md +0 -76
  129. package/skills/edit-spec/SKILL.md +0 -131
  130. package/skills/enforce-rules-format/SKILL.md +0 -71
  131. package/skills/generate-logo/SKILL.md +0 -102
  132. package/skills/generate-rule/SKILL.md +0 -90
  133. package/skills/linter-docs/clippy.md +0 -241
  134. package/skills/linter-docs/eslint.md +0 -384
  135. package/skills/linter-docs/pylint.md +0 -288
  136. package/skills/linter-docs/rubocop.md +0 -277
  137. package/skills/linter-docs/ruff.md +0 -187
  138. package/skills/linter-docs/stylelint.md +0 -247
  139. package/skills/migrate-to-spec/SKILL.md +0 -124
  140. package/skills/pr-to-lint-rule/SKILL.md +0 -97
  141. package/skills/strengthen/SKILL.md +0 -168
  142. package/src/action.ts +0 -214
  143. package/src/cli.test.ts +0 -914
  144. package/src/cli.ts +0 -1631
  145. package/src/compile.ts +0 -691
  146. package/src/evolve.ts +0 -781
  147. package/src/freshness.test.ts +0 -449
  148. package/src/freshness.ts +0 -299
  149. package/src/generate-types.ts +0 -448
  150. package/src/inline.test.ts +0 -206
  151. package/src/inline.ts +0 -164
  152. package/src/linters.ts +0 -739
  153. package/src/proofs.test.ts +0 -1314
  154. package/src/proofs.ts +0 -849
  155. package/src/spec.test.ts +0 -1471
  156. package/src/spec.ts +0 -427
  157. package/src/types.ts +0 -117
  158. package/src/validate.test.ts +0 -701
  159. package/src/validate.ts +0 -381
  160. package/tsconfig.json +0 -23
@@ -73,7 +73,7 @@ function discoverEslintRules(basePath) {
73
73
  const rules = JSON.parse(output.trim() || "[]");
74
74
  if (rules.length === 0)
75
75
  return null;
76
- return { linter: "eslint", rules, via: "flat config" };
76
+ return { linter: "eslint", rules, via: "flat config (v9+/v10)" };
77
77
  }
78
78
  catch {
79
79
  return null;
@@ -199,6 +199,41 @@ function discoverRubocopRules(basePath) {
199
199
  return null;
200
200
  }
201
201
  }
202
+ function discoverCedarPolicies(basePath) {
203
+ const ID_RE = /@id\("([^"]+)"\)/g;
204
+ const STATEMENT_RE = /\b(?:permit|forbid)\s*\(/;
205
+ const policies = new Set();
206
+ for (const dir of [".cedar", "cedar"]) {
207
+ const fullDir = (0, node_path_1.resolve)(basePath, dir);
208
+ if (!(0, node_fs_1.existsSync)(fullDir))
209
+ continue;
210
+ const files = (0, glob_1.globSync)("**/*.cedar", { cwd: fullDir, nodir: true });
211
+ for (const file of files) {
212
+ let content;
213
+ try {
214
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(fullDir, file), "utf-8");
215
+ }
216
+ catch {
217
+ continue;
218
+ }
219
+ const annotated = [...content.matchAll(ID_RE)].map((m) => m[1]);
220
+ if (annotated.length > 0) {
221
+ for (const id of annotated)
222
+ policies.add(id);
223
+ }
224
+ else if (STATEMENT_RE.test(content)) {
225
+ policies.add(file.replace(/\.cedar$/, "").replace(/\\/g, "/"));
226
+ }
227
+ }
228
+ }
229
+ if (policies.size === 0)
230
+ return null;
231
+ return {
232
+ linter: "cedar",
233
+ rules: [...policies].sort(),
234
+ via: ".cedar files",
235
+ };
236
+ }
202
237
  function discoverClippyRules(basePath) {
203
238
  try {
204
239
  const cargoPath = (0, node_path_1.resolve)(basePath, "Cargo.toml");
@@ -300,6 +335,9 @@ function generateTypes(options = {}) {
300
335
  const clippy = discoverClippyRules(basePath);
301
336
  if (clippy)
302
337
  linters.push(clippy);
338
+ const cedar = discoverCedarPolicies(basePath);
339
+ if (cedar)
340
+ linters.push(cedar);
303
341
  const scripts = discoverNpmScripts(basePath);
304
342
  const files = discoverProjectFiles(basePath, fileGlobs);
305
343
  // Generate .d.ts
package/dist/hash.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ declare const __brand: unique symbol;
2
+ export type SHA256Hash = string & {
3
+ readonly [__brand]: "SHA256Hash";
4
+ };
5
+ export declare function sha256short(data: string | Buffer): SHA256Hash;
6
+ export declare function assertNever(x: never): never;
7
+ export {};
8
+ //# sourceMappingURL=hash.d.ts.map
package/dist/hash.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sha256short = sha256short;
4
+ exports.assertNever = assertNever;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const HASH_LENGTH = 16;
7
+ function sha256short(data) {
8
+ return (0, node_crypto_1.createHash)("sha256")
9
+ .update(data)
10
+ .digest("hex")
11
+ .slice(0, HASH_LENGTH);
12
+ }
13
+ function assertNever(x) {
14
+ throw new Error(`Unexpected value: ${String(x)}`);
15
+ }
16
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Integrity check: verify a compiled markdown file hasn't been hand-edited.
3
+ *
4
+ * Every compiled file has a SHA-256 hash in its first line:
5
+ * <!-- vigiles:sha256:<hash> compiled from CLAUDE.md.spec.ts -->
6
+ *
7
+ * If the body content's hash no longer matches what's recorded, someone
8
+ * edited the compiled output directly. The check is one-pass and ~free.
9
+ *
10
+ * This is the entire freshness story now: no input fingerprinting, no
11
+ * recompile diffing. Those responsibilities belong elsewhere:
12
+ *
13
+ * - Hand-edit detection → this module
14
+ * - "Did the spec change?" → guard() rules emitting compile hooks
15
+ * - "Are referenced linter rules / files / scripts still valid?"
16
+ * → enforce() / file() / cmd() catch this at compile time
17
+ * - "Are committed compiled files actually fresh?"
18
+ * → CI runs `vigiles compile` then `git diff --exit-code`
19
+ */
20
+ export interface IntegrityResult {
21
+ intact: boolean;
22
+ reason?: string;
23
+ }
24
+ /**
25
+ * Check whether the compiled markdown's SHA-256 hash matches its body.
26
+ * Files without a hash header are treated as hand-written (intact).
27
+ */
28
+ export declare function checkIntegrity(content: string): IntegrityResult;
29
+ //# sourceMappingURL=integrity.d.ts.map
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * Integrity check: verify a compiled markdown file hasn't been hand-edited.
4
+ *
5
+ * Every compiled file has a SHA-256 hash in its first line:
6
+ * <!-- vigiles:sha256:<hash> compiled from CLAUDE.md.spec.ts -->
7
+ *
8
+ * If the body content's hash no longer matches what's recorded, someone
9
+ * edited the compiled output directly. The check is one-pass and ~free.
10
+ *
11
+ * This is the entire freshness story now: no input fingerprinting, no
12
+ * recompile diffing. Those responsibilities belong elsewhere:
13
+ *
14
+ * - Hand-edit detection → this module
15
+ * - "Did the spec change?" → guard() rules emitting compile hooks
16
+ * - "Are referenced linter rules / files / scripts still valid?"
17
+ * → enforce() / file() / cmd() catch this at compile time
18
+ * - "Are committed compiled files actually fresh?"
19
+ * → CI runs `vigiles compile` then `git diff --exit-code`
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.checkIntegrity = checkIntegrity;
23
+ const hash_js_1 = require("./hash.js");
24
+ const HASH_LINE_RE = /^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
25
+ /**
26
+ * Check whether the compiled markdown's SHA-256 hash matches its body.
27
+ * Files without a hash header are treated as hand-written (intact).
28
+ */
29
+ function checkIntegrity(content) {
30
+ const match = content.match(HASH_LINE_RE);
31
+ if (!match) {
32
+ return { intact: true, reason: "No hash header (hand-written file)" };
33
+ }
34
+ const expectedHash = match[1];
35
+ const body = content.replace(HASH_LINE_RE, "");
36
+ if ((0, hash_js_1.sha256short)(body) !== expectedHash) {
37
+ return {
38
+ intact: false,
39
+ reason: "Compiled file was modified directly — edit the .spec.ts source and recompile",
40
+ };
41
+ }
42
+ return { intact: true };
43
+ }
44
+ //# sourceMappingURL=integrity.js.map
package/dist/linters.d.ts CHANGED
@@ -3,10 +3,11 @@
3
3
  *
4
4
  * Verifies that linter rule references (e.g., "eslint/no-console") point to
5
5
  * real rules that exist and are enabled in project config. Supports:
6
- * ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI).
6
+ * ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI),
7
+ * Cedar (filesystem policies for AWS Bedrock AgentCore / Vectimus).
7
8
  *
8
- * This is the core moat — no other tool resolves rules against 6 linter APIs
9
- * and checks config-enabled status.
9
+ * This is the core moat — no other tool resolves rules across 7 catalog APIs
10
+ * (6 linters + Cedar policy language) and checks config-enabled status.
10
11
  */
11
12
  export type ConfigEnabledStatus = "enabled" | "disabled" | "unknown";
12
13
  export interface LinterCheckResult {
@@ -23,6 +24,7 @@ export interface DetectedLinter {
23
24
  }
24
25
  /** @internal */ export declare function extractLinterName(enforcedBy: string): string;
25
26
  /** @internal */ export declare function extractRuleName(enforcedBy: string): string | null;
27
+ /** @internal */ export declare function clearCedarCache(): void;
26
28
  /**
27
29
  * Check a single linter rule reference (e.g., "eslint/no-console").
28
30
  *
package/dist/linters.js CHANGED
@@ -4,14 +4,16 @@
4
4
  *
5
5
  * Verifies that linter rule references (e.g., "eslint/no-console") point to
6
6
  * real rules that exist and are enabled in project config. Supports:
7
- * ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI).
7
+ * ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI),
8
+ * Cedar (filesystem policies for AWS Bedrock AgentCore / Vectimus).
8
9
  *
9
- * This is the core moat — no other tool resolves rules against 6 linter APIs
10
- * and checks config-enabled status.
10
+ * This is the core moat — no other tool resolves rules across 7 catalog APIs
11
+ * (6 linters + Cedar policy language) and checks config-enabled status.
11
12
  */
12
13
  Object.defineProperty(exports, "__esModule", { value: true });
13
14
  exports.extractLinterName = extractLinterName;
14
15
  exports.extractRuleName = extractRuleName;
16
+ exports.clearCedarCache = clearCedarCache;
15
17
  exports.checkLinterRule = checkLinterRule;
16
18
  const node_fs_1 = require("node:fs");
17
19
  const node_path_1 = require("node:path");
@@ -524,6 +526,114 @@ function getCliRuleSet(linterName, basePath) {
524
526
  return makeResult(ctx, false, "unknown", `Rule "${ctx.ruleName}" not found in ${ctx.linterName}.${hint}`);
525
527
  }
526
528
  }
529
+ // ---------------------------------------------------------------------------
530
+ // Vigiles-internal assertion catalog
531
+ //
532
+ // The `vigiles/<id>` namespace lets specs declare mechanical checks that
533
+ // vigiles itself runs (orphan docs, integrity, etc.) without delegating to
534
+ // an external linter. Existence is verified at compile time against this
535
+ // fixed catalog; the actual check runs at audit time.
536
+ // ---------------------------------------------------------------------------
537
+ const VIGILES_INTERNAL_RULES = new Set(["orphan-docs"]);
538
+ /** @internal */ function tryVigilesInternal(ctx) {
539
+ if (ctx.linterName !== "vigiles")
540
+ return null;
541
+ if (!VIGILES_INTERNAL_RULES.has(ctx.ruleName)) {
542
+ const suggestions = closestRuleNames(ctx.ruleName, VIGILES_INTERNAL_RULES);
543
+ const hint = suggestions.length > 0
544
+ ? ` Did you mean: ${suggestions.map((s) => `"vigiles/${s}"`).join(", ")}?`
545
+ : "";
546
+ return makeResult(ctx, false, "unknown", `Vigiles-internal rule "${ctx.ruleName}" not in known catalog (${[...VIGILES_INTERNAL_RULES].join(", ")}).${hint}`);
547
+ }
548
+ return makeResult(ctx, true, "enabled");
549
+ }
550
+ // ---------------------------------------------------------------------------
551
+ // Cedar policy resolution (filesystem-based — no Node API, no CLI required)
552
+ //
553
+ // Cedar policies live in .cedar files. A policy is identified by its
554
+ // `@id("name")` annotation when present; otherwise by filename. Presence
555
+ // of a policy in the project counts as "enabled" — Cedar has no separate
556
+ // config layer the way ESLint does, the policy bundle IS the config.
557
+ //
558
+ // Default search dirs: .cedar/ and cedar/ (project root). Override via
559
+ // `options.linters.cedar.rulesDir`.
560
+ // ---------------------------------------------------------------------------
561
+ const CEDAR_DEFAULT_DIRS = [".cedar", "cedar"];
562
+ const CEDAR_ID_RE = /@id\("([^"]+)"\)/g;
563
+ const CEDAR_STATEMENT_RE = /\b(?:permit|forbid)\s*\(/;
564
+ const CEDAR_POLICY_CACHE = new Map();
565
+ function cedarCacheKey(basePath, customDirs) {
566
+ const dirs = customDirs
567
+ ? Array.isArray(customDirs)
568
+ ? customDirs.join("|")
569
+ : customDirs
570
+ : "";
571
+ return `${basePath}::${dirs}`;
572
+ }
573
+ function loadCedarPolicies(basePath, customDirs) {
574
+ const policies = new Set();
575
+ const dirs = customDirs
576
+ ? Array.isArray(customDirs)
577
+ ? customDirs
578
+ : [customDirs]
579
+ : CEDAR_DEFAULT_DIRS;
580
+ for (const dir of dirs) {
581
+ const fullDir = (0, node_path_1.resolve)(basePath, dir);
582
+ if (!(0, node_fs_1.existsSync)(fullDir))
583
+ continue;
584
+ const files = (0, glob_1.globSync)("**/*.cedar", { cwd: fullDir, nodir: true });
585
+ for (const file of files) {
586
+ let content;
587
+ try {
588
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(fullDir, file), "utf-8");
589
+ }
590
+ catch {
591
+ continue;
592
+ }
593
+ const annotated = [...content.matchAll(CEDAR_ID_RE)].map((m) => m[1]);
594
+ if (annotated.length > 0) {
595
+ for (const id of annotated)
596
+ policies.add(id);
597
+ }
598
+ else if (CEDAR_STATEMENT_RE.test(content)) {
599
+ const name = file.replace(/\.cedar$/, "").replace(/\\/g, "/");
600
+ policies.add(name);
601
+ }
602
+ }
603
+ }
604
+ return policies;
605
+ }
606
+ function getCedarPolicies(basePath, customDirs) {
607
+ const key = cedarCacheKey(basePath, customDirs);
608
+ const cached = CEDAR_POLICY_CACHE.get(key);
609
+ if (cached)
610
+ return cached;
611
+ const policies = loadCedarPolicies(basePath, customDirs);
612
+ CEDAR_POLICY_CACHE.set(key, policies);
613
+ return policies;
614
+ }
615
+ /** @internal */ function clearCedarCache() {
616
+ CEDAR_POLICY_CACHE.clear();
617
+ }
618
+ /** @internal */ function tryCedarPolicy(ctx) {
619
+ if (ctx.linterName !== "cedar")
620
+ return null;
621
+ const customDirs = ctx.linters?.cedar?.rulesDir;
622
+ const policies = getCedarPolicies(ctx.basePath, customDirs);
623
+ if (policies.size === 0) {
624
+ return makeResult(ctx, false, "unknown", customDirs
625
+ ? `No Cedar policies found in configured rulesDir.`
626
+ : `No Cedar policies found. Add .cedar files under .cedar/ or cedar/, or set linters.cedar.rulesDir.`);
627
+ }
628
+ if (!policies.has(ctx.ruleName)) {
629
+ const suggestions = closestRuleNames(ctx.ruleName, policies);
630
+ const hint = suggestions.length > 0
631
+ ? ` Did you mean: ${suggestions.map((s) => `"cedar/${s}"`).join(", ")}?`
632
+ : "";
633
+ return makeResult(ctx, false, "unknown", `Cedar policy "${ctx.ruleName}" not found.${hint}`);
634
+ }
635
+ return makeResult(ctx, true, "enabled");
636
+ }
527
637
  /** @internal */ function tryCustomRulesDir(ctx) {
528
638
  const linterCfg = ctx.linters?.[ctx.linterName];
529
639
  if (!linterCfg?.rulesDir)
@@ -566,9 +676,11 @@ function checkLinterRule(enforcedBy, basePath, options) {
566
676
  catalogOnly: options?.catalogOnly,
567
677
  linters: options?.linters,
568
678
  };
569
- return (tryNodeResolver(ctx) ??
679
+ return (tryVigilesInternal(ctx) ??
680
+ tryNodeResolver(ctx) ??
570
681
  tryScopedPlugin(ctx) ??
571
682
  tryCliCheck(ctx) ??
683
+ tryCedarPolicy(ctx) ??
572
684
  tryCustomRulesDir(ctx) ??
573
685
  makeResult(ctx, false, "unknown", `Unknown linter: "${linterName}"`));
574
686
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Orphan-docs detection: the inverse of stale-reference validation.
3
+ *
4
+ * Stale-ref detection catches specs that point at files which no longer
5
+ * exist. Orphan detection catches files which exist but no spec or README
6
+ * points at — docs that quietly rot in `docs/` and `research/` because
7
+ * nothing tells the agent they're still load-bearing.
8
+ *
9
+ * The detector operates purely on the filesystem: it enumerates markdown
10
+ * files under configured doc roots and scans every `.md` in the repo for
11
+ * references (markdown links and backtick paths). Works against source
12
+ * README plus compiled CLAUDE.md — no spec loading required.
13
+ */
14
+ export interface OrphanReport {
15
+ /** Include globs that were scanned. */
16
+ readonly include: readonly string[];
17
+ /** Total docs discovered under those globs. */
18
+ readonly totalDocs: number;
19
+ /** Docs referenced from at least one other `.md` file. */
20
+ readonly referencedDocs: readonly string[];
21
+ /** Docs that exist but no other `.md` references them. */
22
+ readonly orphans: readonly string[];
23
+ }
24
+ export interface FindOrphansOptions {
25
+ /** Repository root. Defaults to `process.cwd()`. */
26
+ readonly basePath?: string;
27
+ /**
28
+ * Glob patterns of `.md` files to scan. Defaults to
29
+ * `["docs/**\/*.md", "research/**\/*.md"]` — vigiles-repo convention.
30
+ * Set to `[]` to disable scanning entirely. Set to your project's
31
+ * doc directory globs (e.g. `["wiki/**\/*.md"]`) to override.
32
+ */
33
+ readonly include?: readonly string[];
34
+ /** Glob patterns to exclude within the include scope. */
35
+ readonly exclude?: readonly string[];
36
+ }
37
+ /**
38
+ * Find docs under `include` globs that no other markdown file references.
39
+ *
40
+ * A doc is considered referenced when some OTHER `.md` file in the repo
41
+ * links to it via `[text](path.md)` or mentions it in a backtick span like
42
+ * `` `docs/foo.md` ``. Self-references don't count — an orphan that only
43
+ * links to itself is still an orphan.
44
+ *
45
+ * `include` and `exclude` are tsconfig-style glob arrays. Default include
46
+ * is the vigiles-repo convention `["docs/**\/*.md", "research/**\/*.md"]`;
47
+ * override per-project via `.vigilesrc.json` → `orphans.include`.
48
+ */
49
+ export declare function findOrphanDocs(options?: FindOrphansOptions): OrphanReport;
50
+ /** Format an orphan report as human-readable text. */
51
+ export declare function formatOrphanReport(report: OrphanReport): string;
52
+ //# sourceMappingURL=orphans.d.ts.map
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /**
3
+ * Orphan-docs detection: the inverse of stale-reference validation.
4
+ *
5
+ * Stale-ref detection catches specs that point at files which no longer
6
+ * exist. Orphan detection catches files which exist but no spec or README
7
+ * points at — docs that quietly rot in `docs/` and `research/` because
8
+ * nothing tells the agent they're still load-bearing.
9
+ *
10
+ * The detector operates purely on the filesystem: it enumerates markdown
11
+ * files under configured doc roots and scans every `.md` in the repo for
12
+ * references (markdown links and backtick paths). Works against source
13
+ * README plus compiled CLAUDE.md — no spec loading required.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.findOrphanDocs = findOrphanDocs;
17
+ exports.formatOrphanReport = formatOrphanReport;
18
+ const node_fs_1 = require("node:fs");
19
+ const node_path_1 = require("node:path");
20
+ const glob_1 = require("glob");
21
+ // ---------------------------------------------------------------------------
22
+ // Internals
23
+ // ---------------------------------------------------------------------------
24
+ const DEFAULT_INCLUDE = ["docs/**/*.md", "research/**/*.md"];
25
+ const DEFAULT_IGNORE = [
26
+ "node_modules/**",
27
+ "dist/**",
28
+ ".vigiles/**",
29
+ ".git/**",
30
+ ];
31
+ // Match markdown links ](path.md) or ](path.md#anchor)
32
+ const LINK_RE = /\]\(([^)\s]+\.md)(?:#[^)]*)?\)/g;
33
+ // Match backtick code spans wrapping a path ending in .md
34
+ const BACKTICK_RE = /`([^`\s]+\.md)`/g;
35
+ function normalizePath(p) {
36
+ return p.replace(/^\.\//, "").replace(/\\/g, "/");
37
+ }
38
+ function extractRefs(content) {
39
+ const refs = [];
40
+ for (const m of content.matchAll(LINK_RE))
41
+ refs.push(normalizePath(m[1]));
42
+ for (const m of content.matchAll(BACKTICK_RE))
43
+ refs.push(normalizePath(m[1]));
44
+ return refs;
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Public API
48
+ // ---------------------------------------------------------------------------
49
+ /**
50
+ * Find docs under `include` globs that no other markdown file references.
51
+ *
52
+ * A doc is considered referenced when some OTHER `.md` file in the repo
53
+ * links to it via `[text](path.md)` or mentions it in a backtick span like
54
+ * `` `docs/foo.md` ``. Self-references don't count — an orphan that only
55
+ * links to itself is still an orphan.
56
+ *
57
+ * `include` and `exclude` are tsconfig-style glob arrays. Default include
58
+ * is the vigiles-repo convention `["docs/**\/*.md", "research/**\/*.md"]`;
59
+ * override per-project via `.vigilesrc.json` → `orphans.include`.
60
+ */
61
+ function findOrphanDocs(options = {}) {
62
+ const basePath = options.basePath ?? process.cwd();
63
+ const include = options.include ?? DEFAULT_INCLUDE;
64
+ const userExclude = options.exclude ?? [];
65
+ const ignore = [...DEFAULT_IGNORE, ...userExclude];
66
+ const allDocs = new Set();
67
+ for (const pattern of include) {
68
+ const found = (0, glob_1.globSync)(pattern, { cwd: basePath, ignore });
69
+ for (const p of found)
70
+ allDocs.add(normalizePath(p));
71
+ }
72
+ const allMarkdown = (0, glob_1.globSync)("**/*.md", {
73
+ cwd: basePath,
74
+ ignore: [...DEFAULT_IGNORE],
75
+ });
76
+ const referencedBy = new Map();
77
+ for (const mdPath of allMarkdown) {
78
+ const source = normalizePath(mdPath);
79
+ let content;
80
+ try {
81
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(basePath, mdPath), "utf-8");
82
+ }
83
+ catch {
84
+ continue;
85
+ }
86
+ for (const target of extractRefs(content)) {
87
+ if (target === source)
88
+ continue;
89
+ let sources = referencedBy.get(target);
90
+ if (!sources) {
91
+ sources = new Set();
92
+ referencedBy.set(target, sources);
93
+ }
94
+ sources.add(source);
95
+ }
96
+ }
97
+ const orphans = [];
98
+ const referencedDocs = [];
99
+ for (const doc of [...allDocs].sort()) {
100
+ if (referencedBy.has(doc))
101
+ referencedDocs.push(doc);
102
+ else
103
+ orphans.push(doc);
104
+ }
105
+ return {
106
+ include: [...include],
107
+ totalDocs: allDocs.size,
108
+ referencedDocs,
109
+ orphans,
110
+ };
111
+ }
112
+ /** Format an orphan report as human-readable text. */
113
+ function formatOrphanReport(report) {
114
+ if (report.orphans.length === 0) {
115
+ return `✓ no orphan docs (${String(report.totalDocs)} scanned across ${report.include.join(", ") || "no include patterns"})`;
116
+ }
117
+ const lines = [
118
+ `✗ ${String(report.orphans.length)} orphan doc(s) — referenced by no other .md:`,
119
+ ];
120
+ for (const o of report.orphans)
121
+ lines.push(` ${o}`);
122
+ return lines.join("\n");
123
+ }
124
+ //# sourceMappingURL=orphans.js.map
package/dist/proofs.js CHANGED
@@ -23,20 +23,22 @@ exports.ruleToBloomFilter = ruleToBloomFilter;
23
23
  exports.fixedPoint = fixedPoint;
24
24
  exports.propertyTest = propertyTest;
25
25
  exports.fitness = fitness;
26
- const node_crypto_1 = require("node:crypto");
27
26
  const node_zlib_1 = require("node:zlib");
27
+ const hash_js_1 = require("./hash.js");
28
28
  // ---------------------------------------------------------------------------
29
29
  // 1. Monotonicity Lattice — partial order on rule strength
30
30
  // ---------------------------------------------------------------------------
31
31
  /**
32
32
  * Ordinal strength of each rule kind.
33
33
  *
34
- * guidance (0) < enforce (1)
34
+ * guidance (0) < guard (1) = enforce (1)
35
35
  *
36
- * The lattice ensures specs only get stricter over time.
36
+ * Guard and enforce are both mechanically enforced (different mechanisms,
37
+ * same strength). The lattice ensures specs only get stricter over time.
37
38
  */
38
39
  const STRENGTH = {
39
40
  guidance: 0,
41
+ guard: 1,
40
42
  enforce: 1,
41
43
  };
42
44
  /**
@@ -194,11 +196,14 @@ function ruleToText(rule) {
194
196
  return `${rule.linterRule} ${rule.why}`;
195
197
  case "guidance":
196
198
  return rule.text;
197
- default: {
198
- const unknown = rule._kind;
199
- throw new Error(`Unknown rule kind "${String(unknown)}" — expected "enforce" or "guidance". ` +
200
- `Runtime data is out of sync with the Rule type (legacy spec, JS caller, or cast bypass).`);
199
+ case "guard": {
200
+ const patterns = Array.isArray(rule.watch)
201
+ ? rule.watch.join(" ")
202
+ : rule.watch;
203
+ return `${patterns} ${rule.run} ${rule.description}`;
201
204
  }
205
+ default:
206
+ return (0, hash_js_1.assertNever)(rule);
202
207
  }
203
208
  }
204
209
  // ---------------------------------------------------------------------------
@@ -343,10 +348,10 @@ function fixedPoint(compileFn, initialContent, maxIterations = 10) {
343
348
  const hashes = [];
344
349
  let current = initialContent;
345
350
  for (let i = 0; i < maxIterations; i++) {
346
- const hash = sha256short(current);
351
+ const hash = (0, hash_js_1.sha256short)(current);
347
352
  hashes.push(hash);
348
353
  const next = compileFn(current);
349
- const nextHash = sha256short(next);
354
+ const nextHash = (0, hash_js_1.sha256short)(next);
350
355
  // Fixed point: output === input
351
356
  if (nextHash === hash) {
352
357
  return { converged: true, iterations: i + 1, hashes, cycleLength: 0 };
@@ -370,10 +375,7 @@ function fixedPoint(compileFn, initialContent, maxIterations = 10) {
370
375
  cycleLength: 0,
371
376
  };
372
377
  }
373
- /** Compute a short SHA-256 hash (16 hex chars). */
374
- function sha256short(data) {
375
- return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex").slice(0, 16);
376
- }
378
+ // sha256short imported from ./hash.js
377
379
  /** Compute the hash of a HistoryNode (excluding the hash field itself). */
378
380
  function computeNodeHash(node) {
379
381
  const payload = JSON.stringify({
@@ -383,7 +385,7 @@ function computeNodeHash(node) {
383
385
  proofs: node.proofs,
384
386
  timestamp: node.timestamp,
385
387
  });
386
- return sha256short(payload);
388
+ return (0, hash_js_1.sha256short)(payload);
387
389
  }
388
390
  class MerkleHistory {
389
391
  nodes = [];
@@ -604,8 +606,12 @@ function fitness(spec, options = {}) {
604
606
  if (total === 0) {
605
607
  return { score: 0, coverage: 0, redundancy: 0, budgetPressure: 0 };
606
608
  }
607
- // Coverage: fraction with teeth (enforce vs guidance)
608
- const enforced = rules.filter((r) => r._kind === "enforce").length;
609
+ // Coverage: fraction with teeth (mechanically enforced — STRENGTH ≥ 1).
610
+ // Counts both enforce() (linter-verified) and guard() (file-watch reactive).
611
+ // Without guard here, mutations that introduce/keep guard rules look like
612
+ // they reduce coverage and get rejected by runProofSuite even though
613
+ // enforcement strength didn't drop (see proofs.ts STRENGTH lattice).
614
+ const enforced = rules.filter((r) => STRENGTH[r._kind] >= 1).length;
609
615
  const coverage = enforced / total;
610
616
  // Redundancy: fraction of pairs that are near-duplicates
611
617
  const similarPairs = findSimilarRules(spec.rules, ncdThreshold);