vigiles 14.1.0 → 14.2.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 +16 -13
- package/dist/adapters/claude-code/dialect.d.ts +1 -1
- package/dist/audit-report.template.html +14 -14
- package/dist/cli.js +6 -4
- package/dist/core/rule-catalog.d.ts +1 -1
- package/dist/core/rule-catalog.js +1 -1
- package/dist/instruction-sources.d.ts +1 -1
- package/dist/instruction-sources.js +1 -1
- package/dist/rule-inventory.js +151 -2
- package/dist/rule-routing.d.ts +16 -1
- package/dist/rule-routing.js +172 -138
- package/dist/rule-signals.d.ts +46 -0
- package/dist/rule-signals.js +49 -0
- package/dist/segment.d.ts +16 -5
- package/dist/segment.js +219 -179
- package/package.json +1 -1
package/dist/rule-routing.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LANE_META = void 0;
|
|
3
4
|
exports.routeRules = routeRules;
|
|
4
5
|
exports.mergeRoutings = mergeRoutings;
|
|
5
6
|
/**
|
|
@@ -41,6 +42,7 @@ exports.mergeRoutings = mergeRoutings;
|
|
|
41
42
|
*/
|
|
42
43
|
const segment_js_1 = require("./segment.js");
|
|
43
44
|
const rule_inventory_js_1 = require("./rule-inventory.js");
|
|
45
|
+
const rule_signals_js_1 = require("./rule-signals.js");
|
|
44
46
|
/** The mechanism each category maps to — a fixed, honest ladder. */
|
|
45
47
|
const MECHANISM = {
|
|
46
48
|
reuse: "config-line",
|
|
@@ -50,13 +52,27 @@ const MECHANISM = {
|
|
|
50
52
|
unrouted: "synthesize",
|
|
51
53
|
};
|
|
52
54
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* The user-facing presentation of each routing category — its glyph + lane
|
|
56
|
+
* label. The SINGLE SOURCE the terminal summary reads (and the HTML report
|
|
57
|
+
* mirrors), so the category-name → lane-label mapping lives in one place.
|
|
58
|
+
*
|
|
59
|
+
* NB the type name `unrouted` is a WIRE value (it appears in the versioned
|
|
60
|
+
* `AuditReport` JSON), which is why it isn't renamed to its lane label `custom`;
|
|
61
|
+
* this table is where the human-facing name is resolved. The category meanings
|
|
62
|
+
* are documented in the file header; the mapping is tabled in
|
|
63
|
+
* `research/rule-enforcer-design.md` §4.
|
|
58
64
|
*/
|
|
59
|
-
|
|
65
|
+
exports.LANE_META = {
|
|
66
|
+
reuse: { glyph: "✓", label: "enforceable" },
|
|
67
|
+
hook: { glyph: "⛓", label: "hook" },
|
|
68
|
+
unrouted: { glyph: "⚙", label: "custom" },
|
|
69
|
+
semantic: { glyph: "✎", label: "judgment" },
|
|
70
|
+
meta: { glyph: "☰", label: "agent-note" },
|
|
71
|
+
};
|
|
72
|
+
// NORM_SIGNAL (a deontic modal ANYWHERE) keeps the POSSIBLE review tier to
|
|
73
|
+
// genuine rule-candidates the imperative gate missed ("every function MUST have
|
|
74
|
+
// a docstring") instead of arbitrary prose. It lives in ./rule-signals.ts
|
|
75
|
+
// alongside the segment gate's RULE_PREDICATE twin so the two can't drift.
|
|
60
76
|
/**
|
|
61
77
|
* ACTION-rule cues — things a linter never sees (git, filesystem, shell,
|
|
62
78
|
* process). A hook is the right gate, not a lint rule. Widened to the article's
|
|
@@ -64,7 +80,7 @@ const NORM_SIGNAL = /\b(?:must(?:n't)?|should(?:n't)?|shall|never|always|avoids?
|
|
|
64
80
|
* (git-push + rm-rf only) reported ~2% hooks where the 252-rule hand-sort found
|
|
65
81
|
* **37%** (the largest bucket); it was missing push-to-branch, before-push,
|
|
66
82
|
* after-edit, tool-substitution, amend/rebase-pushed, and dependency guards.
|
|
67
|
-
* See `research/rule-
|
|
83
|
+
* See `research/rule-enforcer-multilang-design.md` §5b (the hook lane).
|
|
68
84
|
*/
|
|
69
85
|
const HOOK_CUES = [
|
|
70
86
|
// — branch / push guards (vcs) —
|
|
@@ -163,38 +179,41 @@ const META_CUES = [
|
|
|
163
179
|
/\bre-?fetch(?:ing)?\b/i,
|
|
164
180
|
/\bwithout code changes\b/i,
|
|
165
181
|
];
|
|
182
|
+
/** Every construct-prohibition maps to the same ESLint rule with a different
|
|
183
|
+
* selector — one named constant so the shared id isn't repeated as a literal. */
|
|
184
|
+
const NO_RESTRICTED_SYNTAX = "no-restricted-syntax";
|
|
166
185
|
const PATTERN_RULE_MAP = [
|
|
167
186
|
{
|
|
168
187
|
construct: "default exports",
|
|
169
|
-
rule:
|
|
188
|
+
rule: NO_RESTRICTED_SYNTAX,
|
|
170
189
|
linter: "eslint",
|
|
171
190
|
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,
|
|
172
191
|
configFix: '"no-restricted-syntax": ["error", { "selector": "ExportDefaultDeclaration", "message": "Use named exports." }]',
|
|
173
192
|
},
|
|
174
193
|
{
|
|
175
194
|
construct: "enums",
|
|
176
|
-
rule:
|
|
195
|
+
rule: NO_RESTRICTED_SYNTAX,
|
|
177
196
|
linter: "eslint",
|
|
178
197
|
pattern: /\b(?:no|never|avoid|don'?t\s+use|do\s+not\s+use|disallow|ban|forbid)\b[^.\n]{0,24}\benums?\b/i,
|
|
179
198
|
configFix: '"no-restricted-syntax": ["error", { "selector": "TSEnumDeclaration", "message": "Use a union or const object instead of an enum." }]',
|
|
180
199
|
},
|
|
181
200
|
{
|
|
182
201
|
construct: "for...in",
|
|
183
|
-
rule:
|
|
202
|
+
rule: NO_RESTRICTED_SYNTAX,
|
|
184
203
|
linter: "eslint",
|
|
185
204
|
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,
|
|
186
205
|
configFix: '"no-restricted-syntax": ["error", { "selector": "ForInStatement", "message": "Use for...of or Object.keys()." }]',
|
|
187
206
|
},
|
|
188
207
|
{
|
|
189
208
|
construct: "namespaces",
|
|
190
|
-
rule:
|
|
209
|
+
rule: NO_RESTRICTED_SYNTAX,
|
|
191
210
|
linter: "eslint",
|
|
192
211
|
pattern: /\b(?:no|never|avoid|don'?t\s+use|do\s+not\s+use|disallow|ban|forbid)\b[^.\n]{0,24}\bnamespaces?\b/i,
|
|
193
212
|
configFix: '"no-restricted-syntax": ["error", { "selector": "TSModuleDeclaration", "message": "Use ES modules instead of namespaces." }]',
|
|
194
213
|
},
|
|
195
214
|
{
|
|
196
215
|
construct: "classes",
|
|
197
|
-
rule:
|
|
216
|
+
rule: NO_RESTRICTED_SYNTAX,
|
|
198
217
|
linter: "eslint",
|
|
199
218
|
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,
|
|
200
219
|
configFix: '"no-restricted-syntax": ["error", { "selector": ":matches(ClassDeclaration, ClassExpression)", "message": "Prefer functions and closures over classes." }]',
|
|
@@ -323,6 +342,60 @@ function looksLikeRuleId(s) {
|
|
|
323
342
|
* is a CLAIM — only a rule-id-shaped value becomes a reuse rule (gated + verified
|
|
324
343
|
* against the catalog when present; never an inferred contradiction).
|
|
325
344
|
*/
|
|
345
|
+
/** A `**Guidance only**` body is prose UNLESS its text is really an action/agent
|
|
346
|
+
* cue (promote-prose): route the whole body through classify and keep it prose
|
|
347
|
+
* (`semantic`) unless classify sees a genuine hook/meta signal. */
|
|
348
|
+
function guidanceClassification(section, catalog) {
|
|
349
|
+
const c = classify(section.slice(1).join(" "), catalog);
|
|
350
|
+
return c.category === "hook" || c.category === "meta"
|
|
351
|
+
? c
|
|
352
|
+
: { category: "semantic" };
|
|
353
|
+
}
|
|
354
|
+
/** Scan a marked section's BODY for the FIRST structured marker and return its
|
|
355
|
+
* classification, or null if the section declares none (or an `**Enforced by:**`
|
|
356
|
+
* whose value is a prose claim, not a rule id). */
|
|
357
|
+
function markerFor(section, catalog) {
|
|
358
|
+
for (const raw of section.slice(1)) {
|
|
359
|
+
const bl = raw.trim();
|
|
360
|
+
const em = ENFORCED_RE.exec(bl);
|
|
361
|
+
if (em) {
|
|
362
|
+
if (!looksLikeRuleId(em[1]))
|
|
363
|
+
return null; // a prose claim, not a rule id
|
|
364
|
+
const rule = em[1].trim();
|
|
365
|
+
const hit = catalog?.get(rule);
|
|
366
|
+
return {
|
|
367
|
+
category: "reuse",
|
|
368
|
+
rule,
|
|
369
|
+
...(hit !== undefined
|
|
370
|
+
? { enabled: hit.enabled, linter: hit.linter }
|
|
371
|
+
: {}),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (GUARD_RE.test(bl))
|
|
375
|
+
return { category: "hook" };
|
|
376
|
+
if (GUIDANCE_RE.test(bl))
|
|
377
|
+
return guidanceClassification(section, catalog);
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
/** Build a definitive (zero-heuristic) marker `RoutedRule` from a classification
|
|
382
|
+
* and its source location. */
|
|
383
|
+
function markerRuleFrom(marked, loc) {
|
|
384
|
+
return {
|
|
385
|
+
text: loc.text,
|
|
386
|
+
quote: loc.quote,
|
|
387
|
+
file: loc.file,
|
|
388
|
+
lineStart: loc.lineStart,
|
|
389
|
+
lineEnd: loc.lineEnd,
|
|
390
|
+
confidence: "high",
|
|
391
|
+
category: marked.category,
|
|
392
|
+
mechanism: MECHANISM[marked.category],
|
|
393
|
+
source: "marker",
|
|
394
|
+
...(marked.rule ? { rule: marked.rule } : {}),
|
|
395
|
+
...(marked.linter ? { linter: marked.linter } : {}),
|
|
396
|
+
...(marked.enabled !== undefined ? { enabled: marked.enabled } : {}),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
326
399
|
function extractMarkedRules(text, file, catalog) {
|
|
327
400
|
const lines = text.split("\n");
|
|
328
401
|
const rules = [];
|
|
@@ -334,66 +407,92 @@ function extractMarkedRules(text, file, catalog) {
|
|
|
334
407
|
let j = i + 1;
|
|
335
408
|
while (j < lines.length && !MARK_HEADING.test(lines[j]))
|
|
336
409
|
j++;
|
|
337
|
-
const
|
|
338
|
-
const heading = h[2].trim();
|
|
339
|
-
let marked = null;
|
|
340
|
-
for (const raw of section.slice(1)) {
|
|
341
|
-
const bl = raw.trim();
|
|
342
|
-
const em = ENFORCED_RE.exec(bl);
|
|
343
|
-
if (em) {
|
|
344
|
-
if (!looksLikeRuleId(em[1]))
|
|
345
|
-
break; // a prose claim, not a rule id
|
|
346
|
-
const hit = catalog?.get(em[1].trim());
|
|
347
|
-
marked = {
|
|
348
|
-
category: "reuse",
|
|
349
|
-
rule: em[1].trim(),
|
|
350
|
-
...(hit !== undefined
|
|
351
|
-
? { enabled: hit.enabled, linter: hit.linter }
|
|
352
|
-
: {}),
|
|
353
|
-
};
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
if (GUARD_RE.test(bl)) {
|
|
357
|
-
marked = { category: "hook" };
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
if (GUIDANCE_RE.test(bl)) {
|
|
361
|
-
// Route the guidance BODY through classify (promote-prose): a guidance
|
|
362
|
-
// whose text is really an action shows up as a would-be hook.
|
|
363
|
-
const body = section.slice(1).join(" ");
|
|
364
|
-
const c = classify(body, catalog);
|
|
365
|
-
// A guidance body that names a catalog rule is still "documented as
|
|
366
|
-
// guidance" — keep it prose unless it's a genuine action/agent cue.
|
|
367
|
-
marked =
|
|
368
|
-
c.category === "hook" || c.category === "meta"
|
|
369
|
-
? c
|
|
370
|
-
: { category: "semantic" };
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
410
|
+
const marked = markerFor(lines.slice(i, j), catalog);
|
|
374
411
|
if (!marked)
|
|
375
412
|
continue;
|
|
376
413
|
// Consume the section BODY lines (heading stays a non-candidate) so the
|
|
377
414
|
// heuristic segmenter never re-emits this marked rule. (1-based.)
|
|
378
415
|
for (let k = i + 1; k < j; k++)
|
|
379
416
|
skip.add(k + 1);
|
|
380
|
-
rules.push({
|
|
381
|
-
text:
|
|
417
|
+
rules.push(markerRuleFrom(marked, {
|
|
418
|
+
text: h[2].trim(),
|
|
382
419
|
quote: lines[i],
|
|
383
420
|
file,
|
|
384
421
|
lineStart: i + 1,
|
|
385
422
|
lineEnd: j,
|
|
386
|
-
|
|
387
|
-
category: marked.category,
|
|
388
|
-
mechanism: MECHANISM[marked.category],
|
|
389
|
-
source: "marker",
|
|
390
|
-
...(marked.rule ? { rule: marked.rule } : {}),
|
|
391
|
-
...(marked.linter ? { linter: marked.linter } : {}),
|
|
392
|
-
...(marked.enabled !== undefined ? { enabled: marked.enabled } : {}),
|
|
393
|
-
});
|
|
423
|
+
}));
|
|
394
424
|
}
|
|
395
425
|
return { rules, skip };
|
|
396
426
|
}
|
|
427
|
+
/** The rescue sources, OR-ed (any one promotes a bullet to confident). They are
|
|
428
|
+
* the higher-precision override of the segmenter's imperative-head cue, which
|
|
429
|
+
* alone would drop these medium-scoring bullets. */
|
|
430
|
+
const RESCUE_SOURCES = [
|
|
431
|
+
// catalog — the text NAMES a rule the repo's live catalog actually has (ground
|
|
432
|
+
// truth, own-repo): rescues "The core layer must not import X (`boundaries/…`)".
|
|
433
|
+
(t, cat) => cat !== undefined && namedRuleTokens(t).some((tok) => cat.has(tok)),
|
|
434
|
+
// pattern — a construct-prohibition ("No default exports") → a real
|
|
435
|
+
// `no-restricted-syntax` rule.
|
|
436
|
+
(t) => PATTERN_RULE_MAP.some((r) => r.pattern.test(t)),
|
|
437
|
+
// intent — an INTENT_MAP keyword match ("No bare except clauses"): a
|
|
438
|
+
// code-shaped, high-precision reuse rule with no imperative verb.
|
|
439
|
+
(t) => rule_inventory_js_1.INTENT_MAP.some((m) => m.keywords.some((kw) => (0, rule_inventory_js_1.matchesWholeToken)(t, kw))),
|
|
440
|
+
];
|
|
441
|
+
/** A bullet is RESCUED — promoted to confident — if any rescue source maps it to
|
|
442
|
+
* a real off-the-shelf rule (independent of the medium opt-in). */
|
|
443
|
+
function isRescued(text, catalog) {
|
|
444
|
+
return RESCUE_SOURCES.some((rescue) => rescue(text, catalog));
|
|
445
|
+
}
|
|
446
|
+
const foldToCandidate = (s) => ({
|
|
447
|
+
text: s.text,
|
|
448
|
+
exactQuote: s.text,
|
|
449
|
+
file: s.file,
|
|
450
|
+
lineStart: s.lineStart,
|
|
451
|
+
lineEnd: s.lineEnd,
|
|
452
|
+
confidence: "medium",
|
|
453
|
+
});
|
|
454
|
+
const toNoSignalSkip = (s) => ({
|
|
455
|
+
text: s.text,
|
|
456
|
+
file: s.file,
|
|
457
|
+
lineStart: s.lineStart,
|
|
458
|
+
lineEnd: s.lineEnd,
|
|
459
|
+
reason: "no-signal",
|
|
460
|
+
});
|
|
461
|
+
/**
|
|
462
|
+
* Split segmenter output into the three routed tiers:
|
|
463
|
+
*
|
|
464
|
+
* - CONFIDENT — high, a medium opt-in, or a RESCUE (names/matches a real rule).
|
|
465
|
+
* - POSSIBLE — a non-confident leftover carrying a norm modal (`NORM_SIGNAL`):
|
|
466
|
+
* a genuine recall-miss surfaced for review ("every function must have a
|
|
467
|
+
* docstring"), not routed as fact.
|
|
468
|
+
* - SKIPPED — the rest (index/description/section rejects + no-signal leftovers).
|
|
469
|
+
*
|
|
470
|
+
* THE LOAD-BEARING ASYMMETRY: a gate-rejected `no-signal` bullet is folded back
|
|
471
|
+
* as a candidate but promoted to confident ONLY by a RESCUE — NEVER by the
|
|
472
|
+
* blanket medium opt-in, which must not resurrect what the gate explicitly
|
|
473
|
+
* rejected. See `research/rule-enforcer-design.md` §2.
|
|
474
|
+
*/
|
|
475
|
+
function partitionCandidates(segments, rawSkipped, catalog, minConfidence) {
|
|
476
|
+
const rescued = (t) => isRescued(t, catalog);
|
|
477
|
+
const isConfident = (s) => minConfidence === "medium" || s.confidence === "high" || rescued(s.text);
|
|
478
|
+
const folds = rawSkipped
|
|
479
|
+
.filter((s) => s.reason === "no-signal")
|
|
480
|
+
.map(foldToCandidate);
|
|
481
|
+
const confident = [
|
|
482
|
+
...segments.filter(isConfident),
|
|
483
|
+
...folds.filter((s) => rescued(s.text)),
|
|
484
|
+
];
|
|
485
|
+
const leftover = [
|
|
486
|
+
...segments.filter((s) => !isConfident(s)),
|
|
487
|
+
...folds.filter((s) => !rescued(s.text)),
|
|
488
|
+
];
|
|
489
|
+
const possible = leftover.filter((s) => rule_signals_js_1.NORM_SIGNAL.test(s.text));
|
|
490
|
+
const skipped = [
|
|
491
|
+
...rawSkipped.filter((s) => s.reason !== "no-signal"),
|
|
492
|
+
...leftover.filter((s) => !rule_signals_js_1.NORM_SIGNAL.test(s.text)).map(toNoSignalSkip),
|
|
493
|
+
];
|
|
494
|
+
return { confident, possible, skipped };
|
|
495
|
+
}
|
|
397
496
|
/**
|
|
398
497
|
* Segment the instruction file and route every atomic rule deterministically.
|
|
399
498
|
* Pure: the caller passes the concatenated instruction text (and an optional
|
|
@@ -404,35 +503,6 @@ function routeRules(instructionText, file, options = {}) {
|
|
|
404
503
|
const catalog = options.availableRules
|
|
405
504
|
? buildCatalogLookup(options.availableRules.rules)
|
|
406
505
|
: undefined;
|
|
407
|
-
// A MEDIUM segment that NAMES a rule the repo's catalog actually has is
|
|
408
|
-
// enforceable — the catalog is ground truth, so it's higher-precision than the
|
|
409
|
-
// segmenter's imperative-head cue. This rescues declarative-subject bullets
|
|
410
|
-
// ("The core layer must not import X (`boundaries/dependencies`)") that score
|
|
411
|
-
// medium (context+shape, no imperative head) and are otherwise dropped by the
|
|
412
|
-
// high-only default. Own-repo only (catalog present ⇒ enumerated with consent);
|
|
413
|
-
// the foreign-safe textual path stays conservative by design.
|
|
414
|
-
const namesCatalogRule = (text) => catalog !== undefined &&
|
|
415
|
-
namedRuleTokens(text).some((tok) => catalog.has(tok));
|
|
416
|
-
// A MEDIUM segment matching a construct-prohibition ("No default exports")
|
|
417
|
-
// scores medium ("No" is a prohibition head, not a verb) but is a real reuse
|
|
418
|
-
// rule (no-restricted-syntax) — rescue it, same as the catalog rescue. The
|
|
419
|
-
// patterns are their own precision gate (prohibition + construct proximity).
|
|
420
|
-
const matchesPatternRule = (text) => PATTERN_RULE_MAP.some((r) => r.pattern.test(text));
|
|
421
|
-
// A MEDIUM segment that matches an INTENT_MAP keyword (code-shaped, high-
|
|
422
|
-
// precision) is a real reuse rule — rescue it, same as catalog/restricted-
|
|
423
|
-
// syntax. Fixes construct-prohibitions with no verb ("No bare except clauses")
|
|
424
|
-
// that score medium and would otherwise drop before classify() reuses them.
|
|
425
|
-
const matchesIntentMap = (text) => rule_inventory_js_1.INTENT_MAP.some((m) => m.keywords.some((kw) => (0, rule_inventory_js_1.matchesWholeToken)(text, kw)));
|
|
426
|
-
// A segment is CONFIDENT if it's high, rescued by the catalog/pattern/intent, or
|
|
427
|
-
// the caller opted into medium. Everything else the segmenter emitted is a
|
|
428
|
-
// POSSIBLE rule (medium, unrescued) — surfaced for review, not routed as fact.
|
|
429
|
-
// A RESCUE — the text NAMES/matches a real rule (catalog / restricted-syntax /
|
|
430
|
-
// intent). This promotes even a gate-rejected bullet to confident, because it
|
|
431
|
-
// provably maps to an off-the-shelf rule; independent of the medium opt-in.
|
|
432
|
-
const isRescued = (text) => namesCatalogRule(text) ||
|
|
433
|
-
matchesPatternRule(text) ||
|
|
434
|
-
matchesIntentMap(text);
|
|
435
|
-
const isConfident = (s) => minConfidence === "medium" || s.confidence === "high" || isRescued(s.text);
|
|
436
506
|
const toRouted = (s) => {
|
|
437
507
|
const c = classify(s.text, catalog);
|
|
438
508
|
return {
|
|
@@ -451,58 +521,16 @@ function routeRules(instructionText, file, options = {}) {
|
|
|
451
521
|
};
|
|
452
522
|
};
|
|
453
523
|
// S0/S1 pre-pass: explicit markers are definitive and are CONSUMED (their body
|
|
454
|
-
// lines are skipped) so the heuristic segmenter can't double-count them.
|
|
524
|
+
// lines are skipped) so the heuristic segmenter can't double-count them. The
|
|
525
|
+
// segmenter output then splits into confident / possible / skipped tiers.
|
|
455
526
|
const marked = extractMarkedRules(instructionText, file, catalog);
|
|
456
527
|
const { segments, skipped: rawSkipped } = (0, segment_js_1.segmentInstructions)(instructionText, file, marked.skip);
|
|
457
|
-
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
// it stays SKIPPED alongside the index/description/section rejects. A folded
|
|
463
|
-
// candidate that NAMES/matches a real rule is still rescued to CONFIDENT.
|
|
464
|
-
const asCandidate = (s) => ({
|
|
465
|
-
text: s.text,
|
|
466
|
-
exactQuote: s.text,
|
|
467
|
-
file: s.file,
|
|
468
|
-
lineStart: s.lineStart,
|
|
469
|
-
lineEnd: s.lineEnd,
|
|
470
|
-
confidence: "medium",
|
|
471
|
-
});
|
|
472
|
-
// Real SEGMENTS route by the full confident check (incl. the medium opt-in).
|
|
473
|
-
// Gate-rejected `no-signal` bullets are folded back in as candidates, but they
|
|
474
|
-
// are promoted to confident ONLY by a real RESCUE — NEVER by the blanket medium
|
|
475
|
-
// opt-in, which must not resurrect bullets the gate explicitly rejected.
|
|
476
|
-
const noSignal = rawSkipped.filter((s) => s.reason === "no-signal");
|
|
477
|
-
const folds = noSignal.map(asCandidate);
|
|
478
|
-
const heuristicRules = [
|
|
479
|
-
...segments.filter(isConfident),
|
|
480
|
-
...folds.filter((s) => isRescued(s.text)),
|
|
481
|
-
].map(toRouted);
|
|
482
|
-
// The non-confident leftovers split by the norm signal: a rule-ish bullet
|
|
483
|
-
// (carries a deontic modal) is a genuine recall-miss → POSSIBLE (review); the
|
|
484
|
-
// rest is prose → SKIPPED with a `no-signal` reason (visible, not dropped).
|
|
485
|
-
const leftover = [
|
|
486
|
-
...segments.filter((s) => !isConfident(s)),
|
|
487
|
-
...folds.filter((s) => !isRescued(s.text)),
|
|
528
|
+
const tiers = partitionCandidates(segments, rawSkipped, catalog, minConfidence);
|
|
529
|
+
// Marker rules first (definitive), then the confident heuristic residue.
|
|
530
|
+
const rules = [
|
|
531
|
+
...marked.rules,
|
|
532
|
+
...tiers.confident.map(toRouted),
|
|
488
533
|
];
|
|
489
|
-
const possible = leftover
|
|
490
|
-
.filter((s) => NORM_SIGNAL.test(s.text))
|
|
491
|
-
.map(toRouted);
|
|
492
|
-
const skipped = [
|
|
493
|
-
...rawSkipped.filter((s) => s.reason !== "no-signal"),
|
|
494
|
-
...leftover
|
|
495
|
-
.filter((s) => !NORM_SIGNAL.test(s.text))
|
|
496
|
-
.map((s) => ({
|
|
497
|
-
text: s.text,
|
|
498
|
-
file: s.file,
|
|
499
|
-
lineStart: s.lineStart,
|
|
500
|
-
lineEnd: s.lineEnd,
|
|
501
|
-
reason: "no-signal",
|
|
502
|
-
})),
|
|
503
|
-
];
|
|
504
|
-
// Marker rules first (definitive), then the heuristic residue.
|
|
505
|
-
const rules = [...marked.rules, ...heuristicRules];
|
|
506
534
|
const counts = {
|
|
507
535
|
reuse: 0,
|
|
508
536
|
hook: 0,
|
|
@@ -512,7 +540,13 @@ function routeRules(instructionText, file, options = {}) {
|
|
|
512
540
|
};
|
|
513
541
|
for (const r of rules)
|
|
514
542
|
counts[r.category]++;
|
|
515
|
-
return {
|
|
543
|
+
return {
|
|
544
|
+
segmented: rules.length,
|
|
545
|
+
counts,
|
|
546
|
+
rules,
|
|
547
|
+
possible: tiers.possible.map(toRouted),
|
|
548
|
+
skipped: tiers.skipped,
|
|
549
|
+
};
|
|
516
550
|
}
|
|
517
551
|
/**
|
|
518
552
|
* Merge per-file routings into one. Each instruction source is routed SEPARATELY
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared LEXICAL signals for "is this text a rule?" — the deontic/imperative
|
|
3
|
+
* vocabulary the detection pipeline keys on, kept in ONE home so the stages that
|
|
4
|
+
* use it can't silently drift. These previously lived as near-duplicate
|
|
5
|
+
* deontic-modal regexes in `segment.ts` (`RULE_PREDICATE`) and `rule-routing.ts`
|
|
6
|
+
* (`NORM_SIGNAL`) with no cross-reference.
|
|
7
|
+
*
|
|
8
|
+
* Two stages consume them:
|
|
9
|
+
*
|
|
10
|
+
* - the segment GATE (`src/segment.ts`) — precision-first accept/reject. Uses
|
|
11
|
+
* `FORM_HEAD` (an imperative/prohibitive sentence HEAD) as a rule cue, and
|
|
12
|
+
* `RULE_PREDICATE` (a deontic modal ANYWHERE) to stop a code-span-led
|
|
13
|
+
* sentence being mis-rejected as a description.
|
|
14
|
+
* - the routing POSSIBLE filter (`src/rule-routing.ts`) — recall recovery. Uses
|
|
15
|
+
* `NORM_SIGNAL` to keep the "possible (review)" tier to genuine recall-misses
|
|
16
|
+
* (a bullet that carries a norm modal) instead of flooding it with prose.
|
|
17
|
+
*
|
|
18
|
+
* `NORM_SIGNAL` and `RULE_PREDICATE` are BOTH "deontic modal anywhere" matchers
|
|
19
|
+
* with slightly different word lists ON PURPOSE — different jobs, calibrated
|
|
20
|
+
* separately against the OSS corpus. They are kept ADJACENT here so a widening
|
|
21
|
+
* of one prompts a review of the other, rather than the two drifting apart in
|
|
22
|
+
* separate files. See `research/rule-enforcer-design.md` §2.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* An imperative/prohibitive sentence HEAD ("Never …", "Avoid …", "No …") — the
|
|
26
|
+
* segment gate's `form` cue. Anchored at the start (a HEAD, not anywhere). The
|
|
27
|
+
* deontic verbs (require/disallow/forbid/ban/enforce) are common rule leads
|
|
28
|
+
* ("Require `curly` braces", "Disallow `var`"). NB "no" is bare `no` + the
|
|
29
|
+
* shared trailing `\b` (a boundary right after "no" — before a space OR a
|
|
30
|
+
* backtick), so "No bare except" / "No default exports" / "No `any`" all match,
|
|
31
|
+
* while "Note"/"Nowhere" (no boundary after "no") are rejected. The earlier
|
|
32
|
+
* `no\s+\S` form silently failed "No bare except" (the measured bug).
|
|
33
|
+
*/
|
|
34
|
+
export declare const FORM_HEAD: RegExp;
|
|
35
|
+
/**
|
|
36
|
+
* A deontic modal ANYWHERE in the text — routing's POSSIBLE-tier recall gate.
|
|
37
|
+
* Narrow (modal verbs only) so the review tier stays genuine recall-misses.
|
|
38
|
+
*/
|
|
39
|
+
export declare const NORM_SIGNAL: RegExp;
|
|
40
|
+
/**
|
|
41
|
+
* A deontic predicate ANYWHERE — the segment gate's description-reject guard: a
|
|
42
|
+
* code-span-led sentence carrying one of these is a RULE ("`const` is preferred
|
|
43
|
+
* over `let`"), not a description, so the description reject must NOT fire.
|
|
44
|
+
*/
|
|
45
|
+
export declare const RULE_PREDICATE: RegExp;
|
|
46
|
+
//# sourceMappingURL=rule-signals.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared LEXICAL signals for "is this text a rule?" — the deontic/imperative
|
|
4
|
+
* vocabulary the detection pipeline keys on, kept in ONE home so the stages that
|
|
5
|
+
* use it can't silently drift. These previously lived as near-duplicate
|
|
6
|
+
* deontic-modal regexes in `segment.ts` (`RULE_PREDICATE`) and `rule-routing.ts`
|
|
7
|
+
* (`NORM_SIGNAL`) with no cross-reference.
|
|
8
|
+
*
|
|
9
|
+
* Two stages consume them:
|
|
10
|
+
*
|
|
11
|
+
* - the segment GATE (`src/segment.ts`) — precision-first accept/reject. Uses
|
|
12
|
+
* `FORM_HEAD` (an imperative/prohibitive sentence HEAD) as a rule cue, and
|
|
13
|
+
* `RULE_PREDICATE` (a deontic modal ANYWHERE) to stop a code-span-led
|
|
14
|
+
* sentence being mis-rejected as a description.
|
|
15
|
+
* - the routing POSSIBLE filter (`src/rule-routing.ts`) — recall recovery. Uses
|
|
16
|
+
* `NORM_SIGNAL` to keep the "possible (review)" tier to genuine recall-misses
|
|
17
|
+
* (a bullet that carries a norm modal) instead of flooding it with prose.
|
|
18
|
+
*
|
|
19
|
+
* `NORM_SIGNAL` and `RULE_PREDICATE` are BOTH "deontic modal anywhere" matchers
|
|
20
|
+
* with slightly different word lists ON PURPOSE — different jobs, calibrated
|
|
21
|
+
* separately against the OSS corpus. They are kept ADJACENT here so a widening
|
|
22
|
+
* of one prompts a review of the other, rather than the two drifting apart in
|
|
23
|
+
* separate files. See `research/rule-enforcer-design.md` §2.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.RULE_PREDICATE = exports.NORM_SIGNAL = exports.FORM_HEAD = void 0;
|
|
27
|
+
/**
|
|
28
|
+
* An imperative/prohibitive sentence HEAD ("Never …", "Avoid …", "No …") — the
|
|
29
|
+
* segment gate's `form` cue. Anchored at the start (a HEAD, not anywhere). The
|
|
30
|
+
* deontic verbs (require/disallow/forbid/ban/enforce) are common rule leads
|
|
31
|
+
* ("Require `curly` braces", "Disallow `var`"). NB "no" is bare `no` + the
|
|
32
|
+
* shared trailing `\b` (a boundary right after "no" — before a space OR a
|
|
33
|
+
* backtick), so "No bare except" / "No default exports" / "No `any`" all match,
|
|
34
|
+
* while "Note"/"Nowhere" (no boundary after "no") are rejected. The earlier
|
|
35
|
+
* `no\s+\S` form silently failed "No bare except" (the measured bug).
|
|
36
|
+
*/
|
|
37
|
+
exports.FORM_HEAD = /^(?:use|avoid|prefer|never|always|don'?t|do not|no|must|should|keep|run|write|add|remove|only|require|requires?|disallow|forbid|ban|enforce)\b/i;
|
|
38
|
+
/**
|
|
39
|
+
* A deontic modal ANYWHERE in the text — routing's POSSIBLE-tier recall gate.
|
|
40
|
+
* Narrow (modal verbs only) so the review tier stays genuine recall-misses.
|
|
41
|
+
*/
|
|
42
|
+
exports.NORM_SIGNAL = /\b(?:must(?:n't)?|should(?:n't)?|shall|never|always|avoids?|require[sd]?|forbidden|disallow(?:ed)?|prohibited|banned?|prefers?|do not|don't)\b/i;
|
|
43
|
+
/**
|
|
44
|
+
* A deontic predicate ANYWHERE — the segment gate's description-reject guard: a
|
|
45
|
+
* code-span-led sentence carrying one of these is a RULE ("`const` is preferred
|
|
46
|
+
* over `let`"), not a description, so the description reject must NOT fire.
|
|
47
|
+
*/
|
|
48
|
+
exports.RULE_PREDICATE = /\b(?:must|should|shall|never|always|require|avoid|prefer|banned|forbidden|prohibited|allowed|disallowed|deprecated|discouraged|mandatory|do not|don'?t|only|instead)\b/i;
|
|
49
|
+
//# sourceMappingURL=rule-signals.js.map
|
package/dist/segment.d.ts
CHANGED
|
@@ -23,10 +23,18 @@ export interface SegmentedRule {
|
|
|
23
23
|
confidence: "high" | "medium";
|
|
24
24
|
}
|
|
25
25
|
/** Why the segmenter decided a bullet is NOT a rule (the transparency signal —
|
|
26
|
-
* see `research/rule-
|
|
27
|
-
* come from the gate; `section` means it sits under a non-rule
|
|
28
|
-
* (Setup / Commands / Key Files / Architecture …).
|
|
29
|
-
|
|
26
|
+
* see `research/rule-enforcer-design.md` §3). `index`/`description`/`leadin`/
|
|
27
|
+
* `no-signal` come from the gate; `section` means it sits under a non-rule
|
|
28
|
+
* heading (Setup / Commands / Key Files / Architecture …).
|
|
29
|
+
*
|
|
30
|
+
* `leadin` is a colon-terminated procedure/enumeration HEADER ("To add a
|
|
31
|
+
* setting:", "Run the full test suite:", "Python check:") whose enforceable
|
|
32
|
+
* content — if any — lives in the sub-bullets/code-block it introduces (verified
|
|
33
|
+
* on the OSS corpus: the sub-items are segmented independently, so dropping the
|
|
34
|
+
* header loses nothing). Kept DISTINCT from `no-signal` on purpose: routing
|
|
35
|
+
* re-surfaces `no-signal` skips in the "possible (review)" recall tier, and a
|
|
36
|
+
* lead-in is a CONFIDENT drop that must not re-enter that tier. */
|
|
37
|
+
export type RejectReason = "index" | "description" | "leadin" | "no-signal" | "section";
|
|
30
38
|
/** A BULLET the segmenter saw but did NOT treat as a rule, with the reason — so
|
|
31
39
|
* the audit report can be honest about what it set aside (a heuristic misses
|
|
32
40
|
* declarative rules; showing skips lets a human eyeball a wrong drop). Bounded to
|
|
@@ -49,7 +57,10 @@ export interface SegmentResult {
|
|
|
49
57
|
*
|
|
50
58
|
* Deterministic Tier-A heuristic. Code fences and tables are excluded from
|
|
51
59
|
* candidacy. Candidate units are (a) list items with attached continuation
|
|
52
|
-
* lines and (b) sentences of paragraphs under a rule-ish heading.
|
|
60
|
+
* lines and (b) sentences of paragraphs under a rule-ish heading. This function
|
|
61
|
+
* is a thin DISPATCHER — each block type is handled by its own pure helper
|
|
62
|
+
* (`handleListItem` / `handleParagraph`); the state it threads is the fence
|
|
63
|
+
* toggle and the current `HeadingState`.
|
|
53
64
|
*/
|
|
54
65
|
export declare function segmentInstructions(markdown: string, file?: string, skipLines?: ReadonlySet<number>): SegmentResult;
|
|
55
66
|
//# sourceMappingURL=segment.d.ts.map
|