vigiles 12.8.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/dist/segment.js CHANGED
@@ -11,10 +11,108 @@
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.segmentInstructions = segmentInstructions;
13
13
  // --- Heuristic vocabulary --------------------------------------------------
14
- /** Imperative/prohibitive head the candidate must START with (form cue). */
15
- const FORM_HEAD = /^(?:use|avoid|prefer|never|always|don'?t|do not|no\s+\S|must|should|keep|run|write|add|remove|only)\b/i;
16
- /** Rule-ish heading gate for prose-under-heading candidacy. */
17
- const RULE_HEADING = /rules?|conventions?|style|guidelines?|standards?|do(?:n'?ts?)?s?|never|always|must|require/i;
14
+ /** Imperative/prohibitive head the candidate must START with (form cue). The
15
+ * deontic verbs (require/disallow/forbid/ban/enforce) are common rule leads —
16
+ * "Require `curly` braces", "Disallow `var`" — so they belong here. NB "no" is
17
+ * `no\s` (the prohibition word + whitespace) NOT the old `no\s+\S`, which — via
18
+ * the shared trailing `\b` — only matched when the word after "No " began at a
19
+ * boundary, so "No bare except" / "No default exports" silently failed the form
20
+ * cue. Bare `no` + the shared `\b` (checked right after "no", a boundary before
21
+ * a space OR a backtick) matches "No bare" AND "No `any`", while "Note"/"Nowhere"
22
+ * (no boundary after "no") are still rejected. */
23
+ const 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;
24
+ /**
25
+ * Rule-ish heading gate for prose-under-heading candidacy. Word-bounded so the
26
+ * `do` alternate can't match inside `Documentation`/`Adoption`/`Download` (the
27
+ * measured bug). Accept-heading vocabulary grounded in the OSS-corpus survey
28
+ * (`## Coding Standards`/`## Code Style`/`## Naming`/`## Good practices`/
29
+ * `## Error Handling` are the real code-norm sections).
30
+ */
31
+ const RULE_HEADING = /\b(?:rules?|conventions?|code[ -]?style|style|guidelines?|standards?|naming|good practices?|error handling|do'?s?\s*(?:and|&|\/)\s*don'?ts?|don'?ts?|never|always|must|require)\b/i;
32
+ /**
33
+ * Anti-context heading: a section whose content is overwhelmingly index /
34
+ * command / setup / narrative, not enforceable norms (the corpus's #1
35
+ * false-positive source). Content under one of these is rejected outright —
36
+ * UNLESS the heading is also rule-ish (`## Testing conventions` keeps its
37
+ * bullets), so the accept signal wins a tie.
38
+ */
39
+ const ANTI_HEADING = /\b(?:commands?|setup|install(?:ation)?|usage|getting started|quick ?start|examples?|key files|(?:code)?base structure|project structure|repository structure|architecture|overview|directory|layout|environment|commits?|pull requests?|testing|scripts?|dependencies|roadmap|changelog|table of contents|where to look)\b/i;
40
+ /**
41
+ * INDEX-SMELL veto: a bullet whose content is a code span followed by a
42
+ * separator + description (`` `src/x.ts` — Type system ``, `` `npm test` — run ``)
43
+ * is a keyFiles/command INDEX entry, never a rule. The single highest-value
44
+ * rejection signal (the corpus's dominant false positive).
45
+ */
46
+ const INDEX_SMELL = /^`[^`]+`\s*[:—–-]\s/;
47
+ /**
48
+ * Broader index/reference-entry shapes the backtick INDEX_SMELL misses — from
49
+ * real-corpus leakage: a path MAPPING (`next-dev.ts → next-dev-server.ts`), a
50
+ * bullet LED by a file path + em-dash (`packages/x/ — Session replay`), or a
51
+ * `Label: <path>` pointer (`Skill file: .agents/skills/…`). Paths in these are
52
+ * backtick-wrapped, so we test a backtick-stripped shadow. The path DISCRIMINATOR
53
+ * — a file extension (`.ts`) or a trailing slash — is what keeps a rule id
54
+ * (`@scope/no-explicit-any`, no extension) from being mistaken for a path, so a
55
+ * rule-naming bullet is never rejected as an index entry.
56
+ */
57
+ const INDEX_ARROW = /[\w./@-]*\.[a-z]{1,6}\b\s*(?:→|->|=>)/;
58
+ // A multi-segment slash PATH before an arrow is a path-mapping/index row
59
+ // (`node_modules/@astrojs/react/… → packages/…`) — ≥2 slashes keeps it path-
60
+ // specific so a prose "A → B" isn't caught.
61
+ const INDEX_ARROW_PATH = /[\w@.-]+(?:\/[\w@.*-]+){2,}\s*(?:→|->|=>)/;
62
+ const INDEX_LABEL_PATH = /^[A-Za-z][\w ]{0,24}:\s+\.?[\w@-]*\/[\w@./-]*(?:\.[a-z0-9]{1,6}\b|\/)/;
63
+ const INDEX_PATH_LED = /^[\w@.-]+\/[\w@./-]*(?:\.[a-z0-9]{1,6}\b|\/)/;
64
+ function looksLikeIndexEntry(t) {
65
+ if (INDEX_SMELL.test(t))
66
+ return true;
67
+ const bare = t.replace(/`/g, " ").trim();
68
+ if (INDEX_ARROW.test(bare) || INDEX_ARROW_PATH.test(bare))
69
+ return true;
70
+ if (INDEX_LABEL_PATH.test(bare))
71
+ return true;
72
+ return INDEX_PATH_LED.test(bare) && /\s[—–]\s/.test(bare);
73
+ }
74
+ /**
75
+ * DESCRIPTION-LED reject: a segment that DESCRIBES a code entity rather than
76
+ * instructing about it. It leads with a backticked identifier/path, then a
77
+ * copula / code-KIND noun / descriptive verb ("`Foo` class in `x` executes …",
78
+ * "`bar` is the loader", "`apps/x` handles …"). A real rule leads with a VERB
79
+ * ("Use `Foo`", "Never `bar`") — never the code span itself — so a code-span
80
+ * lead-in followed by a descriptive word is an architecture/index sentence, the
81
+ * dogfood's #1 segmenter false positive (39% of the "hard" bucket was this
82
+ * kind of noise). High-precision: only when the descriptive word IMMEDIATELY
83
+ * follows the leading code span.
84
+ */
85
+ const DESCRIPTION_LED = /^`[^`]+`\s+(?:is|are|was|were|lives?|live|contains?|holds?|handles?|executes?|provides?|represents?|maps?|points?|implements?|exports?|defines?|wraps?|stores?|returns?|the|a|an|class|function|module|component|file|package|hook|utility|helper|type|interface|enum|constant|method|directory|folder|dir)\b/i;
86
+ // A deontic predicate makes a code-span-led sentence a RULE, not a description
87
+ // ("`const` is preferred over `let`", "`AbstractBase` class must be extended") —
88
+ // so the description reject must NOT fire. Guards the copula/kind-noun ambiguity.
89
+ const 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;
90
+ function looksLikeDescription(t) {
91
+ const s = t.trim();
92
+ return DESCRIPTION_LED.test(s) && !RULE_PREDICATE.test(s);
93
+ }
94
+ /**
95
+ * RULE-NAME cue: a backticked token that is SHAPED like an off-the-shelf lint
96
+ * rule — a scoped/plugin rule (`@typescript-eslint/consistent-type-imports`,
97
+ * `import/no-cycle`) or a ≥3-segment kebab id (`no-floating-promises`). Requiring
98
+ * the backticks kills prose false positives (`up-to-date`, `state-of-the-art`,
99
+ * a file path). Naming a rule is a STRONG signal a bullet is an enforceable rule
100
+ * even when it has no imperative verb — the corpus's rule-naming bullets
101
+ * ("No floating promises (`@ts.../no-floating-promises`)") otherwise score
102
+ * "medium" and get dropped by the high-only default.
103
+ */
104
+ const RULE_NAME_IN_CODE = /`[^`]*(?:@[a-z][\w-]*\/[a-z][\w-]*|[a-z][\w-]*\/[a-z][\w-]*-[\w-]+|(?:no|prefer|require|consistent|max|min|func|id|sort|valid|padding|dot|array|object)-[a-z][a-z0-9-]+)[^`]*`/i;
105
+ /**
106
+ * Leading markdown decoration a rule may be wrapped in — emphasis (`**bold**`),
107
+ * blockquote, checkbox, or a status emoji. Stripped on a SHADOW string before
108
+ * the imperative-head test so `- **Never** …` / `✅ Use const` still read as
109
+ * imperative. Provenance (exactQuote/offsets) is unaffected — only the form cue
110
+ * sees the stripped text.
111
+ */
112
+ const LEAD_DECORATION = /^(?:\s+|>+|\*+|_+|~+|\[[ xX]\]\s*|[✅❌☑✔✖✗⚠ℹ])+/u;
113
+ function stripLeadDecoration(s) {
114
+ return s.replace(LEAD_DECORATION, "").trimStart();
115
+ }
18
116
  /** Declarative subjects — these signal a statement, not an instruction. */
19
117
  const DECLARATION = /^(?:this|these|those|it|we|our|there)\b/i;
20
118
  /** Line consisting only of a bare URL. */
@@ -101,14 +199,9 @@ const VERBS = new Set([
101
199
  "filters",
102
200
  "merge",
103
201
  "merges",
104
- "be",
105
- "is",
106
- "are",
107
- "have",
108
- "has",
109
- "may",
110
- "should",
111
- "must",
202
+ // Copulas/modals (be/is/are/have/has/may/should/must) are deliberately NOT
203
+ // here: as "shape" verbs they made the cue near-vacuous (almost any English
204
+ // sentence passed). Deontic modals still live in FORM_HEAD (the form cue).
112
205
  "pin",
113
206
  "pins",
114
207
  "lint",
@@ -157,6 +250,13 @@ const VERBS = new Set([
157
250
  "squash",
158
251
  "enforce",
159
252
  "enforces",
253
+ "regenerate",
254
+ "regenerates",
255
+ "regen",
256
+ "rebuild",
257
+ "rebuilds",
258
+ "generate",
259
+ "generates",
160
260
  "define",
161
261
  "defines",
162
262
  "declare",
@@ -218,13 +318,31 @@ function isLinkOnly(text) {
218
318
  */
219
319
  function gate(text, isBullet, underRuleHeading) {
220
320
  const t = text.trim();
221
- const form = FORM_HEAD.test(t);
321
+ // Reject an index/command/reference entry outright (`` `path` — description ``,
322
+ // `a.ts → b.ts`, `dir/x — …`, `Label: path`) — the corpus's dominant false
323
+ // positive. No cue count can rescue it.
324
+ if (looksLikeIndexEntry(t))
325
+ return null;
326
+ // Reject a DESCRIPTION-led sentence (`` `Foo` class in `x` executes … ``) — an
327
+ // architecture/index sentence, not a rule (the dogfood's #1 false positive).
328
+ if (looksLikeDescription(t))
329
+ return null;
222
330
  const context = isBullet || underRuleHeading;
331
+ // RULE-NAME cue: a bullet/section line that NAMES an off-the-shelf rule is a
332
+ // strong signal it's enforceable, even without an imperative verb — promote it
333
+ // to high so the high-only default doesn't drop it (recovers rule-naming
334
+ // bullets like "No floating promises (`@ts.../no-floating-promises`)").
335
+ if (context && RULE_NAME_IN_CODE.test(t))
336
+ return "high";
337
+ // The form/declaration cues see the text with leading decoration stripped, so
338
+ // `- **Never** …` reads as imperative and `**We** …` still reads declarative.
339
+ const head = stripLeadDecoration(t);
340
+ const form = FORM_HEAD.test(head);
223
341
  const shape = t.length >= 15 &&
224
342
  t.length <= 300 &&
225
343
  hasVerbish(t) &&
226
344
  !isLinkOnly(t) &&
227
- !DECLARATION.test(t);
345
+ !DECLARATION.test(head);
228
346
  const cues = (form ? 1 : 0) + (context ? 1 : 0) + (shape ? 1 : 0);
229
347
  if (cues >= 3)
230
348
  return "high";
@@ -304,7 +422,9 @@ function emitFromSpan(src, lineOffsets, file, span, confidence) {
304
422
  };
305
423
  }
306
424
  // --- Scanner ---------------------------------------------------------------
307
- const LIST_ITEM = /^(\s*)([-*+])(\s+)(.*)$/;
425
+ // Ordered (`1.`/`1)`) and emoji (`✅`/`❌`) bullets count as list items too —
426
+ // the native `-*+` class missed them, so shouted/numbered rules were invisible.
427
+ const LIST_ITEM = /^(\s*)([-*+]|\d+[.)]|[✅❌☑✔✖✗])(\s+)(.*)$/u;
308
428
  const HEADING = /^(#{1,6})\s+(.*)$/;
309
429
  const FENCE = /^\s*(```|~~~)/;
310
430
  const TABLE_LINE = /^\s*\|/;
@@ -315,12 +435,13 @@ const TABLE_LINE = /^\s*\|/;
315
435
  * candidacy. Candidate units are (a) list items with attached continuation
316
436
  * lines and (b) sentences of paragraphs under a rule-ish heading.
317
437
  */
318
- function segmentInstructions(markdown, file) {
438
+ function segmentInstructions(markdown, file, skipLines) {
319
439
  const lines = markdown.split("\n");
320
440
  const lineOffsets = computeLineOffsets(lines);
321
441
  const out = [];
322
442
  let inFence = false;
323
443
  let currentHeadingIsRuleish = false;
444
+ let currentHeadingIsAntiContext = false;
324
445
  let i = 0;
325
446
  const lineSpan = (a, b) => ({
326
447
  start: lineOffsets[a],
@@ -342,6 +463,10 @@ function segmentInstructions(markdown, file) {
342
463
  const h = HEADING.exec(line);
343
464
  if (h) {
344
465
  currentHeadingIsRuleish = RULE_HEADING.test(h[2]);
466
+ // Anti-context only when it is NOT also rule-ish, so an accept word wins a
467
+ // tie (`## Testing conventions` keeps its bullets; `## Testing` drops them).
468
+ currentHeadingIsAntiContext =
469
+ ANTI_HEADING.test(h[2]) && !currentHeadingIsRuleish;
345
470
  i++;
346
471
  continue;
347
472
  }
@@ -350,6 +475,13 @@ function segmentInstructions(markdown, file) {
350
475
  i++;
351
476
  continue;
352
477
  }
478
+ // A line already CONSUMED by the structured-marker pre-pass (a marked
479
+ // section's body) is not re-segmented — this is the span-consumption that
480
+ // stops a marked rule being double-counted by the heuristic. (1-based.)
481
+ if (skipLines?.has(i + 1)) {
482
+ i++;
483
+ continue;
484
+ }
353
485
  // List items (with attached continuation lines).
354
486
  const li = LIST_ITEM.exec(line);
355
487
  if (li) {
@@ -382,7 +514,9 @@ function segmentInstructions(markdown, file) {
382
514
  const contentSpan = { start: contentStart, end: contentEnd };
383
515
  const wholeText = normalize(markdown.slice(contentStart, contentEnd));
384
516
  const conf = gate(wholeText, true, currentHeadingIsRuleish);
385
- if (conf !== null) {
517
+ // Reject bullets under an anti-context heading (Commands/Setup/Key Files/
518
+ // Architecture/…) — the corpus's dominant false-positive locus.
519
+ if (conf !== null && !currentHeadingIsAntiContext) {
386
520
  // Only attempt splitting for single-line items (keeps offsets exact).
387
521
  const spans = multiLine
388
522
  ? [trimSpan(markdown, contentSpan)]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "12.8.0",
3
+ "version": "14.0.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",
@@ -90,6 +90,7 @@
90
90
  },
91
91
  "devDependencies": {
92
92
  "@eslint/js": "^10.0.1",
93
+ "@jackchuka/mdschema": "^0.12.8",
93
94
  "@microsoft/api-extractor": "^7.58.9",
94
95
  "@types/js-yaml": "^4.0.9",
95
96
  "@types/minimatch": "^5.1.2",
@@ -127,7 +128,6 @@
127
128
  "@ast-grep/lang-rust": "^0.0.7",
128
129
  "@ast-grep/napi": "^0.43.0",
129
130
  "@iarna/toml": "^2.2.5",
130
- "@jackchuka/mdschema": "^0.12.8",
131
131
  "ci-info": "^4.4.0",
132
132
  "cosmiconfig": "^9.0.1",
133
133
  "glob": "^13.0.6",
@@ -1,6 +1,6 @@
1
1
  # Clippy — Reference
2
2
 
3
- Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom lints).
3
+ Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and the planned rule-synthesis skill (write custom lints, not yet shipped).
4
4
 
5
5
  ## Check Existing Lints First
6
6
 
@@ -1,6 +1,6 @@
1
1
  # ESLint — Reference
2
2
 
3
- Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom rules).
3
+ Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and the planned rule-synthesis skill (write custom rules, not yet shipped).
4
4
 
5
5
  ## Check Existing Plugins First
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Pylint — Reference
2
2
 
3
- Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom checkers).
3
+ Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and the planned rule-synthesis skill (write custom checkers, not yet shipped).
4
4
 
5
5
  ## Check Existing Plugins First
6
6
 
@@ -1,6 +1,6 @@
1
1
  # RuboCop — Reference
2
2
 
3
- Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom cops).
3
+ Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and the planned rule-synthesis skill (write custom cops, not yet shipped).
4
4
 
5
5
  ## Check Existing Gems First
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Ruff — Reference
2
2
 
3
- Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom rules).
3
+ Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and the planned rule-synthesis skill (write custom rules, not yet shipped).
4
4
 
5
5
  ## Check Existing Rules First
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Stylelint — Reference
2
2
 
3
- Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom rules).
3
+ Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and the planned rule-synthesis skill (write custom rules, not yet shipped).
4
4
 
5
5
  ## Check Existing Plugins First
6
6
 
@@ -142,12 +142,12 @@ Group the output into tiers:
142
142
  → enforce("eslint/sonarjs/cognitive-complexity", "Keep functions simple")
143
143
  ```
144
144
 
145
- **Tier 4: No match** (stays as guidance, or candidate for `/pr-to-lint-rule`)
145
+ **Tier 4: No match** (stays as guidance — candidate for a future rule-synthesis skill)
146
146
 
147
147
  ```
148
148
  "research-first": guidance("Google unfamiliar APIs first.")
149
- → No linter rule can enforce this. Stays as guidance.
150
- → Want me to run /pr-to-lint-rule to create a custom rule?
149
+ → No linter rule can enforce this. Stays as guidance for now.
150
+ → (Custom-rule synthesis is planned but not yet shipped.)
151
151
  ```
152
152
 
153
153
  ### Step 6: Apply Changes
@@ -168,4 +168,4 @@ Group the output into tiers:
168
168
  4. Run `npm run build && npx vigiles compile` to verify
169
169
  5. If compilation fails, report the error and revert
170
170
 
171
- **For Tier 4 (no match):** Ask the user if they want to run `/pr-to-lint-rule` for any of the unmatched rules to create custom rules.
171
+ **For Tier 4 (no match):** Tell the user these rules stay as guidance — custom-rule synthesis is a planned skill, not yet available.