vigiles 13.0.0 → 14.1.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 +10 -4
- package/dist/audit-report.d.ts +10 -4
- package/dist/audit-report.js +11 -3
- package/dist/audit-report.template.html +29 -29
- package/dist/cli.js +256 -41
- package/dist/core/rule-catalog.d.ts +101 -0
- package/dist/core/rule-catalog.js +294 -0
- package/dist/eval.d.ts +13 -1
- package/dist/eval.js +13 -1
- package/dist/instruction-sources.d.ts +39 -0
- package/dist/instruction-sources.js +71 -0
- package/dist/rule-inventory.d.ts +6 -0
- package/dist/rule-inventory.js +170 -1
- package/dist/rule-routing.d.ts +73 -3
- package/dist/rule-routing.js +437 -27
- package/dist/segment.d.ts +23 -1
- package/dist/segment.js +182 -26
- package/package.json +2 -2
- package/skills/linter-docs/clippy.md +1 -1
- package/skills/linter-docs/eslint.md +1 -1
- package/skills/linter-docs/pylint.md +1 -1
- package/skills/linter-docs/rubocop.md +1 -1
- package/skills/linter-docs/ruff.md +1 -1
- package/skills/linter-docs/stylelint.md +1 -1
- package/skills/strengthen/SKILL.md +4 -4
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles rule-catalog — the DYNAMIC available-rule catalog.
|
|
4
|
+
*
|
|
5
|
+
* Enumerates every lint rule the repo's linter ACTUALLY has — core built-ins PLUS
|
|
6
|
+
* every installed plugin's rules — so prose can be matched against the LIVE
|
|
7
|
+
* catalog instead of a static hand-curated map. On this repo one ESLint API call
|
|
8
|
+
* yields ~702 available rules (292 core + 410 plugin: typescript-eslint / sonarjs
|
|
9
|
+
* / boundaries), of which ~140 are enabled — vs the old static map's ~23. That
|
|
10
|
+
* makes an architecture norm enforceable too (`boundaries/dependencies` is in the
|
|
11
|
+
* catalog), which a static map never captured. See
|
|
12
|
+
* `research/rule-compiler-multilang-design.md` §0 (the spike this productizes).
|
|
13
|
+
*
|
|
14
|
+
* SAFETY — this EXECUTES the linter. Loading ESLint resolves the repo's real
|
|
15
|
+
* config (which can run plugin/config code), so `enumerateEslintCatalog` is an
|
|
16
|
+
* OWN-REPO / consented capability, NOT the foreign-safe default. The deterministic
|
|
17
|
+
* default rule-compile tier stays purely TEXTUAL (it parses config, never loads
|
|
18
|
+
* it); reach for this only where executing the repo's toolchain is already
|
|
19
|
+
* consented (own repo, on the user's machine). Mirrors the subprocess pattern of
|
|
20
|
+
* `discoverEslintRules` in `src/core/generate-types.ts`: ESLint is loaded in a
|
|
21
|
+
* child `node -e` process at the repo's cwd so it stays out of our process.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.parseEslintCatalog = parseEslintCatalog;
|
|
25
|
+
exports.parsePylintCatalog = parsePylintCatalog;
|
|
26
|
+
exports.mergeCatalogs = mergeCatalogs;
|
|
27
|
+
exports.enumerateEslintCatalog = enumerateEslintCatalog;
|
|
28
|
+
exports.enumeratePylintCatalog = enumeratePylintCatalog;
|
|
29
|
+
const node_path_1 = require("node:path");
|
|
30
|
+
const node_child_process_1 = require("node:child_process");
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Pure parse: subprocess JSON → typed catalog (covered by the unit test)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
function isStringArray(v) {
|
|
35
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
36
|
+
}
|
|
37
|
+
function parsePlugins(v) {
|
|
38
|
+
if (typeof v !== "object" || v === null)
|
|
39
|
+
return {};
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [prefix, rules] of Object.entries(v)) {
|
|
42
|
+
if (isStringArray(rules))
|
|
43
|
+
out[prefix] = rules;
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function buildRules(core, plugins, enabledSet) {
|
|
48
|
+
const rules = [];
|
|
49
|
+
for (const id of core) {
|
|
50
|
+
rules.push({
|
|
51
|
+
id,
|
|
52
|
+
linter: "eslint",
|
|
53
|
+
plugin: null,
|
|
54
|
+
enabled: enabledSet.has(id),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const [prefix, pluginRules] of Object.entries(plugins)) {
|
|
58
|
+
for (const rule of pluginRules) {
|
|
59
|
+
const id = `${prefix}/${rule}`;
|
|
60
|
+
rules.push({
|
|
61
|
+
id,
|
|
62
|
+
linter: "eslint",
|
|
63
|
+
plugin: prefix,
|
|
64
|
+
enabled: enabledSet.has(id),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return rules;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parse the enumeration subprocess's JSON payload into a typed {@link RuleCatalog}.
|
|
72
|
+
*
|
|
73
|
+
* Payload shape: `{ core: string[]; enabled: string[]; plugins: Record<prefix, string[]> }`.
|
|
74
|
+
* Returns null on `"null"` / malformed input / an empty catalog (mirrors
|
|
75
|
+
* `discoverEslintRules` returning null when nothing is found).
|
|
76
|
+
*/
|
|
77
|
+
function parseEslintCatalog(raw) {
|
|
78
|
+
const trimmed = raw.trim();
|
|
79
|
+
if (!trimmed || trimmed === "null")
|
|
80
|
+
return null;
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(trimmed);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
89
|
+
return null;
|
|
90
|
+
const obj = parsed;
|
|
91
|
+
const core = isStringArray(obj.core) ? obj.core : [];
|
|
92
|
+
const enabledList = isStringArray(obj.enabled) ? obj.enabled : [];
|
|
93
|
+
const plugins = parsePlugins(obj.plugins);
|
|
94
|
+
const rules = buildRules(core, plugins, new Set(enabledList));
|
|
95
|
+
if (rules.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
const enabled = rules.reduce((n, r) => (r.enabled ? n + 1 : n), 0);
|
|
98
|
+
return { linter: "eslint", available: rules.length, enabled, rules };
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Pure parse: pylint's text listings → typed catalog (covered by the unit test)
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// A message line in `pylint --list-msgs`: `:invalid-name (C0103): *...*`
|
|
104
|
+
// (leading colon, no indent). In `--list-msgs-enabled`: ` invalid-name (C0103)`
|
|
105
|
+
// (indented, no colon). One regex captures the `name (CODE)` core of both.
|
|
106
|
+
const PYLINT_MSG_RE = /^\s*:?([a-z][a-z0-9-]*)\s+\(([A-Z]\d+)\)/;
|
|
107
|
+
/** Collect the symbol names under the `Enabled messages:` header ONLY.
|
|
108
|
+
*
|
|
109
|
+
* `--list-msgs-enabled` also prints `Disabled messages:` and `Non-emittable
|
|
110
|
+
* messages:` sections with the SAME `name (CODE)` line shape, so a naive
|
|
111
|
+
* line-shape parse would mislabel disabled rules as enabled. Track the current
|
|
112
|
+
* section: a non-indented line ending in `:` is a header; only lines while the
|
|
113
|
+
* "enabled" header is active count. */
|
|
114
|
+
function parseEnabledSymbols(listEnabled) {
|
|
115
|
+
const enabled = new Set();
|
|
116
|
+
let inEnabled = false;
|
|
117
|
+
for (const line of listEnabled.split("\n")) {
|
|
118
|
+
// A section header is a flush-left line ending in a colon (e.g.
|
|
119
|
+
// "Enabled messages:", "Disabled messages:"). Indented message lines never
|
|
120
|
+
// start at column 0, so this never eats a rule.
|
|
121
|
+
if (/^\S.*:\s*$/.test(line)) {
|
|
122
|
+
inEnabled = /^enabled messages:/i.test(line.trim());
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!inEnabled)
|
|
126
|
+
continue;
|
|
127
|
+
const m = PYLINT_MSG_RE.exec(line);
|
|
128
|
+
if (m)
|
|
129
|
+
enabled.add(m[1]);
|
|
130
|
+
}
|
|
131
|
+
return enabled;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parse pylint's `--list-msgs` (available) + `--list-msgs-enabled` (enabled)
|
|
135
|
+
* text listings into a typed {@link RuleCatalog}.
|
|
136
|
+
*
|
|
137
|
+
* The available set is every emittable message (`:name (CODE):` lines); enabled
|
|
138
|
+
* is the section-scoped subset. Each rule is matchable by BOTH its symbolic name
|
|
139
|
+
* (`id`) and its numeric code (`code`), since a doc may name either. Returns null
|
|
140
|
+
* when no message parses (pylint absent, or malformed output).
|
|
141
|
+
*/
|
|
142
|
+
function parsePylintCatalog(listMsgs, listEnabled) {
|
|
143
|
+
const enabledSet = parseEnabledSymbols(listEnabled);
|
|
144
|
+
const rules = [];
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
for (const line of listMsgs.split("\n")) {
|
|
147
|
+
// Available lines carry a leading colon; skip anything else (headers, the
|
|
148
|
+
// wrapped description lines, blank lines).
|
|
149
|
+
if (!line.startsWith(":"))
|
|
150
|
+
continue;
|
|
151
|
+
const m = PYLINT_MSG_RE.exec(line);
|
|
152
|
+
if (!m)
|
|
153
|
+
continue;
|
|
154
|
+
const [, name, code] = m;
|
|
155
|
+
if (seen.has(name))
|
|
156
|
+
continue;
|
|
157
|
+
seen.add(name);
|
|
158
|
+
rules.push({
|
|
159
|
+
id: name,
|
|
160
|
+
linter: "pylint",
|
|
161
|
+
plugin: null,
|
|
162
|
+
code,
|
|
163
|
+
enabled: enabledSet.has(name),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (rules.length === 0)
|
|
167
|
+
return null;
|
|
168
|
+
const enabled = rules.reduce((n, r) => (r.enabled ? n + 1 : n), 0);
|
|
169
|
+
return { linter: "pylint", available: rules.length, enabled, rules };
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Merge: a polyglot repo (JS + Python) yields two catalogs → one for routing
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
/**
|
|
175
|
+
* Merge the catalogs of every linter a repo has into ONE catalog for routing.
|
|
176
|
+
*
|
|
177
|
+
* KEEPS EVERY entry — a polyglot repo genuinely has a rule in each linter, so an
|
|
178
|
+
* id shared across ESLint and Pylint (`no-else-return`) is two real rules and
|
|
179
|
+
* both are retained (never dropped by id, the bug that let a Python doc inherit
|
|
180
|
+
* ESLint's state). Collision handling belongs at the ROUTING lookup, not here: a
|
|
181
|
+
* bare id that resolves to two hits is combined conservatively there, while a
|
|
182
|
+
* numeric code (unique to its linter) keeps its own hit — see `buildCatalogLookup`
|
|
183
|
+
* in rule-routing.ts. So the merge is a plain concatenation; each rule carries its
|
|
184
|
+
* own `linter`/`enabled`/`code` provenance. Returns undefined when nothing was
|
|
185
|
+
* enumerated (so a non-JS-non-Python repo is byte-identical to before this existed).
|
|
186
|
+
*/
|
|
187
|
+
function mergeCatalogs(...cats) {
|
|
188
|
+
const present = cats.filter((c) => c != null);
|
|
189
|
+
if (present.length === 0)
|
|
190
|
+
return undefined;
|
|
191
|
+
if (present.length === 1)
|
|
192
|
+
return present[0];
|
|
193
|
+
const rules = present.flatMap((c) => c.rules);
|
|
194
|
+
const enabled = rules.reduce((n, r) => (r.enabled ? n + 1 : n), 0);
|
|
195
|
+
return {
|
|
196
|
+
linter: present[0].linter,
|
|
197
|
+
available: rules.length,
|
|
198
|
+
enabled,
|
|
199
|
+
rules,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Real-IO seam: run ESLint in a child process at the repo's cwd
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
/* v8 ignore start -- spawns a `node -e` subprocess that LOADS ESLint in the
|
|
206
|
+
repo's cwd (the executes-the-linter seam); the pure JSON→typed parse is
|
|
207
|
+
parseEslintCatalog, covered by the unit test, and the gated integration test
|
|
208
|
+
drives this real path when eslint resolves. */
|
|
209
|
+
/**
|
|
210
|
+
* Enumerate the repo's available ESLint rules (core + every installed plugin).
|
|
211
|
+
*
|
|
212
|
+
* EXECUTES the repo's ESLint in a child process — an own-repo / consented
|
|
213
|
+
* capability (see the file header). Returns null if ESLint isn't resolvable, no
|
|
214
|
+
* config applies, or the subprocess fails.
|
|
215
|
+
*/
|
|
216
|
+
function enumerateEslintCatalog(root) {
|
|
217
|
+
try {
|
|
218
|
+
// A `.ts` path under src/ so the repo's flat config applies its TypeScript +
|
|
219
|
+
// plugin blocks (typescript-eslint / sonarjs / boundaries all scope to
|
|
220
|
+
// `src/**/*.ts`); the path need not exist — calculateConfigForFile resolves
|
|
221
|
+
// the config, it does not read the file.
|
|
222
|
+
const probeFile = (0, node_path_1.resolve)(root, "src/index.ts");
|
|
223
|
+
const script = `
|
|
224
|
+
const { loadESLint } = require("eslint");
|
|
225
|
+
const { builtinRules } = require("eslint/use-at-your-own-risk");
|
|
226
|
+
(async () => {
|
|
227
|
+
try {
|
|
228
|
+
const ESLint = await loadESLint();
|
|
229
|
+
const eslint = new ESLint({ cwd: ${JSON.stringify(root)} });
|
|
230
|
+
const cfg = await eslint.calculateConfigForFile(${JSON.stringify(probeFile)});
|
|
231
|
+
const core = [...builtinRules.keys()];
|
|
232
|
+
const enabled = Object.entries(cfg.rules || {})
|
|
233
|
+
.filter(([, v]) => {
|
|
234
|
+
const sev = Array.isArray(v) ? v[0] : v;
|
|
235
|
+
return sev !== 0 && sev !== "off";
|
|
236
|
+
})
|
|
237
|
+
.map(([k]) => k);
|
|
238
|
+
const plugins = {};
|
|
239
|
+
for (const [prefix, plugin] of Object.entries(cfg.plugins || {})) {
|
|
240
|
+
plugins[prefix] = Object.keys((plugin && plugin.rules) || {});
|
|
241
|
+
}
|
|
242
|
+
console.log(JSON.stringify({ core, enabled, plugins }));
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.log("null");
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
`;
|
|
248
|
+
const output = (0, node_child_process_1.execSync)(`node -e '${script.replace(/'/g, "'\\''")}'`, {
|
|
249
|
+
encoding: "utf-8",
|
|
250
|
+
cwd: root,
|
|
251
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
252
|
+
timeout: 15000,
|
|
253
|
+
});
|
|
254
|
+
return parseEslintCatalog(output);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/* v8 ignore stop */
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Real-IO seam: run pylint in a child process at the repo's cwd
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
/* v8 ignore start -- spawns the repo's `pylint` twice (the executes-the-linter
|
|
265
|
+
seam); the pure text→typed parse is parsePylintCatalog, covered by the unit
|
|
266
|
+
test, and the gated integration test drives this real path when pylint is on
|
|
267
|
+
PATH. */
|
|
268
|
+
/**
|
|
269
|
+
* Enumerate the repo's available Pylint messages (core + every loaded plugin)
|
|
270
|
+
* and their enabled state, via `pylint --list-msgs` + `--list-msgs-enabled`.
|
|
271
|
+
*
|
|
272
|
+
* Run at the repo's cwd so its rcfile (`.pylintrc` / `pyproject.toml` /
|
|
273
|
+
* `setup.cfg`) and `load-plugins` apply — so the listing reflects the repo's
|
|
274
|
+
* REAL rule set, plugins included, with correct enabled state. Like the ESLint
|
|
275
|
+
* catalog this EXECUTES the linter (loading a pylint plugin imports its module),
|
|
276
|
+
* so it's an OWN-REPO / consented capability, NOT the foreign-safe default.
|
|
277
|
+
* Returns null when pylint isn't runnable or lists nothing.
|
|
278
|
+
*/
|
|
279
|
+
function enumeratePylintCatalog(root) {
|
|
280
|
+
const run = (args) => (0, node_child_process_1.execSync)(`pylint ${args}`, {
|
|
281
|
+
encoding: "utf-8",
|
|
282
|
+
cwd: root,
|
|
283
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
284
|
+
timeout: 15000,
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
return parsePylintCatalog(run("--list-msgs"), run("--list-msgs-enabled"));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/* v8 ignore stop */
|
|
294
|
+
//# sourceMappingURL=rule-catalog.js.map
|
package/dist/eval.d.ts
CHANGED
|
@@ -755,7 +755,19 @@ export interface EvalDriver {
|
|
|
755
755
|
*/
|
|
756
756
|
readonly harness?: string;
|
|
757
757
|
}
|
|
758
|
-
/**
|
|
758
|
+
/**
|
|
759
|
+
* The default (Claude Code) eval driver: real `claude` + stream-json parsing.
|
|
760
|
+
*
|
|
761
|
+
* This lives at the COMPOSITION ROOT (`src/eval.ts`) on purpose, not in
|
|
762
|
+
* `src/adapters/claude-code/` — it is NOT a boundary leak. Claude Code is the
|
|
763
|
+
* wired DEFAULT (`measureTriggerRate`/`runEval` fall back to it), so `eval.ts`
|
|
764
|
+
* must reference it directly; wiring the default is precisely a composition
|
|
765
|
+
* root's job. `codexEvalDriver` lives in its adapter dir instead because Codex
|
|
766
|
+
* is caller-INJECTED (never a default), so `eval.ts` never imports it. Relocating
|
|
767
|
+
* this into the adapter would make `eval.ts → adapters/claude-code → eval.ts` a
|
|
768
|
+
* circular import (the shared `EvalDriver`/`ModelOutputParser` types live here).
|
|
769
|
+
* The asymmetry reflects default-vs-injected, not a hexagonal violation.
|
|
770
|
+
*/
|
|
759
771
|
export declare const claudeEvalDriver: EvalDriver;
|
|
760
772
|
/**
|
|
761
773
|
* Package loose `<skillsDir>/<name>/SKILL.md` skills into a throwaway plugin dir
|
package/dist/eval.js
CHANGED
|
@@ -1240,7 +1240,19 @@ function formatEvalReport(report) {
|
|
|
1240
1240
|
}
|
|
1241
1241
|
return lines.join("\n");
|
|
1242
1242
|
}
|
|
1243
|
-
/**
|
|
1243
|
+
/**
|
|
1244
|
+
* The default (Claude Code) eval driver: real `claude` + stream-json parsing.
|
|
1245
|
+
*
|
|
1246
|
+
* This lives at the COMPOSITION ROOT (`src/eval.ts`) on purpose, not in
|
|
1247
|
+
* `src/adapters/claude-code/` — it is NOT a boundary leak. Claude Code is the
|
|
1248
|
+
* wired DEFAULT (`measureTriggerRate`/`runEval` fall back to it), so `eval.ts`
|
|
1249
|
+
* must reference it directly; wiring the default is precisely a composition
|
|
1250
|
+
* root's job. `codexEvalDriver` lives in its adapter dir instead because Codex
|
|
1251
|
+
* is caller-INJECTED (never a default), so `eval.ts` never imports it. Relocating
|
|
1252
|
+
* this into the adapter would make `eval.ts → adapters/claude-code → eval.ts` a
|
|
1253
|
+
* circular import (the shared `EvalDriver`/`ModelOutputParser` types live here).
|
|
1254
|
+
* The asymmetry reflects default-vs-injected, not a hexagonal violation.
|
|
1255
|
+
*/
|
|
1244
1256
|
exports.claudeEvalDriver = {
|
|
1245
1257
|
runner: spawnAgent,
|
|
1246
1258
|
parse: parseClaudeRun,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* instruction-sources.ts — the pure policy for WHICH instruction files the audit
|
|
3
|
+
* rule-routing preview reads.
|
|
4
|
+
*
|
|
5
|
+
* A repo's real subdirectory memory (`src/CLAUDE.md`, `research/CLAUDE.md`, a
|
|
6
|
+
* nested `AGENTS.md`) IS worth routing; a fixture/demo/build/test CLAUDE.md is
|
|
7
|
+
* NOISE that would flood the preview. `isFixturePath` is the precision-first
|
|
8
|
+
* discriminator (over-skip a legit `sample-service` before flooding with fixture
|
|
9
|
+
* rules). Pure + unit-tested; the fs discovery/glue lives in cli.ts
|
|
10
|
+
* (`gatherInstructionFiles`). See research/rule-compiler-multilang-design.md §0.
|
|
11
|
+
*/
|
|
12
|
+
/** One instruction file gathered from disk, with its canonical (symlink-resolved)
|
|
13
|
+
* path so a mirror can be detected. */
|
|
14
|
+
export interface RawInstructionFile {
|
|
15
|
+
/** The repo-relative path (kept for provenance). */
|
|
16
|
+
readonly path: string;
|
|
17
|
+
/** The realpath (symlink-resolved) — a symlinked mirror shares this. */
|
|
18
|
+
readonly canonical: string;
|
|
19
|
+
readonly text: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Dedup instruction files so a `CLAUDE.md`⇄`AGENTS.md` MIRROR is routed ONCE, not
|
|
23
|
+
* double-counted (compose-with-sync-tools: a symlinked or byte-identical synced
|
|
24
|
+
* mirror is ONE logical artifact). Dedups by BOTH the canonical path (a symlink)
|
|
25
|
+
* AND the content hash (a byte-identical sync) — relative-path dedup alone caught
|
|
26
|
+
* neither. First occurrence wins (callers pass root files first). Pure.
|
|
27
|
+
*/
|
|
28
|
+
export declare function dedupeInstructionFiles(files: readonly RawInstructionFile[]): {
|
|
29
|
+
path: string;
|
|
30
|
+
text: string;
|
|
31
|
+
}[];
|
|
32
|
+
/**
|
|
33
|
+
* Is this (repo-relative) instruction-file path fixture/demo/build/test noise?
|
|
34
|
+
* True when any DIRECTORY segment (never the filename) is a build/deps/test dir
|
|
35
|
+
* OR starts with a demo/example/sample/fixture/bench/mock/scratch/tmp prefix.
|
|
36
|
+
* Conventional + general (not vigiles-specific), precision over recall.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isFixturePath(relPath: string): boolean;
|
|
39
|
+
//# sourceMappingURL=instruction-sources.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* instruction-sources.ts — the pure policy for WHICH instruction files the audit
|
|
4
|
+
* rule-routing preview reads.
|
|
5
|
+
*
|
|
6
|
+
* A repo's real subdirectory memory (`src/CLAUDE.md`, `research/CLAUDE.md`, a
|
|
7
|
+
* nested `AGENTS.md`) IS worth routing; a fixture/demo/build/test CLAUDE.md is
|
|
8
|
+
* NOISE that would flood the preview. `isFixturePath` is the precision-first
|
|
9
|
+
* discriminator (over-skip a legit `sample-service` before flooding with fixture
|
|
10
|
+
* rules). Pure + unit-tested; the fs discovery/glue lives in cli.ts
|
|
11
|
+
* (`gatherInstructionFiles`). See research/rule-compiler-multilang-design.md §0.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.dedupeInstructionFiles = dedupeInstructionFiles;
|
|
15
|
+
exports.isFixturePath = isFixturePath;
|
|
16
|
+
const hash_js_1 = require("./core/hash.js");
|
|
17
|
+
/**
|
|
18
|
+
* Dedup instruction files so a `CLAUDE.md`⇄`AGENTS.md` MIRROR is routed ONCE, not
|
|
19
|
+
* double-counted (compose-with-sync-tools: a symlinked or byte-identical synced
|
|
20
|
+
* mirror is ONE logical artifact). Dedups by BOTH the canonical path (a symlink)
|
|
21
|
+
* AND the content hash (a byte-identical sync) — relative-path dedup alone caught
|
|
22
|
+
* neither. First occurrence wins (callers pass root files first). Pure.
|
|
23
|
+
*/
|
|
24
|
+
function dedupeInstructionFiles(files) {
|
|
25
|
+
const seenReal = new Set();
|
|
26
|
+
const seenHash = new Set();
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const f of files) {
|
|
29
|
+
if (seenReal.has(f.canonical))
|
|
30
|
+
continue; // symlinked mirror
|
|
31
|
+
const hash = (0, hash_js_1.sha256short)(f.text);
|
|
32
|
+
if (seenHash.has(hash))
|
|
33
|
+
continue; // byte-identical synced mirror
|
|
34
|
+
seenReal.add(f.canonical);
|
|
35
|
+
seenHash.add(hash);
|
|
36
|
+
out.push({ path: f.path, text: f.text });
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** Directory segments that are unambiguously build / deps / test noise. */
|
|
41
|
+
const FIXTURE_DIR_EXACT = new Set([
|
|
42
|
+
"node_modules",
|
|
43
|
+
"dist",
|
|
44
|
+
"build",
|
|
45
|
+
"out",
|
|
46
|
+
"coverage",
|
|
47
|
+
".git",
|
|
48
|
+
".next",
|
|
49
|
+
".cache",
|
|
50
|
+
"test",
|
|
51
|
+
"tests",
|
|
52
|
+
"__tests__",
|
|
53
|
+
"__fixtures__",
|
|
54
|
+
"__mocks__",
|
|
55
|
+
"vendor",
|
|
56
|
+
"third_party",
|
|
57
|
+
]);
|
|
58
|
+
/** Directory-name PREFIXES that mark a fixture / demo / sample / scratch dir.
|
|
59
|
+
* Case-INSENSITIVE so `Examples/`, `Demo/` are skipped too. */
|
|
60
|
+
const FIXTURE_DIR_PREFIX = /^(?:demo|example|sample|fixture|bench|benchmark|mock|stub|scratch|tmp|\.tmp)/i;
|
|
61
|
+
/**
|
|
62
|
+
* Is this (repo-relative) instruction-file path fixture/demo/build/test noise?
|
|
63
|
+
* True when any DIRECTORY segment (never the filename) is a build/deps/test dir
|
|
64
|
+
* OR starts with a demo/example/sample/fixture/bench/mock/scratch/tmp prefix.
|
|
65
|
+
* Conventional + general (not vigiles-specific), precision over recall.
|
|
66
|
+
*/
|
|
67
|
+
function isFixturePath(relPath) {
|
|
68
|
+
const segs = relPath.split(/[/\\]/).slice(0, -1); // directories only
|
|
69
|
+
return segs.some((s) => FIXTURE_DIR_EXACT.has(s.toLowerCase()) || FIXTURE_DIR_PREFIX.test(s));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=instruction-sources.js.map
|
package/dist/rule-inventory.d.ts
CHANGED
|
@@ -84,6 +84,12 @@ export interface RuleInventoryOptions {
|
|
|
84
84
|
* non-`[\w/@.-]` character on each side (so `no-console` matches in
|
|
85
85
|
* `` `no-console` `` and `enforce no-console;` but `no-console-x` does not,
|
|
86
86
|
* and prose containing the substring elsewhere never trips it).
|
|
87
|
+
*
|
|
88
|
+
* The TRAILING boundary is a lookahead, not a consuming class, so a keyword at
|
|
89
|
+
* SENTENCE END ("No wildcard imports.") matches: a `.` is allowed unless it
|
|
90
|
+
* CONTINUES a code token (`.log` in `console.log`), which still blocks a partial
|
|
91
|
+
* match. `(?![\w/@-])` rejects a word/`/`/`@`/`-` continuation; `(?!\.[\w/@-])`
|
|
92
|
+
* rejects a dotted continuation but permits a trailing sentence `.`.
|
|
87
93
|
*/
|
|
88
94
|
export declare function matchesWholeToken(text: string, keyword: string): boolean;
|
|
89
95
|
export declare function buildRuleInventory(instructionText: string, configText: string, options?: RuleInventoryOptions): RuleInventoryItem[];
|
package/dist/rule-inventory.js
CHANGED
|
@@ -37,6 +37,12 @@ exports.buildRuleInventory = buildRuleInventory;
|
|
|
37
37
|
* ESLint-only today — Ruff/Clippy/Pylint/RuboCop/Stylelint entries append here
|
|
38
38
|
* with their own `linter` + rule-name keywords, no code change.
|
|
39
39
|
*/
|
|
40
|
+
// SCOPE (2026-07-14): this hand-curated list is a small high-precision FAST-PATH
|
|
41
|
+
// (alias enrichment for the most common rules), NOT the strategy. The strategy is
|
|
42
|
+
// the DYNAMIC available-rule catalog — enumerate the rules the repo's linter
|
|
43
|
+
// ACTUALLY has (spike: 702 for this repo vs ~23 here) and match prose against
|
|
44
|
+
// THAT, own-repo/consented since it executes the linter. Do NOT keep growing this
|
|
45
|
+
// by hand. See research/rule-compiler-multilang-design.md §0.
|
|
40
46
|
exports.INTENT_MAP = [
|
|
41
47
|
{
|
|
42
48
|
intent: "no console.log / use the logger",
|
|
@@ -196,6 +202,155 @@ exports.INTENT_MAP = [
|
|
|
196
202
|
rule: "no-warning-comments",
|
|
197
203
|
configFix: '"no-warning-comments": ["error", {"terms": ["todo", "fixme"], "location": "anywhere"}]',
|
|
198
204
|
},
|
|
205
|
+
// Grounded in the OSS-corpus sweep — rules real AGENTS.md files actually NAME
|
|
206
|
+
// (cloudflare/workers-sdk names all three inline). Keyword set is rule-name /
|
|
207
|
+
// code-shaped only, so it fires when the doc names the rule, never on prose.
|
|
208
|
+
{
|
|
209
|
+
intent: "require curly braces for control flow",
|
|
210
|
+
linter: "eslint",
|
|
211
|
+
keywords: ["curly", "curly braces"],
|
|
212
|
+
rule: "curly",
|
|
213
|
+
configFix: '"curly": ["error", "all"]',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
intent: "use import type for type-only imports",
|
|
217
|
+
linter: "eslint",
|
|
218
|
+
keywords: [
|
|
219
|
+
"consistent-type-imports",
|
|
220
|
+
"@typescript-eslint/consistent-type-imports",
|
|
221
|
+
],
|
|
222
|
+
rule: "@typescript-eslint/consistent-type-imports",
|
|
223
|
+
configFix: '"@typescript-eslint/consistent-type-imports": "error"',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
intent: "no focused / .only tests committed",
|
|
227
|
+
linter: "eslint",
|
|
228
|
+
keywords: [
|
|
229
|
+
"no-only-tests",
|
|
230
|
+
"no-focused-tests",
|
|
231
|
+
"describe.only",
|
|
232
|
+
"it.only",
|
|
233
|
+
"test.only",
|
|
234
|
+
],
|
|
235
|
+
rule: "no-only-tests/no-only-tests",
|
|
236
|
+
configFix: '"no-only-tests/no-only-tests": "error"',
|
|
237
|
+
},
|
|
238
|
+
// --- Pylint (Python) — routing basics. These feed classify() (routing → reuse);
|
|
239
|
+
// buildRuleInventory is gated to eslint (see below) because pylint is
|
|
240
|
+
// ON-BY-DEFAULT (deny-list), so the eslint-shaped config-state check would
|
|
241
|
+
// MISLABEL it (a symbol in `disable=` reads as "in-config", an absent one as
|
|
242
|
+
// "enable it"). Accurate pylint enabled-state needs the inverted-polarity
|
|
243
|
+
// ConfigProbe (research/rule-compiler-multilang-design.md §3), deferred —
|
|
244
|
+
// classify() needs NO enabled-state, so pylint prose still routes honestly.
|
|
245
|
+
// Keywords are code-shaped symbols + Python-UNAMBIGUOUS compounds (singular AND
|
|
246
|
+
// plural, since matchesWholeToken is boundary-exact); bare ambiguous words
|
|
247
|
+
// (`snake_case` — Rust/Ruby too, `import *` — JS `import * as`, "unused imports"
|
|
248
|
+
// — collides with eslint) are deliberately EXCLUDED to avoid cross-language FPs.
|
|
249
|
+
{
|
|
250
|
+
intent: "no bare except (Python)",
|
|
251
|
+
linter: "pylint",
|
|
252
|
+
keywords: ["bare-except", "bare except", "W0702"],
|
|
253
|
+
rule: "bare-except",
|
|
254
|
+
configFix: "pylint enables bare-except (W0702) by default; keep it out of the disable list",
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
intent: "no broad exception catch (Python)",
|
|
258
|
+
linter: "pylint",
|
|
259
|
+
keywords: [
|
|
260
|
+
"broad-exception-caught",
|
|
261
|
+
"broad except",
|
|
262
|
+
"broad exception",
|
|
263
|
+
"W0718",
|
|
264
|
+
],
|
|
265
|
+
rule: "broad-exception-caught",
|
|
266
|
+
configFix: "pylint enables broad-exception-caught (W0718) by default; keep it out of the disable list",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
intent: "require docstrings (Python)",
|
|
270
|
+
linter: "pylint",
|
|
271
|
+
// Bare "docstring"/"docstrings" removed — it over-fires on docstring
|
|
272
|
+
// CONTENT/STYLE rules (the dogfood caught langchain's "docstring warnings" /
|
|
273
|
+
// "backticks in docstrings"). Presence ("add docstrings", "docstrings for
|
|
274
|
+
// each") is handled by the PATTERN_RULE_MAP docstring-presence pattern in
|
|
275
|
+
// rule-routing.ts; only the rule SYMBOL matches here.
|
|
276
|
+
keywords: ["missing-docstring", "missing-function-docstring", "C0116"],
|
|
277
|
+
rule: "missing-function-docstring",
|
|
278
|
+
configFix: "pylint enables missing-function-docstring (C0116) by default; keep it out of the disable list",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
intent: "no mutable default arguments (Python)",
|
|
282
|
+
linter: "pylint",
|
|
283
|
+
keywords: [
|
|
284
|
+
"dangerous-default-value",
|
|
285
|
+
"mutable default",
|
|
286
|
+
"mutable default argument",
|
|
287
|
+
"mutable default arguments",
|
|
288
|
+
"W0102",
|
|
289
|
+
],
|
|
290
|
+
rule: "dangerous-default-value",
|
|
291
|
+
configFix: "pylint enables dangerous-default-value (W0102) by default; keep it out of the disable list",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
intent: "prefer f-strings (Python)",
|
|
295
|
+
linter: "pylint",
|
|
296
|
+
keywords: ["f-string", "f-strings", "consider-using-f-string", "C0209"],
|
|
297
|
+
rule: "consider-using-f-string",
|
|
298
|
+
configFix: "pylint enables consider-using-f-string (C0209) by default; keep it out of the disable list",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
intent: "consistent naming (Python)",
|
|
302
|
+
linter: "pylint",
|
|
303
|
+
keywords: ["invalid-name", "C0103"],
|
|
304
|
+
rule: "invalid-name",
|
|
305
|
+
configFix: "pylint enables invalid-name (C0103) by default; set naming-style in [tool.pylint], keep it out of disable",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
intent: "limit function arguments (Python)",
|
|
309
|
+
linter: "pylint",
|
|
310
|
+
keywords: ["too-many-arguments", "R0913"],
|
|
311
|
+
rule: "too-many-arguments",
|
|
312
|
+
configFix: "pylint enables too-many-arguments (R0913) by default; set max-args in [tool.pylint]",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
intent: "limit function length (Python)",
|
|
316
|
+
linter: "pylint",
|
|
317
|
+
keywords: ["too-many-statements", "R0915"],
|
|
318
|
+
rule: "too-many-statements",
|
|
319
|
+
configFix: "pylint enables too-many-statements (R0915) by default; set max-statements in [tool.pylint]",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
intent: "no wildcard imports (Python)",
|
|
323
|
+
linter: "pylint",
|
|
324
|
+
keywords: [
|
|
325
|
+
"wildcard-import",
|
|
326
|
+
"wildcard import",
|
|
327
|
+
"wildcard imports",
|
|
328
|
+
"W0401",
|
|
329
|
+
],
|
|
330
|
+
rule: "wildcard-import",
|
|
331
|
+
configFix: "pylint enables wildcard-import (W0401) by default; keep it out of the disable list",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
intent: "no global statement (Python)",
|
|
335
|
+
linter: "pylint",
|
|
336
|
+
keywords: ["global-statement", "global statement", "W0603"],
|
|
337
|
+
rule: "global-statement",
|
|
338
|
+
configFix: "pylint enables global-statement (W0603) by default; keep it out of the disable list",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
intent: "max line length (Python)",
|
|
342
|
+
linter: "pylint",
|
|
343
|
+
keywords: ["line-too-long", "C0301"],
|
|
344
|
+
rule: "line-too-long",
|
|
345
|
+
configFix: "pylint enables line-too-long (C0301) by default; set max-line-length in [tool.pylint]",
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
intent: "no unused imports (Python)",
|
|
349
|
+
linter: "pylint",
|
|
350
|
+
keywords: ["unused-import", "W0611"],
|
|
351
|
+
rule: "unused-import",
|
|
352
|
+
configFix: "pylint enables unused-import (W0611) by default; keep it out of the disable list",
|
|
353
|
+
},
|
|
199
354
|
];
|
|
200
355
|
/** Escape a keyword for use inside a RegExp. */
|
|
201
356
|
function escapeRe(s) {
|
|
@@ -206,9 +361,15 @@ function escapeRe(s) {
|
|
|
206
361
|
* non-`[\w/@.-]` character on each side (so `no-console` matches in
|
|
207
362
|
* `` `no-console` `` and `enforce no-console;` but `no-console-x` does not,
|
|
208
363
|
* and prose containing the substring elsewhere never trips it).
|
|
364
|
+
*
|
|
365
|
+
* The TRAILING boundary is a lookahead, not a consuming class, so a keyword at
|
|
366
|
+
* SENTENCE END ("No wildcard imports.") matches: a `.` is allowed unless it
|
|
367
|
+
* CONTINUES a code token (`.log` in `console.log`), which still blocks a partial
|
|
368
|
+
* match. `(?![\w/@-])` rejects a word/`/`/`@`/`-` continuation; `(?!\.[\w/@-])`
|
|
369
|
+
* rejects a dotted continuation but permits a trailing sentence `.`.
|
|
209
370
|
*/
|
|
210
371
|
function matchesWholeToken(text, keyword) {
|
|
211
|
-
const re = new RegExp(`(^|[^\\w/@.-])${escapeRe(keyword)}([
|
|
372
|
+
const re = new RegExp(`(^|[^\\w/@.-])${escapeRe(keyword)}(?![\\w/@-])(?!\\.[\\w/@-])`, "i");
|
|
212
373
|
return re.test(text);
|
|
213
374
|
}
|
|
214
375
|
/**
|
|
@@ -292,6 +453,14 @@ function buildRuleInventory(instructionText, configText, options = {}) {
|
|
|
292
453
|
for (const m of exports.INTENT_MAP) {
|
|
293
454
|
if (linters && !linters.includes(m.linter))
|
|
294
455
|
continue;
|
|
456
|
+
// ROUTE-ONLY for non-eslint linters: the config-state check below
|
|
457
|
+
// (ruleSetOff/ruleInConfig) is eslint-config-shaped and would MISLABEL a
|
|
458
|
+
// pylint rule, which is ON-BY-DEFAULT (a symbol in `disable=` reads as
|
|
459
|
+
// "in-config"; an absent one as "enable it" — both inverted). The routing
|
|
460
|
+
// preview (classify) still reuses these; accurate pylint enabled-state waits
|
|
461
|
+
// on the inverted-polarity ConfigProbe (design doc §3). Don't cry wolf.
|
|
462
|
+
if (m.linter !== "eslint")
|
|
463
|
+
continue;
|
|
295
464
|
const matched = m.keywords.find((kw) => matchesWholeToken(instructionText, kw));
|
|
296
465
|
if (!matched)
|
|
297
466
|
continue;
|