vigiles 14.0.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.
@@ -42,7 +42,7 @@ exports.buildRuleInventory = buildRuleInventory;
42
42
  // the DYNAMIC available-rule catalog — enumerate the rules the repo's linter
43
43
  // ACTUALLY has (spike: 702 for this repo vs ~23 here) and match prose against
44
44
  // THAT, own-repo/consented since it executes the linter. Do NOT keep growing this
45
- // by hand. See research/rule-compiler-multilang-design.md §0.
45
+ // by hand. See research/rule-enforcer-multilang-design.md §0.
46
46
  exports.INTENT_MAP = [
47
47
  {
48
48
  intent: "no console.log / use the logger",
@@ -235,12 +235,93 @@ exports.INTENT_MAP = [
235
235
  rule: "no-only-tests/no-only-tests",
236
236
  configFix: '"no-only-tests/no-only-tests": "error"',
237
237
  },
238
+ // --- Ruff (Python) — the modern default (allow-list, like ESLint). These are
239
+ // the NET-NEW intents Pylint does NOT already cover (imports-inside-functions,
240
+ // annotations, logger.exception, print, import-sorting). The SHARED Python
241
+ // intents (bare-except, broad-except, mutable-default, wildcard, global,
242
+ // too-many-*, line-length, unused, f-strings, docstrings) stay with the Pylint
243
+ // entries below — one intent, one linter, so keyword disjointness holds (the
244
+ // `rule-routing-dogfood` invariant test) and prose never double-routes. Whether
245
+ // shared Python NL prose should later PREFER Ruff over Pylint is an open
246
+ // ownership decision, deferred. ROUTE-ONLY like Pylint: config-state
247
+ // (buildRuleInventory) stays gated to eslint — Ruff's select/ignore config shape
248
+ // differs from the eslint rules-map, so the eslint-shaped check would MISLABEL;
249
+ // accurate Ruff enabled-state is the catalog/ConfigProbe follow-up
250
+ // (rule-enforcer-multilang-design.md §3). Every CODE was verified to exist
251
+ // against ruff 0.15.8 (`ruff rule <CODE>`); keywords are Python-diagnostic (no
252
+ // cross-language phrase that would mis-attribute the linter in a JS/TS doc).
253
+ {
254
+ intent: "no imports inside functions (Python)",
255
+ linter: "ruff",
256
+ keywords: [
257
+ "PLC0415",
258
+ "inline import",
259
+ "inline imports",
260
+ "imports inside functions",
261
+ "import inside a function",
262
+ "import inside functions",
263
+ ],
264
+ rule: "PLC0415",
265
+ configFix: 'add "PLC0415" (import-outside-top-level) to [tool.ruff.lint] select',
266
+ },
267
+ {
268
+ // E402 flags module-level code BEFORE the imports; the "no imports inside
269
+ // functions" reading is PLC0415 above. Code-only keyword ON PURPOSE: the
270
+ // phrase "imports at the top" is ambiguous between the two AND collides with
271
+ // ESLint `import/first` in a JS doc, so it is NOT a keyword here (a missing
272
+ // route is recoverable; a wrong "enforceable" claim is not).
273
+ intent: "no module code before imports (Python)",
274
+ linter: "ruff",
275
+ keywords: ["E402"],
276
+ rule: "E402",
277
+ configFix: 'add "E402" (module-import-not-at-top-of-file) to [tool.ruff.lint] select',
278
+ },
279
+ {
280
+ intent: "require argument type annotations (Python)",
281
+ linter: "ruff",
282
+ keywords: [
283
+ "ANN001",
284
+ "type hints required",
285
+ "type hints are required",
286
+ "require type hints",
287
+ ],
288
+ rule: "ANN001",
289
+ configFix: 'add "ANN001" (missing-type-function-argument) to [tool.ruff.lint] select',
290
+ },
291
+ {
292
+ intent: "require return type annotations (Python)",
293
+ linter: "ruff",
294
+ keywords: ["ANN201"],
295
+ rule: "ANN201",
296
+ configFix: 'add "ANN201" (missing-return-type-undocumented-public-function) to [tool.ruff.lint] select',
297
+ },
298
+ {
299
+ intent: "use logger.exception in handlers (Python)",
300
+ linter: "ruff",
301
+ keywords: ["TRY400", "logger.exception", "logging.exception"],
302
+ rule: "TRY400",
303
+ configFix: 'add "TRY400" (error-instead-of-exception) to [tool.ruff.lint] select',
304
+ },
305
+ {
306
+ intent: "no print statements — use logging (Python)",
307
+ linter: "ruff",
308
+ keywords: ["T201", "print statement", "print statements"],
309
+ rule: "T201",
310
+ configFix: 'add "T201" (print) to [tool.ruff.lint] select',
311
+ },
312
+ {
313
+ intent: "keep imports sorted (Python)",
314
+ linter: "ruff",
315
+ keywords: ["I001", "sorted imports", "isort"],
316
+ rule: "I001",
317
+ configFix: 'add "I001" (unsorted-imports) to [tool.ruff.lint] select',
318
+ },
238
319
  // --- Pylint (Python) — routing basics. These feed classify() (routing → reuse);
239
320
  // buildRuleInventory is gated to eslint (see below) because pylint is
240
321
  // ON-BY-DEFAULT (deny-list), so the eslint-shaped config-state check would
241
322
  // MISLABEL it (a symbol in `disable=` reads as "in-config", an absent one as
242
323
  // "enable it"). Accurate pylint enabled-state needs the inverted-polarity
243
- // ConfigProbe (research/rule-compiler-multilang-design.md §3), deferred —
324
+ // ConfigProbe (research/rule-enforcer-multilang-design.md §3), deferred —
244
325
  // classify() needs NO enabled-state, so pylint prose still routes honestly.
245
326
  // Keywords are code-shaped symbols + Python-UNAMBIGUOUS compounds (singular AND
246
327
  // plural, since matchesWholeToken is boundary-exact); bare ambiguous words
@@ -351,6 +432,74 @@ exports.INTENT_MAP = [
351
432
  rule: "unused-import",
352
433
  configFix: "pylint enables unused-import (W0611) by default; keep it out of the disable list",
353
434
  },
435
+ // --- Clippy (Rust) — routing basics (backlog #3: the 0%→ win for Rust
436
+ // rulebooks; codex/ghostty routed 0% purely because clippy was unmapped, yet
437
+ // codex literally says "avoid patterns that require `panic!`, `unreachable!`,
438
+ // or `.unwrap()`" and "make `match` exhaustive … avoid wildcard arms"). Every
439
+ // lint is a real clippy restriction lint. Route-only, like ruff/pylint —
440
+ // config-state stays eslint-gated (L639) because clippy's lint levels live in
441
+ // Cargo.toml `[lints.clippy]` / crate attrs, a different shape (accurate
442
+ // enabled-state is the ConfigProbe follow-up, rule-enforcer-multilang-design
443
+ // §3). Keywords are Rust-UNAMBIGUOUS — macro `!` forms, `.method` call forms,
444
+ // `clippy::` symbols, and Rust-only compounds ("wildcard arm(s)", NOT bare
445
+ // "wildcard" which collides with pylint `wildcard-import`) — so a Python/JS
446
+ // doc never mis-attributes clippy (the cross-language-FP test guards this).
447
+ {
448
+ intent: "no .unwrap() (Rust)",
449
+ linter: "clippy",
450
+ keywords: ["clippy::unwrap_used", "unwrap_used", ".unwrap"],
451
+ rule: "clippy::unwrap_used",
452
+ configFix: 'set unwrap_used = "warn" in [lints.clippy] (Cargo.toml), or #![warn(clippy::unwrap_used)]',
453
+ },
454
+ {
455
+ intent: "no .expect() (Rust)",
456
+ linter: "clippy",
457
+ keywords: ["clippy::expect_used", "expect_used", ".expect"],
458
+ rule: "clippy::expect_used",
459
+ configFix: 'set expect_used = "warn" in [lints.clippy] (Cargo.toml), or #![warn(clippy::expect_used)]',
460
+ },
461
+ {
462
+ intent: "no panic! (Rust)",
463
+ linter: "clippy",
464
+ keywords: ["clippy::panic", "panic!"],
465
+ rule: "clippy::panic",
466
+ configFix: 'set panic = "warn" in [lints.clippy] (Cargo.toml), or #![warn(clippy::panic)]',
467
+ },
468
+ {
469
+ intent: "no unreachable! (Rust)",
470
+ linter: "clippy",
471
+ keywords: ["clippy::unreachable", "unreachable!"],
472
+ rule: "clippy::unreachable",
473
+ configFix: 'set unreachable = "warn" in [lints.clippy] (Cargo.toml), or #![warn(clippy::unreachable)]',
474
+ },
475
+ {
476
+ intent: "no todo! (Rust)",
477
+ linter: "clippy",
478
+ keywords: ["clippy::todo", "todo!"],
479
+ rule: "clippy::todo",
480
+ configFix: 'set todo = "warn" in [lints.clippy] (Cargo.toml), or #![warn(clippy::todo)]',
481
+ },
482
+ {
483
+ intent: "no dbg! (Rust)",
484
+ linter: "clippy",
485
+ keywords: ["clippy::dbg_macro", "dbg!"],
486
+ rule: "clippy::dbg_macro",
487
+ configFix: 'set dbg_macro = "warn" in [lints.clippy] (Cargo.toml), or #![warn(clippy::dbg_macro)]',
488
+ },
489
+ {
490
+ intent: "exhaustive match — no wildcard enum arm (Rust)",
491
+ linter: "clippy",
492
+ keywords: [
493
+ "clippy::wildcard_enum_match_arm",
494
+ "wildcard_enum_match_arm",
495
+ "wildcard match arm",
496
+ "wildcard match arms",
497
+ "wildcard arm",
498
+ "wildcard arms",
499
+ ],
500
+ rule: "clippy::wildcard_enum_match_arm",
501
+ configFix: 'set wildcard_enum_match_arm = "warn" in [lints.clippy] (Cargo.toml), or #![warn(clippy::wildcard_enum_match_arm)]',
502
+ },
354
503
  ];
355
504
  /** Escape a keyword for use inside a RegExp. */
356
505
  function escapeRe(s) {
@@ -1,8 +1,61 @@
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). */
4
42
  export type RuleCategory = "reuse" | "hook" | "meta" | "semantic" | "unrouted";
5
43
  export type RuleMechanism = "config-line" | "hook" | "prose" | "synthesize";
44
+ /**
45
+ * The user-facing presentation of each routing category — its glyph + lane
46
+ * label. The SINGLE SOURCE the terminal summary reads (and the HTML report
47
+ * mirrors), so the category-name → lane-label mapping lives in one place.
48
+ *
49
+ * NB the type name `unrouted` is a WIRE value (it appears in the versioned
50
+ * `AuditReport` JSON), which is why it isn't renamed to its lane label `custom`;
51
+ * this table is where the human-facing name is resolved. The category meanings
52
+ * are documented in the file header; the mapping is tabled in
53
+ * `research/rule-enforcer-design.md` §4.
54
+ */
55
+ export declare const LANE_META: Record<RuleCategory, {
56
+ readonly glyph: string;
57
+ readonly label: string;
58
+ }>;
6
59
  /** One segmented, deterministically-routed rule with provenance. */
7
60
  export interface RoutedRule {
8
61
  /** Normalized atomic rule text (from the segmenter). */
@@ -29,10 +82,19 @@ export interface RoutedRule {
29
82
  readonly source?: "marker" | "heuristic";
30
83
  }
31
84
  export interface RuleRouting {
32
- /** How many atomic rules were routed (after the confidence filter). */
85
+ /** How many CONFIDENT atomic rules were routed (high or rescued). */
33
86
  readonly segmented: number;
34
87
  readonly counts: Record<RuleCategory, number>;
88
+ /** The CONFIDENT tier — cleared the precision bar; these are the routed rules. */
35
89
  readonly rules: readonly RoutedRule[];
90
+ /** The POSSIBLE tier — rule-ish bullets (medium confidence) that did NOT clear
91
+ * the bar, still classified so a human can review + promote them. Detection is
92
+ * precision-first, so this is where a declarative rule ("Every X must Y") that
93
+ * the confident tier misses shows up. See `research/rule-enforcer-design.md` §2. */
94
+ readonly possible: readonly RoutedRule[];
95
+ /** Bullets the segmenter decided were NOT rules, each with a reason — so the
96
+ * report is honest about what it set aside (§3). */
97
+ readonly skipped: readonly SkippedBullet[];
36
98
  }
37
99
  export interface RouteOptions {
38
100
  /**