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.
Files changed (184) hide show
  1. package/README.md +182 -134
  2. package/dist/action-gate.d.ts +28 -0
  3. package/dist/action-gate.js +73 -0
  4. package/dist/cli.js +705 -230
  5. package/dist/community-skills.d.ts +22 -0
  6. package/dist/community-skills.js +86 -0
  7. package/dist/compile-generator.d.ts +48 -0
  8. package/dist/compile-generator.js +322 -0
  9. package/dist/compile.d.ts +4 -0
  10. package/dist/compile.js +239 -45
  11. package/dist/coverage.d.ts +56 -0
  12. package/dist/coverage.js +178 -0
  13. package/dist/doc-refs.d.ts +60 -0
  14. package/dist/doc-refs.js +246 -0
  15. package/dist/eval.d.ts +62 -0
  16. package/dist/eval.js +174 -0
  17. package/dist/evolve.js +60 -125
  18. package/dist/frontmatter.d.ts +75 -0
  19. package/dist/frontmatter.js +263 -0
  20. package/dist/generate-schema.d.ts +51 -0
  21. package/dist/generate-schema.js +125 -0
  22. package/dist/generate-types.js +39 -1
  23. package/dist/harness-test.d.ts +38 -0
  24. package/dist/harness-test.js +129 -0
  25. package/dist/hash.d.ts +8 -0
  26. package/dist/hash.js +16 -0
  27. package/dist/inline.d.ts +22 -4
  28. package/dist/inline.js +60 -13
  29. package/dist/integrity.d.ts +29 -0
  30. package/dist/integrity.js +44 -0
  31. package/dist/linters.d.ts +5 -3
  32. package/dist/linters.js +144 -4
  33. package/dist/mock-model.d.ts +31 -0
  34. package/dist/mock-model.js +189 -0
  35. package/dist/orphans.d.ts +52 -0
  36. package/dist/orphans.js +124 -0
  37. package/dist/proofs.js +22 -16
  38. package/dist/refs.d.ts +44 -0
  39. package/dist/refs.js +144 -0
  40. package/dist/session.d.ts +97 -0
  41. package/dist/session.js +306 -0
  42. package/dist/sidecar.d.ts +35 -0
  43. package/dist/sidecar.js +102 -0
  44. package/dist/skill-driver.d.ts +77 -0
  45. package/dist/skill-driver.js +76 -0
  46. package/dist/skill-runtime.d.ts +101 -0
  47. package/dist/skill-runtime.js +289 -0
  48. package/dist/skill-test.d.ts +47 -0
  49. package/dist/skill-test.js +77 -0
  50. package/dist/spec.d.ts +119 -13
  51. package/dist/spec.js +51 -4
  52. package/dist/symbols.d.ts +30 -0
  53. package/dist/symbols.js +142 -0
  54. package/dist/test-utils.d.ts +8 -0
  55. package/dist/test-utils.js +41 -0
  56. package/dist/types.d.ts +34 -8
  57. package/dist/types.js +19 -0
  58. package/dist/validate.js +5 -3
  59. package/package.json +26 -5
  60. package/.claude/settings.json +0 -46
  61. package/.claude/settings.local.json +0 -8
  62. package/.github/workflows/ci.yml +0 -81
  63. package/.prettierignore +0 -1
  64. package/.vigiles/generated.d.ts +0 -205
  65. package/CLAUDE.md +0 -95
  66. package/CLAUDE.md.spec.ts +0 -142
  67. package/CONTRIBUTING.md +0 -121
  68. package/dist/action.d.ts.map +0 -1
  69. package/dist/action.js.map +0 -1
  70. package/dist/cli.d.ts.map +0 -1
  71. package/dist/cli.js.map +0 -1
  72. package/dist/cli.test.d.ts +0 -2
  73. package/dist/cli.test.d.ts.map +0 -1
  74. package/dist/cli.test.js +0 -650
  75. package/dist/cli.test.js.map +0 -1
  76. package/dist/compile.d.ts.map +0 -1
  77. package/dist/compile.js.map +0 -1
  78. package/dist/evolve.d.ts.map +0 -1
  79. package/dist/evolve.js.map +0 -1
  80. package/dist/freshness.d.ts +0 -67
  81. package/dist/freshness.d.ts.map +0 -1
  82. package/dist/freshness.js +0 -244
  83. package/dist/freshness.js.map +0 -1
  84. package/dist/freshness.test.d.ts +0 -2
  85. package/dist/freshness.test.d.ts.map +0 -1
  86. package/dist/freshness.test.js +0 -356
  87. package/dist/freshness.test.js.map +0 -1
  88. package/dist/generate-types.d.ts.map +0 -1
  89. package/dist/generate-types.js.map +0 -1
  90. package/dist/inline.d.ts.map +0 -1
  91. package/dist/inline.js.map +0 -1
  92. package/dist/inline.test.d.ts +0 -5
  93. package/dist/inline.test.d.ts.map +0 -1
  94. package/dist/inline.test.js +0 -152
  95. package/dist/inline.test.js.map +0 -1
  96. package/dist/linters.d.ts.map +0 -1
  97. package/dist/linters.js.map +0 -1
  98. package/dist/proofs.d.ts.map +0 -1
  99. package/dist/proofs.js.map +0 -1
  100. package/dist/proofs.test.d.ts +0 -9
  101. package/dist/proofs.test.d.ts.map +0 -1
  102. package/dist/proofs.test.js +0 -952
  103. package/dist/proofs.test.js.map +0 -1
  104. package/dist/spec.d.ts.map +0 -1
  105. package/dist/spec.js.map +0 -1
  106. package/dist/spec.test.d.ts +0 -2
  107. package/dist/spec.test.d.ts.map +0 -1
  108. package/dist/spec.test.js +0 -1222
  109. package/dist/spec.test.js.map +0 -1
  110. package/dist/types.d.ts.map +0 -1
  111. package/dist/types.js.map +0 -1
  112. package/dist/validate.d.ts.map +0 -1
  113. package/dist/validate.js.map +0 -1
  114. package/dist/validate.test.d.ts +0 -2
  115. package/dist/validate.test.d.ts.map +0 -1
  116. package/dist/validate.test.js +0 -531
  117. package/dist/validate.test.js.map +0 -1
  118. package/docs/agent-setup.md +0 -85
  119. package/docs/agent-workflows.md +0 -103
  120. package/docs/comparison.md +0 -71
  121. package/docs/freshness.md +0 -124
  122. package/docs/inline-mode.md +0 -119
  123. package/docs/linter-support.md +0 -166
  124. package/docs/spec-format.md +0 -194
  125. package/eslint.config.mjs +0 -79
  126. package/examples/CLAUDE.md +0 -54
  127. package/examples/CLAUDE.md.spec.ts +0 -65
  128. package/examples/SKILL.md +0 -50
  129. package/examples/SKILL.md.spec.ts +0 -57
  130. package/fixtures/example-project/CLAUDE.md +0 -11
  131. package/fixtures/example-project/package.json +0 -9
  132. package/fixtures/example-project/src/index.ts +0 -3
  133. package/fixtures/example-project/src/utils.test.ts +0 -2
  134. package/fixtures/example-project/src/utils.ts +0 -3
  135. package/logo.png +0 -0
  136. package/research/adoption-strategy.md +0 -111
  137. package/research/agent-integration.md +0 -145
  138. package/research/ai-code-quality.md +0 -197
  139. package/research/code-search-for-agents.md +0 -313
  140. package/research/competitive-landscape.md +0 -163
  141. package/research/doc-freshness.md +0 -516
  142. package/research/executable-specs.md +0 -368
  143. package/research/feature-ideas.md +0 -464
  144. package/research/formal-proofs-for-agents.md +0 -338
  145. package/research/fp-for-agent-harness.md +0 -150
  146. package/research/fp-for-deterministic-ai.md +0 -131
  147. package/research/self-evolving-specs.md +0 -298
  148. package/schemas/claude-md-strict.yml +0 -18
  149. package/schemas/claude-md.yml +0 -6
  150. package/schemas/skill-strict.yml +0 -12
  151. package/schemas/skill.yml +0 -5
  152. package/skills/audit-feedback-loop/SKILL.md +0 -76
  153. package/skills/edit-spec/SKILL.md +0 -131
  154. package/skills/enforce-rules-format/SKILL.md +0 -71
  155. package/skills/generate-logo/SKILL.md +0 -102
  156. package/skills/generate-rule/SKILL.md +0 -90
  157. package/skills/linter-docs/clippy.md +0 -241
  158. package/skills/linter-docs/eslint.md +0 -384
  159. package/skills/linter-docs/pylint.md +0 -288
  160. package/skills/linter-docs/rubocop.md +0 -277
  161. package/skills/linter-docs/ruff.md +0 -187
  162. package/skills/linter-docs/stylelint.md +0 -247
  163. package/skills/migrate-to-spec/SKILL.md +0 -124
  164. package/skills/pr-to-lint-rule/SKILL.md +0 -97
  165. package/skills/strengthen/SKILL.md +0 -168
  166. package/src/action.ts +0 -214
  167. package/src/cli.test.ts +0 -914
  168. package/src/cli.ts +0 -1631
  169. package/src/compile.ts +0 -691
  170. package/src/evolve.ts +0 -781
  171. package/src/freshness.test.ts +0 -449
  172. package/src/freshness.ts +0 -299
  173. package/src/generate-types.ts +0 -448
  174. package/src/inline.test.ts +0 -206
  175. package/src/inline.ts +0 -164
  176. package/src/linters.ts +0 -739
  177. package/src/proofs.test.ts +0 -1314
  178. package/src/proofs.ts +0 -849
  179. package/src/spec.test.ts +0 -1471
  180. package/src/spec.ts +0 -427
  181. package/src/types.ts +0 -117
  182. package/src/validate.test.ts +0 -701
  183. package/src/validate.ts +0 -381
  184. package/tsconfig.json +0 -23
package/dist/linters.js CHANGED
@@ -4,20 +4,50 @@
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");
18
20
  const node_child_process_1 = require("node:child_process");
19
21
  const node_module_1 = require("node:module");
20
22
  const glob_1 = require("glob");
23
+ /**
24
+ * Prepend version-manager shim directories (rbenv / asdf / rvm) to PATH so
25
+ * gem/pip-installed linters are found in non-login shells and CI, where the
26
+ * shims dir is often missing from PATH even though the tool is installed.
27
+ * Runs once; only adds directories that exist and aren't already present.
28
+ */
29
+ function augmentToolPath() {
30
+ const home = process.env.HOME ?? "";
31
+ // rbenv/asdf shims don't resolve without a selected version, so add the
32
+ // concrete per-version `bin` dirs (where the gem executables actually live).
33
+ const candidates = [
34
+ ...(0, glob_1.globSync)("/opt/rbenv/versions/*/bin", { nodir: false }),
35
+ ...(home
36
+ ? (0, glob_1.globSync)(`${home}/.rbenv/versions/*/bin`, { nodir: false })
37
+ : []),
38
+ ...(home
39
+ ? (0, glob_1.globSync)(`${home}/.asdf/installs/*/*/bin`, { nodir: false })
40
+ : []),
41
+ `${home}/.rvm/bin`,
42
+ `${home}/.local/bin`,
43
+ ];
44
+ const current = (process.env.PATH ?? "").split(":");
45
+ const additions = candidates.filter((d) => d && (0, node_fs_1.existsSync)(d) && !current.includes(d));
46
+ if (additions.length > 0) {
47
+ process.env.PATH = [...current, ...additions].join(":");
48
+ }
49
+ }
50
+ augmentToolPath();
21
51
  // ---------------------------------------------------------------------------
22
52
  // Parsing enforcement references
23
53
  // ---------------------------------------------------------------------------
@@ -524,6 +554,114 @@ function getCliRuleSet(linterName, basePath) {
524
554
  return makeResult(ctx, false, "unknown", `Rule "${ctx.ruleName}" not found in ${ctx.linterName}.${hint}`);
525
555
  }
526
556
  }
557
+ // ---------------------------------------------------------------------------
558
+ // Vigiles-internal assertion catalog
559
+ //
560
+ // The `vigiles/<id>` namespace lets specs declare mechanical checks that
561
+ // vigiles itself runs (orphan docs, integrity, etc.) without delegating to
562
+ // an external linter. Existence is verified at compile time against this
563
+ // fixed catalog; the actual check runs at audit time.
564
+ // ---------------------------------------------------------------------------
565
+ const VIGILES_INTERNAL_RULES = new Set(["orphan-docs"]);
566
+ /** @internal */ function tryVigilesInternal(ctx) {
567
+ if (ctx.linterName !== "vigiles")
568
+ return null;
569
+ if (!VIGILES_INTERNAL_RULES.has(ctx.ruleName)) {
570
+ const suggestions = closestRuleNames(ctx.ruleName, VIGILES_INTERNAL_RULES);
571
+ const hint = suggestions.length > 0
572
+ ? ` Did you mean: ${suggestions.map((s) => `"vigiles/${s}"`).join(", ")}?`
573
+ : "";
574
+ return makeResult(ctx, false, "unknown", `Vigiles-internal rule "${ctx.ruleName}" not in known catalog (${[...VIGILES_INTERNAL_RULES].join(", ")}).${hint}`);
575
+ }
576
+ return makeResult(ctx, true, "enabled");
577
+ }
578
+ // ---------------------------------------------------------------------------
579
+ // Cedar policy resolution (filesystem-based — no Node API, no CLI required)
580
+ //
581
+ // Cedar policies live in .cedar files. A policy is identified by its
582
+ // `@id("name")` annotation when present; otherwise by filename. Presence
583
+ // of a policy in the project counts as "enabled" — Cedar has no separate
584
+ // config layer the way ESLint does, the policy bundle IS the config.
585
+ //
586
+ // Default search dirs: .cedar/ and cedar/ (project root). Override via
587
+ // `options.linters.cedar.rulesDir`.
588
+ // ---------------------------------------------------------------------------
589
+ const CEDAR_DEFAULT_DIRS = [".cedar", "cedar"];
590
+ const CEDAR_ID_RE = /@id\("([^"]+)"\)/g;
591
+ const CEDAR_STATEMENT_RE = /\b(?:permit|forbid)\s*\(/;
592
+ const CEDAR_POLICY_CACHE = new Map();
593
+ function cedarCacheKey(basePath, customDirs) {
594
+ const dirs = customDirs
595
+ ? Array.isArray(customDirs)
596
+ ? customDirs.join("|")
597
+ : customDirs
598
+ : "";
599
+ return `${basePath}::${dirs}`;
600
+ }
601
+ function loadCedarPolicies(basePath, customDirs) {
602
+ const policies = new Set();
603
+ const dirs = customDirs
604
+ ? Array.isArray(customDirs)
605
+ ? customDirs
606
+ : [customDirs]
607
+ : CEDAR_DEFAULT_DIRS;
608
+ for (const dir of dirs) {
609
+ const fullDir = (0, node_path_1.resolve)(basePath, dir);
610
+ if (!(0, node_fs_1.existsSync)(fullDir))
611
+ continue;
612
+ const files = (0, glob_1.globSync)("**/*.cedar", { cwd: fullDir, nodir: true });
613
+ for (const file of files) {
614
+ let content;
615
+ try {
616
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(fullDir, file), "utf-8");
617
+ }
618
+ catch {
619
+ continue;
620
+ }
621
+ const annotated = [...content.matchAll(CEDAR_ID_RE)].map((m) => m[1]);
622
+ if (annotated.length > 0) {
623
+ for (const id of annotated)
624
+ policies.add(id);
625
+ }
626
+ else if (CEDAR_STATEMENT_RE.test(content)) {
627
+ const name = file.replace(/\.cedar$/, "").replace(/\\/g, "/");
628
+ policies.add(name);
629
+ }
630
+ }
631
+ }
632
+ return policies;
633
+ }
634
+ function getCedarPolicies(basePath, customDirs) {
635
+ const key = cedarCacheKey(basePath, customDirs);
636
+ const cached = CEDAR_POLICY_CACHE.get(key);
637
+ if (cached)
638
+ return cached;
639
+ const policies = loadCedarPolicies(basePath, customDirs);
640
+ CEDAR_POLICY_CACHE.set(key, policies);
641
+ return policies;
642
+ }
643
+ /** @internal */ function clearCedarCache() {
644
+ CEDAR_POLICY_CACHE.clear();
645
+ }
646
+ /** @internal */ function tryCedarPolicy(ctx) {
647
+ if (ctx.linterName !== "cedar")
648
+ return null;
649
+ const customDirs = ctx.linters?.cedar?.rulesDir;
650
+ const policies = getCedarPolicies(ctx.basePath, customDirs);
651
+ if (policies.size === 0) {
652
+ return makeResult(ctx, false, "unknown", customDirs
653
+ ? `No Cedar policies found in configured rulesDir.`
654
+ : `No Cedar policies found. Add .cedar files under .cedar/ or cedar/, or set linters.cedar.rulesDir.`);
655
+ }
656
+ if (!policies.has(ctx.ruleName)) {
657
+ const suggestions = closestRuleNames(ctx.ruleName, policies);
658
+ const hint = suggestions.length > 0
659
+ ? ` Did you mean: ${suggestions.map((s) => `"cedar/${s}"`).join(", ")}?`
660
+ : "";
661
+ return makeResult(ctx, false, "unknown", `Cedar policy "${ctx.ruleName}" not found.${hint}`);
662
+ }
663
+ return makeResult(ctx, true, "enabled");
664
+ }
527
665
  /** @internal */ function tryCustomRulesDir(ctx) {
528
666
  const linterCfg = ctx.linters?.[ctx.linterName];
529
667
  if (!linterCfg?.rulesDir)
@@ -566,9 +704,11 @@ function checkLinterRule(enforcedBy, basePath, options) {
566
704
  catalogOnly: options?.catalogOnly,
567
705
  linters: options?.linters,
568
706
  };
569
- return (tryNodeResolver(ctx) ??
707
+ return (tryVigilesInternal(ctx) ??
708
+ tryNodeResolver(ctx) ??
570
709
  tryScopedPlugin(ctx) ??
571
710
  tryCliCheck(ctx) ??
711
+ tryCedarPolicy(ctx) ??
572
712
  tryCustomRulesDir(ctx) ??
573
713
  makeResult(ctx, false, "unknown", `Unknown linter: "${linterName}"`));
574
714
  }
@@ -0,0 +1,31 @@
1
+ /** One scripted assistant turn: a final text answer, or a tool call. */
2
+ export interface ModelTurn {
3
+ /** Final text answer (stops the turn). */
4
+ readonly text?: string;
5
+ /** A tool to invoke, e.g. "Bash" | "Write" | "Edit". */
6
+ readonly tool?: string;
7
+ /** The tool input, e.g. `{ file_path, content }` or `{ command }`. */
8
+ readonly input?: Record<string, unknown>;
9
+ }
10
+ /** Build a scripted model from an ordered list of turns. */
11
+ export declare function scriptModel(turns: readonly ModelTurn[]): ModelTurn[];
12
+ export interface TurnInfo {
13
+ readonly n: number;
14
+ readonly stream: boolean;
15
+ readonly hasToolResult: boolean;
16
+ }
17
+ export interface MockHandle {
18
+ readonly url: string;
19
+ close(): void;
20
+ /** Number of model turns served so far. */
21
+ readonly count: number;
22
+ }
23
+ /**
24
+ * Start the scripted mock on a free port. Each `/v1/messages` POST consumes the
25
+ * next turn (the last turn repeats if the client asks for more). Resolves to a
26
+ * handle with the base `url` and a `close()`.
27
+ */
28
+ export declare function startMock(script: readonly ModelTurn[], opts?: {
29
+ onTurn?: (info: TurnInfo) => void;
30
+ }): Promise<MockHandle>;
31
+ //# sourceMappingURL=mock-model.d.ts.map
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.scriptModel = scriptModel;
7
+ exports.startMock = startMock;
8
+ /**
9
+ * vigiles — a scripted, deterministic Anthropic Messages API mock.
10
+ *
11
+ * Point a Claude Code client at it with `ANTHROPIC_BASE_URL` (and any dummy
12
+ * `ANTHROPIC_API_KEY`) and it serves a fixed *script* of model turns in order —
13
+ * each `POST /v1/messages` returns the next. This is the seam that makes harness
14
+ * testing deterministic: the real `claude` CLI runs your real hooks/settings,
15
+ * but the model's turns are scripted, so the outcome is reproducible and free.
16
+ *
17
+ * scriptModel([
18
+ * { tool: "Write", input: { file_path: "SKILL.md", content: "..." } },
19
+ * { text: "done" },
20
+ * ])
21
+ *
22
+ * Implements the parts a real client needs: SSE streaming (flushed per event),
23
+ * `/v1/messages/count_tokens` (else Claude Code hangs), HEAD/health tolerance,
24
+ * and echoing the requested model.
25
+ */
26
+ const node_http_1 = __importDefault(require("node:http"));
27
+ /** Build a scripted model from an ordered list of turns. */
28
+ function scriptModel(turns) {
29
+ return [...turns];
30
+ }
31
+ function writeEvent(res, event, data) {
32
+ res.write(`event: ${event}\n`);
33
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
34
+ }
35
+ const rid = (p) => `${p}${Math.random().toString(36).slice(2, 10)}`;
36
+ function streamTurn(res, turn, model) {
37
+ res.writeHead(200, {
38
+ "content-type": "text/event-stream",
39
+ "cache-control": "no-cache",
40
+ connection: "keep-alive",
41
+ });
42
+ writeEvent(res, "message_start", {
43
+ type: "message_start",
44
+ message: {
45
+ id: rid("msg_"),
46
+ type: "message",
47
+ role: "assistant",
48
+ model,
49
+ content: [],
50
+ stop_reason: null,
51
+ usage: { input_tokens: 10, output_tokens: 1 },
52
+ },
53
+ });
54
+ if (turn.tool) {
55
+ writeEvent(res, "content_block_start", {
56
+ type: "content_block_start",
57
+ index: 0,
58
+ content_block: {
59
+ type: "tool_use",
60
+ id: rid("toolu_"),
61
+ name: turn.tool,
62
+ input: {},
63
+ },
64
+ });
65
+ writeEvent(res, "content_block_delta", {
66
+ type: "content_block_delta",
67
+ index: 0,
68
+ delta: {
69
+ type: "input_json_delta",
70
+ partial_json: JSON.stringify(turn.input ?? {}),
71
+ },
72
+ });
73
+ writeEvent(res, "content_block_stop", {
74
+ type: "content_block_stop",
75
+ index: 0,
76
+ });
77
+ writeEvent(res, "message_delta", {
78
+ type: "message_delta",
79
+ delta: { stop_reason: "tool_use", stop_sequence: null },
80
+ usage: { output_tokens: 5 },
81
+ });
82
+ }
83
+ else {
84
+ writeEvent(res, "content_block_start", {
85
+ type: "content_block_start",
86
+ index: 0,
87
+ content_block: { type: "text", text: "" },
88
+ });
89
+ writeEvent(res, "content_block_delta", {
90
+ type: "content_block_delta",
91
+ index: 0,
92
+ delta: { type: "text_delta", text: turn.text ?? "" },
93
+ });
94
+ writeEvent(res, "content_block_stop", {
95
+ type: "content_block_stop",
96
+ index: 0,
97
+ });
98
+ writeEvent(res, "message_delta", {
99
+ type: "message_delta",
100
+ delta: { stop_reason: "end_turn", stop_sequence: null },
101
+ usage: { output_tokens: 5 },
102
+ });
103
+ }
104
+ writeEvent(res, "message_stop", { type: "message_stop" });
105
+ res.end();
106
+ }
107
+ function jsonTurn(res, turn, model) {
108
+ const content = turn.tool
109
+ ? [
110
+ {
111
+ type: "tool_use",
112
+ id: rid("toolu_"),
113
+ name: turn.tool,
114
+ input: turn.input ?? {},
115
+ },
116
+ ]
117
+ : [{ type: "text", text: turn.text ?? "" }];
118
+ res.writeHead(200, { "content-type": "application/json" });
119
+ res.end(JSON.stringify({
120
+ id: rid("msg_"),
121
+ type: "message",
122
+ role: "assistant",
123
+ model,
124
+ stop_reason: turn.tool ? "tool_use" : "end_turn",
125
+ stop_sequence: null,
126
+ content,
127
+ usage: { input_tokens: 10, output_tokens: 5 },
128
+ }));
129
+ }
130
+ /**
131
+ * Start the scripted mock on a free port. Each `/v1/messages` POST consumes the
132
+ * next turn (the last turn repeats if the client asks for more). Resolves to a
133
+ * handle with the base `url` and a `close()`.
134
+ */
135
+ function startMock(script, opts = {}) {
136
+ let i = 0;
137
+ const server = node_http_1.default.createServer((req, res) => {
138
+ let body = "";
139
+ req.on("data", (c) => (body += c));
140
+ req.on("end", () => {
141
+ const url = req.url ?? "";
142
+ const isCount = url.includes("count_tokens");
143
+ const isMessages = url.includes("/v1/messages") && !isCount;
144
+ let reqBody = {};
145
+ try {
146
+ reqBody = JSON.parse(body);
147
+ }
148
+ catch {
149
+ /* HEAD / health checks have no JSON body */
150
+ }
151
+ if (req.method === "HEAD" || (!isMessages && !isCount)) {
152
+ res.writeHead(200, { "content-type": "application/json" });
153
+ res.end("{}");
154
+ return;
155
+ }
156
+ if (isCount) {
157
+ res.writeHead(200, { "content-type": "application/json" });
158
+ res.end(JSON.stringify({ input_tokens: 10 }));
159
+ return;
160
+ }
161
+ const last = JSON.stringify(reqBody.messages?.at(-1)?.content ?? "");
162
+ opts.onTurn?.({
163
+ n: i,
164
+ stream: reqBody.stream === true,
165
+ hasToolResult: last.includes('"tool_result"'),
166
+ });
167
+ const turn = script[Math.min(i, script.length - 1)] ?? { text: "" };
168
+ i++;
169
+ const model = reqBody.model ?? "claude-mock";
170
+ if (reqBody.stream === true)
171
+ streamTurn(res, turn, model);
172
+ else
173
+ jsonTurn(res, turn, model);
174
+ });
175
+ });
176
+ return new Promise((resolve) => {
177
+ server.listen(0, "127.0.0.1", () => {
178
+ const { port } = server.address();
179
+ resolve({
180
+ url: `http://127.0.0.1:${String(port)}`,
181
+ close: () => server.close(),
182
+ get count() {
183
+ return i;
184
+ },
185
+ });
186
+ });
187
+ });
188
+ }
189
+ //# sourceMappingURL=mock-model.js.map
@@ -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);