vigiles 12.6.0 → 12.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.
@@ -29,6 +29,17 @@ export interface PluginLayout {
29
29
  readonly instructionFile: string;
30
30
  /** Surface dirs materialized into the sandbox, e.g. skills/agents/commands. */
31
31
  readonly surfaceDirs: readonly string[];
32
+ /**
33
+ * Project-level dir under which an END-USER (not a plugin author) keeps the
34
+ * same surfaces, e.g. `.claude` → `.claude/skills`, `.claude/agents`. When set,
35
+ * the loader reads each surface from BOTH `<root>/<surface>` (the plugin /
36
+ * skills-library shape) AND `<root>/<userSurfaceRoot>/<surface>` (the shape a
37
+ * plain Claude Code user has), normalizing to the same materialized key. Most
38
+ * Claude Code users are NOT publishing a plugin — their skills live here, so
39
+ * without this the loader would see an empty machine for a normal repo.
40
+ * Undefined ⇒ only the primary location is read (backwards-compatible).
41
+ */
42
+ readonly userSurfaceRoot?: string;
32
43
  /** Skills dir, holding the nested `<dir>/<name>/SKILL.md`, e.g. `skills`. */
33
44
  readonly skillDir: string;
34
45
  /** Subagents dir, holding flat `<dir>/<name>.md`, e.g. `agents` (`""` = none). */
@@ -14,6 +14,22 @@ export interface SkillResourceFinding {
14
14
  export interface SkillResourceOptions {
15
15
  /** Injectable existence check (default: node:fs existsSync). */
16
16
  readonly existsSync?: (p: string) => boolean;
17
+ /**
18
+ * Repo root, used only together with `sharedDirs` (below). Off by default.
19
+ */
20
+ readonly repoRoot?: string;
21
+ /**
22
+ * OPT-IN shared-resource dirs — top-level dir names a repo shares across skills
23
+ * (`.vigilesrc.json` `sharedDirs`, e.g. `["scripts", "references"]`). Many skill
24
+ * libraries keep ONE top-level `scripts/` tree instead of a copy beside every
25
+ * SKILL.md, so a ref like `scripts/promptfoo/x.py` lives at the repo root. When
26
+ * a ref's FIRST path segment is a declared shared dir, it may ALSO resolve
27
+ * against `repoRoot`. Scoped to declared dirs on PURPOSE: a repo that sets no
28
+ * `sharedDirs` is byte-identical to before (skill-dir-only), and even with it a
29
+ * ref OUTSIDE a shared dir still can't be masked by a same-named repo-root file.
30
+ * The controlled fix for feedback P1-4 (opt-in, never a default behavior change).
31
+ */
32
+ readonly sharedDirs?: readonly string[];
17
33
  }
18
34
  /**
19
35
  * The bundled-resource references in a SKILL.md body that don't resolve on disk
@@ -72,11 +72,28 @@ function localResourceTarget(rawTarget) {
72
72
  // and almost always a plugin-root or runtime path, not a bundled file.
73
73
  if (target.includes("$"))
74
74
  return null;
75
- // Drop a URL fragment / query suffix so `references/api.md#auth` resolves to
76
- // the file. (Only after the scheme check above, so we never mangle a URL.)
75
+ // SKIP: `~/`-rooted paths — the user's home / machine-global config, referenced
76
+ // intentionally from OUTSIDE the repo (JIT routing like `Read ~/.claude/docs/x.md`).
77
+ // Not a repo-bundled resource; unverifiable from the repo and never a "broken ref".
78
+ if (target === "~" || target.startsWith("~/"))
79
+ return null;
80
+ // Drop a URL fragment / query suffix so `references/api.md#auth` AND
81
+ // `references/schema.json?raw=1` resolve to the file. Done BEFORE the glob skip
82
+ // below so a legitimate `?query` suffix on a real bundled ref isn't mistaken for
83
+ // a glob `?` and wrongly skipped — the file must still be checked. (Only after
84
+ // the scheme check above, so we never mangle a URL.)
77
85
  const path = target.replace(/[?#].*$/, "");
78
86
  if (path.length === 0)
79
87
  return null;
88
+ // SKIP: globs and template placeholders — a ref carrying a glob metacharacter
89
+ // (`*`) or a brace/angle-bracket placeholder (`{trivial,…}`, `<linter>`) is a
90
+ // directory CONVENTION or an example, not a concrete file (`references/*.md`,
91
+ // `references/linter-cards/{a,b}/<linter>.md`). Resolving it as a literal path and
92
+ // reporting "missing" is a false positive (feedback P1-3). `?` is intentionally
93
+ // NOT in this class — it's the query separator stripped above; a genuine `?`-glob
94
+ // truncates to an extensionless path and is dropped by HAS_EXT below anyway.
95
+ if (/[*{}<>]/.test(path))
96
+ return null;
80
97
  // SKIP: a `../` escape OUT of the skill dir — undecidable / not a bundled
81
98
  // resource (it points at a sibling skill or the repo). A leading `./` is fine.
82
99
  const normalized = path.replace(/^\.\//, "");
@@ -134,6 +151,18 @@ function candidatesInLine(line, lineNo) {
134
151
  */
135
152
  function skillResourceIssues(skillBody, skillDir, opts = {}) {
136
153
  const exists = opts.existsSync ?? node_fs_1.existsSync;
154
+ const sharedDirs = new Set(opts.sharedDirs ?? []);
155
+ // A ref resolves if it exists under the skill's own dir. If (and only if) its
156
+ // first segment is a DECLARED shared dir, it may also resolve against the repo
157
+ // root — the opt-in shared-tree case. No shared dirs → skill-dir-only (unchanged).
158
+ const resolvesAnywhere = (rel) => {
159
+ if (exists((0, node_path_1.resolve)(skillDir, rel)))
160
+ return true;
161
+ const firstSeg = rel.split("/")[0];
162
+ return (opts.repoRoot !== undefined &&
163
+ sharedDirs.has(firstSeg) &&
164
+ exists((0, node_path_1.resolve)(opts.repoRoot, rel)));
165
+ };
137
166
  const findings = [];
138
167
  const seen = new Set();
139
168
  const lines = skillBody.split("\n");
@@ -146,8 +175,7 @@ function skillResourceIssues(skillBody, skillDir, opts = {}) {
146
175
  if (inFence)
147
176
  continue;
148
177
  for (const c of candidatesInLine(lines[i], i + 1)) {
149
- const full = (0, node_path_1.resolve)(skillDir, c.resolved);
150
- if (exists(full))
178
+ if (resolvesAnywhere(c.resolved))
151
179
  continue;
152
180
  // De-dupe the same missing file referenced several times in the body.
153
181
  const key = `${c.kind}:${c.resolved}`;
@@ -333,6 +333,18 @@ export interface VigilesConfig {
333
333
  * excluded.
334
334
  */
335
335
  exclude?: readonly string[];
336
+ /**
337
+ * Top-level dir names shared across skills, e.g. `["scripts", "references"]`.
338
+ * OPT-IN: many skill libraries keep ONE top-level `scripts/`/`references/` tree
339
+ * rather than a copy beside every `SKILL.md`, so a bundled ref like
340
+ * `scripts/promptfoo/x.py` lives at the REPO ROOT. When a `SKILL.md` body ref's
341
+ * first path segment is a declared shared dir, `skill-resource-resolves` / audit
342
+ * ALSO resolves it against the repo root, not only the skill's own dir. Scoped
343
+ * to declared dirs on purpose: a repo that omits this key behaves exactly as
344
+ * before (skill-dir-only resolution), and a ref outside a shared dir is never
345
+ * masked by a same-named repo-root file. See feedback P1-4.
346
+ */
347
+ sharedDirs?: readonly string[];
336
348
  /**
337
349
  * The harness(es) this repo targets — selects the compile dialect / skill
338
350
  * frontmatter profile / instruction-file shape, instead of sniffing the cwd.
@@ -9,7 +9,7 @@
9
9
  * the `claude` CLI) + a running session tally. We deliberately do NOT show a
10
10
  * "% of your subscription" — Anthropic does not expose a subscription's quota or
11
11
  * limit programmatically (and the real limits are rolling rate windows, not a
12
- * dollar bucket), so any percentage would be fiction. See docs/eval-architecture.md.
12
+ * dollar bucket), so any percentage would be fiction. See research/eval-architecture.md.
13
13
  *
14
14
  * Pure + injectable (env + an output sink), so the whole thing is unit-tested
15
15
  * without a model or a real key.
package/dist/eval.d.ts CHANGED
@@ -43,7 +43,7 @@ export interface EvalArm {
43
43
  * opus: { model: "claude-opus-4-8" } }` — so model-as-an-arm answers "does my
44
44
  * harness still hold on the cheaper tier / after a model upgrade?" through the
45
45
  * same significance machinery, with no separate model-matrix runner. Omit to
46
- * use the eval-level model. See `docs/eval-architecture.md` (model strategy).
46
+ * use the eval-level model. See `research/eval-architecture.md` (model strategy).
47
47
  */
48
48
  readonly model?: string;
49
49
  }
@@ -491,7 +491,7 @@ export declare function aggregateUsage(usages: readonly EvalUsage[]): ArmUsage;
491
491
  * e.g. `claude-haiku-4-5-20251001`. A floating alias (`haiku`, `sonnet`, or even
492
492
  * `claude-sonnet-4-6` with no date) can change underneath you — so a cached or
493
493
  * baselined result pinned to it can silently hide model drift. See
494
- * `docs/eval-architecture.md` (honest model pinning).
494
+ * `research/eval-architecture.md` (honest model pinning).
495
495
  */
496
496
  export declare function isDatedModel(model: string): boolean;
497
497
  /**
package/dist/eval.js CHANGED
@@ -665,7 +665,7 @@ function isRecord(v) {
665
665
  * e.g. `claude-haiku-4-5-20251001`. A floating alias (`haiku`, `sonnet`, or even
666
666
  * `claude-sonnet-4-6` with no date) can change underneath you — so a cached or
667
667
  * baselined result pinned to it can silently hide model drift. See
668
- * `docs/eval-architecture.md` (honest model pinning).
668
+ * `research/eval-architecture.md` (honest model pinning).
669
669
  */
670
670
  function isDatedModel(model) {
671
671
  return /\d{8}$/.test(model);
@@ -214,6 +214,12 @@ function computeIntegrityScore(deductions) {
214
214
  }
215
215
  return { score: Math.max(0, 100 - penalty), penalty };
216
216
  }
217
+ /** Resolve the terse "thing(s)" plural placeholder against a count:
218
+ * n===1 drops the "(s)" ("1 tool"); otherwise it becomes "s" ("3 tools").
219
+ * (Kept local — audit-score.ts has its own copy to avoid a circular import.) */
220
+ function pluralizeLabel(n, label) {
221
+ return label.replace(/\(s\)/g, n === 1 ? "" : "s");
222
+ }
217
223
  /** Deterministic structural-health score for one scanned plugin. */
218
224
  function scoreReport(r) {
219
225
  // An empty/unloadable machine isn't healthy — it's a non-plugin or a broken
@@ -230,7 +236,7 @@ function scoreReport(r) {
230
236
  for (const d of deductions) {
231
237
  if (d.n === 0)
232
238
  continue;
233
- issues.push(`${String(d.n)} ${d.label}`);
239
+ issues.push(`${String(d.n)} ${pluralizeLabel(d.n, d.label)}`);
234
240
  }
235
241
  // Sort issues by cost (worst first) so the report leads with what matters.
236
242
  issues.sort((a, b) => Number(b.split(" ")[0]) - Number(a.split(" ")[0]));
@@ -241,10 +247,10 @@ function scoreReport(r) {
241
247
  // - untested surfaces: a hardening gap, not breakage.
242
248
  const noContract = r.agents.filter((a) => a.tools === null).length;
243
249
  if (noContract > 0) {
244
- issues.push(`${String(noContract)} agent(s) inherit all tools (no contract) (advisory)`);
250
+ issues.push(`${String(noContract)} ${pluralizeLabel(noContract, "agent(s) inherit all tools (no contract) (advisory)")}`);
245
251
  }
246
252
  if (r.untested > 0) {
247
- issues.push(`${String(r.untested)} untested surface(s) (advisory)`);
253
+ issues.push(`${String(r.untested)} ${pluralizeLabel(r.untested, "untested surface(s) (advisory)")}`);
248
254
  }
249
255
  return { score, issues };
250
256
  }
@@ -14,6 +14,15 @@ export interface LoadedPlugin {
14
14
  * read it in a test, or just to know what the deterministic run won't reach.
15
15
  */
16
16
  readonly warnings: readonly string[];
17
+ /**
18
+ * Map from a materialized `files` key to the ABSOLUTE on-disk path it was read
19
+ * from. A surface can be materialized under a canonical key (`.claude/skills/
20
+ * foo/SKILL.md`) while living on disk at a different root (the repo-root
21
+ * `skills/` OR the project-level `.claude/skills/`), so a consumer that needs
22
+ * the real dir (e.g. resolving a skill's bundled resources) must reverse-map
23
+ * through this instead of guessing from the key. Present for every surface file.
24
+ */
25
+ readonly sources: Record<string, string>;
17
26
  }
18
27
  /**
19
28
  * Load the real harness at `pluginPath`. Returns the resolved settings (hooks),
@@ -36,6 +36,7 @@ exports.resolveHarness = resolveHarness;
36
36
  const node_fs_1 = require("node:fs");
37
37
  const node_path_1 = require("node:path");
38
38
  const toml_1 = require("@iarna/toml");
39
+ const hash_js_1 = require("./core/hash.js");
39
40
  const MAX_SKILL_FILE_BYTES = 256 * 1024;
40
41
  /** Parse a JSON file, or null on any error (missing / malformed). */
41
42
  function safeReadJson(path) {
@@ -140,32 +141,108 @@ function loadPlugin(pluginPath, layout) {
140
141
  ? JSON.parse(JSON.stringify(hooks).replaceAll(layout.pluginRootToken, root))
141
142
  : undefined;
142
143
  const files = {};
144
+ const sources = {};
143
145
  const instructions = (0, node_path_1.join)(root, layout.instructionFile);
144
146
  if ((0, node_fs_1.existsSync)(instructions)) {
145
147
  files[layout.instructionFile] = (0, node_fs_1.readFileSync)(instructions, "utf-8");
148
+ sources[layout.instructionFile] = instructions;
146
149
  }
147
- // Materialize each project-level surface under the materialize root so the
148
- // assembled context is present in the sandbox (best-effort — headless
149
- // activation of plugin skills/subagents/commands is not guaranteed; the body
150
- // is present for the agent to read either way). Counting what we materialize
151
- // also lets us warn about surfaces the deterministic tier can't drive.
152
- const counts = {};
153
- for (const surface of layout.surfaceDirs) {
154
- const dir = (0, node_path_1.join)(root, surface);
155
- if (!(0, node_fs_1.existsSync)(dir) || !(0, node_fs_1.statSync)(dir).isDirectory())
156
- continue;
157
- const tree = readTree(dir, root);
158
- for (const [rel, content] of Object.entries(tree)) {
159
- files[(0, node_path_1.join)(layout.materializeRoot, rel)] = content;
160
- }
161
- counts[surface] = Object.keys(tree).length;
162
- }
150
+ const counts = materializeSurfaces(root, layout, files, sources);
163
151
  return {
164
152
  settings: resolvedHooks ? { hooks: resolvedHooks } : {},
165
153
  files,
154
+ sources,
166
155
  warnings: pluginWarnings(root, counts, resolvedHooks, files, layout),
167
156
  };
168
157
  }
158
+ /**
159
+ * A surface holds a LOADABLE file — a `<name>/SKILL.md` for skills, a `.md` for
160
+ * agents/commands. A stray non-surface file (`skills/README.md`, `.gitkeep`) does
161
+ * NOT count, else it would mark the root populated and shadow a plain user's real
162
+ * `.claude/skills`.
163
+ */
164
+ function surfaceHasLoadable(layout, surface, tree) {
165
+ const keys = Object.keys(tree);
166
+ return surface === layout.skillDir
167
+ ? keys.some((k) => (0, node_path_1.basename)(k) === "SKILL.md")
168
+ : keys.some((k) => k.endsWith(".md"));
169
+ }
170
+ /**
171
+ * Classify the repo shape from disk, with EXPLICIT precedence:
172
+ * 1. a `<root>/SKILL.md` → the target IS one skill dir (single-skill).
173
+ * 2. any root-surface with LOADABLE content, OR a plugin manifest / hooks
174
+ * convention → read the ROOT surfaces. A plugin ships from its manifest even
175
+ * with no root surface dirs, so its dev `.claude/…` is never a fallback.
176
+ * 3. else, if the layout declares a user-surface root → a plain user repo.
177
+ * 4. else → nothing loadable.
178
+ * Pure over the pre-read `rootTrees` + a few existence checks — one place to test.
179
+ */
180
+ function classifySurfaceSource(root, layout, rootTrees) {
181
+ if (layout.skillDir && (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "SKILL.md"))) {
182
+ return { kind: "single-skill", skillName: (0, node_path_1.basename)(root) };
183
+ }
184
+ const rootHasLoadable = layout.surfaceDirs.some((s) => surfaceHasLoadable(layout, s, rootTrees.get(s) ?? {}));
185
+ const isPluginShaped = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, layout.manifestPath)) ||
186
+ (0, node_fs_1.existsSync)((0, node_path_1.join)(root, layout.hooksConventionPath));
187
+ if (rootHasLoadable || isPluginShaped)
188
+ return { kind: "root" };
189
+ if (layout.userSurfaceRoot !== undefined) {
190
+ return { kind: "user", sub: layout.userSurfaceRoot };
191
+ }
192
+ return { kind: "none" };
193
+ }
194
+ function materializeSurfaces(root, layout, files, sources) {
195
+ const counts = {};
196
+ const isDir = (p) => (0, node_fs_1.existsSync)(p) && (0, node_fs_1.statSync)(p).isDirectory();
197
+ // Read each ROOT-level surface tree once (keys relative to the surface dir).
198
+ const rootTrees = new Map();
199
+ for (const surface of layout.surfaceDirs) {
200
+ const dir = (0, node_path_1.join)(root, surface);
201
+ if (isDir(dir))
202
+ rootTrees.set(surface, readTree(dir, dir));
203
+ }
204
+ const add = (key, content, onDisk) => {
205
+ files[key] = content;
206
+ sources[key] = onDisk;
207
+ };
208
+ const source = classifySurfaceSource(root, layout, rootTrees);
209
+ switch (source.kind) {
210
+ case "single-skill": {
211
+ // Materialize the WHOLE skill dir under the canonical skills key, so its
212
+ // bundled resources (scripts/references/assets) ship too, not just SKILL.md.
213
+ const tree = readTree(root, root);
214
+ for (const [rel, content] of Object.entries(tree)) {
215
+ add((0, node_path_1.join)(layout.materializeRoot, layout.skillDir, source.skillName, rel), content, (0, node_path_1.join)(root, rel));
216
+ }
217
+ counts[layout.skillDir] = Object.keys(tree).length;
218
+ break;
219
+ }
220
+ case "root":
221
+ case "user": {
222
+ const base = source.kind === "user" ? (0, node_path_1.join)(root, source.sub) : root;
223
+ for (const surface of layout.surfaceDirs) {
224
+ const dir = (0, node_path_1.join)(base, surface);
225
+ // Root surfaces were pre-read; user surfaces are read fresh here.
226
+ const tree = source.kind === "root"
227
+ ? (rootTrees.get(surface) ?? {})
228
+ : isDir(dir)
229
+ ? readTree(dir, dir)
230
+ : {};
231
+ for (const [rel, content] of Object.entries(tree)) {
232
+ add((0, node_path_1.join)(layout.materializeRoot, surface, rel), content, (0, node_path_1.join)(dir, rel));
233
+ }
234
+ counts[surface] = Object.keys(tree).length;
235
+ }
236
+ break;
237
+ }
238
+ case "none":
239
+ break;
240
+ /* v8 ignore next 2 -- exhaustiveness guard, unreachable given SurfaceSource */
241
+ default:
242
+ (0, hash_js_1.assertNever)(source);
243
+ }
244
+ return counts;
245
+ }
169
246
  /**
170
247
  * Flag surfaces present-but-not-deterministically-exercisable. Subagents
171
248
  * (`agents/`) and slash commands (`commands/`) are materialized into the sandbox
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Rule-inventory — the deterministic, foreign-safe teaser surface of the
3
+ * `audit` rule-compile tier (design: `research/audit-rule-compile-tier.md`).
4
+ *
5
+ * Finds prose rules in a `CLAUDE.md` / `AGENTS.md` that map to an off-the-shelf
6
+ * lint rule, and whether that rule already appears in the repo's lint config.
7
+ * The remedy for a documented-but-unconfigured intent is a one-line config
8
+ * change, not synthesis — so this is a cheap, high-value nudge.
9
+ *
10
+ * NO model. NO config execution (textual grep only — never resolves/executes
11
+ * `eslint.config.js`, which would be the RCE path). HIGH PRECISION by
12
+ * construction: only rule-name / code-token-shaped keywords are matched, and
13
+ * only as whole tokens. Bare prose words are excluded on purpose — a raw
14
+ * keyword match (`token`, `!`, `await`, `secret`, `silently`, …) sprays false
15
+ * positives on real instruction files (measured: 107 raw hits over 4 real
16
+ * CLAUDE.md files, ~all garbage). The model-driven analysis (extract → classify
17
+ * → compile → gate → run) lives in the OPT-IN tier, not here.
18
+ *
19
+ * MULTI-LINTER by shape, ESLint-first by data. The matcher is linter-agnostic;
20
+ * each mapping is keyed by linter, so adding Ruff / Clippy / Pylint / RuboCop /
21
+ * Stylelint is additive DATA (a curation task), not a refactor. The OPT-IN tier
22
+ * should resolve "is this rule enabled" via vigiles's existing multi-linter
23
+ * `checkLinterRule` engine (which execs the config) — kept out of this
24
+ * exec-free, foreign-safe surface on purpose.
25
+ */
26
+ /** Linters vigiles's cross-reference engine already understands. */
27
+ export type LinterName = "eslint" | "ruff" | "clippy" | "pylint" | "rubocop" | "stylelint";
28
+ /** One prose→rule mapping for a single linter. `keywords` are rule-name/token-shaped. */
29
+ export interface IntentMapping {
30
+ readonly intent: string;
31
+ readonly linter: LinterName;
32
+ /** Whole-token, code-shaped triggers only (no bare English words). */
33
+ readonly keywords: readonly string[];
34
+ /** The off-the-shelf rule that enforces this intent. */
35
+ readonly rule: string;
36
+ /** The one-line config change that turns it on. */
37
+ readonly configFix: string;
38
+ /** True if a `recommended` preset typically enables this rule (so a bare
39
+ * recommended-extends is evidence it may already be on). */
40
+ readonly inRecommended?: boolean;
41
+ }
42
+ /**
43
+ * Curated from agent-rules-compiler's `rule-map.json`, keeping ONLY the
44
+ * specific (rule-name / code-token) keywords and dropping every bare-word
45
+ * trigger the FP measurement flagged (`token`, `secret`, `password`, `await`,
46
+ * `!`, `aria`, `silently`, `prefix`, `complexity`, `barrel`, `cycle`, …).
47
+ *
48
+ * ESLint-only today — Ruff/Clippy/Pylint/RuboCop/Stylelint entries append here
49
+ * with their own `linter` + rule-name keywords, no code change.
50
+ */
51
+ export declare const INTENT_MAP: readonly IntentMapping[];
52
+ /**
53
+ * Whether the mapped rule is visible in the lint config text (textual grep —
54
+ * imperfect, labelled). `contradiction` is the sharpest state: the harness
55
+ * documents the rule as a norm, yet the config EXPLICITLY sets it to off/0 —
56
+ * the docs and the config disagree.
57
+ */
58
+ export type ConfigState = "in-config" | "not-in-config" | "preset-maybe" | "contradiction";
59
+ /** One documented-intent → off-the-shelf-rule finding. */
60
+ export interface RuleInventoryItem {
61
+ readonly intent: string;
62
+ readonly linter: LinterName;
63
+ /** The rule-name/token that matched in the instruction file. */
64
+ readonly matched: string;
65
+ /** The off-the-shelf rule that enforces it. */
66
+ readonly rule: string;
67
+ /** Whether `rule` appears anywhere in the provided config text. */
68
+ readonly configState: ConfigState;
69
+ /** The one-line config change to enforce it (shown when not in config). */
70
+ readonly configFix: string;
71
+ }
72
+ /** Options for {@link buildRuleInventory}. */
73
+ export interface RuleInventoryOptions {
74
+ /**
75
+ * Restrict to the repo's detected linter(s). When omitted, all linters are
76
+ * considered — safe because the keywords are rule-name-specific, but a caller
77
+ * that knows the repo is Python-only can pass `["ruff"]` to avoid a stray
78
+ * cross-language rule-name collision.
79
+ */
80
+ readonly linters?: readonly LinterName[];
81
+ }
82
+ /**
83
+ * A keyword matches only as a WHOLE token: bounded by start/end or a
84
+ * non-`[\w/@.-]` character on each side (so `no-console` matches in
85
+ * `` `no-console` `` and `enforce no-console;` but `no-console-x` does not,
86
+ * and prose containing the substring elsewhere never trips it).
87
+ */
88
+ export declare function matchesWholeToken(text: string, keyword: string): boolean;
89
+ export declare function buildRuleInventory(instructionText: string, configText: string, options?: RuleInventoryOptions): RuleInventoryItem[];
90
+ //# sourceMappingURL=rule-inventory.d.ts.map