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/README.md +10 -4
- package/dist/audit-report.d.ts +10 -4
- package/dist/audit-report.js +7 -2
- package/dist/audit-report.template.html +28 -28
- package/dist/cli.js +105 -28
- package/dist/core/orphans.d.ts +16 -6
- package/dist/core/orphans.js +45 -19
- package/dist/core/rule-catalog.d.ts +56 -0
- package/dist/core/rule-catalog.js +146 -0
- package/dist/core/rule-meta.js +2 -2
- package/dist/core/types.d.ts +10 -4
- package/dist/eval.d.ts +13 -1
- package/dist/eval.js +13 -1
- package/dist/instruction-sources.d.ts +39 -0
- package/dist/instruction-sources.js +71 -0
- package/dist/rule-inventory.d.ts +6 -0
- package/dist/rule-inventory.js +170 -1
- package/dist/rule-routing.d.ts +25 -2
- package/dist/rule-routing.js +337 -26
- package/dist/segment.d.ts +1 -1
- package/dist/segment.js +151 -17
- package/package.json +2 -2
- package/skills/linter-docs/clippy.md +1 -1
- package/skills/linter-docs/eslint.md +1 -1
- package/skills/linter-docs/pylint.md +1 -1
- package/skills/linter-docs/rubocop.md +1 -1
- package/skills/linter-docs/ruff.md +1 -1
- package/skills/linter-docs/stylelint.md +1 -1
- package/skills/strengthen/SKILL.md +4 -4
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 —
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
1507
|
-
* rules are often documented in AGENTS.md even under a claude-code harness
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
|
|
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
|
|
1524
|
-
if (!
|
|
1585
|
+
const files = gatherInstructionFiles(root, instructionFile);
|
|
1586
|
+
if (files.every((f) => !f.text.trim()))
|
|
1525
1587
|
return undefined;
|
|
1526
|
-
|
|
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 {
|
package/dist/core/orphans.d.ts
CHANGED
|
@@ -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
|
-
* `
|
|
30
|
-
* Set to `[]` to disable scanning
|
|
31
|
-
* doc
|
|
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
|
|
47
|
-
*
|
|
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. */
|
package/dist/core/orphans.js
CHANGED
|
@@ -21,7 +21,7 @@ const glob_1 = require("glob");
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Internals
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
|
-
const DEFAULT_INCLUDE = ["docs/**/*.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 —
|
|
40
|
-
* `
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* the
|
|
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 ===
|
|
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
|
-
|
|
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; //
|
|
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
|
|
125
|
-
*
|
|
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
|
|
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
|
package/dist/core/rule-meta.js
CHANGED
|
@@ -256,10 +256,10 @@ exports.RULE_META = {
|
|
|
256
256
|
// --- Docs hygiene ---------------------------------------------------------
|
|
257
257
|
"orphan-docs": {
|
|
258
258
|
id: "orphan-docs",
|
|
259
|
-
bucket: "
|
|
259
|
+
bucket: "heuristic-behavioral",
|
|
260
260
|
surface: ["docs"],
|
|
261
261
|
defaultSeverity: "warn",
|
|
262
|
-
summary: "
|
|
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
|
};
|
package/dist/core/types.d.ts
CHANGED
|
@@ -54,13 +54,19 @@ export interface CoverageThresholds {
|
|
|
54
54
|
/** Min % of npm scripts documented in spec commands. */
|
|
55
55
|
scripts?: number;
|
|
56
56
|
}
|
|
57
|
-
/**
|
|
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.
|
|
62
|
-
* convention
|
|
63
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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,
|