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.
@@ -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
- * A deontic/norm signal ANYWHERE in a bullet — the marker of a rule the imperative
54
- * gate missed because the norm isn't at the head ("every function MUST have a
55
- * docstring", "public APIs SHOULD stay stable"). Used to keep the POSSIBLE review
56
- * tier to genuine rule-candidates instead of arbitrary unparsed prose. Deliberately
57
- * narrow (modal verbs only) so it doesn't re-admit the noise it exists to exclude.
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
- const 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;
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-compiler-multilang-design.md` §5b (the hook lane).
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: "no-restricted-syntax",
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: "no-restricted-syntax",
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: "no-restricted-syntax",
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: "no-restricted-syntax",
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: "no-restricted-syntax",
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 section = lines.slice(i, j); // [heading … next-heading)
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: heading,
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
- confidence: "high",
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
- // A bullet the gate rejected as `no-signal` is a rule CANDIDATE only if it
458
- // carries a deontic/norm signal (a modal like must/should/never/avoid) — that
459
- // keeps the POSSIBLE review tier to genuine recall-misses ("every function must
460
- // have a docstring") instead of flooding it with prose ("README.md documents
461
- // v2"). A no-signal bullet WITHOUT a norm signal is confidently not a rule, so
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 { segmented: rules.length, counts, rules, possible, skipped };
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-compiler-design.md` §3). `index`/`description`/`no-signal`
27
- * come from the gate; `section` means it sits under a non-rule heading
28
- * (Setup / Commands / Key Files / Architecture …). */
29
- export type RejectReason = "index" | "description" | "no-signal" | "section";
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