vigiles 12.7.0 → 13.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.
- package/README.md +43 -15
- 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 +162 -13
- package/dist/core/orphans.d.ts +16 -6
- package/dist/core/orphans.js +45 -19
- package/dist/core/rule-meta.js +2 -2
- package/dist/core/types.d.ts +10 -4
- 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/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/segment.d.ts +33 -0
- package/dist/segment.js +454 -0
- package/package.json +1 -1
package/dist/core/orphans.js
CHANGED
|
@@ -21,7 +21,7 @@ const glob_1 = require("glob");
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Internals
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
|
-
const DEFAULT_INCLUDE = ["docs/**/*.md"
|
|
24
|
+
const DEFAULT_INCLUDE = ["docs/**/*.md"];
|
|
25
25
|
const DEFAULT_IGNORE = [
|
|
26
26
|
"node_modules/**",
|
|
27
27
|
"dist/**",
|
|
@@ -35,24 +35,48 @@ const DEFAULT_IGNORE = [
|
|
|
35
35
|
* that nothing else links to but is not rot.
|
|
36
36
|
*/
|
|
37
37
|
const DISABLE_RE = /<!--\s*vigiles-disable\s+orphan-docs\s*-->/;
|
|
38
|
+
/** The one universal cross-harness skill-entry filename (CC + Codex). */
|
|
39
|
+
const SKILL_FILE = "SKILL.md";
|
|
38
40
|
/**
|
|
39
|
-
* Files the HARNESS loads directly —
|
|
40
|
-
* `
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* the
|
|
41
|
+
* Files the HARNESS loads directly — its instruction file
|
|
42
|
+
* (`layout.instructionFile`, e.g. `CLAUDE.md` / `AGENTS.md`), a skill
|
|
43
|
+
* (`SKILL.md`), a subagent (`<agentDir>/*.md`), or a slash command
|
|
44
|
+
* (`<commandDir>/*.md`) — are load-bearing by their NAME/LOCATION, not because
|
|
45
|
+
* another `.md` links to them. They are categorically NOT docs, so they are
|
|
46
|
+
* never orphans even when `orphans.include` broadens to the whole repo.
|
|
47
|
+
*
|
|
48
|
+
* The surface names come from the INJECTED layouts, so core stays
|
|
49
|
+
* harness-agnostic — no Claude Code literal here; the CLI passes every
|
|
50
|
+
* registered adapter's layout, and `SKILL.md` is the one universal convention.
|
|
51
|
+
* (They are still scanned as REFERENCERS, so a real doc that only a `CLAUDE.md`
|
|
52
|
+
* links to is still credited — this exemption only removes them from the
|
|
53
|
+
* orphan-CANDIDATE set.)
|
|
47
54
|
*/
|
|
48
|
-
function isHarnessLoadedFile(path) {
|
|
55
|
+
function isHarnessLoadedFile(path, layouts) {
|
|
49
56
|
const norm = normalizePath(path);
|
|
50
57
|
const base = norm.slice(norm.lastIndexOf("/") + 1);
|
|
51
|
-
if (base ===
|
|
58
|
+
if (base === SKILL_FILE)
|
|
52
59
|
return true;
|
|
60
|
+
for (const layout of layouts) {
|
|
61
|
+
if (base === layout.instructionFile)
|
|
62
|
+
return true;
|
|
63
|
+
// Subagent / slash-command surfaces live at a REAL surface root — the repo
|
|
64
|
+
// root, the user-surface root (e.g. `.claude/`), or the materialize root —
|
|
65
|
+
// NOT any nested dir that merely shares the name. A doc under `docs/prompts/`
|
|
66
|
+
// is documentation, not Codex's `prompts` command surface.
|
|
67
|
+
const roots = [
|
|
68
|
+
"",
|
|
69
|
+
...[layout.userSurfaceRoot, layout.materializeRoot]
|
|
70
|
+
.filter((r) => !!r)
|
|
71
|
+
.map((r) => `${r}/`),
|
|
72
|
+
];
|
|
73
|
+
for (const dir of [layout.agentDir, layout.commandDir]) {
|
|
74
|
+
if (dir && roots.some((r) => norm.startsWith(`${r}${dir}/`))) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
53
78
|
}
|
|
54
|
-
|
|
55
|
-
return /(^|\/)(agents|commands)\//.test(norm);
|
|
79
|
+
return false;
|
|
56
80
|
}
|
|
57
81
|
// Match markdown links ](path.md) or ](path.md#anchor)
|
|
58
82
|
const LINK_RE = /\]\(([^)\s]+\.md)(?:#[^)]*)?\)/g;
|
|
@@ -71,12 +95,12 @@ function isOrphanExempt(absPath) {
|
|
|
71
95
|
}
|
|
72
96
|
}
|
|
73
97
|
/** Discover docs under `include`, dropping any that carry the inline opt-out. */
|
|
74
|
-
function collectDocs(basePath, include, ignore) {
|
|
98
|
+
function collectDocs(basePath, include, ignore, layouts) {
|
|
75
99
|
const docs = new Set();
|
|
76
100
|
for (const pattern of include) {
|
|
77
101
|
for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore: [...ignore] })) {
|
|
78
|
-
if (isHarnessLoadedFile(p))
|
|
79
|
-
continue; //
|
|
102
|
+
if (isHarnessLoadedFile(p, layouts))
|
|
103
|
+
continue; // harness files are never orphans
|
|
80
104
|
if (isOrphanExempt((0, node_path_1.resolve)(basePath, p)))
|
|
81
105
|
continue;
|
|
82
106
|
docs.add(normalizePath(p));
|
|
@@ -121,15 +145,17 @@ function refTargets(sourcePath, ref) {
|
|
|
121
145
|
* links to itself is still an orphan.
|
|
122
146
|
*
|
|
123
147
|
* `include` and `exclude` are tsconfig-style glob arrays. Default include
|
|
124
|
-
* is
|
|
125
|
-
*
|
|
148
|
+
* is `["docs/**\/*.md"]` (the common convention); override per-project via
|
|
149
|
+
* `.vigilesrc.json` → `orphans.include` (whose presence also opts the repo
|
|
150
|
+
* into the scan — see the CLI gate in `vigiles lint`).
|
|
126
151
|
*/
|
|
127
152
|
function findOrphanDocs(options = {}) {
|
|
128
153
|
const basePath = options.basePath ?? process.cwd();
|
|
129
154
|
const include = options.include ?? DEFAULT_INCLUDE;
|
|
130
155
|
const userExclude = options.exclude ?? [];
|
|
131
156
|
const ignore = [...DEFAULT_IGNORE, ...userExclude];
|
|
132
|
-
const
|
|
157
|
+
const layouts = options.layouts ?? [];
|
|
158
|
+
const allDocs = collectDocs(basePath, include, ignore, layouts);
|
|
133
159
|
const allMarkdown = (0, glob_1.globSync)("**/*.md", {
|
|
134
160
|
cwd: basePath,
|
|
135
161
|
ignore: [...DEFAULT_IGNORE],
|
package/dist/core/rule-meta.js
CHANGED
|
@@ -256,10 +256,10 @@ exports.RULE_META = {
|
|
|
256
256
|
// --- Docs hygiene ---------------------------------------------------------
|
|
257
257
|
"orphan-docs": {
|
|
258
258
|
id: "orphan-docs",
|
|
259
|
-
bucket: "
|
|
259
|
+
bucket: "heuristic-behavioral",
|
|
260
260
|
surface: ["docs"],
|
|
261
261
|
defaultSeverity: "warn",
|
|
262
|
-
summary: "
|
|
262
|
+
summary: "Opt-in: a doc in a configured dir (default docs/) that no other .md references.",
|
|
263
263
|
detector: "findOrphanDocs",
|
|
264
264
|
},
|
|
265
265
|
};
|
package/dist/core/types.d.ts
CHANGED
|
@@ -54,13 +54,19 @@ export interface CoverageThresholds {
|
|
|
54
54
|
/** Min % of npm scripts documented in spec commands. */
|
|
55
55
|
scripts?: number;
|
|
56
56
|
}
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Options for the orphan-docs check. The PRESENCE of this block in
|
|
59
|
+
* `.vigilesrc.json` OPTS THE REPO IN — the scan is off unless declared,
|
|
60
|
+
* because "unreferenced" only means "rot" for a hand-cross-linked corpus,
|
|
61
|
+
* not for a nav-managed doc site (Docusaurus/MkDocs) where the page graph
|
|
62
|
+
* lives in config. `include` is the optional dir override.
|
|
63
|
+
*/
|
|
58
64
|
export interface OrphansConfig {
|
|
59
65
|
/**
|
|
60
66
|
* Glob patterns of `.md` files to scan for orphans. A doc is "orphaned"
|
|
61
|
-
* when no other markdown file references it.
|
|
62
|
-
* convention
|
|
63
|
-
*
|
|
67
|
+
* when no other markdown file references it. Omitted → `["docs/**\/*.md"]`
|
|
68
|
+
* (the common convention); add your own dirs (e.g. a `research/` notes
|
|
69
|
+
* tree) explicitly. Set to `[]` to opt in but scan nothing.
|
|
64
70
|
*/
|
|
65
71
|
include?: readonly string[];
|
|
66
72
|
/**
|
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
|
}
|
|
@@ -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
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rule-inventory — the deterministic, foreign-safe teaser surface of the
|
|
4
|
+
* `audit` rule-compile tier (design: `research/audit-rule-compile-tier.md`).
|
|
5
|
+
*
|
|
6
|
+
* Finds prose rules in a `CLAUDE.md` / `AGENTS.md` that map to an off-the-shelf
|
|
7
|
+
* lint rule, and whether that rule already appears in the repo's lint config.
|
|
8
|
+
* The remedy for a documented-but-unconfigured intent is a one-line config
|
|
9
|
+
* change, not synthesis — so this is a cheap, high-value nudge.
|
|
10
|
+
*
|
|
11
|
+
* NO model. NO config execution (textual grep only — never resolves/executes
|
|
12
|
+
* `eslint.config.js`, which would be the RCE path). HIGH PRECISION by
|
|
13
|
+
* construction: only rule-name / code-token-shaped keywords are matched, and
|
|
14
|
+
* only as whole tokens. Bare prose words are excluded on purpose — a raw
|
|
15
|
+
* keyword match (`token`, `!`, `await`, `secret`, `silently`, …) sprays false
|
|
16
|
+
* positives on real instruction files (measured: 107 raw hits over 4 real
|
|
17
|
+
* CLAUDE.md files, ~all garbage). The model-driven analysis (extract → classify
|
|
18
|
+
* → compile → gate → run) lives in the OPT-IN tier, not here.
|
|
19
|
+
*
|
|
20
|
+
* MULTI-LINTER by shape, ESLint-first by data. The matcher is linter-agnostic;
|
|
21
|
+
* each mapping is keyed by linter, so adding Ruff / Clippy / Pylint / RuboCop /
|
|
22
|
+
* Stylelint is additive DATA (a curation task), not a refactor. The OPT-IN tier
|
|
23
|
+
* should resolve "is this rule enabled" via vigiles's existing multi-linter
|
|
24
|
+
* `checkLinterRule` engine (which execs the config) — kept out of this
|
|
25
|
+
* exec-free, foreign-safe surface on purpose.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.INTENT_MAP = void 0;
|
|
29
|
+
exports.matchesWholeToken = matchesWholeToken;
|
|
30
|
+
exports.buildRuleInventory = buildRuleInventory;
|
|
31
|
+
/**
|
|
32
|
+
* Curated from agent-rules-compiler's `rule-map.json`, keeping ONLY the
|
|
33
|
+
* specific (rule-name / code-token) keywords and dropping every bare-word
|
|
34
|
+
* trigger the FP measurement flagged (`token`, `secret`, `password`, `await`,
|
|
35
|
+
* `!`, `aria`, `silently`, `prefix`, `complexity`, `barrel`, `cycle`, …).
|
|
36
|
+
*
|
|
37
|
+
* ESLint-only today — Ruff/Clippy/Pylint/RuboCop/Stylelint entries append here
|
|
38
|
+
* with their own `linter` + rule-name keywords, no code change.
|
|
39
|
+
*/
|
|
40
|
+
exports.INTENT_MAP = [
|
|
41
|
+
{
|
|
42
|
+
intent: "no console.log / use the logger",
|
|
43
|
+
linter: "eslint",
|
|
44
|
+
keywords: ["console.log", "no-console"],
|
|
45
|
+
rule: "no-console",
|
|
46
|
+
configFix: '"no-console": "error"',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
intent: "no `any` type",
|
|
50
|
+
linter: "eslint",
|
|
51
|
+
keywords: ["no-explicit-any", "@typescript-eslint/no-explicit-any"],
|
|
52
|
+
rule: "@typescript-eslint/no-explicit-any",
|
|
53
|
+
inRecommended: true,
|
|
54
|
+
configFix: '"@typescript-eslint/no-explicit-any": "error"',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
intent: "no eslint-disable / no linter suppressors",
|
|
58
|
+
linter: "eslint",
|
|
59
|
+
keywords: ["eslint-disable", "eslint-comments/no-use"],
|
|
60
|
+
rule: "eslint-comments/no-use",
|
|
61
|
+
configFix: "enable eslint-comments/no-use OR linterOptions.noInlineConfig: true",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
intent: "no @ts-ignore / @ts-expect-error abuse",
|
|
65
|
+
linter: "eslint",
|
|
66
|
+
keywords: [
|
|
67
|
+
"@ts-ignore",
|
|
68
|
+
"ts-expect-error",
|
|
69
|
+
"ban-ts-comment",
|
|
70
|
+
"@typescript-eslint/ban-ts-comment",
|
|
71
|
+
],
|
|
72
|
+
rule: "@typescript-eslint/ban-ts-comment",
|
|
73
|
+
inRecommended: true,
|
|
74
|
+
configFix: '"@typescript-eslint/ban-ts-comment": "error"',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
intent: "no hardcoded secrets",
|
|
78
|
+
linter: "eslint",
|
|
79
|
+
keywords: ["no-secrets"],
|
|
80
|
+
rule: "no-secrets/no-secrets",
|
|
81
|
+
configFix: "add eslint-plugin-no-secrets rule no-secrets/no-secrets",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
intent: "no empty catch / no swallowed errors",
|
|
85
|
+
linter: "eslint",
|
|
86
|
+
keywords: ["no-empty"],
|
|
87
|
+
rule: "no-empty",
|
|
88
|
+
inRecommended: true,
|
|
89
|
+
configFix: '"no-empty": ["error", {"allowEmptyCatch": false}]',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
intent: "no var / prefer const-let",
|
|
93
|
+
linter: "eslint",
|
|
94
|
+
keywords: ["no-var", "prefer-const"],
|
|
95
|
+
rule: "no-var",
|
|
96
|
+
configFix: '"no-var": "error"',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
intent: "strict equality ===",
|
|
100
|
+
linter: "eslint",
|
|
101
|
+
keywords: ["eqeqeq"],
|
|
102
|
+
rule: "eqeqeq",
|
|
103
|
+
configFix: '"eqeqeq": "error"',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
intent: "template literals over concatenation",
|
|
107
|
+
linter: "eslint",
|
|
108
|
+
keywords: ["prefer-template"],
|
|
109
|
+
rule: "prefer-template",
|
|
110
|
+
configFix: '"prefer-template": "error"',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
intent: "no debugger",
|
|
114
|
+
linter: "eslint",
|
|
115
|
+
keywords: ["no-debugger"],
|
|
116
|
+
rule: "no-debugger",
|
|
117
|
+
inRecommended: true,
|
|
118
|
+
configFix: '"no-debugger": "error"',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
intent: "restricted / deprecated imports",
|
|
122
|
+
linter: "eslint",
|
|
123
|
+
keywords: ["no-restricted-imports", "import/no-restricted-paths"],
|
|
124
|
+
rule: "no-restricted-imports",
|
|
125
|
+
configFix: '"no-restricted-imports": ["error", {…}]',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
intent: "no circular deps",
|
|
129
|
+
linter: "eslint",
|
|
130
|
+
keywords: ["import/no-cycle", "no-cycle"],
|
|
131
|
+
rule: "import/no-cycle",
|
|
132
|
+
configFix: '"import/no-cycle": "error"',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
intent: "function length / complexity caps",
|
|
136
|
+
linter: "eslint",
|
|
137
|
+
keywords: [
|
|
138
|
+
"max-lines-per-function",
|
|
139
|
+
"max-depth",
|
|
140
|
+
"max-params",
|
|
141
|
+
"max-statements",
|
|
142
|
+
],
|
|
143
|
+
rule: "max-lines-per-function",
|
|
144
|
+
configFix: '"max-lines-per-function": ["error", 40]',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
intent: "no unused vars",
|
|
148
|
+
linter: "eslint",
|
|
149
|
+
keywords: ["no-unused-vars", "@typescript-eslint/no-unused-vars"],
|
|
150
|
+
rule: "@typescript-eslint/no-unused-vars",
|
|
151
|
+
inRecommended: true,
|
|
152
|
+
configFix: '"@typescript-eslint/no-unused-vars": "error"',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
intent: "no non-null assertion",
|
|
156
|
+
linter: "eslint",
|
|
157
|
+
keywords: [
|
|
158
|
+
"no-non-null-assertion",
|
|
159
|
+
"@typescript-eslint/no-non-null-assertion",
|
|
160
|
+
],
|
|
161
|
+
rule: "@typescript-eslint/no-non-null-assertion",
|
|
162
|
+
inRecommended: true,
|
|
163
|
+
configFix: '"@typescript-eslint/no-non-null-assertion": "error"',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
intent: "no floating promises",
|
|
167
|
+
linter: "eslint",
|
|
168
|
+
keywords: [
|
|
169
|
+
"no-floating-promises",
|
|
170
|
+
"@typescript-eslint/no-floating-promises",
|
|
171
|
+
],
|
|
172
|
+
rule: "@typescript-eslint/no-floating-promises",
|
|
173
|
+
inRecommended: true,
|
|
174
|
+
configFix: '"@typescript-eslint/no-floating-promises": "error"',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
intent: "react hooks deps",
|
|
178
|
+
linter: "eslint",
|
|
179
|
+
keywords: ["react-hooks/exhaustive-deps", "exhaustive-deps"],
|
|
180
|
+
rule: "react-hooks/exhaustive-deps",
|
|
181
|
+
configFix: '"react-hooks/exhaustive-deps": "error"',
|
|
182
|
+
},
|
|
183
|
+
// Added from the hand-verified rule-adherence corpus (real repos: motion,
|
|
184
|
+
// mapbox) — both are documented in the wild and have an off-the-shelf rule.
|
|
185
|
+
{
|
|
186
|
+
intent: "no default exports",
|
|
187
|
+
linter: "eslint",
|
|
188
|
+
keywords: ["import/no-default-export", "no-default-export"],
|
|
189
|
+
rule: "import/no-default-export",
|
|
190
|
+
configFix: '"import/no-default-export": "error"',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
intent: "no TODO / FIXME comments",
|
|
194
|
+
linter: "eslint",
|
|
195
|
+
keywords: ["no-warning-comments"],
|
|
196
|
+
rule: "no-warning-comments",
|
|
197
|
+
configFix: '"no-warning-comments": ["error", {"terms": ["todo", "fixme"], "location": "anywhere"}]',
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
/** Escape a keyword for use inside a RegExp. */
|
|
201
|
+
function escapeRe(s) {
|
|
202
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* A keyword matches only as a WHOLE token: bounded by start/end or a
|
|
206
|
+
* non-`[\w/@.-]` character on each side (so `no-console` matches in
|
|
207
|
+
* `` `no-console` `` and `enforce no-console;` but `no-console-x` does not,
|
|
208
|
+
* and prose containing the substring elsewhere never trips it).
|
|
209
|
+
*/
|
|
210
|
+
function matchesWholeToken(text, keyword) {
|
|
211
|
+
const re = new RegExp(`(^|[^\\w/@.-])${escapeRe(keyword)}([^\\w/@.-]|$)`, "i");
|
|
212
|
+
return re.test(text);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Build the deterministic rule inventory. Pure: caller passes the instruction
|
|
216
|
+
* file text (concatenated CLAUDE.md/AGENTS.md) and the lint config text (any
|
|
217
|
+
* `eslint.config.*` / `.eslintrc*` / `ruff.toml` / … contents concatenated, or
|
|
218
|
+
* "" if none). Returns one item per documented intent whose keyword resolves.
|
|
219
|
+
* `not-in-config` items are the actionable nudges.
|
|
220
|
+
*/
|
|
221
|
+
/** ESLint re-exports some core rules under `@typescript-eslint/`; treat the base
|
|
222
|
+
* and scoped names as the same rule when checking the config text (so a repo that
|
|
223
|
+
* has base `no-unused-vars` satisfies the `@typescript-eslint/no-unused-vars`
|
|
224
|
+
* intent, and vice-versa). oxlint/biome use the SAME rule names, so once their
|
|
225
|
+
* config files are in the read set this handles them for free. */
|
|
226
|
+
function variantsOf(rule) {
|
|
227
|
+
const TS = "@typescript-eslint/";
|
|
228
|
+
if (rule.startsWith(TS))
|
|
229
|
+
return [rule, rule.slice(TS.length)];
|
|
230
|
+
if (!rule.includes("/"))
|
|
231
|
+
return [rule, TS + rule];
|
|
232
|
+
return [rule];
|
|
233
|
+
}
|
|
234
|
+
/** True if the rule (or a base/scoped variant) appears in the config text. */
|
|
235
|
+
function ruleInConfig(configText, rule) {
|
|
236
|
+
return variantsOf(rule).some((v) => matchesWholeToken(configText, v));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Whether the rule (or a variant) is EXPLICITLY disabled in the config text —
|
|
240
|
+
* `"no-console": "off"`, `no-console: 0`, `"no-console": ["off", …]`. Distinct
|
|
241
|
+
* from mere presence ({@link ruleInConfig}): a documented rule that the config
|
|
242
|
+
* turns OFF is a contradiction (docs say enforce, config disables), not an
|
|
243
|
+
* enforcement. Textual only — never resolves/executes the config (the RCE path).
|
|
244
|
+
* Conservative: matches only a literal off/0 severity right after the rule key,
|
|
245
|
+
* so a real `"error"`/`"warn"`/`1`/`2` never trips it.
|
|
246
|
+
*/
|
|
247
|
+
function ruleSetOff(configText, rule) {
|
|
248
|
+
return variantsOf(rule).some((v) => {
|
|
249
|
+
const re = new RegExp(`["']?${escapeRe(v)}["']?\\s*:\\s*(?:\\[\\s*)?["']?(?:off|0)\\b`, "i");
|
|
250
|
+
return re.test(configText);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/** Index of the first WHOLE-token occurrence of `keyword` in `text` (the keyword
|
|
254
|
+
* itself, not the boundary char), or -1. */
|
|
255
|
+
function firstTokenIndex(text, keyword) {
|
|
256
|
+
const re = new RegExp(`(^|[^\\w/@.-])(${escapeRe(keyword)})([^\\w/@.-]|$)`, "i");
|
|
257
|
+
const m = re.exec(text);
|
|
258
|
+
return m ? m.index + m[1].length : -1;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Whether the matched mention is a documented opt-OUT ("`no-explicit-any` is off
|
|
262
|
+
* intentionally", "we disable X") rather than a norm to enforce. When the author
|
|
263
|
+
* deliberately turns a rule off, a not-in-config state is CONSISTENT, not a gap —
|
|
264
|
+
* nudging them to enable it is actively wrong advice (found dogfooding
|
|
265
|
+
* pmndrs/react-spring). Deterministic negation window around the matched token;
|
|
266
|
+
* conservative on purpose — only strong off/disable cues, so it never suppresses
|
|
267
|
+
* a genuine "enforce this" nudge. */
|
|
268
|
+
function isDocumentedOptOut(text, keyword) {
|
|
269
|
+
const idx = firstTokenIndex(text, keyword);
|
|
270
|
+
if (idx < 0)
|
|
271
|
+
return false;
|
|
272
|
+
// Test the context on EITHER SIDE of the token, never the token itself — else
|
|
273
|
+
// the cue `disable` would match inside the rule name `eslint-disable` and
|
|
274
|
+
// self-suppress every mention of it. Left/right windows are separate strings.
|
|
275
|
+
// NB: `disabled` (full word) not `disable` — `disable` would match inside the
|
|
276
|
+
// rule name `eslint-disable`, which can appear in a NEIGHBOURING token's window.
|
|
277
|
+
const CUE = /\b(?:off|disabled|not enforced|not enabled|turned off)\b/i;
|
|
278
|
+
const left = text.slice(Math.max(0, idx - 48), idx);
|
|
279
|
+
const right = text.slice(idx + keyword.length, idx + keyword.length + 48);
|
|
280
|
+
return CUE.test(left) || CUE.test(right);
|
|
281
|
+
}
|
|
282
|
+
/** A `recommended`-style preset extend anywhere in the config text — evidence
|
|
283
|
+
* that preset-enabled rules may already be on even when not named literally.
|
|
284
|
+
* Coarse on purpose: presence of a preset downgrades a not-named preset rule to
|
|
285
|
+
* `preset-maybe` (no false "unenforced" alarm) rather than claiming it's off. */
|
|
286
|
+
function extendsRecommended(configText) {
|
|
287
|
+
return /recommended/i.test(configText);
|
|
288
|
+
}
|
|
289
|
+
function buildRuleInventory(instructionText, configText, options = {}) {
|
|
290
|
+
const linters = options.linters;
|
|
291
|
+
const items = [];
|
|
292
|
+
for (const m of exports.INTENT_MAP) {
|
|
293
|
+
if (linters && !linters.includes(m.linter))
|
|
294
|
+
continue;
|
|
295
|
+
const matched = m.keywords.find((kw) => matchesWholeToken(instructionText, kw));
|
|
296
|
+
if (!matched)
|
|
297
|
+
continue;
|
|
298
|
+
const off = ruleSetOff(configText, m.rule);
|
|
299
|
+
const inConfig = ruleInConfig(configText, m.rule);
|
|
300
|
+
// A rule the author documents as deliberately OFF, and whose config agrees
|
|
301
|
+
// (absent, or literally set to off), is consistent — not a gap. Skip it so we
|
|
302
|
+
// never nudge "enable X" against an intentional opt-out, and never flag a
|
|
303
|
+
// documented opt-out as a contradiction.
|
|
304
|
+
if (isDocumentedOptOut(instructionText, matched) && (off || !inConfig)) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
// Precedence: an explicit off/0 is a contradiction even though the rule name
|
|
308
|
+
// is technically "in config" — so it must be checked before `in-config`.
|
|
309
|
+
const configState = off
|
|
310
|
+
? "contradiction"
|
|
311
|
+
: inConfig
|
|
312
|
+
? "in-config"
|
|
313
|
+
: m.inRecommended && extendsRecommended(configText)
|
|
314
|
+
? "preset-maybe"
|
|
315
|
+
: "not-in-config";
|
|
316
|
+
items.push({
|
|
317
|
+
intent: m.intent,
|
|
318
|
+
linter: m.linter,
|
|
319
|
+
matched,
|
|
320
|
+
rule: m.rule,
|
|
321
|
+
configState,
|
|
322
|
+
configFix: m.configFix,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return items;
|
|
326
|
+
}
|
|
327
|
+
//# sourceMappingURL=rule-inventory.js.map
|