vigiles 14.0.0 → 14.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit-report.js +4 -1
- package/dist/audit-report.template.html +28 -28
- package/dist/cli.js +191 -34
- package/dist/core/rule-catalog.d.ts +48 -3
- package/dist/core/rule-catalog.js +150 -2
- package/dist/rule-routing.d.ts +48 -1
- package/dist/rule-routing.js +127 -28
- package/dist/segment.d.ts +23 -1
- package/dist/segment.js +34 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1580,28 +1580,73 @@ function gatherInstructionFiles(root, instructionFile) {
|
|
|
1580
1580
|
* sticky `audit.measure` consent as the other executing checks AND on own-repo
|
|
1581
1581
|
* (never a stranger's toolchain). The textual routing is the foreign-safe default;
|
|
1582
1582
|
* the catalog only ADDS enabled-state nudges and matches named-but-`/`-broken rules. */
|
|
1583
|
+
/** Does the repo actually USE Pylint — i.e. is there a real Pylint config to run
|
|
1584
|
+
* against? A DEDICATED pylintrc file (`.pylintrc`, `pylintrc`, `.pylintrc.toml`,
|
|
1585
|
+
* `pylintrc.toml`) is unambiguous. A SHARED file (`pyproject.toml`, `setup.cfg`,
|
|
1586
|
+
* `tox.ini`) counts ONLY when it carries a Pylint section — otherwise a Ruff-only
|
|
1587
|
+
* / packaging-only `pyproject.toml` would falsely make us spawn pylint and route
|
|
1588
|
+
* docs as `reuse`/`enabled` against a linter the repo doesn't use. Mirrors
|
|
1589
|
+
* Pylint's own config search order. */
|
|
1590
|
+
function hasPythonSurface(root) {
|
|
1591
|
+
const dedicated = [
|
|
1592
|
+
".pylintrc",
|
|
1593
|
+
"pylintrc",
|
|
1594
|
+
".pylintrc.toml",
|
|
1595
|
+
"pylintrc.toml",
|
|
1596
|
+
];
|
|
1597
|
+
if (dedicated.some((f) => (0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, f))))
|
|
1598
|
+
return true;
|
|
1599
|
+
// A pylint section: `[tool.pylint...]` (pyproject) or `[pylint...]` / `[MASTER]`
|
|
1600
|
+
// / `[MESSAGES CONTROL]` (setup.cfg / tox.ini, incl. case variants).
|
|
1601
|
+
const pylintSection = /(\[tool\.pylint)|(\[pylint)|(\[MASTER\])|(\[MESSAGES CONTROL\])/i;
|
|
1602
|
+
for (const f of ["pyproject.toml", "setup.cfg", "tox.ini"]) {
|
|
1603
|
+
const p = (0, node_path_1.resolve)(root, f);
|
|
1604
|
+
if ((0, node_fs_1.existsSync)(p)) {
|
|
1605
|
+
try {
|
|
1606
|
+
if (pylintSection.test((0, node_fs_1.readFileSync)(p, "utf-8")))
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
catch {
|
|
1610
|
+
/* unreadable — treat as no pylint config */
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1583
1616
|
function computeRuleRouting(root, instructionFile) {
|
|
1584
1617
|
try {
|
|
1585
1618
|
const files = gatherInstructionFiles(root, instructionFile);
|
|
1586
1619
|
if (files.every((f) => !f.text.trim()))
|
|
1587
1620
|
return undefined;
|
|
1588
|
-
// Own-repo + consented → enumerate the live
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1594
|
-
// repo
|
|
1621
|
+
// Own-repo + consented → enumerate the live rule catalog of whichever
|
|
1622
|
+
// linter(s) the repo has: ESLint (JS/TS) and/or Pylint (Python), merged so a
|
|
1623
|
+
// polyglot repo matches against both. NOT gated on the agent harness — a
|
|
1624
|
+
// catalog is a property of the repo's LINTER (its config on disk), not of
|
|
1625
|
+
// Claude-Code-vs-Codex (adapter-aware-lint-rules: never gate a harness-
|
|
1626
|
+
// agnostic capability on CC). Each enumerate returns null when its linter
|
|
1627
|
+
// doesn't apply, so a repo with neither falls back to the foreign-safe textual
|
|
1628
|
+
// routing. Enumerating EXECUTES the linter (plugin code loads), hence own-repo
|
|
1629
|
+
// + consent — same posture for both.
|
|
1595
1630
|
const ownRepo = (0, node_path_1.resolve)(root) === (0, node_path_1.resolve)(process.cwd());
|
|
1596
1631
|
const consented = (0, validate_js_1.loadConfig)().audit?.measure === true;
|
|
1597
1632
|
const availableRules = ownRepo && consented
|
|
1598
|
-
? ((0, rule_catalog_js_1.enumerateEslintCatalog)(root)
|
|
1633
|
+
? (0, rule_catalog_js_1.mergeCatalogs)((0, rule_catalog_js_1.enumerateEslintCatalog)(root),
|
|
1634
|
+
// Only spawn pylint when the repo actually has a Python surface —
|
|
1635
|
+
// avoids a needless (and possibly noisy) pylint run on a pure-JS repo.
|
|
1636
|
+
hasPythonSurface(root) ? (0, rule_catalog_js_1.enumeratePylintCatalog)(root) : null)
|
|
1599
1637
|
: undefined;
|
|
1600
1638
|
// Route each source SEPARATELY (each rule keeps its own file + line numbers),
|
|
1601
1639
|
// then merge — so a CLAUDE.md rule and an AGENTS.md rule carry correct
|
|
1602
1640
|
// provenance instead of line numbers offset by a concatenation.
|
|
1603
1641
|
const routing = (0, rule_routing_js_1.mergeRoutings)(files.map((f) => (0, rule_routing_js_1.routeRules)(f.text, f.path, { availableRules })));
|
|
1604
|
-
|
|
1642
|
+
// Keep the routing if it found ANY confident rule, possible rule, or skipped
|
|
1643
|
+
// bullet — so the two-tier + skipped report surfaces even a doc with 0
|
|
1644
|
+
// confident rules (all its bullets landed in possible/skipped).
|
|
1645
|
+
return routing.segmented > 0 ||
|
|
1646
|
+
routing.possible.length > 0 ||
|
|
1647
|
+
routing.skipped.length > 0
|
|
1648
|
+
? routing
|
|
1649
|
+
: undefined;
|
|
1605
1650
|
}
|
|
1606
1651
|
catch {
|
|
1607
1652
|
return undefined;
|
|
@@ -1635,6 +1680,34 @@ function formatTriggerNudge(triggerableSkills) {
|
|
|
1635
1680
|
return (`ℹ Do your ${String(n)} skill${n === 1 ? "" : "s"} actually fire? The deterministic read can't tell — ` +
|
|
1636
1681
|
`run \`audit\` interactively to measure, or test with \`measureTriggerRate\` (vigiles/testing).`);
|
|
1637
1682
|
}
|
|
1683
|
+
/** A terminal summary of the rule map: the CONFIDENT lane counts + the POSSIBLE
|
|
1684
|
+
* (review) and SKIPPED tiers, with the honest caveat that detection is a
|
|
1685
|
+
* heuristic filter. The full per-rule map + skipped list live in the HTML/JSON
|
|
1686
|
+
* report; this is the "be clear about what was detected" headline. "" when there
|
|
1687
|
+
* is nothing to show. */
|
|
1688
|
+
function formatRuleMapSummary(routing) {
|
|
1689
|
+
if (!routing)
|
|
1690
|
+
return "";
|
|
1691
|
+
const { counts, possible, skipped } = routing;
|
|
1692
|
+
const confident = counts.reuse +
|
|
1693
|
+
counts.hook +
|
|
1694
|
+
counts.unrouted +
|
|
1695
|
+
counts.semantic +
|
|
1696
|
+
counts.meta;
|
|
1697
|
+
if (confident === 0 && possible.length === 0 && skipped.length === 0)
|
|
1698
|
+
return "";
|
|
1699
|
+
const lines = [
|
|
1700
|
+
"Rule map — how your prose rules could be enforced (heuristic, precision-first):",
|
|
1701
|
+
` ✓ ${String(counts.reuse)} enforceable · ⛓ ${String(counts.hook)} hook · ⚙ ${String(counts.unrouted)} custom · ✎ ${String(counts.semantic)} judgment` +
|
|
1702
|
+
(counts.meta > 0 ? ` · ☰ ${String(counts.meta)} agent-note` : ""),
|
|
1703
|
+
];
|
|
1704
|
+
if (possible.length > 0)
|
|
1705
|
+
lines.push(` ? ${String(possible.length)} possible — rule-ish, but below the confidence bar (review these)`);
|
|
1706
|
+
if (skipped.length > 0)
|
|
1707
|
+
lines.push(` ⊘ ${String(skipped.length)} skipped — not treated as rules (setup steps, descriptions, index entries, no norm signal)`);
|
|
1708
|
+
lines.push(" Detection is a best-effort filter — it won't catch every rule. Full map + skipped list in the report (or --json).");
|
|
1709
|
+
return lines.join("\n");
|
|
1710
|
+
}
|
|
1638
1711
|
function scaffoldSpec(args) {
|
|
1639
1712
|
const targetFlag = args.find((a) => a.startsWith("--target="));
|
|
1640
1713
|
const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
|
|
@@ -3905,7 +3978,7 @@ function printUsage(command) {
|
|
|
3905
3978
|
console.log(" vigiles eject [file] Un-manage a compiled file → plain hand-owned markdown (--keep-spec)");
|
|
3906
3979
|
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
3907
3980
|
console.log(" vigiles audit [dir...] Lighthouse for your harness — a LOCAL report: rings + what's broken + fixes (a deterministic read; 2+ dirs → leaderboard)");
|
|
3908
|
-
console.log(" writes vigiles-report.html +
|
|
3981
|
+
console.log(" writes vigiles-report.html + .json (auto-gitignored; --out=<dir> · --no-html/--no-json · --no-open · --json for machine output). NOT a CI step — use `vigiles lint` in CI.");
|
|
3909
3982
|
console.log(" the executing checks (run your hooks · live MCP · do skills fire?) run only interactively — `audit` asks once (remembered); automation uses the vigiles/testing API");
|
|
3910
3983
|
console.log(" --serve opens a LIVE local report whose buttons create specs in one click (own repo only; loopback + token-guarded) · --no-serve to skip the prompt");
|
|
3911
3984
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
@@ -4821,18 +4894,50 @@ async function runHookProgramCommand(file) {
|
|
|
4821
4894
|
}
|
|
4822
4895
|
}
|
|
4823
4896
|
}
|
|
4897
|
+
/**
|
|
4898
|
+
* Idempotently keep the generated report artifacts OUT of git — the tool that
|
|
4899
|
+
* writes a build artifact keeps it ignored (the `next build` → `.next` pattern),
|
|
4900
|
+
* so `vigiles audit` (which runs zero-config, without `init`) never leaves
|
|
4901
|
+
* surprise untracked files in `git status`. Appends only the MISSING entries
|
|
4902
|
+
* under a labelled block if a `.gitignore` exists; if none exists, prints a
|
|
4903
|
+
* one-line nudge rather than creating one silently. Best-effort — a failure here
|
|
4904
|
+
* never breaks the audit. `entries` are paths relative to the git root (cwd).
|
|
4905
|
+
*/
|
|
4906
|
+
function ensureReportGitignored(cwd, entries) {
|
|
4907
|
+
if (entries.length === 0)
|
|
4908
|
+
return;
|
|
4909
|
+
const gi = (0, node_path_1.resolve)(cwd, ".gitignore");
|
|
4910
|
+
try {
|
|
4911
|
+
if (!(0, node_fs_1.existsSync)(gi)) {
|
|
4912
|
+
console.log(`\nℹ tip: add ${entries.join(" + ")} to a .gitignore (generated report artifacts)`);
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
const content = (0, node_fs_1.readFileSync)(gi, "utf-8");
|
|
4916
|
+
const present = new Set(content.split("\n").map((l) => l.trim()));
|
|
4917
|
+
const missing = entries.filter((e) => !present.has(e));
|
|
4918
|
+
if (missing.length === 0)
|
|
4919
|
+
return;
|
|
4920
|
+
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
4921
|
+
(0, node_fs_1.writeFileSync)(gi, `${content}${sep}\n# vigiles audit report (generated)\n${missing.join("\n")}\n`);
|
|
4922
|
+
console.log(`✓ Added ${missing.join(" + ")} to .gitignore`);
|
|
4923
|
+
}
|
|
4924
|
+
catch {
|
|
4925
|
+
/* best-effort — a read-only or missing .gitignore never breaks the audit */
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4824
4928
|
/**
|
|
4825
4929
|
* Write the versioned JSON artifact (`vigiles-report.json`) — the upload/CI
|
|
4826
4930
|
* boundary a hosted dashboard ingests. Stamps `meta.generatedAt` here (at write
|
|
4827
4931
|
* time, not in the pure builder, so the HTML-embedded form stays deterministic).
|
|
4828
4932
|
*/
|
|
4829
|
-
function writeAuditJson(report) {
|
|
4830
|
-
const jsonPath = (0, node_path_1.resolve)(
|
|
4933
|
+
function writeAuditJson(report, outDir) {
|
|
4934
|
+
const jsonPath = (0, node_path_1.resolve)(outDir, "vigiles-report.json");
|
|
4831
4935
|
const stamped = {
|
|
4832
4936
|
...report,
|
|
4833
4937
|
meta: { ...report.meta, generatedAt: new Date().toISOString() },
|
|
4834
4938
|
};
|
|
4835
4939
|
try {
|
|
4940
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
4836
4941
|
(0, node_fs_1.writeFileSync)(jsonPath, JSON.stringify(stamped, null, 2) + "\n");
|
|
4837
4942
|
console.log("✓ Wrote vigiles-report.json — the upload/CI artifact");
|
|
4838
4943
|
}
|
|
@@ -4864,12 +4969,16 @@ function openBestEffort(file) {
|
|
|
4864
4969
|
* for a human at a TTY, open it best-effort. The shareable Lighthouse artifact;
|
|
4865
4970
|
* never spawns a browser for an agent / CI run.
|
|
4866
4971
|
*/
|
|
4867
|
-
function writeAuditHtml(report) {
|
|
4868
|
-
const htmlPath = (0, node_path_1.resolve)(
|
|
4972
|
+
function writeAuditHtml(report, outDir, open) {
|
|
4973
|
+
const htmlPath = (0, node_path_1.resolve)(outDir, "vigiles-report.html");
|
|
4869
4974
|
try {
|
|
4975
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
4870
4976
|
(0, node_fs_1.writeFileSync)(htmlPath, (0, audit_html_js_1.renderAuditHtml)(report));
|
|
4871
4977
|
console.log("\n✓ Wrote vigiles-report.html — open it for the full report");
|
|
4872
|
-
|
|
4978
|
+
// Great DX: pop the report open in the browser for a human at a TTY (the
|
|
4979
|
+
// `lighthouse --view` behaviour). `--no-open` suppresses it; an agent / CI
|
|
4980
|
+
// run (no TTY) never spawns a browser.
|
|
4981
|
+
if (open && process.stdout.isTTY)
|
|
4873
4982
|
openBestEffort(htmlPath);
|
|
4874
4983
|
}
|
|
4875
4984
|
catch (e) {
|
|
@@ -5309,26 +5418,30 @@ async function main() {
|
|
|
5309
5418
|
// Read the local flight recorder ONCE — feeds both the JSON report
|
|
5310
5419
|
// (structured summary, the product boundary) and the terminal render.
|
|
5311
5420
|
const ledgerRecords = (0, observe_js_1.readObservations)(root);
|
|
5312
|
-
|
|
5421
|
+
// The report scaffold WITHOUT the rule map — the map's catalog
|
|
5422
|
+
// enrichment (enabled-state / "documented but OFF") enumerates the repo's
|
|
5423
|
+
// linter, which is gated on the SAME audit.measure consent as the
|
|
5424
|
+
// executing checks. So the map is routed AFTER consent (resolved below)
|
|
5425
|
+
// and folded into the report there, so a first-time "yes" enriches THIS
|
|
5426
|
+
// run — not the next one.
|
|
5427
|
+
const auditReportBase = (0, audit_report_js_1.buildAuditReport)(report, {
|
|
5313
5428
|
harness: adapter.name,
|
|
5314
5429
|
vigilesVersion: getVersion(),
|
|
5315
5430
|
adoptableSurfaces,
|
|
5316
5431
|
observations: (0, observe_js_1.summarizeObservations)(ledgerRecords),
|
|
5317
5432
|
rulesInventory: computeRuleInventory(root, adapter.layout.instructionFile),
|
|
5318
|
-
ruleRouting: computeRuleRouting(root, adapter.layout.instructionFile),
|
|
5319
5433
|
});
|
|
5320
|
-
const sc =
|
|
5434
|
+
const sc = auditReportBase.score;
|
|
5321
5435
|
const plan = (0, optimize_js_1.optimize)(report);
|
|
5436
|
+
// The deterministic READ leads: rings + report + fixes + nudges print
|
|
5437
|
+
// BEFORE the consent prompt, so a plain `audit` shows its findings first.
|
|
5438
|
+
// (JSON stays silent until the single blob below, after consent.)
|
|
5322
5439
|
if (!json) {
|
|
5323
5440
|
// The Lighthouse rings: per-category 0–100 + the weighted overall,
|
|
5324
5441
|
// shown before the detailed report so the headline signal leads.
|
|
5325
5442
|
console.log((0, audit_score_js_1.formatAuditScore)(sc));
|
|
5326
5443
|
console.log("");
|
|
5327
|
-
|
|
5328
|
-
console.log(json
|
|
5329
|
-
? JSON.stringify(auditReport, null, 2)
|
|
5330
|
-
: (0, scan_js_1.formatScanReport)(report));
|
|
5331
|
-
if (!json) {
|
|
5444
|
+
console.log((0, scan_js_1.formatScanReport)(report));
|
|
5332
5445
|
// Fold each finding's fix inline (replaces the former --fix-plan/--explain
|
|
5333
5446
|
// flags): the deterministic, free recommendation list under the report.
|
|
5334
5447
|
const fixes = (0, optimize_js_1.formatRecommendations)(plan);
|
|
@@ -5346,17 +5459,14 @@ async function main() {
|
|
|
5346
5459
|
.length);
|
|
5347
5460
|
if (fireNudge)
|
|
5348
5461
|
console.log("\n" + fireNudge);
|
|
5349
|
-
// The flight recorder: a compact summary of what the harness actually
|
|
5350
|
-
// DID in real sessions (hook/agent decisions), read off the local
|
|
5351
|
-
// agent-readable ledger. Empty (skipped) until something is recorded.
|
|
5352
|
-
const ledgerSummary = (0, observe_js_1.formatLedgerSummary)(ledgerRecords);
|
|
5353
|
-
if (ledgerSummary)
|
|
5354
|
-
console.log("\n" + ledgerSummary);
|
|
5355
5462
|
}
|
|
5356
5463
|
// ONE read-vs-run decision for the EXECUTING checks (live MCP + skill
|
|
5357
|
-
// firing)
|
|
5358
|
-
//
|
|
5359
|
-
//
|
|
5464
|
+
// firing) AND the rule map's catalog enrichment (enumerating the repo's
|
|
5465
|
+
// linter also executes it). A plain `audit` is a deterministic READ;
|
|
5466
|
+
// these run only on consent — ASK once at a TTY (remembered); headless
|
|
5467
|
+
// stays a read + a nudge (no execution flag — automation uses the
|
|
5468
|
+
// vigiles/testing API). Resolved AFTER the report (so the read leads) but
|
|
5469
|
+
// BEFORE the rule map is routed, so a first-time "yes" enriches THIS run.
|
|
5360
5470
|
// (The safety battery is NOT here — it needs cross-platform confinement
|
|
5361
5471
|
// that isn't shipped, so it lives in the vigiles/testing API.)
|
|
5362
5472
|
const isForeign = root !== process.cwd();
|
|
@@ -5368,6 +5478,30 @@ async function main() {
|
|
|
5368
5478
|
(0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, adapter.layout.instructionFile)),
|
|
5369
5479
|
};
|
|
5370
5480
|
const { execute, note: execNote } = await resolveExecution(surfaces, json, args, adapter.name);
|
|
5481
|
+
// Consent is now settled (and remembered via .vigilesrc.json, which
|
|
5482
|
+
// computeRuleRouting re-reads) — route the prose rules, enumerating the
|
|
5483
|
+
// live catalog when consented + own-repo. Feeds the JSON report, the
|
|
5484
|
+
// written artifacts, and the terminal rule-map summary.
|
|
5485
|
+
const ruleRouting = computeRuleRouting(root, adapter.layout.instructionFile);
|
|
5486
|
+
const auditReport = ruleRouting
|
|
5487
|
+
? { ...auditReportBase, ruleRouting }
|
|
5488
|
+
: auditReportBase;
|
|
5489
|
+
if (json) {
|
|
5490
|
+
console.log(JSON.stringify(auditReport, null, 2));
|
|
5491
|
+
}
|
|
5492
|
+
else {
|
|
5493
|
+
// The rule map — what audit detected in your instruction file and how
|
|
5494
|
+
// each rule could be enforced (confident / possible / skipped tiers).
|
|
5495
|
+
const ruleMap = formatRuleMapSummary(ruleRouting);
|
|
5496
|
+
if (ruleMap)
|
|
5497
|
+
console.log("\n" + ruleMap);
|
|
5498
|
+
// The flight recorder: a compact summary of what the harness actually
|
|
5499
|
+
// DID in real sessions (hook/agent decisions), read off the local
|
|
5500
|
+
// agent-readable ledger. Empty (skipped) until something is recorded.
|
|
5501
|
+
const ledgerSummary = (0, observe_js_1.formatLedgerSummary)(ledgerRecords);
|
|
5502
|
+
if (ledgerSummary)
|
|
5503
|
+
console.log("\n" + ledgerSummary);
|
|
5504
|
+
}
|
|
5371
5505
|
// LIVE MCP tool resolution STARTS each declared MCP server — a server is
|
|
5372
5506
|
// exactly what connects to a real Postgres / authenticates a real API on
|
|
5373
5507
|
// boot. So it runs only under consent (`execute`) AND own-repo (never
|
|
@@ -5465,10 +5599,19 @@ async function main() {
|
|
|
5465
5599
|
const finalReport = adoptabilityResult
|
|
5466
5600
|
? { ...auditReport, adoptability: adoptabilityResult }
|
|
5467
5601
|
: auditReport;
|
|
5602
|
+
// Where the report artifacts land: cwd by default, or `--out=<dir>` (for
|
|
5603
|
+
// CI upload / a custom location). The dir is created if missing.
|
|
5604
|
+
const outFlag = args.find((a) => a.startsWith("--out="));
|
|
5605
|
+
const outDir = outFlag
|
|
5606
|
+
? (0, node_path_1.resolve)(process.cwd(), outFlag.slice("--out=".length))
|
|
5607
|
+
: process.cwd();
|
|
5608
|
+
const openReport = !args.includes("--no-open");
|
|
5609
|
+
const wroteReports = [];
|
|
5468
5610
|
// The versioned JSON artifact — the upload/CI boundary (a hosted dashboard
|
|
5469
5611
|
// ingests this). Written by default in the human path; --no-json to skip.
|
|
5470
5612
|
if (!json && !args.includes("--no-json")) {
|
|
5471
|
-
writeAuditJson(finalReport);
|
|
5613
|
+
writeAuditJson(finalReport, outDir);
|
|
5614
|
+
wroteReports.push("vigiles-report.json");
|
|
5472
5615
|
}
|
|
5473
5616
|
// The HTML report has two deliveries. STATIC (default): write the
|
|
5474
5617
|
// shareable file whose buttons copy the `init` command. LIVE (`--serve`,
|
|
@@ -5493,7 +5636,21 @@ async function main() {
|
|
|
5493
5636
|
await runAuditServe(finalReport, finalReport.adoptable, errMsg);
|
|
5494
5637
|
}
|
|
5495
5638
|
else if (!json && !args.includes("--no-html")) {
|
|
5496
|
-
writeAuditHtml(finalReport);
|
|
5639
|
+
writeAuditHtml(finalReport, outDir, openReport);
|
|
5640
|
+
wroteReports.push("vigiles-report.html");
|
|
5641
|
+
}
|
|
5642
|
+
// Keep the generated artifacts out of git so a zero-config `audit` leaves
|
|
5643
|
+
// a clean `git status` (works without `init`). Entries are relative to the
|
|
5644
|
+
// git root (cwd); a custom --out dir is ignored by its relative path.
|
|
5645
|
+
// .gitignore patterns are POSIX-separated, so normalize away Windows
|
|
5646
|
+
// backslashes (`relative()` yields `reports\x` on Windows, which would
|
|
5647
|
+
// never match `reports/x`).
|
|
5648
|
+
if (wroteReports.length > 0) {
|
|
5649
|
+
const rel = wroteReports.map((f) => {
|
|
5650
|
+
const r = (0, node_path_1.relative)(process.cwd(), (0, node_path_1.resolve)(outDir, f)) || f;
|
|
5651
|
+
return node_path_1.sep === "/" ? r : r.split(node_path_1.sep).join("/");
|
|
5652
|
+
});
|
|
5653
|
+
ensureReportGitignored(process.cwd(), rel);
|
|
5497
5654
|
}
|
|
5498
5655
|
}
|
|
5499
5656
|
break;
|
|
@@ -21,16 +21,25 @@
|
|
|
21
21
|
*/
|
|
22
22
|
/** One rule the repo's linter has available, with its enabled state. */
|
|
23
23
|
export interface AvailableRule {
|
|
24
|
-
/** The rule id: `no-console`, `@typescript-eslint/no-explicit-any`, `boundaries/dependencies`.
|
|
24
|
+
/** The rule id: `no-console`, `@typescript-eslint/no-explicit-any`, `boundaries/dependencies`.
|
|
25
|
+
* For Pylint this is the SYMBOLIC name (`missing-function-docstring`). */
|
|
25
26
|
id: string;
|
|
26
|
-
/** The
|
|
27
|
+
/** The linter this rule belongs to. Carried PER-RULE (not only on the catalog)
|
|
28
|
+
* so a merged polyglot catalog keeps each rule's provenance — a routed reuse
|
|
29
|
+
* hit can then say `pylint:invalid-name` vs `eslint:no-console`. */
|
|
30
|
+
linter: "eslint" | "pylint";
|
|
31
|
+
/** The plugin prefix (`@typescript-eslint`, `boundaries`), or null for a core rule.
|
|
32
|
+
* Pylint's `--list-msgs` doesn't attribute a message to its plugin, so it's null there. */
|
|
27
33
|
plugin: string | null;
|
|
34
|
+
/** An alternate id the rule is ALSO matchable by (Pylint's numeric code, e.g. `C0116`),
|
|
35
|
+
* so a doc naming either the symbol or the code resolves. Absent for ESLint. */
|
|
36
|
+
code?: string;
|
|
28
37
|
/** Whether the rule is enabled (severity not 0/"off") in the resolved config. */
|
|
29
38
|
enabled: boolean;
|
|
30
39
|
}
|
|
31
40
|
/** The full available-rule catalog for a repo's linter. */
|
|
32
41
|
export interface RuleCatalog {
|
|
33
|
-
linter: "eslint";
|
|
42
|
+
linter: "eslint" | "pylint";
|
|
34
43
|
/** Total rules available (core + every installed plugin). */
|
|
35
44
|
available: number;
|
|
36
45
|
/** How many of those are enabled in the resolved config. */
|
|
@@ -45,6 +54,30 @@ export interface RuleCatalog {
|
|
|
45
54
|
* `discoverEslintRules` returning null when nothing is found).
|
|
46
55
|
*/
|
|
47
56
|
export declare function parseEslintCatalog(raw: string): RuleCatalog | null;
|
|
57
|
+
/**
|
|
58
|
+
* Parse pylint's `--list-msgs` (available) + `--list-msgs-enabled` (enabled)
|
|
59
|
+
* text listings into a typed {@link RuleCatalog}.
|
|
60
|
+
*
|
|
61
|
+
* The available set is every emittable message (`:name (CODE):` lines); enabled
|
|
62
|
+
* is the section-scoped subset. Each rule is matchable by BOTH its symbolic name
|
|
63
|
+
* (`id`) and its numeric code (`code`), since a doc may name either. Returns null
|
|
64
|
+
* when no message parses (pylint absent, or malformed output).
|
|
65
|
+
*/
|
|
66
|
+
export declare function parsePylintCatalog(listMsgs: string, listEnabled: string): RuleCatalog | null;
|
|
67
|
+
/**
|
|
68
|
+
* Merge the catalogs of every linter a repo has into ONE catalog for routing.
|
|
69
|
+
*
|
|
70
|
+
* KEEPS EVERY entry — a polyglot repo genuinely has a rule in each linter, so an
|
|
71
|
+
* id shared across ESLint and Pylint (`no-else-return`) is two real rules and
|
|
72
|
+
* both are retained (never dropped by id, the bug that let a Python doc inherit
|
|
73
|
+
* ESLint's state). Collision handling belongs at the ROUTING lookup, not here: a
|
|
74
|
+
* bare id that resolves to two hits is combined conservatively there, while a
|
|
75
|
+
* numeric code (unique to its linter) keeps its own hit — see `buildCatalogLookup`
|
|
76
|
+
* in rule-routing.ts. So the merge is a plain concatenation; each rule carries its
|
|
77
|
+
* own `linter`/`enabled`/`code` provenance. Returns undefined when nothing was
|
|
78
|
+
* enumerated (so a non-JS-non-Python repo is byte-identical to before this existed).
|
|
79
|
+
*/
|
|
80
|
+
export declare function mergeCatalogs(...cats: (RuleCatalog | null | undefined)[]): RuleCatalog | undefined;
|
|
48
81
|
/**
|
|
49
82
|
* Enumerate the repo's available ESLint rules (core + every installed plugin).
|
|
50
83
|
*
|
|
@@ -53,4 +86,16 @@ export declare function parseEslintCatalog(raw: string): RuleCatalog | null;
|
|
|
53
86
|
* config applies, or the subprocess fails.
|
|
54
87
|
*/
|
|
55
88
|
export declare function enumerateEslintCatalog(root: string): RuleCatalog | null;
|
|
89
|
+
/**
|
|
90
|
+
* Enumerate the repo's available Pylint messages (core + every loaded plugin)
|
|
91
|
+
* and their enabled state, via `pylint --list-msgs` + `--list-msgs-enabled`.
|
|
92
|
+
*
|
|
93
|
+
* Run at the repo's cwd so its rcfile (`.pylintrc` / `pyproject.toml` /
|
|
94
|
+
* `setup.cfg`) and `load-plugins` apply — so the listing reflects the repo's
|
|
95
|
+
* REAL rule set, plugins included, with correct enabled state. Like the ESLint
|
|
96
|
+
* catalog this EXECUTES the linter (loading a pylint plugin imports its module),
|
|
97
|
+
* so it's an OWN-REPO / consented capability, NOT the foreign-safe default.
|
|
98
|
+
* Returns null when pylint isn't runnable or lists nothing.
|
|
99
|
+
*/
|
|
100
|
+
export declare function enumeratePylintCatalog(root: string): RuleCatalog | null;
|
|
56
101
|
//# sourceMappingURL=rule-catalog.d.ts.map
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.parseEslintCatalog = parseEslintCatalog;
|
|
25
|
+
exports.parsePylintCatalog = parsePylintCatalog;
|
|
26
|
+
exports.mergeCatalogs = mergeCatalogs;
|
|
25
27
|
exports.enumerateEslintCatalog = enumerateEslintCatalog;
|
|
28
|
+
exports.enumeratePylintCatalog = enumeratePylintCatalog;
|
|
26
29
|
const node_path_1 = require("node:path");
|
|
27
30
|
const node_child_process_1 = require("node:child_process");
|
|
28
31
|
// ---------------------------------------------------------------------------
|
|
@@ -44,12 +47,22 @@ function parsePlugins(v) {
|
|
|
44
47
|
function buildRules(core, plugins, enabledSet) {
|
|
45
48
|
const rules = [];
|
|
46
49
|
for (const id of core) {
|
|
47
|
-
rules.push({
|
|
50
|
+
rules.push({
|
|
51
|
+
id,
|
|
52
|
+
linter: "eslint",
|
|
53
|
+
plugin: null,
|
|
54
|
+
enabled: enabledSet.has(id),
|
|
55
|
+
});
|
|
48
56
|
}
|
|
49
57
|
for (const [prefix, pluginRules] of Object.entries(plugins)) {
|
|
50
58
|
for (const rule of pluginRules) {
|
|
51
59
|
const id = `${prefix}/${rule}`;
|
|
52
|
-
rules.push({
|
|
60
|
+
rules.push({
|
|
61
|
+
id,
|
|
62
|
+
linter: "eslint",
|
|
63
|
+
plugin: prefix,
|
|
64
|
+
enabled: enabledSet.has(id),
|
|
65
|
+
});
|
|
53
66
|
}
|
|
54
67
|
}
|
|
55
68
|
return rules;
|
|
@@ -85,6 +98,108 @@ function parseEslintCatalog(raw) {
|
|
|
85
98
|
return { linter: "eslint", available: rules.length, enabled, rules };
|
|
86
99
|
}
|
|
87
100
|
// ---------------------------------------------------------------------------
|
|
101
|
+
// Pure parse: pylint's text listings → typed catalog (covered by the unit test)
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// A message line in `pylint --list-msgs`: `:invalid-name (C0103): *...*`
|
|
104
|
+
// (leading colon, no indent). In `--list-msgs-enabled`: ` invalid-name (C0103)`
|
|
105
|
+
// (indented, no colon). One regex captures the `name (CODE)` core of both.
|
|
106
|
+
const PYLINT_MSG_RE = /^\s*:?([a-z][a-z0-9-]*)\s+\(([A-Z]\d+)\)/;
|
|
107
|
+
/** Collect the symbol names under the `Enabled messages:` header ONLY.
|
|
108
|
+
*
|
|
109
|
+
* `--list-msgs-enabled` also prints `Disabled messages:` and `Non-emittable
|
|
110
|
+
* messages:` sections with the SAME `name (CODE)` line shape, so a naive
|
|
111
|
+
* line-shape parse would mislabel disabled rules as enabled. Track the current
|
|
112
|
+
* section: a non-indented line ending in `:` is a header; only lines while the
|
|
113
|
+
* "enabled" header is active count. */
|
|
114
|
+
function parseEnabledSymbols(listEnabled) {
|
|
115
|
+
const enabled = new Set();
|
|
116
|
+
let inEnabled = false;
|
|
117
|
+
for (const line of listEnabled.split("\n")) {
|
|
118
|
+
// A section header is a flush-left line ending in a colon (e.g.
|
|
119
|
+
// "Enabled messages:", "Disabled messages:"). Indented message lines never
|
|
120
|
+
// start at column 0, so this never eats a rule.
|
|
121
|
+
if (/^\S.*:\s*$/.test(line)) {
|
|
122
|
+
inEnabled = /^enabled messages:/i.test(line.trim());
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!inEnabled)
|
|
126
|
+
continue;
|
|
127
|
+
const m = PYLINT_MSG_RE.exec(line);
|
|
128
|
+
if (m)
|
|
129
|
+
enabled.add(m[1]);
|
|
130
|
+
}
|
|
131
|
+
return enabled;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parse pylint's `--list-msgs` (available) + `--list-msgs-enabled` (enabled)
|
|
135
|
+
* text listings into a typed {@link RuleCatalog}.
|
|
136
|
+
*
|
|
137
|
+
* The available set is every emittable message (`:name (CODE):` lines); enabled
|
|
138
|
+
* is the section-scoped subset. Each rule is matchable by BOTH its symbolic name
|
|
139
|
+
* (`id`) and its numeric code (`code`), since a doc may name either. Returns null
|
|
140
|
+
* when no message parses (pylint absent, or malformed output).
|
|
141
|
+
*/
|
|
142
|
+
function parsePylintCatalog(listMsgs, listEnabled) {
|
|
143
|
+
const enabledSet = parseEnabledSymbols(listEnabled);
|
|
144
|
+
const rules = [];
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
for (const line of listMsgs.split("\n")) {
|
|
147
|
+
// Available lines carry a leading colon; skip anything else (headers, the
|
|
148
|
+
// wrapped description lines, blank lines).
|
|
149
|
+
if (!line.startsWith(":"))
|
|
150
|
+
continue;
|
|
151
|
+
const m = PYLINT_MSG_RE.exec(line);
|
|
152
|
+
if (!m)
|
|
153
|
+
continue;
|
|
154
|
+
const [, name, code] = m;
|
|
155
|
+
if (seen.has(name))
|
|
156
|
+
continue;
|
|
157
|
+
seen.add(name);
|
|
158
|
+
rules.push({
|
|
159
|
+
id: name,
|
|
160
|
+
linter: "pylint",
|
|
161
|
+
plugin: null,
|
|
162
|
+
code,
|
|
163
|
+
enabled: enabledSet.has(name),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (rules.length === 0)
|
|
167
|
+
return null;
|
|
168
|
+
const enabled = rules.reduce((n, r) => (r.enabled ? n + 1 : n), 0);
|
|
169
|
+
return { linter: "pylint", available: rules.length, enabled, rules };
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Merge: a polyglot repo (JS + Python) yields two catalogs → one for routing
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
/**
|
|
175
|
+
* Merge the catalogs of every linter a repo has into ONE catalog for routing.
|
|
176
|
+
*
|
|
177
|
+
* KEEPS EVERY entry — a polyglot repo genuinely has a rule in each linter, so an
|
|
178
|
+
* id shared across ESLint and Pylint (`no-else-return`) is two real rules and
|
|
179
|
+
* both are retained (never dropped by id, the bug that let a Python doc inherit
|
|
180
|
+
* ESLint's state). Collision handling belongs at the ROUTING lookup, not here: a
|
|
181
|
+
* bare id that resolves to two hits is combined conservatively there, while a
|
|
182
|
+
* numeric code (unique to its linter) keeps its own hit — see `buildCatalogLookup`
|
|
183
|
+
* in rule-routing.ts. So the merge is a plain concatenation; each rule carries its
|
|
184
|
+
* own `linter`/`enabled`/`code` provenance. Returns undefined when nothing was
|
|
185
|
+
* enumerated (so a non-JS-non-Python repo is byte-identical to before this existed).
|
|
186
|
+
*/
|
|
187
|
+
function mergeCatalogs(...cats) {
|
|
188
|
+
const present = cats.filter((c) => c != null);
|
|
189
|
+
if (present.length === 0)
|
|
190
|
+
return undefined;
|
|
191
|
+
if (present.length === 1)
|
|
192
|
+
return present[0];
|
|
193
|
+
const rules = present.flatMap((c) => c.rules);
|
|
194
|
+
const enabled = rules.reduce((n, r) => (r.enabled ? n + 1 : n), 0);
|
|
195
|
+
return {
|
|
196
|
+
linter: present[0].linter,
|
|
197
|
+
available: rules.length,
|
|
198
|
+
enabled,
|
|
199
|
+
rules,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
88
203
|
// Real-IO seam: run ESLint in a child process at the repo's cwd
|
|
89
204
|
// ---------------------------------------------------------------------------
|
|
90
205
|
/* v8 ignore start -- spawns a `node -e` subprocess that LOADS ESLint in the
|
|
@@ -143,4 +258,37 @@ function enumerateEslintCatalog(root) {
|
|
|
143
258
|
}
|
|
144
259
|
}
|
|
145
260
|
/* v8 ignore stop */
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Real-IO seam: run pylint in a child process at the repo's cwd
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
/* v8 ignore start -- spawns the repo's `pylint` twice (the executes-the-linter
|
|
265
|
+
seam); the pure text→typed parse is parsePylintCatalog, covered by the unit
|
|
266
|
+
test, and the gated integration test drives this real path when pylint is on
|
|
267
|
+
PATH. */
|
|
268
|
+
/**
|
|
269
|
+
* Enumerate the repo's available Pylint messages (core + every loaded plugin)
|
|
270
|
+
* and their enabled state, via `pylint --list-msgs` + `--list-msgs-enabled`.
|
|
271
|
+
*
|
|
272
|
+
* Run at the repo's cwd so its rcfile (`.pylintrc` / `pyproject.toml` /
|
|
273
|
+
* `setup.cfg`) and `load-plugins` apply — so the listing reflects the repo's
|
|
274
|
+
* REAL rule set, plugins included, with correct enabled state. Like the ESLint
|
|
275
|
+
* catalog this EXECUTES the linter (loading a pylint plugin imports its module),
|
|
276
|
+
* so it's an OWN-REPO / consented capability, NOT the foreign-safe default.
|
|
277
|
+
* Returns null when pylint isn't runnable or lists nothing.
|
|
278
|
+
*/
|
|
279
|
+
function enumeratePylintCatalog(root) {
|
|
280
|
+
const run = (args) => (0, node_child_process_1.execSync)(`pylint ${args}`, {
|
|
281
|
+
encoding: "utf-8",
|
|
282
|
+
cwd: root,
|
|
283
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
284
|
+
timeout: 15000,
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
return parsePylintCatalog(run("--list-msgs"), run("--list-msgs-enabled"));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/* v8 ignore stop */
|
|
146
294
|
//# sourceMappingURL=rule-catalog.js.map
|