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/cli.js CHANGED
@@ -37,6 +37,8 @@ const audit_serve_js_1 = require("./audit-serve.js");
37
37
  const audit_report_js_1 = require("./audit-report.js");
38
38
  const rule_inventory_js_1 = require("./rule-inventory.js");
39
39
  const rule_routing_js_1 = require("./rule-routing.js");
40
+ const instruction_sources_js_1 = require("./instruction-sources.js");
41
+ const rule_catalog_js_1 = require("./core/rule-catalog.js");
40
42
  const adoptability_js_1 = require("./adoptability.js");
41
43
  const compile_js_1 = require("./core/compile.js");
42
44
  const proofs_js_1 = require("./core/proofs.js");
@@ -1011,23 +1013,42 @@ async function runLint(restArgs, flags, config) {
1011
1013
  }
1012
1014
  // 6. Coverage thresholds (gates CI when severity is "error")
1013
1015
  const coverageErrors = await checkCoverageThresholds(coverage, config, silent);
1014
- // 7. Orphan docs check — find .md files no other markdown references.
1015
- // Enforces the `vigiles/orphan-docs` built-in rule when declared in a
1016
- // spec. Include/exclude come from .vigilesrc.json#orphans (tsconfig-
1017
- // style globs); default include is docs/ + research/ for the
1018
- // vigiles-repo convention.
1016
+ // 7. Orphan docs check — OPT-IN (the `orphans` block in .vigilesrc.json is
1017
+ // the on-switch). "Unreferenced" only means "rot" for a hand-cross-linked
1018
+ // corpus; on a nav-managed doc site (Docusaurus/MkDocs) the page graph lives
1019
+ // in config, not inline links, so an unconditional scan is ~all false
1020
+ // positives there (an OSS sweep confirmed it). So we scan only when the repo
1021
+ // declares the block; its `include` defaults to docs/ (research/ etc. are
1022
+ // opted into explicitly). `enforce("vigiles/orphan-docs")` in a spec only
1023
+ // validates the rule NAME — the block is what drives the scan.
1024
+ const orphansCfg = config?.orphans;
1019
1025
  if (!silent)
1020
1026
  console.log("\nOrphan docs check:\n");
1021
- const orphanReport = (0, orphans_js_1.findOrphanDocs)({
1022
- basePath: process.cwd(),
1023
- include: config?.orphans?.include,
1024
- exclude: config?.orphans?.exclude,
1025
- });
1026
- if (!silent) {
1027
- for (const line of (0, orphans_js_1.formatOrphanReport)(orphanReport).split("\n")) {
1028
- console.log(` ${line}`);
1027
+ let orphanReport = {
1028
+ include: [],
1029
+ totalDocs: 0,
1030
+ referencedDocs: [],
1031
+ orphans: [],
1032
+ };
1033
+ if (orphansCfg) {
1034
+ orphanReport = (0, orphans_js_1.findOrphanDocs)({
1035
+ basePath: process.cwd(),
1036
+ include: orphansCfg.include,
1037
+ exclude: orphansCfg.exclude,
1038
+ // Exempt every registered harness's surface files (instruction file,
1039
+ // SKILL.md, subagents, commands) as orphan candidates — layout-driven so
1040
+ // core carries no harness literal (see src/core/orphans.ts).
1041
+ layouts: adapter_registry_js_1.ADAPTERS.map((a) => a.layout),
1042
+ });
1043
+ if (!silent) {
1044
+ for (const line of (0, orphans_js_1.formatOrphanReport)(orphanReport).split("\n")) {
1045
+ console.log(` ${line}`);
1046
+ }
1029
1047
  }
1030
1048
  }
1049
+ else if (!silent) {
1050
+ console.log(" ⊘ not enabled — add an `orphans` block to .vigilesrc.json to opt in (scans docs/ by default)");
1051
+ }
1031
1052
  // 7b. Untested-surface check — skills/agents/hooks shipping without a test or
1032
1053
  // eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
1033
1054
  // hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
@@ -1494,7 +1515,11 @@ function collectLintConfigText(root) {
1494
1515
  * NO model, NO config execution — safe on any repo. Composition-root. */
1495
1516
  function computeRuleInventory(root, instructionFile) {
1496
1517
  try {
1497
- const instructionText = readInstructionText(root, instructionFile);
1518
+ // The inventory maps intents → rules; it has no per-file line provenance, so
1519
+ // the concatenated text is fine here (unlike the routing preview below).
1520
+ const instructionText = gatherInstructionFiles(root, instructionFile)
1521
+ .map((f) => f.text)
1522
+ .join("\n");
1498
1523
  if (!instructionText.trim())
1499
1524
  return [];
1500
1525
  return (0, rule_inventory_js_1.buildRuleInventory)(instructionText, collectLintConfigText(root));
@@ -1503,27 +1528,79 @@ function computeRuleInventory(root, instructionFile) {
1503
1528
  return [];
1504
1529
  }
1505
1530
  }
1506
- /** Read EVERY agent instruction file present (not just the harness-native one) —
1507
- * rules are often documented in AGENTS.md even under a claude-code harness. */
1508
- function readInstructionText(root, instructionFile) {
1509
- let instructionText = "";
1510
- for (const name of new Set([instructionFile, "CLAUDE.md", "AGENTS.md"])) {
1511
- const p = (0, node_path_1.resolve)(root, name);
1512
- if ((0, node_fs_1.existsSync)(p))
1513
- instructionText += (0, node_fs_1.readFileSync)(p, "utf-8") + "\n";
1531
+ /** Gather EVERY agent instruction file present (not just the harness-native one) —
1532
+ * rules are often documented in AGENTS.md even under a claude-code harness — as a
1533
+ * list of {path, text} so each is routed SEPARATELY and keeps its OWN provenance
1534
+ * (concatenating first would corrupt per-file line numbers). Reads the ROOT
1535
+ * instruction files PLUS nested subdirectory-memory (`src/CLAUDE.md`,
1536
+ * `research/CLAUDE.md`, …), skipping fixture/demo/build/test dirs (`isFixturePath`)
1537
+ * so a repo's real memory is read without the test-fixture noise. `.claude/` rule
1538
+ * sources remain a future source. research/rule-compiler-multilang-design.md §0. */
1539
+ function gatherInstructionFiles(root, instructionFile) {
1540
+ const raw = [];
1541
+ const collect = (rel) => {
1542
+ const p = (0, node_path_1.resolve)(root, rel);
1543
+ if (!(0, node_fs_1.existsSync)(p))
1544
+ return;
1545
+ raw.push({
1546
+ path: rel,
1547
+ canonical: (0, node_fs_1.realpathSync)(p),
1548
+ text: (0, node_fs_1.readFileSync)(p, "utf-8"),
1549
+ });
1550
+ };
1551
+ // Root instruction files first (stable, deterministic order).
1552
+ for (const name of new Set([instructionFile, "CLAUDE.md", "AGENTS.md"]))
1553
+ collect(name);
1554
+ // Nested subdirectory memory, minus fixture/demo/build/test noise.
1555
+ try {
1556
+ const nested = (0, glob_1.globSync)(["**/CLAUDE.md", "**/AGENTS.md"], {
1557
+ cwd: root,
1558
+ ignore: [...IGNORE_NODE_MODULES, "dist/**", ".git/**"],
1559
+ })
1560
+ .filter((rel) => !(0, instruction_sources_js_1.isFixturePath)(rel))
1561
+ .sort();
1562
+ for (const rel of nested)
1563
+ collect(rel);
1514
1564
  }
1515
- return instructionText;
1565
+ catch {
1566
+ // best-effort — a glob failure just means root-only, never breaks the audit
1567
+ }
1568
+ // Dedup a CLAUDE.md⇄AGENTS.md mirror (symlink or byte-identical sync) to ONE
1569
+ // artifact so its rules aren't double-counted (compose-with-sync-tools).
1570
+ return (0, instruction_sources_js_1.dedupeInstructionFiles)(raw);
1516
1571
  }
1517
1572
  /** The deterministic State-B routing preview for `audit`: segment the instruction
1518
- * file(s) into atomic rules and route each (reuse / hook / semantic / unrouted) —
1573
+ * file(s) into atomic rules and route each (reuse / hook / meta / semantic / unrouted) —
1519
1574
  * NO model, fs-only. `undefined` when there's nothing to segment (kept off the
1520
- * report). Best-effort; a routing failure never breaks the audit. */
1575
+ * report). Best-effort; a routing failure never breaks the audit.
1576
+ *
1577
+ * The DYNAMIC catalog (every rule the repo's ESLint actually has + its enabled
1578
+ * state) sharpens matching — but enumerating it EXECUTES the linter, so it is an
1579
+ * OWN-REPO / CONSENTED capability (audit-side-effect-free): gated on the same
1580
+ * sticky `audit.measure` consent as the other executing checks AND on own-repo
1581
+ * (never a stranger's toolchain). The textual routing is the foreign-safe default;
1582
+ * the catalog only ADDS enabled-state nudges and matches named-but-`/`-broken rules. */
1521
1583
  function computeRuleRouting(root, instructionFile) {
1522
1584
  try {
1523
- const instructionText = readInstructionText(root, instructionFile);
1524
- if (!instructionText.trim())
1585
+ const files = gatherInstructionFiles(root, instructionFile);
1586
+ if (files.every((f) => !f.text.trim()))
1525
1587
  return undefined;
1526
- const routing = (0, rule_routing_js_1.routeRules)(instructionText, instructionFile);
1588
+ // Own-repo + consented → enumerate the live ESLint catalog. NOT gated on the
1589
+ // agent harness: the catalog is a property of the repo's LINTER (its ESLint
1590
+ // config on disk), not of Claude-Code-vs-Codex, so a Codex JS/TS repo gets the
1591
+ // same catalog match + enabled-state (adapter-aware-lint-rules: never gate a
1592
+ // harness-agnostic capability on CC). enumerateEslintCatalog returns null when
1593
+ // no ESLint config resolves (e.g. a pure-Python repo) → undefined, so a non-JS
1594
+ // repo simply falls back to the foreign-safe textual routing regardless.
1595
+ const ownRepo = (0, node_path_1.resolve)(root) === (0, node_path_1.resolve)(process.cwd());
1596
+ const consented = (0, validate_js_1.loadConfig)().audit?.measure === true;
1597
+ const availableRules = ownRepo && consented
1598
+ ? ((0, rule_catalog_js_1.enumerateEslintCatalog)(root) ?? undefined)
1599
+ : undefined;
1600
+ // Route each source SEPARATELY (each rule keeps its own file + line numbers),
1601
+ // then merge — so a CLAUDE.md rule and an AGENTS.md rule carry correct
1602
+ // provenance instead of line numbers offset by a concatenation.
1603
+ const routing = (0, rule_routing_js_1.mergeRoutings)(files.map((f) => (0, rule_routing_js_1.routeRules)(f.text, f.path, { availableRules })));
1527
1604
  return routing.segmented > 0 ? routing : undefined;
1528
1605
  }
1529
1606
  catch {
@@ -11,6 +11,7 @@
11
11
  * references (markdown links and backtick paths). Works against source
12
12
  * README plus compiled CLAUDE.md — no spec loading required.
13
13
  */
14
+ import type { PluginLayout } from "./layout.js";
14
15
  export interface OrphanReport {
15
16
  /** Include globs that were scanned. */
16
17
  readonly include: readonly string[];
@@ -25,14 +26,22 @@ export interface FindOrphansOptions {
25
26
  /** Repository root. Defaults to `process.cwd()`. */
26
27
  readonly basePath?: string;
27
28
  /**
28
- * Glob patterns of `.md` files to scan. Defaults to
29
- * `["docs/**\/*.md", "research/**\/*.md"]` — vigiles-repo convention.
30
- * Set to `[]` to disable scanning entirely. Set to your project's
31
- * doc directory globs (e.g. `["wiki/**\/*.md"]`) to override.
29
+ * Glob patterns of `.md` files to scan. Defaults to `["docs/**\/*.md"]`
30
+ * (`docs/` is the near-universal convention; a vigiles-specific dir like
31
+ * `research/` is opted into explicitly). Set to `[]` to disable scanning,
32
+ * or to your project's doc globs (e.g. `["wiki/**\/*.md"]`) to override.
32
33
  */
33
34
  readonly include?: readonly string[];
34
35
  /** Glob patterns to exclude within the include scope. */
35
36
  readonly exclude?: readonly string[];
37
+ /**
38
+ * Harnesses whose surface files (instruction file, `SKILL.md`, subagents,
39
+ * commands) are load-bearing by location and thus never orphan CANDIDATES
40
+ * (still counted as referencers). Injected by the CLI from the registered
41
+ * adapters so core stays harness-agnostic; a direct caller passes its own.
42
+ * Omitted ⇒ only the universal `SKILL.md` convention is exempt.
43
+ */
44
+ readonly layouts?: readonly PluginLayout[];
36
45
  }
37
46
  /**
38
47
  * Find docs under `include` globs that no other markdown file references.
@@ -43,8 +52,9 @@ export interface FindOrphansOptions {
43
52
  * links to itself is still an orphan.
44
53
  *
45
54
  * `include` and `exclude` are tsconfig-style glob arrays. Default include
46
- * is the vigiles-repo convention `["docs/**\/*.md", "research/**\/*.md"]`;
47
- * override per-project via `.vigilesrc.json` → `orphans.include`.
55
+ * is `["docs/**\/*.md"]` (the common convention); override per-project via
56
+ * `.vigilesrc.json` → `orphans.include` (whose presence also opts the repo
57
+ * into the scan — see the CLI gate in `vigiles lint`).
48
58
  */
49
59
  export declare function findOrphanDocs(options?: FindOrphansOptions): OrphanReport;
50
60
  /** Format an orphan report as human-readable text. */
@@ -21,7 +21,7 @@ const glob_1 = require("glob");
21
21
  // ---------------------------------------------------------------------------
22
22
  // Internals
23
23
  // ---------------------------------------------------------------------------
24
- const DEFAULT_INCLUDE = ["docs/**/*.md", "research/**/*.md"];
24
+ const DEFAULT_INCLUDE = ["docs/**/*.md"];
25
25
  const DEFAULT_IGNORE = [
26
26
  "node_modules/**",
27
27
  "dist/**",
@@ -35,24 +35,48 @@ const DEFAULT_IGNORE = [
35
35
  * that nothing else links to but is not rot.
36
36
  */
37
37
  const DISABLE_RE = /<!--\s*vigiles-disable\s+orphan-docs\s*-->/;
38
+ /** The one universal cross-harness skill-entry filename (CC + Codex). */
39
+ const SKILL_FILE = "SKILL.md";
38
40
  /**
39
- * Files the HARNESS loads directly — an instruction file (`CLAUDE.md` /
40
- * `AGENTS.md`), a skill (`SKILL.md`), a subagent (`agents/*.md`), or a slash
41
- * command (`commands/*.md`) — are load-bearing by their NAME/LOCATION, not
42
- * because another `.md` links to them. They are categorically NOT docs, so they
43
- * are never orphans, even if a project broadens `orphans.include` to scan the
44
- * whole repo. (They are still scanned as REFERENCERS, so a real doc that only
45
- * a CLAUDE.md links to is still credited — this exemption only removes them from
46
- * the orphan-CANDIDATE set.)
41
+ * Files the HARNESS loads directly — its instruction file
42
+ * (`layout.instructionFile`, e.g. `CLAUDE.md` / `AGENTS.md`), a skill
43
+ * (`SKILL.md`), a subagent (`<agentDir>/*.md`), or a slash command
44
+ * (`<commandDir>/*.md`) — are load-bearing by their NAME/LOCATION, not because
45
+ * another `.md` links to them. They are categorically NOT docs, so they are
46
+ * never orphans even when `orphans.include` broadens to the whole repo.
47
+ *
48
+ * The surface names come from the INJECTED layouts, so core stays
49
+ * harness-agnostic — no Claude Code literal here; the CLI passes every
50
+ * registered adapter's layout, and `SKILL.md` is the one universal convention.
51
+ * (They are still scanned as REFERENCERS, so a real doc that only a `CLAUDE.md`
52
+ * links to is still credited — this exemption only removes them from the
53
+ * orphan-CANDIDATE set.)
47
54
  */
48
- function isHarnessLoadedFile(path) {
55
+ function isHarnessLoadedFile(path, layouts) {
49
56
  const norm = normalizePath(path);
50
57
  const base = norm.slice(norm.lastIndexOf("/") + 1);
51
- if (base === "CLAUDE.md" || base === "AGENTS.md" || base === "SKILL.md") {
58
+ if (base === SKILL_FILE)
52
59
  return true;
60
+ for (const layout of layouts) {
61
+ if (base === layout.instructionFile)
62
+ return true;
63
+ // Subagent / slash-command surfaces live at a REAL surface root — the repo
64
+ // root, the user-surface root (e.g. `.claude/`), or the materialize root —
65
+ // NOT any nested dir that merely shares the name. A doc under `docs/prompts/`
66
+ // is documentation, not Codex's `prompts` command surface.
67
+ const roots = [
68
+ "",
69
+ ...[layout.userSurfaceRoot, layout.materializeRoot]
70
+ .filter((r) => !!r)
71
+ .map((r) => `${r}/`),
72
+ ];
73
+ for (const dir of [layout.agentDir, layout.commandDir]) {
74
+ if (dir && roots.some((r) => norm.startsWith(`${r}${dir}/`))) {
75
+ return true;
76
+ }
77
+ }
53
78
  }
54
- // Subagent / slash-command surfaces the harness enumerates by directory.
55
- return /(^|\/)(agents|commands)\//.test(norm);
79
+ return false;
56
80
  }
57
81
  // Match markdown links ](path.md) or ](path.md#anchor)
58
82
  const LINK_RE = /\]\(([^)\s]+\.md)(?:#[^)]*)?\)/g;
@@ -71,12 +95,12 @@ function isOrphanExempt(absPath) {
71
95
  }
72
96
  }
73
97
  /** Discover docs under `include`, dropping any that carry the inline opt-out. */
74
- function collectDocs(basePath, include, ignore) {
98
+ function collectDocs(basePath, include, ignore, layouts) {
75
99
  const docs = new Set();
76
100
  for (const pattern of include) {
77
101
  for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore: [...ignore] })) {
78
- if (isHarnessLoadedFile(p))
79
- continue; // instruction files are never orphans
102
+ if (isHarnessLoadedFile(p, layouts))
103
+ continue; // harness files are never orphans
80
104
  if (isOrphanExempt((0, node_path_1.resolve)(basePath, p)))
81
105
  continue;
82
106
  docs.add(normalizePath(p));
@@ -121,15 +145,17 @@ function refTargets(sourcePath, ref) {
121
145
  * links to itself is still an orphan.
122
146
  *
123
147
  * `include` and `exclude` are tsconfig-style glob arrays. Default include
124
- * is the vigiles-repo convention `["docs/**\/*.md", "research/**\/*.md"]`;
125
- * override per-project via `.vigilesrc.json` → `orphans.include`.
148
+ * is `["docs/**\/*.md"]` (the common convention); override per-project via
149
+ * `.vigilesrc.json` → `orphans.include` (whose presence also opts the repo
150
+ * into the scan — see the CLI gate in `vigiles lint`).
126
151
  */
127
152
  function findOrphanDocs(options = {}) {
128
153
  const basePath = options.basePath ?? process.cwd();
129
154
  const include = options.include ?? DEFAULT_INCLUDE;
130
155
  const userExclude = options.exclude ?? [];
131
156
  const ignore = [...DEFAULT_IGNORE, ...userExclude];
132
- const allDocs = collectDocs(basePath, include, ignore);
157
+ const layouts = options.layouts ?? [];
158
+ const allDocs = collectDocs(basePath, include, ignore, layouts);
133
159
  const allMarkdown = (0, glob_1.globSync)("**/*.md", {
134
160
  cwd: basePath,
135
161
  ignore: [...DEFAULT_IGNORE],
@@ -0,0 +1,56 @@
1
+ /**
2
+ * vigiles rule-catalog — the DYNAMIC available-rule catalog.
3
+ *
4
+ * Enumerates every lint rule the repo's linter ACTUALLY has — core built-ins PLUS
5
+ * every installed plugin's rules — so prose can be matched against the LIVE
6
+ * catalog instead of a static hand-curated map. On this repo one ESLint API call
7
+ * yields ~702 available rules (292 core + 410 plugin: typescript-eslint / sonarjs
8
+ * / boundaries), of which ~140 are enabled — vs the old static map's ~23. That
9
+ * makes an architecture norm enforceable too (`boundaries/dependencies` is in the
10
+ * catalog), which a static map never captured. See
11
+ * `research/rule-compiler-multilang-design.md` §0 (the spike this productizes).
12
+ *
13
+ * SAFETY — this EXECUTES the linter. Loading ESLint resolves the repo's real
14
+ * config (which can run plugin/config code), so `enumerateEslintCatalog` is an
15
+ * OWN-REPO / consented capability, NOT the foreign-safe default. The deterministic
16
+ * default rule-compile tier stays purely TEXTUAL (it parses config, never loads
17
+ * it); reach for this only where executing the repo's toolchain is already
18
+ * consented (own repo, on the user's machine). Mirrors the subprocess pattern of
19
+ * `discoverEslintRules` in `src/core/generate-types.ts`: ESLint is loaded in a
20
+ * child `node -e` process at the repo's cwd so it stays out of our process.
21
+ */
22
+ /** One rule the repo's linter has available, with its enabled state. */
23
+ export interface AvailableRule {
24
+ /** The rule id: `no-console`, `@typescript-eslint/no-explicit-any`, `boundaries/dependencies`. */
25
+ id: string;
26
+ /** The plugin prefix (`@typescript-eslint`, `boundaries`), or null for a core rule. */
27
+ plugin: string | null;
28
+ /** Whether the rule is enabled (severity not 0/"off") in the resolved config. */
29
+ enabled: boolean;
30
+ }
31
+ /** The full available-rule catalog for a repo's linter. */
32
+ export interface RuleCatalog {
33
+ linter: "eslint";
34
+ /** Total rules available (core + every installed plugin). */
35
+ available: number;
36
+ /** How many of those are enabled in the resolved config. */
37
+ enabled: number;
38
+ rules: AvailableRule[];
39
+ }
40
+ /**
41
+ * Parse the enumeration subprocess's JSON payload into a typed {@link RuleCatalog}.
42
+ *
43
+ * Payload shape: `{ core: string[]; enabled: string[]; plugins: Record<prefix, string[]> }`.
44
+ * Returns null on `"null"` / malformed input / an empty catalog (mirrors
45
+ * `discoverEslintRules` returning null when nothing is found).
46
+ */
47
+ export declare function parseEslintCatalog(raw: string): RuleCatalog | null;
48
+ /**
49
+ * Enumerate the repo's available ESLint rules (core + every installed plugin).
50
+ *
51
+ * EXECUTES the repo's ESLint in a child process — an own-repo / consented
52
+ * capability (see the file header). Returns null if ESLint isn't resolvable, no
53
+ * config applies, or the subprocess fails.
54
+ */
55
+ export declare function enumerateEslintCatalog(root: string): RuleCatalog | null;
56
+ //# sourceMappingURL=rule-catalog.d.ts.map
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ /**
3
+ * vigiles rule-catalog — the DYNAMIC available-rule catalog.
4
+ *
5
+ * Enumerates every lint rule the repo's linter ACTUALLY has — core built-ins PLUS
6
+ * every installed plugin's rules — so prose can be matched against the LIVE
7
+ * catalog instead of a static hand-curated map. On this repo one ESLint API call
8
+ * yields ~702 available rules (292 core + 410 plugin: typescript-eslint / sonarjs
9
+ * / boundaries), of which ~140 are enabled — vs the old static map's ~23. That
10
+ * makes an architecture norm enforceable too (`boundaries/dependencies` is in the
11
+ * catalog), which a static map never captured. See
12
+ * `research/rule-compiler-multilang-design.md` §0 (the spike this productizes).
13
+ *
14
+ * SAFETY — this EXECUTES the linter. Loading ESLint resolves the repo's real
15
+ * config (which can run plugin/config code), so `enumerateEslintCatalog` is an
16
+ * OWN-REPO / consented capability, NOT the foreign-safe default. The deterministic
17
+ * default rule-compile tier stays purely TEXTUAL (it parses config, never loads
18
+ * it); reach for this only where executing the repo's toolchain is already
19
+ * consented (own repo, on the user's machine). Mirrors the subprocess pattern of
20
+ * `discoverEslintRules` in `src/core/generate-types.ts`: ESLint is loaded in a
21
+ * child `node -e` process at the repo's cwd so it stays out of our process.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.parseEslintCatalog = parseEslintCatalog;
25
+ exports.enumerateEslintCatalog = enumerateEslintCatalog;
26
+ const node_path_1 = require("node:path");
27
+ const node_child_process_1 = require("node:child_process");
28
+ // ---------------------------------------------------------------------------
29
+ // Pure parse: subprocess JSON → typed catalog (covered by the unit test)
30
+ // ---------------------------------------------------------------------------
31
+ function isStringArray(v) {
32
+ return Array.isArray(v) && v.every((x) => typeof x === "string");
33
+ }
34
+ function parsePlugins(v) {
35
+ if (typeof v !== "object" || v === null)
36
+ return {};
37
+ const out = {};
38
+ for (const [prefix, rules] of Object.entries(v)) {
39
+ if (isStringArray(rules))
40
+ out[prefix] = rules;
41
+ }
42
+ return out;
43
+ }
44
+ function buildRules(core, plugins, enabledSet) {
45
+ const rules = [];
46
+ for (const id of core) {
47
+ rules.push({ id, plugin: null, enabled: enabledSet.has(id) });
48
+ }
49
+ for (const [prefix, pluginRules] of Object.entries(plugins)) {
50
+ for (const rule of pluginRules) {
51
+ const id = `${prefix}/${rule}`;
52
+ rules.push({ id, plugin: prefix, enabled: enabledSet.has(id) });
53
+ }
54
+ }
55
+ return rules;
56
+ }
57
+ /**
58
+ * Parse the enumeration subprocess's JSON payload into a typed {@link RuleCatalog}.
59
+ *
60
+ * Payload shape: `{ core: string[]; enabled: string[]; plugins: Record<prefix, string[]> }`.
61
+ * Returns null on `"null"` / malformed input / an empty catalog (mirrors
62
+ * `discoverEslintRules` returning null when nothing is found).
63
+ */
64
+ function parseEslintCatalog(raw) {
65
+ const trimmed = raw.trim();
66
+ if (!trimmed || trimmed === "null")
67
+ return null;
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(trimmed);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ if (typeof parsed !== "object" || parsed === null)
76
+ return null;
77
+ const obj = parsed;
78
+ const core = isStringArray(obj.core) ? obj.core : [];
79
+ const enabledList = isStringArray(obj.enabled) ? obj.enabled : [];
80
+ const plugins = parsePlugins(obj.plugins);
81
+ const rules = buildRules(core, plugins, new Set(enabledList));
82
+ if (rules.length === 0)
83
+ return null;
84
+ const enabled = rules.reduce((n, r) => (r.enabled ? n + 1 : n), 0);
85
+ return { linter: "eslint", available: rules.length, enabled, rules };
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Real-IO seam: run ESLint in a child process at the repo's cwd
89
+ // ---------------------------------------------------------------------------
90
+ /* v8 ignore start -- spawns a `node -e` subprocess that LOADS ESLint in the
91
+ repo's cwd (the executes-the-linter seam); the pure JSON→typed parse is
92
+ parseEslintCatalog, covered by the unit test, and the gated integration test
93
+ drives this real path when eslint resolves. */
94
+ /**
95
+ * Enumerate the repo's available ESLint rules (core + every installed plugin).
96
+ *
97
+ * EXECUTES the repo's ESLint in a child process — an own-repo / consented
98
+ * capability (see the file header). Returns null if ESLint isn't resolvable, no
99
+ * config applies, or the subprocess fails.
100
+ */
101
+ function enumerateEslintCatalog(root) {
102
+ try {
103
+ // A `.ts` path under src/ so the repo's flat config applies its TypeScript +
104
+ // plugin blocks (typescript-eslint / sonarjs / boundaries all scope to
105
+ // `src/**/*.ts`); the path need not exist — calculateConfigForFile resolves
106
+ // the config, it does not read the file.
107
+ const probeFile = (0, node_path_1.resolve)(root, "src/index.ts");
108
+ const script = `
109
+ const { loadESLint } = require("eslint");
110
+ const { builtinRules } = require("eslint/use-at-your-own-risk");
111
+ (async () => {
112
+ try {
113
+ const ESLint = await loadESLint();
114
+ const eslint = new ESLint({ cwd: ${JSON.stringify(root)} });
115
+ const cfg = await eslint.calculateConfigForFile(${JSON.stringify(probeFile)});
116
+ const core = [...builtinRules.keys()];
117
+ const enabled = Object.entries(cfg.rules || {})
118
+ .filter(([, v]) => {
119
+ const sev = Array.isArray(v) ? v[0] : v;
120
+ return sev !== 0 && sev !== "off";
121
+ })
122
+ .map(([k]) => k);
123
+ const plugins = {};
124
+ for (const [prefix, plugin] of Object.entries(cfg.plugins || {})) {
125
+ plugins[prefix] = Object.keys((plugin && plugin.rules) || {});
126
+ }
127
+ console.log(JSON.stringify({ core, enabled, plugins }));
128
+ } catch (e) {
129
+ console.log("null");
130
+ }
131
+ })();
132
+ `;
133
+ const output = (0, node_child_process_1.execSync)(`node -e '${script.replace(/'/g, "'\\''")}'`, {
134
+ encoding: "utf-8",
135
+ cwd: root,
136
+ stdio: ["pipe", "pipe", "pipe"],
137
+ timeout: 15000,
138
+ });
139
+ return parseEslintCatalog(output);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ /* v8 ignore stop */
146
+ //# sourceMappingURL=rule-catalog.js.map
@@ -256,10 +256,10 @@ exports.RULE_META = {
256
256
  // --- Docs hygiene ---------------------------------------------------------
257
257
  "orphan-docs": {
258
258
  id: "orphan-docs",
259
- bucket: "external-decidable",
259
+ bucket: "heuristic-behavioral",
260
260
  surface: ["docs"],
261
261
  defaultSeverity: "warn",
262
- summary: "Every docs/ + research/ .md is referenced by another .md.",
262
+ summary: "Opt-in: a doc in a configured dir (default docs/) that no other .md references.",
263
263
  detector: "findOrphanDocs",
264
264
  },
265
265
  };
@@ -54,13 +54,19 @@ export interface CoverageThresholds {
54
54
  /** Min % of npm scripts documented in spec commands. */
55
55
  scripts?: number;
56
56
  }
57
- /** Options for the orphan-docs check. */
57
+ /**
58
+ * Options for the orphan-docs check. The PRESENCE of this block in
59
+ * `.vigilesrc.json` OPTS THE REPO IN — the scan is off unless declared,
60
+ * because "unreferenced" only means "rot" for a hand-cross-linked corpus,
61
+ * not for a nav-managed doc site (Docusaurus/MkDocs) where the page graph
62
+ * lives in config. `include` is the optional dir override.
63
+ */
58
64
  export interface OrphansConfig {
59
65
  /**
60
66
  * Glob patterns of `.md` files to scan for orphans. A doc is "orphaned"
61
- * when no other markdown file references it. Defaults to vigiles-repo
62
- * convention: `["docs/**\/*.md", "research/**\/*.md"]`. Set to `[]` to
63
- * disable orphan detection entirely.
67
+ * when no other markdown file references it. Omitted → `["docs/**\/*.md"]`
68
+ * (the common convention); add your own dirs (e.g. a `research/` notes
69
+ * tree) explicitly. Set to `[]` to opt in but scan nothing.
64
70
  */
65
71
  include?: readonly string[];
66
72
  /**
package/dist/eval.d.ts CHANGED
@@ -755,7 +755,19 @@ export interface EvalDriver {
755
755
  */
756
756
  readonly harness?: string;
757
757
  }
758
- /** The default (Claude Code) eval driver: real `claude` + stream-json parsing. */
758
+ /**
759
+ * The default (Claude Code) eval driver: real `claude` + stream-json parsing.
760
+ *
761
+ * This lives at the COMPOSITION ROOT (`src/eval.ts`) on purpose, not in
762
+ * `src/adapters/claude-code/` — it is NOT a boundary leak. Claude Code is the
763
+ * wired DEFAULT (`measureTriggerRate`/`runEval` fall back to it), so `eval.ts`
764
+ * must reference it directly; wiring the default is precisely a composition
765
+ * root's job. `codexEvalDriver` lives in its adapter dir instead because Codex
766
+ * is caller-INJECTED (never a default), so `eval.ts` never imports it. Relocating
767
+ * this into the adapter would make `eval.ts → adapters/claude-code → eval.ts` a
768
+ * circular import (the shared `EvalDriver`/`ModelOutputParser` types live here).
769
+ * The asymmetry reflects default-vs-injected, not a hexagonal violation.
770
+ */
759
771
  export declare const claudeEvalDriver: EvalDriver;
760
772
  /**
761
773
  * Package loose `<skillsDir>/<name>/SKILL.md` skills into a throwaway plugin dir
package/dist/eval.js CHANGED
@@ -1240,7 +1240,19 @@ function formatEvalReport(report) {
1240
1240
  }
1241
1241
  return lines.join("\n");
1242
1242
  }
1243
- /** The default (Claude Code) eval driver: real `claude` + stream-json parsing. */
1243
+ /**
1244
+ * The default (Claude Code) eval driver: real `claude` + stream-json parsing.
1245
+ *
1246
+ * This lives at the COMPOSITION ROOT (`src/eval.ts`) on purpose, not in
1247
+ * `src/adapters/claude-code/` — it is NOT a boundary leak. Claude Code is the
1248
+ * wired DEFAULT (`measureTriggerRate`/`runEval` fall back to it), so `eval.ts`
1249
+ * must reference it directly; wiring the default is precisely a composition
1250
+ * root's job. `codexEvalDriver` lives in its adapter dir instead because Codex
1251
+ * is caller-INJECTED (never a default), so `eval.ts` never imports it. Relocating
1252
+ * this into the adapter would make `eval.ts → adapters/claude-code → eval.ts` a
1253
+ * circular import (the shared `EvalDriver`/`ModelOutputParser` types live here).
1254
+ * The asymmetry reflects default-vs-injected, not a hexagonal violation.
1255
+ */
1244
1256
  exports.claudeEvalDriver = {
1245
1257
  runner: spawnAgent,
1246
1258
  parse: parseClaudeRun,