vigiles 13.0.0 → 14.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 +10 -4
- package/dist/audit-report.d.ts +10 -4
- package/dist/audit-report.js +7 -2
- package/dist/audit-report.template.html +28 -28
- package/dist/cli.js +73 -15
- package/dist/core/rule-catalog.d.ts +56 -0
- package/dist/core/rule-catalog.js +146 -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 +25 -2
- package/dist/rule-routing.js +337 -26
- package/dist/segment.d.ts +1 -1
- package/dist/segment.js +151 -17
- 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
package/dist/rule-routing.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.routeRules = routeRules;
|
|
4
|
+
exports.mergeRoutings = mergeRoutings;
|
|
4
5
|
/**
|
|
5
6
|
* rule-routing.ts — the deterministic (no-model) State-B routing PREVIEW.
|
|
6
7
|
*
|
|
@@ -11,18 +12,29 @@ exports.routeRules = routeRules;
|
|
|
11
12
|
* enforcement path would take — WITHOUT running a model:
|
|
12
13
|
*
|
|
13
14
|
* reuse → the rule text names an off-the-shelf lint rule ({@link INTENT_MAP})
|
|
14
|
-
* → mechanism: flip one config line.
|
|
15
|
+
* → mechanism: flip one config line. The "narrow list we compile
|
|
16
|
+
* very well" — everything else is honestly labelled, not force-fit.
|
|
15
17
|
* hook → an ACTION rule a linter can't see (git push, rm -rf, "before you
|
|
16
18
|
* commit") → mechanism: a pre-commit / PreToolUse hook.
|
|
19
|
+
* meta → an agent-instruction, not a code rule ("read X first", "tell the
|
|
20
|
+
* user", "you are …") → mechanism: stays prose. Split out of
|
|
21
|
+
* `unrouted` so it never reads as "compilable but hard" (it isn't).
|
|
17
22
|
* semantic → a judgment call ("readable", "single responsibility") no checker
|
|
18
23
|
* can honestly decide → mechanism: stays prose.
|
|
19
|
-
* unrouted →
|
|
20
|
-
*
|
|
24
|
+
* unrouted → looks like a code rule but matched no off-the-shelf rule: HARD to
|
|
25
|
+
* codify → mechanism `synthesize`: the opt-in SYNTHESIS tier (a
|
|
26
|
+
* skill on your subscription) MIGHT write a custom checker, gated —
|
|
27
|
+
* but it is NOT guaranteed (the gate may abstain). This is the
|
|
28
|
+
* bucket audit must present clearly as "hard", never as done.
|
|
29
|
+
*
|
|
30
|
+
* NB "compile" is NOT used here — `vigiles compile` is the unrelated spec→markdown
|
|
31
|
+
* verb. Synthesis is its own opt-in tier; the mechanism value is `synthesize`.
|
|
21
32
|
*
|
|
22
33
|
* HONESTY BY CONSTRUCTION: the deterministic tier NEVER claims a rule is
|
|
23
34
|
* "synthesizable" — deciding that a custom rule can be written (and gating it)
|
|
24
|
-
* is exactly the work the opt-in model tier does.
|
|
25
|
-
*
|
|
35
|
+
* is exactly the work the opt-in model tier does. `unrouted` means "hard — a
|
|
36
|
+
* synthesis skill may try", never a promise; `meta`/`semantic` mean "not an
|
|
37
|
+
* enforceable code rule at all" (a different, honest kind of no).
|
|
26
38
|
*
|
|
27
39
|
* Pure, deterministic, dependency-free. Reuses `rule-inventory`'s hardened
|
|
28
40
|
* whole-token matcher + `INTENT_MAP`, and `segment`'s Tier-A segmenter.
|
|
@@ -33,33 +45,66 @@ const rule_inventory_js_1 = require("./rule-inventory.js");
|
|
|
33
45
|
const MECHANISM = {
|
|
34
46
|
reuse: "config-line",
|
|
35
47
|
hook: "hook",
|
|
48
|
+
meta: "prose",
|
|
36
49
|
semantic: "prose",
|
|
37
|
-
unrouted: "
|
|
50
|
+
unrouted: "synthesize",
|
|
38
51
|
};
|
|
39
52
|
/**
|
|
40
53
|
* ACTION-rule cues — things a linter never sees (git, filesystem, shell,
|
|
41
|
-
* process). A hook is the right gate, not a lint rule.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
54
|
+
* process). A hook is the right gate, not a lint rule. Widened to the article's
|
|
55
|
+
* measured surfaces (vcs / process / shell / redirect) — the narrow original
|
|
56
|
+
* (git-push + rm-rf only) reported ~2% hooks where the 252-rule hand-sort found
|
|
57
|
+
* **37%** (the largest bucket); it was missing push-to-branch, before-push,
|
|
58
|
+
* after-edit, tool-substitution, amend/rebase-pushed, and dependency guards.
|
|
59
|
+
* See `research/rule-compiler-multilang-design.md` §5b (the hook lane).
|
|
44
60
|
*/
|
|
45
61
|
const HOOK_CUES = [
|
|
62
|
+
// — branch / push guards (vcs) —
|
|
46
63
|
/\bgit\s+push\b/i,
|
|
47
|
-
// "push … to main/master/prod" — tolerate backticks/adverbs between
|
|
48
|
-
|
|
49
|
-
/\bpush\w*\b[^.\n]{0,24}\b(main|master|prod)\b/i,
|
|
64
|
+
// "push … to main/master/prod/origin" — tolerate backticks/adverbs between.
|
|
65
|
+
/\bpush\w*\b[^.\n]{0,24}\b(main|master|prod|production|origin|development)\b/i,
|
|
50
66
|
/\bforce[- ]?push/i,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
/\
|
|
67
|
+
/\bpush\w*\b[^.\n]{0,16}\bbranch/i,
|
|
68
|
+
// — protected paths (vcs) —
|
|
69
|
+
/\b(don'?t|do not|never)\s+(edit|modify|touch|change)\b[^.\n]{0,30}\b(generated|vendored|lock(file)?|\.pb\.|proto-gen|_mock|snapshot)/i,
|
|
70
|
+
/\bgenerated\s+files?\b/i,
|
|
71
|
+
// — sequencing / tests-before / after-edit (process) —
|
|
72
|
+
/\b(run|execute)\b[^.\n]{0,30}\b(tests?|lint|check|type-?check|format|prettier|ruff)\b[^.\n]{0,20}\bbefore\b/i,
|
|
73
|
+
/\bbefore\s+(you\s+|each\s+|every\s+)?(commit|push|merg|committing|pushing)/i,
|
|
55
74
|
/\brun\b[^.\n]{0,20}\btests?\b[^.\n]{0,14}\bbefore\b/i,
|
|
75
|
+
/\b(format|lint|check|run)\b[^.\n]{0,30}\b(after|immediately after)\b[^.\n]{0,16}\b(writ|edit)/i,
|
|
76
|
+
// — tool substitution / redirect —
|
|
77
|
+
/\b(never|do not|don'?t)\s+run\b[^.\n]{0,30}\b(directly|instead)\b/i,
|
|
78
|
+
/\b(never|do not|don'?t)\s+run\b[^.\n]{0,20}`?(eslint|prettier|npm|npx|cargo|yarn|pnpm|pip|black|ruff)\b/i,
|
|
79
|
+
/\buse\b\s+`?[\w:.-]+`?\s+(instead of|not|over|rather than)\s+`?(npm|npx|cargo|yarn|pnpm|eslint|prettier)\b/i,
|
|
80
|
+
// — regenerate-on-change guard (vigiles's own guard() path→cmd shape: "run X
|
|
81
|
+
// after/when Y changes") — the single biggest missed-hook pattern in the OSS
|
|
82
|
+
// dogfood. Both clause orders; the trigger MUST be a change-verb so a benign
|
|
83
|
+
// "run it when ready" doesn't match.
|
|
84
|
+
/\b(re-?run|regenerate|re-?generate|rebuild|regen|run|update)\b[^.\n]{0,48}\b(after|when|whenever|once|if)\b[^.\n]{0,24}\b(chang|add(?:ing|ed)?|modif|updat|edit|new\b)/i,
|
|
85
|
+
/\b(after|when|whenever|once)\b[^.\n]{0,24}\b(chang|add(?:ing|ed)?|modif|updat|edit)[^.\n]{0,48}\b(re-?run|regenerate|rebuild|regen|run|update)\b/i,
|
|
86
|
+
// — commit content / history (vcs) —
|
|
87
|
+
/\b(amend|squash|rebase)\b[^.\n]{0,40}\b(pushed|review|remote|shared)\b/i,
|
|
88
|
+
/--no-verify/i,
|
|
89
|
+
/\b(never|do not|don'?t)\s+commit\b/i,
|
|
56
90
|
/\bsigned-off-by\b/i,
|
|
57
|
-
/\
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
/\bco-?author(?:s|ed|ing|ed-by)?\b/i,
|
|
92
|
+
// commit/PR METADATA hygiene phrased without a push verb (attribution, PR
|
|
93
|
+
// title, commit message, semantic/conventional commits) — a VCS-surface gate.
|
|
94
|
+
/\b(generated with|attribution)\b[^.\n]{0,24}\b(claude|ai|footer|commit|pr)\b/i,
|
|
95
|
+
/\b(ai|claude|assistant)\b[^.\n]{0,16}\battribution\b/i,
|
|
96
|
+
/\b(semantic|conventional)\s+(commit|pr|pull request)/i,
|
|
97
|
+
// NB: deliberately NO broad `(commit|pr) (title|message)` cue — it mislabels
|
|
98
|
+
// style sentences ("use sentence case for PR titles", "keep commit messages
|
|
99
|
+
// concise") as gates. The enforceable format/attribution rules are caught by
|
|
100
|
+
// the semantic/conventional + attribution cues above; the rest stay prose.
|
|
101
|
+
// — dependency / config guard —
|
|
102
|
+
/\b(never|do not|don'?t)\b[^.\n]{0,20}\bupdate\b[^.\n]{0,20}\b(depend|package\.json|lock)/i,
|
|
103
|
+
// — destructive / shell —
|
|
60
104
|
/\brm\s+-rf\b/i,
|
|
61
105
|
/\bcurl\b.*\|\s*(sh|bash)/i,
|
|
62
106
|
/\bchmod\b/i,
|
|
107
|
+
/\bdestructive\s+git\b/i,
|
|
63
108
|
];
|
|
64
109
|
/**
|
|
65
110
|
* Judgment / no-checker cues — a rule no linter can honestly decide, so it stays
|
|
@@ -82,23 +127,231 @@ const SEMANTIC_CUES = [
|
|
|
82
127
|
/\bbest practices?\b/i,
|
|
83
128
|
/\bclean code\b/i,
|
|
84
129
|
];
|
|
130
|
+
/**
|
|
131
|
+
* META cues — an instruction to the AGENT, not a norm about the CODE ("read X
|
|
132
|
+
* first", "tell the user", "you are …", "when in doubt ask"). It is not a lint
|
|
133
|
+
* rule and never will be, so it must NOT land in `unrouted` (which reads as
|
|
134
|
+
* "compilable, just hard"). High-precision by design — specific phrasings a real
|
|
135
|
+
* code rule would not use.
|
|
136
|
+
*/
|
|
137
|
+
const META_CUES = [
|
|
138
|
+
// allow `.` in the gap — the referenced thing is often a filename (CLAUDE.md)
|
|
139
|
+
/\bread\b[^\n]{0,30}\bfirst\b/i,
|
|
140
|
+
/\bwhen in doubt\b/i,
|
|
141
|
+
/\bif (you'?re |you are )?unsure\b/i,
|
|
142
|
+
/\bask (the user|first|before)\b/i,
|
|
143
|
+
/\btell (the user|me)\b/i,
|
|
144
|
+
/\byou are\b[^.\n]{0,40}\b(assistant|agent|engineer|claude|model)\b/i,
|
|
145
|
+
/\byour (job|task|role) is\b/i,
|
|
146
|
+
/\b(do not|don'?t|never) (tell|mention|reveal|say)\b/i,
|
|
147
|
+
/\bin (chat|your (reply|response|answer))\b/i,
|
|
148
|
+
// H5 agent-ATTENTION norms (Fable's hook-lane taxonomy): re-read / re-run /
|
|
149
|
+
// re-fetch "without code changes". NOTHING reliably gates these — blocking a
|
|
150
|
+
// re-read breaks post-compaction recovery (false safety worse than an
|
|
151
|
+
// under-blocking guard) — so they are agent-guidance (meta), never a gate. The
|
|
152
|
+
// right instrument is MEASUREMENT (the flight recorder), not enforcement.
|
|
153
|
+
/\bre-?read(?:ing)?\b/i,
|
|
154
|
+
/\bre-?run(?:ning)?\b[^\n]{0,30}\b(?:test|command|suite)\b/i,
|
|
155
|
+
/\bre-?fetch(?:ing)?\b/i,
|
|
156
|
+
/\bwithout code changes\b/i,
|
|
157
|
+
];
|
|
158
|
+
const PATTERN_RULE_MAP = [
|
|
159
|
+
{
|
|
160
|
+
construct: "default exports",
|
|
161
|
+
rule: "no-restricted-syntax",
|
|
162
|
+
linter: "eslint",
|
|
163
|
+
pattern: /\b(?:no|never|avoid|don'?t\s+use|do\s+not\s+use|disallow|ban|forbid|prefer\s+named\s+(?:exports?\s+)?over)\b[^.\n]{0,24}\bdefault\s+exports?\b/i,
|
|
164
|
+
configFix: '"no-restricted-syntax": ["error", { "selector": "ExportDefaultDeclaration", "message": "Use named exports." }]',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
construct: "enums",
|
|
168
|
+
rule: "no-restricted-syntax",
|
|
169
|
+
linter: "eslint",
|
|
170
|
+
pattern: /\b(?:no|never|avoid|don'?t\s+use|do\s+not\s+use|disallow|ban|forbid)\b[^.\n]{0,24}\benums?\b/i,
|
|
171
|
+
configFix: '"no-restricted-syntax": ["error", { "selector": "TSEnumDeclaration", "message": "Use a union or const object instead of an enum." }]',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
construct: "for...in",
|
|
175
|
+
rule: "no-restricted-syntax",
|
|
176
|
+
linter: "eslint",
|
|
177
|
+
pattern: /\b(?:no|never|avoid|don'?t\s+use|do\s+not\s+use|disallow|ban|forbid)\b[^.\n]{0,16}\bfor[\s.]{0,3}in\b/i,
|
|
178
|
+
configFix: '"no-restricted-syntax": ["error", { "selector": "ForInStatement", "message": "Use for...of or Object.keys()." }]',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
construct: "namespaces",
|
|
182
|
+
rule: "no-restricted-syntax",
|
|
183
|
+
linter: "eslint",
|
|
184
|
+
pattern: /\b(?:no|never|avoid|don'?t\s+use|do\s+not\s+use|disallow|ban|forbid)\b[^.\n]{0,24}\bnamespaces?\b/i,
|
|
185
|
+
configFix: '"no-restricted-syntax": ["error", { "selector": "TSModuleDeclaration", "message": "Use ES modules instead of namespaces." }]',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
construct: "classes",
|
|
189
|
+
rule: "no-restricted-syntax",
|
|
190
|
+
linter: "eslint",
|
|
191
|
+
pattern: /\b(?:no|never|avoid|don'?t\s+use|do\s+not\s+use|disallow|ban|forbid)\b[^.\n]{0,12}\b(?<!css |style |styling |utility |tailwind |dom |react |component )(?:es6?\s+|javascript\s+)?class(?:es)?\b(?![\s-]*(?:name|attribute|selector|list))/i,
|
|
192
|
+
configFix: '"no-restricted-syntax": ["error", { "selector": ":matches(ClassDeclaration, ClassExpression)", "message": "Prefer functions and closures over classes." }]',
|
|
193
|
+
},
|
|
194
|
+
// Pylint: REQUIRE docstrings → missing-function-docstring. A PRESENCE context is
|
|
195
|
+
// required (a presence verb near "docstring", or "docstrings required/for each")
|
|
196
|
+
// so a bare "docstring" mention does NOT over-fire: the dogfood caught langchain
|
|
197
|
+
// routing docstring-CONTENT/STYLE rules ("docstring warnings", "backticks in
|
|
198
|
+
// docstrings", "don't repeat the default in the docstring") to this presence
|
|
199
|
+
// check — those are pydocstyle/ruff-D territory, not missing-docstring.
|
|
200
|
+
{
|
|
201
|
+
construct: "docstrings (presence)",
|
|
202
|
+
rule: "missing-function-docstring",
|
|
203
|
+
linter: "pylint",
|
|
204
|
+
pattern: /\b(?:add|require|requires?|write|writing|include|need|needs?|use|using|provide|document|must\s+have)\b[^.\n]{0,24}\bdocstrings?\b|\bdocstrings?\b[^.\n]{0,24}\b(?:required|mandatory|for\s+(?:all|every|each|every|public)|on\s+(?:all|every|each))\b/i,
|
|
205
|
+
configFix: "pylint enables missing-function-docstring (C0116) by default; keep it out of the disable list",
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
/**
|
|
209
|
+
* Backticked rule-id-shaped tokens a bullet names — `curly`, `curly: error` →
|
|
210
|
+
* `curly`, `@typescript-eslint/consistent-type-imports`, `no-only-tests/no-only-tests`.
|
|
211
|
+
* The leading id is taken (severity/args after a `:`/space are dropped), so the
|
|
212
|
+
* token can be looked up against the dynamic catalog.
|
|
213
|
+
*/
|
|
214
|
+
const CODE_SPAN_RE = /`([^`]+)`/g;
|
|
215
|
+
function namedRuleTokens(text) {
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const m of text.matchAll(CODE_SPAN_RE)) {
|
|
218
|
+
const id = m[1].trim().match(/^@?[a-z][\w-]*(?:\/[a-z][\w-]*)*/i);
|
|
219
|
+
if (id)
|
|
220
|
+
out.push(id[0]);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
85
224
|
/**
|
|
86
225
|
* 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
|
-
*
|
|
226
|
+
* rule-name mention ("never commit console.log" is a hook, not a lint rule); a
|
|
227
|
+
* META agent-instruction is pulled out before reuse so it isn't mismatched to a
|
|
228
|
+
* rule; the DYNAMIC catalog (if present) and the static `INTENT_MAP` both feed
|
|
229
|
+
* `reuse`; reuse wins over a soft semantic cue.
|
|
89
230
|
*/
|
|
90
|
-
function classify(text) {
|
|
231
|
+
function classify(text, catalog) {
|
|
91
232
|
if (HOOK_CUES.some((re) => re.test(text)))
|
|
92
233
|
return { category: "hook" };
|
|
234
|
+
if (META_CUES.some((re) => re.test(text)))
|
|
235
|
+
return { category: "meta" };
|
|
236
|
+
// Dynamic catalog: a bullet that NAMES one of the repo's real rules → reuse,
|
|
237
|
+
// carrying whether it's currently enabled (a disabled hit = the "documented but
|
|
238
|
+
// OFF" nudge). Own-repo only — catalog is present only when the linter was
|
|
239
|
+
// enumerated with consent.
|
|
240
|
+
if (catalog) {
|
|
241
|
+
for (const tok of namedRuleTokens(text)) {
|
|
242
|
+
const enabled = catalog.get(tok);
|
|
243
|
+
if (enabled !== undefined)
|
|
244
|
+
return { category: "reuse", rule: tok, enabled };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
93
247
|
for (const m of rule_inventory_js_1.INTENT_MAP) {
|
|
94
248
|
if (m.keywords.some((kw) => (0, rule_inventory_js_1.matchesWholeToken)(text, kw))) {
|
|
95
249
|
return { category: "reuse", rule: m.rule, linter: m.linter };
|
|
96
250
|
}
|
|
97
251
|
}
|
|
252
|
+
// Pattern → a built-in parameterized rule (eslint no-restricted-syntax
|
|
253
|
+
// construct-prohibitions; pylint docstring-presence). These LOOK custom but
|
|
254
|
+
// are reuse; each carries its own linter.
|
|
255
|
+
for (const r of PATTERN_RULE_MAP) {
|
|
256
|
+
if (r.pattern.test(text))
|
|
257
|
+
return { category: "reuse", rule: r.rule, linter: r.linter };
|
|
258
|
+
}
|
|
98
259
|
if (SEMANTIC_CUES.some((re) => re.test(text)))
|
|
99
260
|
return { category: "semantic" };
|
|
100
261
|
return { category: "unrouted" };
|
|
101
262
|
}
|
|
263
|
+
// --- Structured-marker pre-pass (S0/S1) ------------------------------------
|
|
264
|
+
const ENFORCED_RE = /^\*\*Enforced by:\*\*\s*`([^`]+)`/;
|
|
265
|
+
const GUARD_RE = /^\*\*Guard:\*\*/;
|
|
266
|
+
const GUIDANCE_RE = /^\*\*Guidance only\*\*/;
|
|
267
|
+
const MARK_HEADING = /^(#{2,6})\s+(.*)$/;
|
|
268
|
+
const RULE_ID_SHAPE = /^@?[a-z][a-z0-9._/-]*$/;
|
|
269
|
+
/** Does this `**Enforced by:**` value parse as a lint-rule id (vs a prose claim
|
|
270
|
+
* like "CI" or "the linter")? A hand-written marker is a CLAIM — only a rule-id
|
|
271
|
+
* shape is treated as a real reuse rule. */
|
|
272
|
+
function looksLikeRuleId(s) {
|
|
273
|
+
return s.length >= 3 && RULE_ID_SHAPE.test(s.trim());
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Extract rules from EXPLICIT structured markers (`**Enforced by:** \`rule\``,
|
|
277
|
+
* `**Guard:**`, `**Guidance only**`) — the S0/S1 tier. A compiled/marked doc
|
|
278
|
+
* declares its own routing, so these are definitive (zero-heuristic) and are
|
|
279
|
+
* CONSUMED before the heuristic segmenter runs (the returned `skip` line set) so
|
|
280
|
+
* a marked rule is never double-counted. Each marker → ONE atom named by its
|
|
281
|
+
* `##`/`###` heading; a `**Guidance only**` body is still routed through
|
|
282
|
+
* `classify` (the promote-prose signal: a guidance whose body says "before
|
|
283
|
+
* commit" surfaces as a would-be hook). Foreign hand-written `**Enforced by:**`
|
|
284
|
+
* is a CLAIM — only a rule-id-shaped value becomes a reuse rule (gated + verified
|
|
285
|
+
* against the catalog when present; never an inferred contradiction).
|
|
286
|
+
*/
|
|
287
|
+
function extractMarkedRules(text, file, catalog) {
|
|
288
|
+
const lines = text.split("\n");
|
|
289
|
+
const rules = [];
|
|
290
|
+
const skip = new Set();
|
|
291
|
+
for (let i = 0; i < lines.length; i++) {
|
|
292
|
+
const h = MARK_HEADING.exec(lines[i]);
|
|
293
|
+
if (!h)
|
|
294
|
+
continue;
|
|
295
|
+
let j = i + 1;
|
|
296
|
+
while (j < lines.length && !MARK_HEADING.test(lines[j]))
|
|
297
|
+
j++;
|
|
298
|
+
const section = lines.slice(i, j); // [heading … next-heading)
|
|
299
|
+
const heading = h[2].trim();
|
|
300
|
+
let marked = null;
|
|
301
|
+
for (const raw of section.slice(1)) {
|
|
302
|
+
const bl = raw.trim();
|
|
303
|
+
const em = ENFORCED_RE.exec(bl);
|
|
304
|
+
if (em) {
|
|
305
|
+
if (!looksLikeRuleId(em[1]))
|
|
306
|
+
break; // a prose claim, not a rule id
|
|
307
|
+
const enabled = catalog?.get(em[1].trim());
|
|
308
|
+
marked = {
|
|
309
|
+
category: "reuse",
|
|
310
|
+
rule: em[1].trim(),
|
|
311
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
312
|
+
};
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (GUARD_RE.test(bl)) {
|
|
316
|
+
marked = { category: "hook" };
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
if (GUIDANCE_RE.test(bl)) {
|
|
320
|
+
// Route the guidance BODY through classify (promote-prose): a guidance
|
|
321
|
+
// whose text is really an action shows up as a would-be hook.
|
|
322
|
+
const body = section.slice(1).join(" ");
|
|
323
|
+
const c = classify(body, catalog);
|
|
324
|
+
// A guidance body that names a catalog rule is still "documented as
|
|
325
|
+
// guidance" — keep it prose unless it's a genuine action/agent cue.
|
|
326
|
+
marked =
|
|
327
|
+
c.category === "hook" || c.category === "meta"
|
|
328
|
+
? c
|
|
329
|
+
: { category: "semantic" };
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!marked)
|
|
334
|
+
continue;
|
|
335
|
+
// Consume the section BODY lines (heading stays a non-candidate) so the
|
|
336
|
+
// heuristic segmenter never re-emits this marked rule. (1-based.)
|
|
337
|
+
for (let k = i + 1; k < j; k++)
|
|
338
|
+
skip.add(k + 1);
|
|
339
|
+
rules.push({
|
|
340
|
+
text: heading,
|
|
341
|
+
quote: lines[i],
|
|
342
|
+
file,
|
|
343
|
+
lineStart: i + 1,
|
|
344
|
+
lineEnd: j,
|
|
345
|
+
confidence: "high",
|
|
346
|
+
category: marked.category,
|
|
347
|
+
mechanism: MECHANISM[marked.category],
|
|
348
|
+
source: "marker",
|
|
349
|
+
...(marked.rule ? { rule: marked.rule } : {}),
|
|
350
|
+
...(marked.enabled !== undefined ? { enabled: marked.enabled } : {}),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return { rules, skip };
|
|
354
|
+
}
|
|
102
355
|
/**
|
|
103
356
|
* Segment the instruction file and route every atomic rule deterministically.
|
|
104
357
|
* Pure: the caller passes the concatenated instruction text (and an optional
|
|
@@ -106,9 +359,38 @@ function classify(text) {
|
|
|
106
359
|
*/
|
|
107
360
|
function routeRules(instructionText, file, options = {}) {
|
|
108
361
|
const minConfidence = options.minConfidence ?? "high";
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
362
|
+
const catalog = options.availableRules
|
|
363
|
+
? new Map(options.availableRules.rules.map((r) => [r.id, r.enabled]))
|
|
364
|
+
: undefined;
|
|
365
|
+
// A MEDIUM segment that NAMES a rule the repo's catalog actually has is
|
|
366
|
+
// enforceable — the catalog is ground truth, so it's higher-precision than the
|
|
367
|
+
// segmenter's imperative-head cue. This rescues declarative-subject bullets
|
|
368
|
+
// ("The core layer must not import X (`boundaries/dependencies`)") that score
|
|
369
|
+
// medium (context+shape, no imperative head) and are otherwise dropped by the
|
|
370
|
+
// high-only default. Own-repo only (catalog present ⇒ enumerated with consent);
|
|
371
|
+
// the foreign-safe textual path stays conservative by design.
|
|
372
|
+
const namesCatalogRule = (text) => catalog !== undefined &&
|
|
373
|
+
namedRuleTokens(text).some((tok) => catalog.has(tok));
|
|
374
|
+
// A MEDIUM segment matching a construct-prohibition ("No default exports")
|
|
375
|
+
// scores medium ("No" is a prohibition head, not a verb) but is a real reuse
|
|
376
|
+
// rule (no-restricted-syntax) — rescue it, same as the catalog rescue. The
|
|
377
|
+
// patterns are their own precision gate (prohibition + construct proximity).
|
|
378
|
+
const matchesPatternRule = (text) => PATTERN_RULE_MAP.some((r) => r.pattern.test(text));
|
|
379
|
+
// A MEDIUM segment that matches an INTENT_MAP keyword (code-shaped, high-
|
|
380
|
+
// precision) is a real reuse rule — rescue it, same as catalog/restricted-
|
|
381
|
+
// syntax. Fixes construct-prohibitions with no verb ("No bare except clauses")
|
|
382
|
+
// that score medium and would otherwise drop before classify() reuses them.
|
|
383
|
+
const matchesIntentMap = (text) => rule_inventory_js_1.INTENT_MAP.some((m) => m.keywords.some((kw) => (0, rule_inventory_js_1.matchesWholeToken)(text, kw)));
|
|
384
|
+
// S0/S1 pre-pass: explicit markers are definitive and are CONSUMED (their body
|
|
385
|
+
// lines are skipped) so the heuristic segmenter can't double-count them.
|
|
386
|
+
const marked = extractMarkedRules(instructionText, file, catalog);
|
|
387
|
+
const segments = (0, segment_js_1.segmentInstructions)(instructionText, file, marked.skip).filter((s) => minConfidence === "medium" ||
|
|
388
|
+
s.confidence === "high" ||
|
|
389
|
+
namesCatalogRule(s.text) ||
|
|
390
|
+
matchesPatternRule(s.text) ||
|
|
391
|
+
matchesIntentMap(s.text));
|
|
392
|
+
const heuristicRules = segments.map((s) => {
|
|
393
|
+
const c = classify(s.text, catalog);
|
|
112
394
|
return {
|
|
113
395
|
text: s.text,
|
|
114
396
|
quote: s.exactQuote,
|
|
@@ -118,18 +400,47 @@ function routeRules(instructionText, file, options = {}) {
|
|
|
118
400
|
confidence: s.confidence,
|
|
119
401
|
category: c.category,
|
|
120
402
|
mechanism: MECHANISM[c.category],
|
|
403
|
+
source: "heuristic",
|
|
121
404
|
...(c.rule ? { rule: c.rule } : {}),
|
|
122
405
|
...(c.linter ? { linter: c.linter } : {}),
|
|
406
|
+
...(c.enabled !== undefined ? { enabled: c.enabled } : {}),
|
|
123
407
|
};
|
|
124
408
|
});
|
|
409
|
+
// Marker rules first (definitive), then the heuristic residue.
|
|
410
|
+
const rules = [...marked.rules, ...heuristicRules];
|
|
125
411
|
const counts = {
|
|
126
412
|
reuse: 0,
|
|
127
413
|
hook: 0,
|
|
414
|
+
meta: 0,
|
|
128
415
|
semantic: 0,
|
|
129
416
|
unrouted: 0,
|
|
130
417
|
};
|
|
131
418
|
for (const r of rules)
|
|
132
419
|
counts[r.category]++;
|
|
133
|
-
return { segmented:
|
|
420
|
+
return { segmented: rules.length, counts, rules };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Merge per-file routings into one. Each instruction source is routed SEPARATELY
|
|
424
|
+
* (so every rule keeps its OWN file path + line numbers — concatenating first
|
|
425
|
+
* would corrupt the provenance the preview promises), then folded here: rules
|
|
426
|
+
* concatenated, counts + segmented summed. Pure. `[]` → an empty routing.
|
|
427
|
+
*/
|
|
428
|
+
function mergeRoutings(routings) {
|
|
429
|
+
const counts = {
|
|
430
|
+
reuse: 0,
|
|
431
|
+
hook: 0,
|
|
432
|
+
meta: 0,
|
|
433
|
+
semantic: 0,
|
|
434
|
+
unrouted: 0,
|
|
435
|
+
};
|
|
436
|
+
const rules = [];
|
|
437
|
+
let segmented = 0;
|
|
438
|
+
for (const r of routings) {
|
|
439
|
+
segmented += r.segmented;
|
|
440
|
+
rules.push(...r.rules);
|
|
441
|
+
for (const k of Object.keys(counts))
|
|
442
|
+
counts[k] += r.counts[k];
|
|
443
|
+
}
|
|
444
|
+
return { segmented, counts, rules };
|
|
134
445
|
}
|
|
135
446
|
//# sourceMappingURL=rule-routing.js.map
|
package/dist/segment.d.ts
CHANGED
|
@@ -29,5 +29,5 @@ export interface SegmentedRule {
|
|
|
29
29
|
* candidacy. Candidate units are (a) list items with attached continuation
|
|
30
30
|
* lines and (b) sentences of paragraphs under a rule-ish heading.
|
|
31
31
|
*/
|
|
32
|
-
export declare function segmentInstructions(markdown: string, file?: string): SegmentedRule[];
|
|
32
|
+
export declare function segmentInstructions(markdown: string, file?: string, skipLines?: ReadonlySet<number>): SegmentedRule[];
|
|
33
33
|
//# sourceMappingURL=segment.d.ts.map
|