vigiles 14.0.0 → 14.1.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/dist/audit-report.js +4 -1
- package/dist/audit-report.template.html +28 -28
- package/dist/cli.js +191 -34
- package/dist/core/rule-catalog.d.ts +48 -3
- package/dist/core/rule-catalog.js +150 -2
- package/dist/rule-routing.d.ts +48 -1
- package/dist/rule-routing.js +127 -28
- package/dist/segment.d.ts +23 -1
- package/dist/segment.js +34 -12
- package/package.json +1 -1
package/dist/rule-routing.d.ts
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rule-routing.ts — the deterministic (no-model) State-B routing PREVIEW.
|
|
3
|
+
*
|
|
4
|
+
* `rule-inventory.ts` answers a narrow question ("which prose lines name an
|
|
5
|
+
* off-the-shelf lint rule, and is it enabled?"). This goes one honest step
|
|
6
|
+
* further: it SEGMENTS the whole instruction file into atomic rules
|
|
7
|
+
* ({@link segmentInstructions}) and routes each one into the class that a real
|
|
8
|
+
* enforcement path would take — WITHOUT running a model:
|
|
9
|
+
*
|
|
10
|
+
* reuse → the rule text names an off-the-shelf lint rule ({@link INTENT_MAP})
|
|
11
|
+
* → mechanism: flip one config line. The "narrow list we compile
|
|
12
|
+
* very well" — everything else is honestly labelled, not force-fit.
|
|
13
|
+
* hook → an ACTION rule a linter can't see (git push, rm -rf, "before you
|
|
14
|
+
* commit") → mechanism: a pre-commit / PreToolUse hook.
|
|
15
|
+
* meta → an agent-instruction, not a code rule ("read X first", "tell the
|
|
16
|
+
* user", "you are …") → mechanism: stays prose. Split out of
|
|
17
|
+
* `unrouted` so it never reads as "compilable but hard" (it isn't).
|
|
18
|
+
* semantic → a judgment call ("readable", "single responsibility") no checker
|
|
19
|
+
* can honestly decide → mechanism: stays prose.
|
|
20
|
+
* unrouted → looks like a code rule but matched no off-the-shelf rule: HARD to
|
|
21
|
+
* codify → mechanism `synthesize`: the opt-in SYNTHESIS tier (a
|
|
22
|
+
* skill on your subscription) MIGHT write a custom checker, gated —
|
|
23
|
+
* but it is NOT guaranteed (the gate may abstain). This is the
|
|
24
|
+
* bucket audit must present clearly as "hard", never as done.
|
|
25
|
+
*
|
|
26
|
+
* NB "compile" is NOT used here — `vigiles compile` is the unrelated spec→markdown
|
|
27
|
+
* verb. Synthesis is its own opt-in tier; the mechanism value is `synthesize`.
|
|
28
|
+
*
|
|
29
|
+
* HONESTY BY CONSTRUCTION: the deterministic tier NEVER claims a rule is
|
|
30
|
+
* "synthesizable" — deciding that a custom rule can be written (and gating it)
|
|
31
|
+
* is exactly the work the opt-in model tier does. `unrouted` means "hard — a
|
|
32
|
+
* synthesis skill may try", never a promise; `meta`/`semantic` mean "not an
|
|
33
|
+
* enforceable code rule at all" (a different, honest kind of no).
|
|
34
|
+
*
|
|
35
|
+
* Pure, deterministic, dependency-free. Reuses `rule-inventory`'s hardened
|
|
36
|
+
* whole-token matcher + `INTENT_MAP`, and `segment`'s Tier-A segmenter.
|
|
37
|
+
*/
|
|
38
|
+
import { type SkippedBullet } from "./segment.js";
|
|
1
39
|
import { type LinterName } from "./rule-inventory.js";
|
|
2
40
|
import type { RuleCatalog } from "./core/rule-catalog.js";
|
|
3
41
|
/** How a routed rule would be enforced (a MECHANISM ladder, not a 1-10 score). */
|
|
@@ -29,10 +67,19 @@ export interface RoutedRule {
|
|
|
29
67
|
readonly source?: "marker" | "heuristic";
|
|
30
68
|
}
|
|
31
69
|
export interface RuleRouting {
|
|
32
|
-
/** How many atomic rules were routed (
|
|
70
|
+
/** How many CONFIDENT atomic rules were routed (high or rescued). */
|
|
33
71
|
readonly segmented: number;
|
|
34
72
|
readonly counts: Record<RuleCategory, number>;
|
|
73
|
+
/** The CONFIDENT tier — cleared the precision bar; these are the routed rules. */
|
|
35
74
|
readonly rules: readonly RoutedRule[];
|
|
75
|
+
/** The POSSIBLE tier — rule-ish bullets (medium confidence) that did NOT clear
|
|
76
|
+
* the bar, still classified so a human can review + promote them. Detection is
|
|
77
|
+
* precision-first, so this is where a declarative rule ("Every X must Y") that
|
|
78
|
+
* the confident tier misses shows up. See `research/rule-compiler-design.md` §2. */
|
|
79
|
+
readonly possible: readonly RoutedRule[];
|
|
80
|
+
/** Bullets the segmenter decided were NOT rules, each with a reason — so the
|
|
81
|
+
* report is honest about what it set aside (§3). */
|
|
82
|
+
readonly skipped: readonly SkippedBullet[];
|
|
36
83
|
}
|
|
37
84
|
export interface RouteOptions {
|
|
38
85
|
/**
|
package/dist/rule-routing.js
CHANGED
|
@@ -49,6 +49,14 @@ const MECHANISM = {
|
|
|
49
49
|
semantic: "prose",
|
|
50
50
|
unrouted: "synthesize",
|
|
51
51
|
};
|
|
52
|
+
/**
|
|
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.
|
|
58
|
+
*/
|
|
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;
|
|
52
60
|
/**
|
|
53
61
|
* ACTION-rule cues — things a linter never sees (git, filesystem, shell,
|
|
54
62
|
* process). A hook is the right gate, not a lint rule. Widened to the article's
|
|
@@ -221,27 +229,54 @@ function namedRuleTokens(text) {
|
|
|
221
229
|
}
|
|
222
230
|
return out;
|
|
223
231
|
}
|
|
224
|
-
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
/** Combine two hits that a doc-token resolves to (a cross-linter id collision).
|
|
233
|
+
* enabled OR-s — a "**Enforced by:** X" claim is satisfied if ANY linter has X
|
|
234
|
+
* on, so we never cry "documented but OFF" when one linter enforces it — and
|
|
235
|
+
* provenance follows the enforcing linter. */
|
|
236
|
+
function combineHits(a, b) {
|
|
237
|
+
if (a.enabled === b.enabled)
|
|
238
|
+
return { enabled: a.enabled, linter: a.linter };
|
|
239
|
+
return a.enabled ? a : b; // exactly one is on → it wins (enabled OR-s to true)
|
|
240
|
+
}
|
|
241
|
+
/** Build the doc-token → hit lookup from a (possibly polyglot) rule list. A rule
|
|
242
|
+
* is matchable by its id AND, for Pylint, its numeric code. A bare id CAN collide
|
|
243
|
+
* across linters (`no-else-return` is in both ESLint and Pylint) → combine
|
|
244
|
+
* conservatively. A numeric code is unique to its linter, so it never collides
|
|
245
|
+
* and KEEPS its own (linter, enabled) — a doc naming the Pylint code `R1705`
|
|
246
|
+
* still surfaces "documented but OFF" even when the symbol is enabled in ESLint. */
|
|
247
|
+
function buildCatalogLookup(rules) {
|
|
248
|
+
const map = new Map();
|
|
249
|
+
const put = (key, hit) => {
|
|
250
|
+
const prev = map.get(key);
|
|
251
|
+
map.set(key, prev ? combineHits(prev, hit) : hit);
|
|
252
|
+
};
|
|
253
|
+
for (const r of rules) {
|
|
254
|
+
const hit = { enabled: r.enabled, linter: r.linter };
|
|
255
|
+
put(r.id, hit);
|
|
256
|
+
if (r.code)
|
|
257
|
+
put(r.code, hit);
|
|
258
|
+
}
|
|
259
|
+
return map;
|
|
260
|
+
}
|
|
231
261
|
function classify(text, catalog) {
|
|
232
262
|
if (HOOK_CUES.some((re) => re.test(text)))
|
|
233
263
|
return { category: "hook" };
|
|
234
264
|
if (META_CUES.some((re) => re.test(text)))
|
|
235
265
|
return { category: "meta" };
|
|
236
266
|
// 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
|
|
238
|
-
// OFF" nudge). Own-repo only — catalog is present only when the
|
|
239
|
-
// enumerated with consent.
|
|
267
|
+
// carrying its linter + whether it's currently enabled (a disabled hit = the
|
|
268
|
+
// "documented but OFF" nudge). Own-repo only — catalog is present only when the
|
|
269
|
+
// linter was enumerated with consent.
|
|
240
270
|
if (catalog) {
|
|
241
271
|
for (const tok of namedRuleTokens(text)) {
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
244
|
-
return {
|
|
272
|
+
const hit = catalog.get(tok);
|
|
273
|
+
if (hit !== undefined)
|
|
274
|
+
return {
|
|
275
|
+
category: "reuse",
|
|
276
|
+
rule: tok,
|
|
277
|
+
enabled: hit.enabled,
|
|
278
|
+
linter: hit.linter,
|
|
279
|
+
};
|
|
245
280
|
}
|
|
246
281
|
}
|
|
247
282
|
for (const m of rule_inventory_js_1.INTENT_MAP) {
|
|
@@ -266,11 +301,15 @@ const GUARD_RE = /^\*\*Guard:\*\*/;
|
|
|
266
301
|
const GUIDANCE_RE = /^\*\*Guidance only\*\*/;
|
|
267
302
|
const MARK_HEADING = /^(#{2,6})\s+(.*)$/;
|
|
268
303
|
const RULE_ID_SHAPE = /^@?[a-z][a-z0-9._/-]*$/;
|
|
304
|
+
// Pylint's numeric alias (C0116, W9006) — the catalog advertises these as
|
|
305
|
+
// matchable, so a marker using one must parse as a rule id, not a prose claim.
|
|
306
|
+
const PYLINT_CODE_SHAPE = /^[A-Z]\d+$/;
|
|
269
307
|
/** Does this `**Enforced by:**` value parse as a lint-rule id (vs a prose claim
|
|
270
308
|
* like "CI" or "the linter")? A hand-written marker is a CLAIM — only a rule-id
|
|
271
309
|
* shape is treated as a real reuse rule. */
|
|
272
310
|
function looksLikeRuleId(s) {
|
|
273
|
-
|
|
311
|
+
const t = s.trim();
|
|
312
|
+
return (t.length >= 3 && RULE_ID_SHAPE.test(t)) || PYLINT_CODE_SHAPE.test(t);
|
|
274
313
|
}
|
|
275
314
|
/**
|
|
276
315
|
* Extract rules from EXPLICIT structured markers (`**Enforced by:** \`rule\``,
|
|
@@ -304,11 +343,13 @@ function extractMarkedRules(text, file, catalog) {
|
|
|
304
343
|
if (em) {
|
|
305
344
|
if (!looksLikeRuleId(em[1]))
|
|
306
345
|
break; // a prose claim, not a rule id
|
|
307
|
-
const
|
|
346
|
+
const hit = catalog?.get(em[1].trim());
|
|
308
347
|
marked = {
|
|
309
348
|
category: "reuse",
|
|
310
349
|
rule: em[1].trim(),
|
|
311
|
-
...(
|
|
350
|
+
...(hit !== undefined
|
|
351
|
+
? { enabled: hit.enabled, linter: hit.linter }
|
|
352
|
+
: {}),
|
|
312
353
|
};
|
|
313
354
|
break;
|
|
314
355
|
}
|
|
@@ -347,6 +388,7 @@ function extractMarkedRules(text, file, catalog) {
|
|
|
347
388
|
mechanism: MECHANISM[marked.category],
|
|
348
389
|
source: "marker",
|
|
349
390
|
...(marked.rule ? { rule: marked.rule } : {}),
|
|
391
|
+
...(marked.linter ? { linter: marked.linter } : {}),
|
|
350
392
|
...(marked.enabled !== undefined ? { enabled: marked.enabled } : {}),
|
|
351
393
|
});
|
|
352
394
|
}
|
|
@@ -360,7 +402,7 @@ function extractMarkedRules(text, file, catalog) {
|
|
|
360
402
|
function routeRules(instructionText, file, options = {}) {
|
|
361
403
|
const minConfidence = options.minConfidence ?? "high";
|
|
362
404
|
const catalog = options.availableRules
|
|
363
|
-
?
|
|
405
|
+
? buildCatalogLookup(options.availableRules.rules)
|
|
364
406
|
: undefined;
|
|
365
407
|
// A MEDIUM segment that NAMES a rule the repo's catalog actually has is
|
|
366
408
|
// enforceable — the catalog is ground truth, so it's higher-precision than the
|
|
@@ -381,15 +423,17 @@ function routeRules(instructionText, file, options = {}) {
|
|
|
381
423
|
// syntax. Fixes construct-prohibitions with no verb ("No bare except clauses")
|
|
382
424
|
// that score medium and would otherwise drop before classify() reuses them.
|
|
383
425
|
const matchesIntentMap = (text) => rule_inventory_js_1.INTENT_MAP.some((m) => m.keywords.some((kw) => (0, rule_inventory_js_1.matchesWholeToken)(text, kw)));
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
+
const toRouted = (s) => {
|
|
393
437
|
const c = classify(s.text, catalog);
|
|
394
438
|
return {
|
|
395
439
|
text: s.text,
|
|
@@ -405,7 +449,58 @@ function routeRules(instructionText, file, options = {}) {
|
|
|
405
449
|
...(c.linter ? { linter: c.linter } : {}),
|
|
406
450
|
...(c.enabled !== undefined ? { enabled: c.enabled } : {}),
|
|
407
451
|
};
|
|
452
|
+
};
|
|
453
|
+
// 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.
|
|
455
|
+
const marked = extractMarkedRules(instructionText, file, catalog);
|
|
456
|
+
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",
|
|
408
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)),
|
|
488
|
+
];
|
|
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
|
+
];
|
|
409
504
|
// Marker rules first (definitive), then the heuristic residue.
|
|
410
505
|
const rules = [...marked.rules, ...heuristicRules];
|
|
411
506
|
const counts = {
|
|
@@ -417,7 +512,7 @@ function routeRules(instructionText, file, options = {}) {
|
|
|
417
512
|
};
|
|
418
513
|
for (const r of rules)
|
|
419
514
|
counts[r.category]++;
|
|
420
|
-
return { segmented: rules.length, counts, rules };
|
|
515
|
+
return { segmented: rules.length, counts, rules, possible, skipped };
|
|
421
516
|
}
|
|
422
517
|
/**
|
|
423
518
|
* Merge per-file routings into one. Each instruction source is routed SEPARATELY
|
|
@@ -434,13 +529,17 @@ function mergeRoutings(routings) {
|
|
|
434
529
|
unrouted: 0,
|
|
435
530
|
};
|
|
436
531
|
const rules = [];
|
|
532
|
+
const possible = [];
|
|
533
|
+
const skipped = [];
|
|
437
534
|
let segmented = 0;
|
|
438
535
|
for (const r of routings) {
|
|
439
536
|
segmented += r.segmented;
|
|
440
537
|
rules.push(...r.rules);
|
|
538
|
+
possible.push(...r.possible);
|
|
539
|
+
skipped.push(...r.skipped);
|
|
441
540
|
for (const k of Object.keys(counts))
|
|
442
541
|
counts[k] += r.counts[k];
|
|
443
542
|
}
|
|
444
|
-
return { segmented, counts, rules };
|
|
543
|
+
return { segmented, counts, rules, possible, skipped };
|
|
445
544
|
}
|
|
446
545
|
//# sourceMappingURL=rule-routing.js.map
|
package/dist/segment.d.ts
CHANGED
|
@@ -22,6 +22,28 @@ export interface SegmentedRule {
|
|
|
22
22
|
/** 3/3 cues => "high"; 2/3 => "medium". (Rejected candidates are never emitted.) */
|
|
23
23
|
confidence: "high" | "medium";
|
|
24
24
|
}
|
|
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";
|
|
30
|
+
/** A BULLET the segmenter saw but did NOT treat as a rule, with the reason — so
|
|
31
|
+
* the audit report can be honest about what it set aside (a heuristic misses
|
|
32
|
+
* declarative rules; showing skips lets a human eyeball a wrong drop). Bounded to
|
|
33
|
+
* list items on purpose; rejected paragraph prose is not reported (too noisy). */
|
|
34
|
+
export interface SkippedBullet {
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly file: string | undefined;
|
|
37
|
+
readonly lineStart: number;
|
|
38
|
+
readonly lineEnd: number;
|
|
39
|
+
readonly reason: RejectReason;
|
|
40
|
+
}
|
|
41
|
+
/** The segmenter's full output: the confident/medium candidate rules PLUS the
|
|
42
|
+
* bullets it rejected (with reasons), so nothing is silently dropped. */
|
|
43
|
+
export interface SegmentResult {
|
|
44
|
+
readonly segments: SegmentedRule[];
|
|
45
|
+
readonly skipped: SkippedBullet[];
|
|
46
|
+
}
|
|
25
47
|
/**
|
|
26
48
|
* Split a CLAUDE.md / AGENTS.md into atomic candidate rules with provenance.
|
|
27
49
|
*
|
|
@@ -29,5 +51,5 @@ export interface SegmentedRule {
|
|
|
29
51
|
* candidacy. Candidate units are (a) list items with attached continuation
|
|
30
52
|
* lines and (b) sentences of paragraphs under a rule-ish heading.
|
|
31
53
|
*/
|
|
32
|
-
export declare function segmentInstructions(markdown: string, file?: string, skipLines?: ReadonlySet<number>):
|
|
54
|
+
export declare function segmentInstructions(markdown: string, file?: string, skipLines?: ReadonlySet<number>): SegmentResult;
|
|
33
55
|
//# sourceMappingURL=segment.d.ts.map
|
package/dist/segment.js
CHANGED
|
@@ -310,8 +310,13 @@ function isLinkOnly(text) {
|
|
|
310
310
|
const t = text.trim();
|
|
311
311
|
return URL_ONLY.test(t) || LINK_ONLY.test(t);
|
|
312
312
|
}
|
|
313
|
+
/** The accept/reject view of a gate result, for sites that only need the split
|
|
314
|
+
* (atomize, the sub-span loop) and don't care about the reason. */
|
|
315
|
+
function confidenceOf(g) {
|
|
316
|
+
return "confidence" in g ? g.confidence : null;
|
|
317
|
+
}
|
|
313
318
|
/**
|
|
314
|
-
* Score the 3 cues. Returns confidence
|
|
319
|
+
* Score the 3 cues. Returns a confidence OR a reject reason.
|
|
315
320
|
* - form: starts with an imperative/prohibitive head (or "No X").
|
|
316
321
|
* - context: is a bullet OR sits under a rule-ish heading.
|
|
317
322
|
* - shape: 15–300 chars, has a verb-ish token, not link-only, not a declaration.
|
|
@@ -322,18 +327,18 @@ function gate(text, isBullet, underRuleHeading) {
|
|
|
322
327
|
// `a.ts → b.ts`, `dir/x — …`, `Label: path`) — the corpus's dominant false
|
|
323
328
|
// positive. No cue count can rescue it.
|
|
324
329
|
if (looksLikeIndexEntry(t))
|
|
325
|
-
return
|
|
330
|
+
return { reject: "index" };
|
|
326
331
|
// Reject a DESCRIPTION-led sentence (`` `Foo` class in `x` executes … ``) — an
|
|
327
332
|
// architecture/index sentence, not a rule (the dogfood's #1 false positive).
|
|
328
333
|
if (looksLikeDescription(t))
|
|
329
|
-
return
|
|
334
|
+
return { reject: "description" };
|
|
330
335
|
const context = isBullet || underRuleHeading;
|
|
331
336
|
// RULE-NAME cue: a bullet/section line that NAMES an off-the-shelf rule is a
|
|
332
337
|
// strong signal it's enforceable, even without an imperative verb — promote it
|
|
333
338
|
// to high so the high-only default doesn't drop it (recovers rule-naming
|
|
334
339
|
// bullets like "No floating promises (`@ts.../no-floating-promises`)").
|
|
335
340
|
if (context && RULE_NAME_IN_CODE.test(t))
|
|
336
|
-
return "high";
|
|
341
|
+
return { confidence: "high" };
|
|
337
342
|
// The form/declaration cues see the text with leading decoration stripped, so
|
|
338
343
|
// `- **Never** …` reads as imperative and `**We** …` still reads declarative.
|
|
339
344
|
const head = stripLeadDecoration(t);
|
|
@@ -345,10 +350,10 @@ function gate(text, isBullet, underRuleHeading) {
|
|
|
345
350
|
!DECLARATION.test(head);
|
|
346
351
|
const cues = (form ? 1 : 0) + (context ? 1 : 0) + (shape ? 1 : 0);
|
|
347
352
|
if (cues >= 3)
|
|
348
|
-
return "high";
|
|
353
|
+
return { confidence: "high" };
|
|
349
354
|
if (cues === 2)
|
|
350
|
-
return "medium";
|
|
351
|
-
return
|
|
355
|
+
return { confidence: "medium" };
|
|
356
|
+
return { reject: "no-signal" };
|
|
352
357
|
}
|
|
353
358
|
// --- Atomicity split -------------------------------------------------------
|
|
354
359
|
/** Never split when an exception clause carries polarity/meaning. */
|
|
@@ -404,7 +409,7 @@ function atomize(src, contentSpan, isBullet, underRuleHeading) {
|
|
|
404
409
|
// Both/all halves must independently pass the gate, else keep whole.
|
|
405
410
|
for (const p of pieces) {
|
|
406
411
|
const text = normalize(src.slice(p.start, p.end));
|
|
407
|
-
if (gate(text, isBullet, underRuleHeading) === null)
|
|
412
|
+
if (confidenceOf(gate(text, isBullet, underRuleHeading)) === null)
|
|
408
413
|
return [whole];
|
|
409
414
|
}
|
|
410
415
|
return pieces.length > 1 ? pieces : [whole];
|
|
@@ -439,6 +444,7 @@ function segmentInstructions(markdown, file, skipLines) {
|
|
|
439
444
|
const lines = markdown.split("\n");
|
|
440
445
|
const lineOffsets = computeLineOffsets(lines);
|
|
441
446
|
const out = [];
|
|
447
|
+
const skipped = [];
|
|
442
448
|
let inFence = false;
|
|
443
449
|
let currentHeadingIsRuleish = false;
|
|
444
450
|
let currentHeadingIsAntiContext = false;
|
|
@@ -513,7 +519,8 @@ function segmentInstructions(markdown, file, skipLines) {
|
|
|
513
519
|
const contentEnd = lineOffsets[endLine] + lines[endLine].length;
|
|
514
520
|
const contentSpan = { start: contentStart, end: contentEnd };
|
|
515
521
|
const wholeText = normalize(markdown.slice(contentStart, contentEnd));
|
|
516
|
-
const
|
|
522
|
+
const g = gate(wholeText, true, currentHeadingIsRuleish);
|
|
523
|
+
const conf = confidenceOf(g);
|
|
517
524
|
// Reject bullets under an anti-context heading (Commands/Setup/Key Files/
|
|
518
525
|
// Architecture/…) — the corpus's dominant false-positive locus.
|
|
519
526
|
if (conf !== null && !currentHeadingIsAntiContext) {
|
|
@@ -528,12 +535,27 @@ function segmentInstructions(markdown, file, skipLines) {
|
|
|
528
535
|
else {
|
|
529
536
|
for (const s of spans) {
|
|
530
537
|
const text = normalize(markdown.slice(s.start, s.end));
|
|
531
|
-
const c = gate(text, true, currentHeadingIsRuleish);
|
|
538
|
+
const c = confidenceOf(gate(text, true, currentHeadingIsRuleish));
|
|
532
539
|
if (c !== null)
|
|
533
540
|
out.push(emitFromSpan(markdown, lineOffsets, file, s, c));
|
|
534
541
|
}
|
|
535
542
|
}
|
|
536
543
|
}
|
|
544
|
+
else {
|
|
545
|
+
// This bullet was NOT treated as a rule — record it + why, so the audit
|
|
546
|
+
// report can be honest about what it set aside (transparency, §3). A
|
|
547
|
+
// rejection under an anti-context heading is a "section" skip; otherwise
|
|
548
|
+
// it's the gate's own reason.
|
|
549
|
+
skipped.push({
|
|
550
|
+
text: wholeText,
|
|
551
|
+
file,
|
|
552
|
+
lineStart: offsetToLine(lineOffsets, contentStart),
|
|
553
|
+
lineEnd: offsetToLine(lineOffsets, contentEnd - 1),
|
|
554
|
+
reason: currentHeadingIsAntiContext || "confidence" in g
|
|
555
|
+
? "section"
|
|
556
|
+
: g.reject,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
537
559
|
i = endLine + 1;
|
|
538
560
|
continue;
|
|
539
561
|
}
|
|
@@ -573,7 +595,7 @@ function segmentInstructions(markdown, file, skipLines) {
|
|
|
573
595
|
if (s.start >= s.end)
|
|
574
596
|
continue;
|
|
575
597
|
const text = normalize(markdown.slice(s.start, s.end));
|
|
576
|
-
const c = gate(text, false, true);
|
|
598
|
+
const c = confidenceOf(gate(text, false, true));
|
|
577
599
|
if (c !== null)
|
|
578
600
|
out.push(emitFromSpan(markdown, lineOffsets, file, s, c));
|
|
579
601
|
}
|
|
@@ -583,6 +605,6 @@ function segmentInstructions(markdown, file, skipLines) {
|
|
|
583
605
|
}
|
|
584
606
|
i++;
|
|
585
607
|
}
|
|
586
|
-
return out;
|
|
608
|
+
return { segments: out, skipped };
|
|
587
609
|
}
|
|
588
610
|
//# sourceMappingURL=segment.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.1.0",
|
|
4
4
|
"description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|