vigiles 14.6.7 → 14.8.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 (53) hide show
  1. package/dist/adapters/claude-code/agent-runtime.d.ts +2 -19
  2. package/dist/adapters/claude-code/agent-runtime.js +5 -30
  3. package/dist/adapters/claude-code/agent-tools.d.ts +20 -0
  4. package/dist/adapters/claude-code/agent-tools.js +40 -0
  5. package/dist/audit-report.d.ts +2 -2
  6. package/dist/audit-report.template.html +15 -15
  7. package/dist/audit-score.d.ts +1 -1
  8. package/dist/audit-score.js +20 -20
  9. package/dist/audit-verdict.d.ts +2 -2
  10. package/dist/audit-verdict.js +5 -5
  11. package/dist/core/assert-never.d.ts +9 -0
  12. package/dist/core/assert-never.js +14 -0
  13. package/dist/core/description-overlap.js +2 -2
  14. package/dist/core/edit-distance.d.ts +12 -0
  15. package/dist/core/edit-distance.js +36 -0
  16. package/dist/core/effects.js +3 -3
  17. package/dist/core/hash.d.ts +1 -2
  18. package/dist/core/hash.js +6 -4
  19. package/dist/core/hook-block-ineffective.d.ts +55 -6
  20. package/dist/core/hook-block-ineffective.js +9 -14
  21. package/dist/core/hook-events.js +2 -2
  22. package/dist/core/linters.d.ts +2 -6
  23. package/dist/core/linters.js +4 -29
  24. package/dist/core/mcp-contract-message.d.ts +22 -0
  25. package/dist/core/mcp-contract-message.js +29 -0
  26. package/dist/core/mcp.d.ts +4 -12
  27. package/dist/core/mcp.js +3 -14
  28. package/dist/core/ncd.d.ts +12 -0
  29. package/dist/core/ncd.js +50 -0
  30. package/dist/core/plugin-dir-layout.d.ts +5 -5
  31. package/dist/core/plugin-dir-layout.js +10 -22
  32. package/dist/core/proofs.d.ts +2 -11
  33. package/dist/core/proofs.js +4 -39
  34. package/dist/core/skill-resources.d.ts +3 -3
  35. package/dist/core/skill-resources.js +9 -8
  36. package/dist/core/tool-contract.js +2 -2
  37. package/dist/leaderboard.d.ts +2 -51
  38. package/dist/leaderboard.js +20 -225
  39. package/dist/optimize.d.ts +1 -1
  40. package/dist/optimize.js +3 -3
  41. package/dist/posix-path.d.ts +40 -0
  42. package/dist/posix-path.js +293 -0
  43. package/dist/scan-core.d.ts +154 -0
  44. package/dist/scan-core.js +690 -0
  45. package/dist/scan-files.d.ts +28 -0
  46. package/dist/scan-files.js +489 -0
  47. package/dist/scan.d.ts +11 -34
  48. package/dist/scan.js +55 -668
  49. package/dist/score-core.d.ts +73 -0
  50. package/dist/score-core.js +226 -0
  51. package/dist/test-coverage-files.d.ts +11 -0
  52. package/dist/test-coverage-files.js +208 -0
  53. package/package.json +1 -1
@@ -26,7 +26,7 @@
26
26
  * A category that can't be assessed scores `null` (n/a) and is EXCLUDED from the
27
27
  * overall — never a false 0. Pure over the `ScanReport`, so it's fully testable.
28
28
  */
29
- import { type PluginScore } from "./leaderboard.js";
29
+ import { type PluginScore } from "./score-core.js";
30
30
  import type { ScanReport } from "./scan.js";
31
31
  export type CategoryKey = "Truthfulness" | "Triggering" | "Structure" | "Safety" | "Tested";
32
32
  export interface CategoryScore {
@@ -30,7 +30,7 @@ exports.formatAuditScore = formatAuditScore;
30
30
  * A category that can't be assessed scores `null` (n/a) and is EXCLUDED from the
31
31
  * overall — never a false 0. Pure over the `ScanReport`, so it's fully testable.
32
32
  */
33
- const leaderboard_js_1 = require("./leaderboard.js");
33
+ const score_core_js_1 = require("./score-core.js");
34
34
  // Per-item penalties are the SHARED leaderboard weights (imported above) so the
35
35
  // category rings and the single health number can never drift. W_UNTESTED is
36
36
  // audit-only — untested surfaces are advisory (shown, never scored into overall).
@@ -64,12 +64,12 @@ function truthfulness(r) {
64
64
  const { score, findings } = scoreFrom([
65
65
  {
66
66
  n: r.danglingRefs.length,
67
- weight: leaderboard_js_1.W_DANGLING_REF,
67
+ weight: score_core_js_1.W_DANGLING_REF,
68
68
  label: "broken intra-plugin reference(s)",
69
69
  },
70
70
  {
71
71
  n: missingHooks,
72
- weight: leaderboard_js_1.W_MISSING_HOOK,
72
+ weight: score_core_js_1.W_MISSING_HOOK,
73
73
  label: "hook script(s) missing (never run)",
74
74
  },
75
75
  ]);
@@ -80,12 +80,12 @@ function triggering(r) {
80
80
  const { score, findings } = scoreFrom([
81
81
  {
82
82
  n: noDesc,
83
- weight: leaderboard_js_1.W_NO_DESCRIPTION,
83
+ weight: score_core_js_1.W_NO_DESCRIPTION,
84
84
  label: "skill(s) with no usable description (can't trigger)",
85
85
  },
86
86
  {
87
87
  n: r.descriptionOverlaps.length,
88
- weight: leaderboard_js_1.W_OVERLAP,
88
+ weight: score_core_js_1.W_OVERLAP,
89
89
  label: "near-identical skill description(s) (wrong one fires)",
90
90
  },
91
91
  ]);
@@ -99,42 +99,42 @@ function structure(r) {
99
99
  const { score, findings } = scoreFrom([
100
100
  {
101
101
  n: deadTools,
102
- weight: leaderboard_js_1.W_DANGLING_REF,
103
- label: "agent tool(s) that don't exist (typo / never-available)",
102
+ weight: score_core_js_1.W_DANGLING_REF,
103
+ label: "unavailable agent tool(s) (typo / never-available)",
104
104
  },
105
105
  {
106
106
  n: deadMcpTools,
107
- weight: leaderboard_js_1.W_DANGLING_REF,
107
+ weight: score_core_js_1.W_DANGLING_REF,
108
108
  label: "agent MCP tool(s) whose server isn't declared",
109
109
  },
110
110
  {
111
111
  n: r.hookEventIssues.length,
112
- weight: leaderboard_js_1.W_MISSING_HOOK,
112
+ weight: score_core_js_1.W_MISSING_HOOK,
113
113
  label: "hook(s) on an unknown event (never fire)",
114
114
  },
115
115
  {
116
116
  n: r.mcpIssues.length,
117
- weight: leaderboard_js_1.W_DANGLING_REF,
117
+ weight: score_core_js_1.W_DANGLING_REF,
118
118
  label: "MCP server(s) that can't start (no command/url)",
119
119
  },
120
120
  {
121
121
  n: r.mcpHookIssues.length,
122
- weight: leaderboard_js_1.W_DANGLING_REF,
122
+ weight: score_core_js_1.W_DANGLING_REF,
123
123
  label: "mcp_tool hook(s) incomplete / undeclared server",
124
124
  },
125
125
  {
126
126
  n: r.frontmatterIssues.length,
127
- weight: leaderboard_js_1.W_NO_DESCRIPTION,
127
+ weight: score_core_js_1.W_NO_DESCRIPTION,
128
128
  label: "surface(s) missing required frontmatter",
129
129
  },
130
130
  {
131
131
  n: r.frontmatterValueIssues.length,
132
- weight: leaderboard_js_1.W_NO_CONTRACT,
132
+ weight: score_core_js_1.W_NO_CONTRACT,
133
133
  label: "agent(s) with an invalid model/color (silent fallback)",
134
134
  },
135
135
  {
136
136
  n: deadDisallowed,
137
- weight: leaderboard_js_1.W_NO_CONTRACT,
137
+ weight: score_core_js_1.W_NO_CONTRACT,
138
138
  label: "disallowedTools typo(s) that block nothing",
139
139
  },
140
140
  ]);
@@ -185,7 +185,7 @@ function safety(r) {
185
185
  const { score, findings } = scoreFrom([
186
186
  {
187
187
  n: hard.length,
188
- weight: leaderboard_js_1.W_TRIFECTA,
188
+ weight: score_core_js_1.W_TRIFECTA,
189
189
  label: "unit(s) holding all three lethal-trifecta legs (prompt-injection exfil path)",
190
190
  },
191
191
  ]);
@@ -193,7 +193,7 @@ function safety(r) {
193
193
  // note but never graded (mirrors the Structure inherits-all advisory).
194
194
  const advisory = r.trifectaFindings
195
195
  .filter((f) => f.finding.severity === "advisory")
196
- .map((f) => `${f.name} inherits all tools — maximal trifecta blast radius (advisory)`);
196
+ .map((f) => `${f.name} inherits all tools — the "lethal trifecta" (reads data, reaches the web, runs commands), so a prompt injection could exfiltrate secrets (advisory)`);
197
197
  return {
198
198
  key: "Safety",
199
199
  score,
@@ -217,7 +217,7 @@ function tested(r) {
217
217
  * an instruction file as a surface.)
218
218
  */
219
219
  function isEmptyAudit(r) {
220
- return (0, leaderboard_js_1.isEmptyMachine)(r) && !r.instructions;
220
+ return (0, score_core_js_1.isEmptyMachine)(r) && !r.instructions;
221
221
  }
222
222
  /**
223
223
  * Bucket a scan report into the five deterministic Lighthouse categories as a
@@ -237,7 +237,7 @@ function auditScore(report) {
237
237
  ];
238
238
  return {
239
239
  overall: 0,
240
- grade: (0, leaderboard_js_1.gradeFor)(0),
240
+ grade: (0, score_core_js_1.gradeFor)(0),
241
241
  categories: categories.map((key) => ({
242
242
  key,
243
243
  score: null,
@@ -258,8 +258,8 @@ function auditScore(report) {
258
258
  // of the rings — averaging would let a real problem in one category be diluted
259
259
  // by clean siblings. The rings above stay a diagnostic breakdown; Tested
260
260
  // (advisory) is never summed in (untested surfaces don't drag the grade).
261
- const { score: overall } = (0, leaderboard_js_1.computeIntegrityScore)((0, leaderboard_js_1.reportDeductions)(report));
262
- return { overall, grade: (0, leaderboard_js_1.gradeFor)(overall), categories, empty: false };
261
+ const { score: overall } = (0, score_core_js_1.computeIntegrityScore)((0, score_core_js_1.reportDeductions)(report));
262
+ return { overall, grade: (0, score_core_js_1.gradeFor)(overall), categories, empty: false };
263
263
  }
264
264
  // A 22-cell bar gauge ("ring" in the terminal; the real rings are the HTML).
265
265
  const BAR_CELLS = 22;
@@ -11,7 +11,7 @@
11
11
  * 1. `pointsIfFixed` per recommendation — the exact number of overall points the
12
12
  * grade gains if THAT one fix is applied (so a fix card can show `+N pts` and
13
13
  * sort by it). Computed as `overall(report − thisFinding) − overall(report)`.
14
- * 2. A verdict `sentence` for the report header — e.g. "Two one-line fixes away
14
+ * 2. A verdict `sentence` for the report header — e.g. "Two fixes away
15
15
  * from an A." — where the COUNT is the minimal number of fixes whose COMBINED
16
16
  * removal actually crosses the next grade threshold (a real cumulative
17
17
  * re-score), and `pointsToNextGrade` is the real threshold gap.
@@ -40,7 +40,7 @@
40
40
  */
41
41
  import { type AuditScore } from "./audit-score.js";
42
42
  import type { Recommendation } from "./optimize.js";
43
- import { type PluginScore } from "./leaderboard.js";
43
+ import { type PluginScore } from "./score-core.js";
44
44
  import type { ScanReport } from "./scan.js";
45
45
  /**
46
46
  * The inputs the verdict needs — exactly the pieces {@link buildAuditReport}
@@ -12,7 +12,7 @@
12
12
  * 1. `pointsIfFixed` per recommendation — the exact number of overall points the
13
13
  * grade gains if THAT one fix is applied (so a fix card can show `+N pts` and
14
14
  * sort by it). Computed as `overall(report − thisFinding) − overall(report)`.
15
- * 2. A verdict `sentence` for the report header — e.g. "Two one-line fixes away
15
+ * 2. A verdict `sentence` for the report header — e.g. "Two fixes away
16
16
  * from an A." — where the COUNT is the minimal number of fixes whose COMBINED
17
17
  * removal actually crosses the next grade threshold (a real cumulative
18
18
  * re-score), and `pointsToNextGrade` is the real threshold gap.
@@ -42,7 +42,7 @@
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.computeVerdict = computeVerdict;
44
44
  const audit_score_js_1 = require("./audit-score.js");
45
- const leaderboard_js_1 = require("./leaderboard.js");
45
+ const score_core_js_1 = require("./score-core.js");
46
46
  // The grade-band FLOORS (A ≥90 … D ≥60; below 60 is F), mirroring gradeFor.
47
47
  const GRADE_FLOORS = [60, 70, 80, 90];
48
48
  /** The smallest band floor strictly above `overall`, or null when already an A. */
@@ -195,7 +195,7 @@ function overallWithout(report, recs) {
195
195
  */
196
196
  function dominantDeduction(report) {
197
197
  let best = null;
198
- for (const d of (0, leaderboard_js_1.reportDeductions)(report)) {
198
+ for (const d of (0, score_core_js_1.reportDeductions)(report)) {
199
199
  if (d.n <= 0)
200
200
  continue;
201
201
  const cost = d.n * d.weight;
@@ -238,10 +238,10 @@ function buildSentence(input, pointsToNextGrade, fixesToNextGrade) {
238
238
  return `A — nothing blocking the grade; ${numberWord(n)} deterministic ${fixNoun(n)} would harden it further.`;
239
239
  }
240
240
  // The next band's grade is gradeFor(its FLOOR); the floor is base + the gap.
241
- const nextGrade = (0, leaderboard_js_1.gradeFor)(score.overall + pointsToNextGrade);
241
+ const nextGrade = (0, score_core_js_1.gradeFor)(score.overall + pointsToNextGrade);
242
242
  // Reachable by the deterministic fix list: fix-count-forward (the actionable framing).
243
243
  if (fixesToNextGrade !== null) {
244
- return `${capitalize(numberWord(fixesToNextGrade))} one-line ${fixNoun(fixesToNextGrade)} away from ${article(nextGrade)} ${nextGrade}.`;
244
+ return `${capitalize(numberWord(fixesToNextGrade))} ${fixNoun(fixesToNextGrade)} away from ${article(nextGrade)} ${nextGrade}.`;
245
245
  }
246
246
  // Not reachable by recommendations alone — lead with the dominant blocking finding.
247
247
  const dom = dominantDeduction(report);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `assertNever` — the exhaustive-switch guard, in its own NODE-FREE leaf so a
3
+ * consumer can import it WITHOUT pulling `core/hash.ts` (which imports
4
+ * `node:crypto` for `sha256short`) into a browser bundle. The deterministic audit
5
+ * engine reaches this via `core/effects.ts`; `hash.ts` re-exports it so every
6
+ * existing `import { assertNever } from "./hash.js"` is unchanged.
7
+ */
8
+ export declare function assertNever(x: never): never;
9
+ //# sourceMappingURL=assert-never.d.ts.map
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertNever = assertNever;
4
+ /**
5
+ * `assertNever` — the exhaustive-switch guard, in its own NODE-FREE leaf so a
6
+ * consumer can import it WITHOUT pulling `core/hash.ts` (which imports
7
+ * `node:crypto` for `sha256short`) into a browser bundle. The deterministic audit
8
+ * engine reaches this via `core/effects.ts`; `hash.ts` re-exports it so every
9
+ * existing `import { assertNever } from "./hash.js"` is unchanged.
10
+ */
11
+ function assertNever(x) {
12
+ throw new Error(`Unexpected value: ${String(x)}`);
13
+ }
14
+ //# sourceMappingURL=assert-never.js.map
@@ -17,7 +17,7 @@ exports.findDescriptionOverlaps = findDescriptionOverlaps;
17
17
  * copy-pasted description with a word or two changed) — never on a parallel but
18
18
  * distinct pair. Warn-level, reports the PAIR (not a unilateral defect).
19
19
  */
20
- const proofs_js_1 = require("./proofs.js");
20
+ const ncd_js_1 = require("./ncd.js");
21
21
  /**
22
22
  * The NCD cutoff below which two descriptions count as a near-duplicate. 0.2 sits
23
23
  * safely under the sweep's most-similar legitimately-distinct pair (0.25), so
@@ -35,7 +35,7 @@ function findDescriptionOverlaps(surfaces, cutoff = exports.OVERLAP_NCD_CUTOFF)
35
35
  const overlaps = [];
36
36
  for (let i = 0; i < surfaces.length; i++) {
37
37
  for (let j = i + 1; j < surfaces.length; j++) {
38
- const d = (0, proofs_js_1.ncd)(surfaces[i].description, surfaces[j].description);
38
+ const d = (0, ncd_js_1.ncd)(surfaces[i].description, surfaces[j].description);
39
39
  if (d >= cutoff)
40
40
  continue;
41
41
  const a = surfaces[i].name;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Levenshtein distance for short-string typo detection. Rule names, tool names,
3
+ * and hook events are short, so edit distance is more appropriate than NCD
4
+ * (which is tuned for longer texts).
5
+ *
6
+ * Extracted to its own zero-dependency leaf module so the typo detectors
7
+ * (tool-contract, hook-events) can import it WITHOUT pulling in `core/linters.ts`,
8
+ * which runs a `node:fs`/`process`/`glob` side effect at import time — the
9
+ * blocker to running those detectors in a browser (the in-browser audit demo).
10
+ */
11
+ export declare function editDistance(a: string, b: string): number;
12
+ //# sourceMappingURL=edit-distance.d.ts.map
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.editDistance = editDistance;
4
+ /**
5
+ * Levenshtein distance for short-string typo detection. Rule names, tool names,
6
+ * and hook events are short, so edit distance is more appropriate than NCD
7
+ * (which is tuned for longer texts).
8
+ *
9
+ * Extracted to its own zero-dependency leaf module so the typo detectors
10
+ * (tool-contract, hook-events) can import it WITHOUT pulling in `core/linters.ts`,
11
+ * which runs a `node:fs`/`process`/`glob` side effect at import time — the
12
+ * blocker to running those detectors in a browser (the in-browser audit demo).
13
+ */
14
+ function editDistance(a, b) {
15
+ if (a === b)
16
+ return 0;
17
+ const m = a.length;
18
+ const n = b.length;
19
+ if (m === 0)
20
+ return n;
21
+ if (n === 0)
22
+ return m;
23
+ const dp = Array.from({ length: n + 1 }, (_, i) => i);
24
+ for (let i = 1; i <= m; i++) {
25
+ let prev = dp[0];
26
+ dp[0] = i;
27
+ for (let j = 1; j <= n; j++) {
28
+ const tmp = dp[j];
29
+ dp[j] =
30
+ a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
31
+ prev = tmp;
32
+ }
33
+ }
34
+ return dp[n];
35
+ }
36
+ //# sourceMappingURL=edit-distance.js.map
@@ -5,7 +5,7 @@ exports.effectSurface = effectSurface;
5
5
  exports.purityViolations = purityViolations;
6
6
  exports.pureContractViolations = pureContractViolations;
7
7
  exports.decidePurityGate = decidePurityGate;
8
- const hash_js_1 = require("./hash.js");
8
+ const assert_never_js_1 = require("./assert-never.js");
9
9
  const bash_effects_js_1 = require("./bash-effects.js");
10
10
  // ---------------------------------------------------------------------------
11
11
  // Internal helpers
@@ -83,7 +83,7 @@ function effectSurface(tools, dialect) {
83
83
  unknown.add(base);
84
84
  break;
85
85
  default:
86
- (0, hash_js_1.assertNever)(effect);
86
+ (0, assert_never_js_1.assertNever)(effect);
87
87
  }
88
88
  }
89
89
  const purity = hasWildcard || hasBash || unknown.size > 0
@@ -170,7 +170,7 @@ function purityViolations(tools, dialect, declared) {
170
170
  });
171
171
  break;
172
172
  default:
173
- (0, hash_js_1.assertNever)(effect);
173
+ (0, assert_never_js_1.assertNever)(effect);
174
174
  }
175
175
  }
176
176
  return violations;
@@ -1,8 +1,7 @@
1
+ export { assertNever } from "./assert-never.js";
1
2
  declare const __brand: unique symbol;
2
3
  export type SHA256Hash = string & {
3
4
  readonly [__brand]: "SHA256Hash";
4
5
  };
5
6
  export declare function sha256short(data: string | Buffer): SHA256Hash;
6
- export declare function assertNever(x: never): never;
7
- export {};
8
7
  //# sourceMappingURL=hash.d.ts.map
package/dist/core/hash.js CHANGED
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertNever = void 0;
3
4
  exports.sha256short = sha256short;
4
- exports.assertNever = assertNever;
5
5
  const node_crypto_1 = require("node:crypto");
6
+ // `assertNever` lives in a node-free leaf (no crypto), re-exported here so every
7
+ // existing `import { assertNever } from "./hash.js"` is unchanged while a
8
+ // browser-bound module can import it from ./assert-never.js without pulling crypto.
9
+ var assert_never_js_1 = require("./assert-never.js");
10
+ Object.defineProperty(exports, "assertNever", { enumerable: true, get: function () { return assert_never_js_1.assertNever; } });
6
11
  const HASH_LENGTH = 16;
7
12
  function sha256short(data) {
8
13
  return (0, node_crypto_1.createHash)("sha256")
@@ -10,7 +15,4 @@ function sha256short(data) {
10
15
  .digest("hex")
11
16
  .slice(0, HASH_LENGTH);
12
17
  }
13
- function assertNever(x) {
14
- throw new Error(`Unexpected value: ${String(x)}`);
15
- }
16
18
  //# sourceMappingURL=hash.js.map
@@ -1,3 +1,51 @@
1
+ /**
2
+ * Hook-block-ineffective detector — the #1 verified hook pain ("false confidence").
3
+ *
4
+ * A safety hook that LOOKS like it blocks but SILENTLY DOESN'T. Two shapes:
5
+ *
6
+ * 1. **wrong-event** — the script tries to block (contains `exit 2`, a legacy
7
+ * `"decision":"block"` JSON, or a `"permissionDecision":"deny"`) but is
8
+ * registered on an event where a block is SILENTLY IGNORED ENTIRELY —
9
+ * SessionStart / SessionEnd / Notification / PreCompact, where exit 2 writes
10
+ * stderr only to the user (no veto, no model feedback). The author believes a
11
+ * gate is in place; nothing happens. (#19009 names the class.) NB `PostToolUse`
12
+ * is deliberately NOT flagged: there exit 2 feeds stderr back to the model — a
13
+ * legitimate FEEDBACK channel (a nudge/lint hook), not a failed block, and the
14
+ * block-vs-feedback intent isn't deterministically separable.
15
+ *
16
+ * 2. **wrong-field** — the hook IS on a permission-gated event (e.g. PreToolUse)
17
+ * but emits the LEGACY top-level `"decision":"block"` field instead of the
18
+ * required `hookSpecificOutput.permissionDecision:"deny"`. A copied template
19
+ * (PostToolUse style → PreToolUse registration) is the usual cause; the deny
20
+ * is silently discarded, nothing is blocked.
21
+ *
22
+ * Both shapes cause identical user-visible behaviour: the hook "works" (exits,
23
+ * no crash) but never actually stops anything. The only signal is "why did this
24
+ * run anyway?" after an incident.
25
+ *
26
+ * FP-SAFETY — conservative literal patterns only, `warn` severity by default:
27
+ * - `exit 2` is matched by a shell-context regex that requires surrounding
28
+ * whitespace/control chars to avoid false-positives on `exit 200` or a
29
+ * `git status --exit-code 2` argument.
30
+ * - JSON decision patterns are matched literally (no partial JSON walking).
31
+ * - Nothing is flagged on an unknown event (we only know which events CAN
32
+ * block because the caller injected that set).
33
+ *
34
+ * HARNESS-NEUTRAL — the sets of blocking events and permission-decision events
35
+ * are INJECTED from the dialect (never hard-coded here). The caller supplies the
36
+ * Claude Code sets; a Codex adapter supplies its own. This is the same
37
+ * dependency-injection pattern used by `verifyHookEvents` and `verifyToolContract`
38
+ * (core ⊄ adapter — one-detector-no-drift).
39
+ *
40
+ * ONE detector reused by scan + the `hook-block-ineffective` lint rule (two
41
+ * callers, no drift). See `research/hook-pain-points.md` for the verified corpus
42
+ * and `docs/compiled-hooks.md` for the authoritative fix (compiled hooks make
43
+ * this whole class unrepresentable).
44
+ *
45
+ * Node-free: the `readFileSync` IO is REQUIRED (injected by the caller — the disk
46
+ * scan passes `node:fs`, the browser engine a map-backed read), so this module
47
+ * never statically imports `node:fs` and bundles clean in a browser.
48
+ */
1
49
  /** The two "looks like it blocks but doesn't" failure shapes. */
2
50
  export type HookBlockKind = "wrong-event" | "wrong-field";
3
51
  /** One false-confidence finding: a hook that appears to block but silently won't. */
@@ -41,11 +89,12 @@ export interface HookBlockOptions {
41
89
  */
42
90
  readonly permissionDecisionEvents: ReadonlySet<string>;
43
91
  /**
44
- * Injectable file read (default: node:fs `readFileSync(p, "utf8")`).
45
- * Returns `""` on any error so a missing / unreadable script doesn't crash
46
- * the detector — it simply produces no findings for that entry.
92
+ * REQUIRED file read (the disk caller injects `node:fs`
93
+ * `readFileSync(p, "utf8")`; the browser engine a map-backed read). Should
94
+ * return `""` on any error so a missing / unreadable script doesn't crash the
95
+ * detector — it simply produces no findings for that entry.
47
96
  */
48
- readonly readFileSync?: (p: string) => string;
97
+ readonly readFileSync: (p: string) => string;
49
98
  }
50
99
  /**
51
100
  * Detect false-confidence "blocks" across a set of hook entries.
@@ -55,8 +104,8 @@ export interface HookBlockOptions {
55
104
  * (event, kind, scriptPath) pairs are de-duped.
56
105
  *
57
106
  * @param entries - The hook registrations to inspect (event + command/script).
58
- * @param opts - Injected sets of blocking/permission events, and an optional
59
- * `readFileSync` (default: node:fs).
107
+ * @param opts - Injected sets of blocking/permission events, and the required
108
+ * `readFileSync` (disk caller: node:fs; browser: map-backed).
60
109
  */
61
110
  export declare function hookBlockIssues(entries: readonly HookScriptEntry[], opts: HookBlockOptions): HookBlockFinding[];
62
111
  //# sourceMappingURL=hook-block-ineffective.d.ts.map
@@ -1,6 +1,4 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hookBlockIssues = hookBlockIssues;
4
2
  /**
5
3
  * Hook-block-ineffective detector — the #1 verified hook pain ("false confidence").
6
4
  *
@@ -44,8 +42,13 @@ exports.hookBlockIssues = hookBlockIssues;
44
42
  * callers, no drift). See `research/hook-pain-points.md` for the verified corpus
45
43
  * and `docs/compiled-hooks.md` for the authoritative fix (compiled hooks make
46
44
  * this whole class unrepresentable).
45
+ *
46
+ * Node-free: the `readFileSync` IO is REQUIRED (injected by the caller — the disk
47
+ * scan passes `node:fs`, the browser engine a map-backed read), so this module
48
+ * never statically imports `node:fs` and bundles clean in a browser.
47
49
  */
48
- const node_fs_1 = require("node:fs");
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.hookBlockIssues = hookBlockIssues;
49
52
  // ---------------------------------------------------------------------------
50
53
  // Block-mechanism patterns (conservative / FP-safe)
51
54
  // ---------------------------------------------------------------------------
@@ -79,14 +82,6 @@ const PERMISSION_DENY = /"permissionDecision"\s*:\s*"(deny|ask)"/;
79
82
  // ---------------------------------------------------------------------------
80
83
  // Detector
81
84
  // ---------------------------------------------------------------------------
82
- function defaultReadFile(p) {
83
- try {
84
- return (0, node_fs_1.readFileSync)(p, "utf8");
85
- }
86
- catch {
87
- return "";
88
- }
89
- }
90
85
  /**
91
86
  * Detect false-confidence "blocks" across a set of hook entries.
92
87
  *
@@ -95,12 +90,12 @@ function defaultReadFile(p) {
95
90
  * (event, kind, scriptPath) pairs are de-duped.
96
91
  *
97
92
  * @param entries - The hook registrations to inspect (event + command/script).
98
- * @param opts - Injected sets of blocking/permission events, and an optional
99
- * `readFileSync` (default: node:fs).
93
+ * @param opts - Injected sets of blocking/permission events, and the required
94
+ * `readFileSync` (disk caller: node:fs; browser: map-backed).
100
95
  */
101
96
  function hookBlockIssues(entries, opts) {
102
97
  const { noEffectEvents, permissionDecisionEvents } = opts;
103
- const readFile = opts.readFileSync ?? defaultReadFile;
98
+ const readFile = opts.readFileSync;
104
99
  const findings = [];
105
100
  const seen = new Set();
106
101
  for (const entry of entries) {
@@ -2,13 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.confidentHookEventIssues = confidentHookEventIssues;
4
4
  exports.verifyHookEvents = verifyHookEvents;
5
- const linters_js_1 = require("./linters.js");
5
+ const edit_distance_js_1 = require("./edit-distance.js");
6
6
  /** Closest known hook event by edit distance (≤ 2) — a confidence signal. */
7
7
  function closestEvent(event, dialect) {
8
8
  let best = null;
9
9
  let bestDistance = Infinity;
10
10
  for (const known of dialect.hookEvents) {
11
- const d = (0, linters_js_1.editDistance)(event.toLowerCase(), known.toLowerCase());
11
+ const d = (0, edit_distance_js_1.editDistance)(event.toLowerCase(), known.toLowerCase());
12
12
  if (d < bestDistance) {
13
13
  bestDistance = d;
14
14
  best = known;
@@ -9,6 +9,7 @@
9
9
  * This is the core moat — no other tool resolves rules across 7 catalog APIs
10
10
  * (6 linters + Cedar policy language) and checks config-enabled status.
11
11
  */
12
+ import { editDistance } from "./edit-distance.js";
12
13
  export type ConfigEnabledStatus = "enabled" | "disabled" | "unknown";
13
14
  export interface LinterCheckResult {
14
15
  exists: boolean;
@@ -24,12 +25,7 @@ export interface DetectedLinter {
24
25
  }
25
26
  /** @internal */ export declare function extractLinterName(enforcedBy: string): string;
26
27
  /** @internal */ export declare function extractRuleName(enforcedBy: string): string | null;
27
- /**
28
- * Levenshtein distance for short-string typo detection. Rule names are
29
- * short so edit distance is more appropriate than NCD (which is tuned
30
- * for longer texts).
31
- */
32
- export declare function editDistance(a: string, b: string): number;
28
+ export { editDistance };
33
29
  /** @internal */ export declare function clearCedarCache(): void;
34
30
  /**
35
31
  * Check a single linter rule reference (e.g., "eslint/no-console").
@@ -11,13 +11,15 @@
11
11
  * (6 linters + Cedar policy language) and checks config-enabled status.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.editDistance = void 0;
14
15
  exports.extractLinterName = extractLinterName;
15
16
  exports.extractRuleName = extractRuleName;
16
- exports.editDistance = editDistance;
17
17
  exports.clearCedarCache = clearCedarCache;
18
18
  exports.checkLinterRule = checkLinterRule;
19
19
  const node_fs_1 = require("node:fs");
20
20
  const node_path_1 = require("node:path");
21
+ const edit_distance_js_1 = require("./edit-distance.js");
22
+ Object.defineProperty(exports, "editDistance", { enumerable: true, get: function () { return edit_distance_js_1.editDistance; } });
21
23
  const node_child_process_1 = require("node:child_process");
22
24
  const node_module_1 = require("node:module");
23
25
  const glob_1 = require("glob");
@@ -378,38 +380,11 @@ function ruleFileExists(ruleName, rulesDir, basePath) {
378
380
  function makeResult(ctx, exists, enabled = "unknown", error) {
379
381
  return { exists, enabled, linter: ctx.linterName, rule: ctx.ruleName, error };
380
382
  }
381
- /**
382
- * Levenshtein distance for short-string typo detection. Rule names are
383
- * short so edit distance is more appropriate than NCD (which is tuned
384
- * for longer texts).
385
- */
386
- function editDistance(a, b) {
387
- if (a === b)
388
- return 0;
389
- const m = a.length;
390
- const n = b.length;
391
- if (m === 0)
392
- return n;
393
- if (n === 0)
394
- return m;
395
- const dp = Array.from({ length: n + 1 }, (_, i) => i);
396
- for (let i = 1; i <= m; i++) {
397
- let prev = dp[0];
398
- dp[0] = i;
399
- for (let j = 1; j <= n; j++) {
400
- const tmp = dp[j];
401
- dp[j] =
402
- a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
403
- prev = tmp;
404
- }
405
- }
406
- return dp[n];
407
- }
408
383
  /** Top-N closest rule names by edit distance, filtered by a max distance. */
409
384
  function closestRuleNames(target, candidates, limit = 3, maxDistance = 4) {
410
385
  const scored = [];
411
386
  for (const c of candidates) {
412
- const d = editDistance(target, c);
387
+ const d = (0, edit_distance_js_1.editDistance)(target, c);
413
388
  if (d <= maxDistance)
414
389
  scored.push({ name: c, dist: d });
415
390
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The pure, node-free half of the live MCP contract-tool check: the error shape
3
+ * and its human-readable formatter, split out of `core/mcp.ts` (which statically
4
+ * imports `node:child_process` + `./refs.js` for the SPAWN path) so a consumer
5
+ * that only needs the message — the deterministic audit report — doesn't drag the
6
+ * live-verification machinery into its bundle. `mcp.ts` re-exports both, so its
7
+ * existing consumers are unchanged; `verifyMcpContractTools` (the spawn) stays
8
+ * there. Node-free by construction (a local `assertNever`, no `node:` import).
9
+ */
10
+ export type McpContractToolReason = "server-unreachable" | "tool-missing";
11
+ export interface McpContractToolError {
12
+ /** The full `mcp__server__tool` reference (restriction suffix stripped). */
13
+ readonly tool: string;
14
+ readonly server: string;
15
+ /** The tool segment (what's looked up on the server). */
16
+ readonly toolName: string;
17
+ readonly reason: McpContractToolReason;
18
+ readonly suggestions: string[];
19
+ }
20
+ /** Human-readable message for a contract-tool error (with "did you mean"). */
21
+ export declare function mcpContractToolMessage(e: McpContractToolError): string;
22
+ //# sourceMappingURL=mcp-contract-message.d.ts.map
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ /**
3
+ * The pure, node-free half of the live MCP contract-tool check: the error shape
4
+ * and its human-readable formatter, split out of `core/mcp.ts` (which statically
5
+ * imports `node:child_process` + `./refs.js` for the SPAWN path) so a consumer
6
+ * that only needs the message — the deterministic audit report — doesn't drag the
7
+ * live-verification machinery into its bundle. `mcp.ts` re-exports both, so its
8
+ * existing consumers are unchanged; `verifyMcpContractTools` (the spawn) stays
9
+ * there. Node-free by construction (a local `assertNever`, no `node:` import).
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.mcpContractToolMessage = mcpContractToolMessage;
13
+ function assertNever(x) {
14
+ throw new Error(`Unexpected value: ${String(x)}`);
15
+ }
16
+ /** Human-readable message for a contract-tool error (with "did you mean"). */
17
+ function mcpContractToolMessage(e) {
18
+ switch (e.reason) {
19
+ case "server-unreachable":
20
+ return `MCP tool "${e.tool}" — server "${e.server}" failed to start`;
21
+ case "tool-missing":
22
+ return `MCP tool "${e.tool}" not found on server "${e.server}"${e.suggestions.length > 0
23
+ ? ` — did you mean ${e.suggestions.map((s) => `"${s}"`).join(", ")}?`
24
+ : ""}`;
25
+ default:
26
+ return assertNever(e.reason);
27
+ }
28
+ }
29
+ //# sourceMappingURL=mcp-contract-message.js.map