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.
- package/README.md +43 -15
- package/dist/adapters/claude-code/layout.js +4 -0
- package/dist/audit-report.d.ts +38 -0
- package/dist/audit-report.js +13 -2
- package/dist/audit-report.template.html +44 -29
- package/dist/audit-score.js +9 -1
- package/dist/audit-verdict.d.ts +89 -0
- package/dist/audit-verdict.js +281 -0
- package/dist/cli.js +235 -66
- package/dist/core/layout.d.ts +11 -0
- package/dist/core/skill-resources.d.ts +16 -0
- package/dist/core/skill-resources.js +32 -4
- package/dist/core/types.d.ts +12 -0
- package/dist/eval-cost.d.ts +1 -1
- package/dist/eval.d.ts +2 -2
- package/dist/eval.js +1 -1
- package/dist/leaderboard.js +9 -3
- package/dist/plugin-loader.d.ts +9 -0
- package/dist/plugin-loader.js +93 -16
- package/dist/rule-inventory.d.ts +90 -0
- package/dist/rule-inventory.js +327 -0
- package/dist/rule-routing.d.ts +46 -0
- package/dist/rule-routing.js +135 -0
- package/dist/scaffold-test.js +1 -1
- package/dist/scan.d.ts +4 -1
- package/dist/scan.js +36 -5
- package/dist/segment.d.ts +33 -0
- package/dist/segment.js +454 -0
- package/dist/test-coverage.js +29 -3
- package/package.json +1 -1
package/dist/core/layout.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
76
|
-
//
|
|
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
|
-
|
|
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}`;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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.
|
package/dist/eval-cost.d.ts
CHANGED
|
@@ -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
|
|
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 `
|
|
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
|
-
* `
|
|
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
|
-
* `
|
|
668
|
+
* `research/eval-architecture.md` (honest model pinning).
|
|
669
669
|
*/
|
|
670
670
|
function isDatedModel(model) {
|
|
671
671
|
return /\d{8}$/.test(model);
|
package/dist/leaderboard.js
CHANGED
|
@@ -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
|
}
|
package/dist/plugin-loader.d.ts
CHANGED
|
@@ -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),
|
package/dist/plugin-loader.js
CHANGED
|
@@ -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
|
-
|
|
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
|