vigiles 12.7.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/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 +130 -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/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
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type LinterName } from "./rule-inventory.js";
|
|
2
|
+
/** How a routed rule would be enforced (a MECHANISM ladder, not a 1-10 score). */
|
|
3
|
+
export type RuleCategory = "reuse" | "hook" | "semantic" | "unrouted";
|
|
4
|
+
export type RuleMechanism = "config-line" | "hook" | "prose" | "compile";
|
|
5
|
+
/** One segmented, deterministically-routed rule with provenance. */
|
|
6
|
+
export interface RoutedRule {
|
|
7
|
+
/** Normalized atomic rule text (from the segmenter). */
|
|
8
|
+
readonly text: string;
|
|
9
|
+
/** Verbatim source slice — for a UI highlight. */
|
|
10
|
+
readonly quote: string;
|
|
11
|
+
readonly file: string | undefined;
|
|
12
|
+
readonly lineStart: number;
|
|
13
|
+
readonly lineEnd: number;
|
|
14
|
+
/** Segmenter confidence that this IS a rule (3/3 cues → high, 2/3 → medium). */
|
|
15
|
+
readonly confidence: "high" | "medium";
|
|
16
|
+
readonly category: RuleCategory;
|
|
17
|
+
readonly mechanism: RuleMechanism;
|
|
18
|
+
/** reuse only: the off-the-shelf rule that enforces it. */
|
|
19
|
+
readonly rule?: string;
|
|
20
|
+
/** reuse only: the linter that rule belongs to. */
|
|
21
|
+
readonly linter?: LinterName;
|
|
22
|
+
}
|
|
23
|
+
export interface RuleRouting {
|
|
24
|
+
/** How many atomic rules were routed (after the confidence filter). */
|
|
25
|
+
readonly segmented: number;
|
|
26
|
+
readonly counts: Record<RuleCategory, number>;
|
|
27
|
+
readonly rules: readonly RoutedRule[];
|
|
28
|
+
}
|
|
29
|
+
export interface RouteOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Minimum segmenter confidence to route. The segmenter emits `high` (3/3 cues
|
|
32
|
+
* — an imperative rule) and `medium` (2/3). For the audit PREVIEW we default to
|
|
33
|
+
* `high` only: precision over recall. A doc-heavy instruction file (e.g. a
|
|
34
|
+
* keyFiles index of "`path` — description" bullets) trips the medium tier with
|
|
35
|
+
* non-rules, which would bury the real rules and overstate "unrouted". Pass
|
|
36
|
+
* `"medium"` to include both.
|
|
37
|
+
*/
|
|
38
|
+
readonly minConfidence?: "high" | "medium";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Segment the instruction file and route every atomic rule deterministically.
|
|
42
|
+
* Pure: the caller passes the concatenated instruction text (and an optional
|
|
43
|
+
* source path for provenance). Returns per-category counts + the routed rules.
|
|
44
|
+
*/
|
|
45
|
+
export declare function routeRules(instructionText: string, file?: string, options?: RouteOptions): RuleRouting;
|
|
46
|
+
//# sourceMappingURL=rule-routing.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.routeRules = routeRules;
|
|
4
|
+
/**
|
|
5
|
+
* rule-routing.ts — the deterministic (no-model) State-B routing PREVIEW.
|
|
6
|
+
*
|
|
7
|
+
* `rule-inventory.ts` answers a narrow question ("which prose lines name an
|
|
8
|
+
* off-the-shelf lint rule, and is it enabled?"). This goes one honest step
|
|
9
|
+
* further: it SEGMENTS the whole instruction file into atomic rules
|
|
10
|
+
* ({@link segmentInstructions}) and routes each one into the class that a real
|
|
11
|
+
* enforcement path would take — WITHOUT running a model:
|
|
12
|
+
*
|
|
13
|
+
* reuse → the rule text names an off-the-shelf lint rule ({@link INTENT_MAP})
|
|
14
|
+
* → mechanism: flip one config line.
|
|
15
|
+
* hook → an ACTION rule a linter can't see (git push, rm -rf, "before you
|
|
16
|
+
* commit") → mechanism: a pre-commit / PreToolUse hook.
|
|
17
|
+
* semantic → a judgment call ("readable", "single responsibility") no checker
|
|
18
|
+
* can honestly decide → mechanism: stays prose.
|
|
19
|
+
* unrouted → none of the above fired deterministically → mechanism: the opt-in
|
|
20
|
+
* `compile` tier routes it (reuse / synthesize / hook / prose).
|
|
21
|
+
*
|
|
22
|
+
* HONESTY BY CONSTRUCTION: the deterministic tier NEVER claims a rule is
|
|
23
|
+
* "synthesizable" — deciding that a custom rule can be written (and gating it)
|
|
24
|
+
* is exactly the work the opt-in model tier does. Everything this file can't
|
|
25
|
+
* pin to a concrete cue is `unrouted` ("compile to find out"), not a promise.
|
|
26
|
+
*
|
|
27
|
+
* Pure, deterministic, dependency-free. Reuses `rule-inventory`'s hardened
|
|
28
|
+
* whole-token matcher + `INTENT_MAP`, and `segment`'s Tier-A segmenter.
|
|
29
|
+
*/
|
|
30
|
+
const segment_js_1 = require("./segment.js");
|
|
31
|
+
const rule_inventory_js_1 = require("./rule-inventory.js");
|
|
32
|
+
/** The mechanism each category maps to — a fixed, honest ladder. */
|
|
33
|
+
const MECHANISM = {
|
|
34
|
+
reuse: "config-line",
|
|
35
|
+
hook: "hook",
|
|
36
|
+
semantic: "prose",
|
|
37
|
+
unrouted: "compile",
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* ACTION-rule cues — things a linter never sees (git, filesystem, shell,
|
|
41
|
+
* process). A hook is the right gate, not a lint rule. Ported from the compiler's
|
|
42
|
+
* classifier; deliberately specific so it doesn't grab a lint rule that merely
|
|
43
|
+
* mentions a file.
|
|
44
|
+
*/
|
|
45
|
+
const HOOK_CUES = [
|
|
46
|
+
/\bgit\s+push\b/i,
|
|
47
|
+
// "push … to main/master/prod" — tolerate backticks/adverbs between (real
|
|
48
|
+
// phrasings: "push directly to `main`", "pushing straight to master").
|
|
49
|
+
/\bpush\w*\b[^.\n]{0,24}\b(main|master|prod)\b/i,
|
|
50
|
+
/\bforce[- ]?push/i,
|
|
51
|
+
/--no-verify/i,
|
|
52
|
+
/\bnever\s+commit\b/i,
|
|
53
|
+
// "before you/each/every commit", "before committing".
|
|
54
|
+
/\bbefore\s+(you\s+|each\s+|every\s+)?commit(ting)?\b/i,
|
|
55
|
+
/\brun\b[^.\n]{0,20}\btests?\b[^.\n]{0,14}\bbefore\b/i,
|
|
56
|
+
/\bsigned-off-by\b/i,
|
|
57
|
+
/\b(don'?t|do not|never)\s+edit\b.*\b(generated|\.pb\.|_mock|proto-gen|lock)/i,
|
|
58
|
+
/\bgenerated\s+files?\b/i,
|
|
59
|
+
/\bco[- ]?authored[- ]?by\b/i,
|
|
60
|
+
/\brm\s+-rf\b/i,
|
|
61
|
+
/\bcurl\b.*\|\s*(sh|bash)/i,
|
|
62
|
+
/\bchmod\b/i,
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Judgment / no-checker cues — a rule no linter can honestly decide, so it stays
|
|
66
|
+
* labeled prose. Ported from the compiler's classifier (the static markers only —
|
|
67
|
+
* no ruleMap dependency, to keep this file model-free and dep-free).
|
|
68
|
+
*/
|
|
69
|
+
const SEMANTIC_CUES = [
|
|
70
|
+
/\bself[- ]?documenting\b/i,
|
|
71
|
+
/\bclear(er)?\s+(names?|code|over clever)/i,
|
|
72
|
+
/\breadable\b/i,
|
|
73
|
+
/\bkeep it simple\b/i,
|
|
74
|
+
/\bover[- ]?engineer/i,
|
|
75
|
+
/\bsingle responsibility\b/i,
|
|
76
|
+
/\bcomposition over inheritance\b/i,
|
|
77
|
+
/\bmeaningful\b/i,
|
|
78
|
+
/\bidiomatic\b/i,
|
|
79
|
+
/\bwhere (it )?makes sense\b/i,
|
|
80
|
+
/\bappropriate(ly)?\b/i,
|
|
81
|
+
/\bsolid\s+principles?\b/i,
|
|
82
|
+
/\bbest practices?\b/i,
|
|
83
|
+
/\bclean code\b/i,
|
|
84
|
+
];
|
|
85
|
+
/**
|
|
86
|
+
* Route one atomic rule. Order matters: an ACTION cue (git push) wins over a
|
|
87
|
+
* rule-name mention ("never commit console.log" is a hook, not a lint rule);
|
|
88
|
+
* reuse (a concrete off-the-shelf rule) wins over a soft semantic cue.
|
|
89
|
+
*/
|
|
90
|
+
function classify(text) {
|
|
91
|
+
if (HOOK_CUES.some((re) => re.test(text)))
|
|
92
|
+
return { category: "hook" };
|
|
93
|
+
for (const m of rule_inventory_js_1.INTENT_MAP) {
|
|
94
|
+
if (m.keywords.some((kw) => (0, rule_inventory_js_1.matchesWholeToken)(text, kw))) {
|
|
95
|
+
return { category: "reuse", rule: m.rule, linter: m.linter };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (SEMANTIC_CUES.some((re) => re.test(text)))
|
|
99
|
+
return { category: "semantic" };
|
|
100
|
+
return { category: "unrouted" };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Segment the instruction file and route every atomic rule deterministically.
|
|
104
|
+
* Pure: the caller passes the concatenated instruction text (and an optional
|
|
105
|
+
* source path for provenance). Returns per-category counts + the routed rules.
|
|
106
|
+
*/
|
|
107
|
+
function routeRules(instructionText, file, options = {}) {
|
|
108
|
+
const minConfidence = options.minConfidence ?? "high";
|
|
109
|
+
const segments = (0, segment_js_1.segmentInstructions)(instructionText, file).filter((s) => minConfidence === "medium" || s.confidence === "high");
|
|
110
|
+
const rules = segments.map((s) => {
|
|
111
|
+
const c = classify(s.text);
|
|
112
|
+
return {
|
|
113
|
+
text: s.text,
|
|
114
|
+
quote: s.exactQuote,
|
|
115
|
+
file: s.file,
|
|
116
|
+
lineStart: s.lineStart,
|
|
117
|
+
lineEnd: s.lineEnd,
|
|
118
|
+
confidence: s.confidence,
|
|
119
|
+
category: c.category,
|
|
120
|
+
mechanism: MECHANISM[c.category],
|
|
121
|
+
...(c.rule ? { rule: c.rule } : {}),
|
|
122
|
+
...(c.linter ? { linter: c.linter } : {}),
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
const counts = {
|
|
126
|
+
reuse: 0,
|
|
127
|
+
hook: 0,
|
|
128
|
+
semantic: 0,
|
|
129
|
+
unrouted: 0,
|
|
130
|
+
};
|
|
131
|
+
for (const r of rules)
|
|
132
|
+
counts[r.category]++;
|
|
133
|
+
return { segmented: segments.length, counts, rules };
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=rule-routing.js.map
|
package/dist/scaffold-test.js
CHANGED
|
@@ -210,7 +210,7 @@ function safetySection(input, sideEffecting) {
|
|
|
210
210
|
// --- Safety (deterministic) — generated from ${input.name}'s side-effecting tools: ${sideEffecting.join(", ")} ---
|
|
211
211
|
// In a real run, replace this constructed Trace with a real \`runHarness\` /
|
|
212
212
|
// \`measure\` turn (use interceptTools so a real model's attempt is DENIED, never
|
|
213
|
-
// executed — see
|
|
213
|
+
// executed — see research/eval-architecture.md). The checks below are derived from the
|
|
214
214
|
// declared tools contract — the agent's "hole" asserted to stay in its lane.
|
|
215
215
|
{
|
|
216
216
|
const trace = {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* segment.ts — Tier-A deterministic (no-model) segmenter.
|
|
3
|
+
*
|
|
4
|
+
* Splits a CLAUDE.md / AGENTS.md into ATOMIC candidate rules with provenance.
|
|
5
|
+
* Pure, deterministic, strict TS, no external deps.
|
|
6
|
+
*
|
|
7
|
+
* Design bias: PRECISION over recall. A missed rule costs a row; a garbage
|
|
8
|
+
* atom costs credibility. When in doubt we UNDER-split and REJECT.
|
|
9
|
+
*/
|
|
10
|
+
/** A single atomic candidate rule extracted from an instructions file. */
|
|
11
|
+
export interface SegmentedRule {
|
|
12
|
+
/** Normalized rule text (bullet marker stripped, continuation joined, whitespace collapsed). */
|
|
13
|
+
text: string;
|
|
14
|
+
/** Source file path (as supplied by the caller), or undefined. */
|
|
15
|
+
file: string | undefined;
|
|
16
|
+
/** 1-based inclusive start line in the source. */
|
|
17
|
+
lineStart: number;
|
|
18
|
+
/** 1-based inclusive end line in the source. */
|
|
19
|
+
lineEnd: number;
|
|
20
|
+
/** Verbatim slice of the source spanning [lineStart..lineEnd] — for UI highlight. */
|
|
21
|
+
exactQuote: string;
|
|
22
|
+
/** 3/3 cues => "high"; 2/3 => "medium". (Rejected candidates are never emitted.) */
|
|
23
|
+
confidence: "high" | "medium";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Split a CLAUDE.md / AGENTS.md into atomic candidate rules with provenance.
|
|
27
|
+
*
|
|
28
|
+
* Deterministic Tier-A heuristic. Code fences and tables are excluded from
|
|
29
|
+
* candidacy. Candidate units are (a) list items with attached continuation
|
|
30
|
+
* lines and (b) sentences of paragraphs under a rule-ish heading.
|
|
31
|
+
*/
|
|
32
|
+
export declare function segmentInstructions(markdown: string, file?: string): SegmentedRule[];
|
|
33
|
+
//# sourceMappingURL=segment.d.ts.map
|