vigiles 13.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/README.md +10 -4
- package/dist/audit-report.d.ts +10 -4
- package/dist/audit-report.js +11 -3
- package/dist/audit-report.template.html +29 -29
- package/dist/cli.js +256 -41
- package/dist/core/rule-catalog.d.ts +101 -0
- package/dist/core/rule-catalog.js +294 -0
- 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 +73 -3
- package/dist/rule-routing.js +437 -27
- package/dist/segment.d.ts +23 -1
- package/dist/segment.js +182 -26
- 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");
|
|
@@ -1513,7 +1515,11 @@ function collectLintConfigText(root) {
|
|
|
1513
1515
|
* NO model, NO config execution — safe on any repo. Composition-root. */
|
|
1514
1516
|
function computeRuleInventory(root, instructionFile) {
|
|
1515
1517
|
try {
|
|
1516
|
-
|
|
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");
|
|
1517
1523
|
if (!instructionText.trim())
|
|
1518
1524
|
return [];
|
|
1519
1525
|
return (0, rule_inventory_js_1.buildRuleInventory)(instructionText, collectLintConfigText(root));
|
|
@@ -1522,28 +1528,125 @@ function computeRuleInventory(root, instructionFile) {
|
|
|
1522
1528
|
return [];
|
|
1523
1529
|
}
|
|
1524
1530
|
}
|
|
1525
|
-
/**
|
|
1526
|
-
* rules are often documented in AGENTS.md even under a claude-code harness
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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);
|
|
1564
|
+
}
|
|
1565
|
+
catch {
|
|
1566
|
+
// best-effort — a glob failure just means root-only, never breaks the audit
|
|
1533
1567
|
}
|
|
1534
|
-
|
|
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);
|
|
1535
1571
|
}
|
|
1536
1572
|
/** The deterministic State-B routing preview for `audit`: segment the instruction
|
|
1537
|
-
* 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) —
|
|
1538
1574
|
* NO model, fs-only. `undefined` when there's nothing to segment (kept off the
|
|
1539
|
-
* 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. */
|
|
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
|
+
}
|
|
1540
1616
|
function computeRuleRouting(root, instructionFile) {
|
|
1541
1617
|
try {
|
|
1542
|
-
const
|
|
1543
|
-
if (!
|
|
1618
|
+
const files = gatherInstructionFiles(root, instructionFile);
|
|
1619
|
+
if (files.every((f) => !f.text.trim()))
|
|
1544
1620
|
return undefined;
|
|
1545
|
-
|
|
1546
|
-
|
|
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.
|
|
1630
|
+
const ownRepo = (0, node_path_1.resolve)(root) === (0, node_path_1.resolve)(process.cwd());
|
|
1631
|
+
const consented = (0, validate_js_1.loadConfig)().audit?.measure === true;
|
|
1632
|
+
const availableRules = ownRepo && consented
|
|
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)
|
|
1637
|
+
: undefined;
|
|
1638
|
+
// Route each source SEPARATELY (each rule keeps its own file + line numbers),
|
|
1639
|
+
// then merge — so a CLAUDE.md rule and an AGENTS.md rule carry correct
|
|
1640
|
+
// provenance instead of line numbers offset by a concatenation.
|
|
1641
|
+
const routing = (0, rule_routing_js_1.mergeRoutings)(files.map((f) => (0, rule_routing_js_1.routeRules)(f.text, f.path, { availableRules })));
|
|
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;
|
|
1547
1650
|
}
|
|
1548
1651
|
catch {
|
|
1549
1652
|
return undefined;
|
|
@@ -1577,6 +1680,34 @@ function formatTriggerNudge(triggerableSkills) {
|
|
|
1577
1680
|
return (`ℹ Do your ${String(n)} skill${n === 1 ? "" : "s"} actually fire? The deterministic read can't tell — ` +
|
|
1578
1681
|
`run \`audit\` interactively to measure, or test with \`measureTriggerRate\` (vigiles/testing).`);
|
|
1579
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
|
+
}
|
|
1580
1711
|
function scaffoldSpec(args) {
|
|
1581
1712
|
const targetFlag = args.find((a) => a.startsWith("--target="));
|
|
1582
1713
|
const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
|
|
@@ -3847,7 +3978,7 @@ function printUsage(command) {
|
|
|
3847
3978
|
console.log(" vigiles eject [file] Un-manage a compiled file → plain hand-owned markdown (--keep-spec)");
|
|
3848
3979
|
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
3849
3980
|
console.log(" vigiles audit [dir...] Lighthouse for your harness — a LOCAL report: rings + what's broken + fixes (a deterministic read; 2+ dirs → leaderboard)");
|
|
3850
|
-
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.");
|
|
3851
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");
|
|
3852
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");
|
|
3853
3984
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
@@ -4763,18 +4894,50 @@ async function runHookProgramCommand(file) {
|
|
|
4763
4894
|
}
|
|
4764
4895
|
}
|
|
4765
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
|
+
}
|
|
4766
4928
|
/**
|
|
4767
4929
|
* Write the versioned JSON artifact (`vigiles-report.json`) — the upload/CI
|
|
4768
4930
|
* boundary a hosted dashboard ingests. Stamps `meta.generatedAt` here (at write
|
|
4769
4931
|
* time, not in the pure builder, so the HTML-embedded form stays deterministic).
|
|
4770
4932
|
*/
|
|
4771
|
-
function writeAuditJson(report) {
|
|
4772
|
-
const jsonPath = (0, node_path_1.resolve)(
|
|
4933
|
+
function writeAuditJson(report, outDir) {
|
|
4934
|
+
const jsonPath = (0, node_path_1.resolve)(outDir, "vigiles-report.json");
|
|
4773
4935
|
const stamped = {
|
|
4774
4936
|
...report,
|
|
4775
4937
|
meta: { ...report.meta, generatedAt: new Date().toISOString() },
|
|
4776
4938
|
};
|
|
4777
4939
|
try {
|
|
4940
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
4778
4941
|
(0, node_fs_1.writeFileSync)(jsonPath, JSON.stringify(stamped, null, 2) + "\n");
|
|
4779
4942
|
console.log("✓ Wrote vigiles-report.json — the upload/CI artifact");
|
|
4780
4943
|
}
|
|
@@ -4806,12 +4969,16 @@ function openBestEffort(file) {
|
|
|
4806
4969
|
* for a human at a TTY, open it best-effort. The shareable Lighthouse artifact;
|
|
4807
4970
|
* never spawns a browser for an agent / CI run.
|
|
4808
4971
|
*/
|
|
4809
|
-
function writeAuditHtml(report) {
|
|
4810
|
-
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");
|
|
4811
4974
|
try {
|
|
4975
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
4812
4976
|
(0, node_fs_1.writeFileSync)(htmlPath, (0, audit_html_js_1.renderAuditHtml)(report));
|
|
4813
4977
|
console.log("\n✓ Wrote vigiles-report.html — open it for the full report");
|
|
4814
|
-
|
|
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)
|
|
4815
4982
|
openBestEffort(htmlPath);
|
|
4816
4983
|
}
|
|
4817
4984
|
catch (e) {
|
|
@@ -5251,26 +5418,30 @@ async function main() {
|
|
|
5251
5418
|
// Read the local flight recorder ONCE — feeds both the JSON report
|
|
5252
5419
|
// (structured summary, the product boundary) and the terminal render.
|
|
5253
5420
|
const ledgerRecords = (0, observe_js_1.readObservations)(root);
|
|
5254
|
-
|
|
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, {
|
|
5255
5428
|
harness: adapter.name,
|
|
5256
5429
|
vigilesVersion: getVersion(),
|
|
5257
5430
|
adoptableSurfaces,
|
|
5258
5431
|
observations: (0, observe_js_1.summarizeObservations)(ledgerRecords),
|
|
5259
5432
|
rulesInventory: computeRuleInventory(root, adapter.layout.instructionFile),
|
|
5260
|
-
ruleRouting: computeRuleRouting(root, adapter.layout.instructionFile),
|
|
5261
5433
|
});
|
|
5262
|
-
const sc =
|
|
5434
|
+
const sc = auditReportBase.score;
|
|
5263
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.)
|
|
5264
5439
|
if (!json) {
|
|
5265
5440
|
// The Lighthouse rings: per-category 0–100 + the weighted overall,
|
|
5266
5441
|
// shown before the detailed report so the headline signal leads.
|
|
5267
5442
|
console.log((0, audit_score_js_1.formatAuditScore)(sc));
|
|
5268
5443
|
console.log("");
|
|
5269
|
-
|
|
5270
|
-
console.log(json
|
|
5271
|
-
? JSON.stringify(auditReport, null, 2)
|
|
5272
|
-
: (0, scan_js_1.formatScanReport)(report));
|
|
5273
|
-
if (!json) {
|
|
5444
|
+
console.log((0, scan_js_1.formatScanReport)(report));
|
|
5274
5445
|
// Fold each finding's fix inline (replaces the former --fix-plan/--explain
|
|
5275
5446
|
// flags): the deterministic, free recommendation list under the report.
|
|
5276
5447
|
const fixes = (0, optimize_js_1.formatRecommendations)(plan);
|
|
@@ -5288,17 +5459,14 @@ async function main() {
|
|
|
5288
5459
|
.length);
|
|
5289
5460
|
if (fireNudge)
|
|
5290
5461
|
console.log("\n" + fireNudge);
|
|
5291
|
-
// The flight recorder: a compact summary of what the harness actually
|
|
5292
|
-
// DID in real sessions (hook/agent decisions), read off the local
|
|
5293
|
-
// agent-readable ledger. Empty (skipped) until something is recorded.
|
|
5294
|
-
const ledgerSummary = (0, observe_js_1.formatLedgerSummary)(ledgerRecords);
|
|
5295
|
-
if (ledgerSummary)
|
|
5296
|
-
console.log("\n" + ledgerSummary);
|
|
5297
5462
|
}
|
|
5298
5463
|
// ONE read-vs-run decision for the EXECUTING checks (live MCP + skill
|
|
5299
|
-
// firing)
|
|
5300
|
-
//
|
|
5301
|
-
//
|
|
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.
|
|
5302
5470
|
// (The safety battery is NOT here — it needs cross-platform confinement
|
|
5303
5471
|
// that isn't shipped, so it lives in the vigiles/testing API.)
|
|
5304
5472
|
const isForeign = root !== process.cwd();
|
|
@@ -5310,6 +5478,30 @@ async function main() {
|
|
|
5310
5478
|
(0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, adapter.layout.instructionFile)),
|
|
5311
5479
|
};
|
|
5312
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
|
+
}
|
|
5313
5505
|
// LIVE MCP tool resolution STARTS each declared MCP server — a server is
|
|
5314
5506
|
// exactly what connects to a real Postgres / authenticates a real API on
|
|
5315
5507
|
// boot. So it runs only under consent (`execute`) AND own-repo (never
|
|
@@ -5407,10 +5599,19 @@ async function main() {
|
|
|
5407
5599
|
const finalReport = adoptabilityResult
|
|
5408
5600
|
? { ...auditReport, adoptability: adoptabilityResult }
|
|
5409
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 = [];
|
|
5410
5610
|
// The versioned JSON artifact — the upload/CI boundary (a hosted dashboard
|
|
5411
5611
|
// ingests this). Written by default in the human path; --no-json to skip.
|
|
5412
5612
|
if (!json && !args.includes("--no-json")) {
|
|
5413
|
-
writeAuditJson(finalReport);
|
|
5613
|
+
writeAuditJson(finalReport, outDir);
|
|
5614
|
+
wroteReports.push("vigiles-report.json");
|
|
5414
5615
|
}
|
|
5415
5616
|
// The HTML report has two deliveries. STATIC (default): write the
|
|
5416
5617
|
// shareable file whose buttons copy the `init` command. LIVE (`--serve`,
|
|
@@ -5435,7 +5636,21 @@ async function main() {
|
|
|
5435
5636
|
await runAuditServe(finalReport, finalReport.adoptable, errMsg);
|
|
5436
5637
|
}
|
|
5437
5638
|
else if (!json && !args.includes("--no-html")) {
|
|
5438
|
-
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);
|
|
5439
5654
|
}
|
|
5440
5655
|
}
|
|
5441
5656
|
break;
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
* For Pylint this is the SYMBOLIC name (`missing-function-docstring`). */
|
|
26
|
+
id: string;
|
|
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. */
|
|
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;
|
|
37
|
+
/** Whether the rule is enabled (severity not 0/"off") in the resolved config. */
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** The full available-rule catalog for a repo's linter. */
|
|
41
|
+
export interface RuleCatalog {
|
|
42
|
+
linter: "eslint" | "pylint";
|
|
43
|
+
/** Total rules available (core + every installed plugin). */
|
|
44
|
+
available: number;
|
|
45
|
+
/** How many of those are enabled in the resolved config. */
|
|
46
|
+
enabled: number;
|
|
47
|
+
rules: AvailableRule[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse the enumeration subprocess's JSON payload into a typed {@link RuleCatalog}.
|
|
51
|
+
*
|
|
52
|
+
* Payload shape: `{ core: string[]; enabled: string[]; plugins: Record<prefix, string[]> }`.
|
|
53
|
+
* Returns null on `"null"` / malformed input / an empty catalog (mirrors
|
|
54
|
+
* `discoverEslintRules` returning null when nothing is found).
|
|
55
|
+
*/
|
|
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;
|
|
81
|
+
/**
|
|
82
|
+
* Enumerate the repo's available ESLint rules (core + every installed plugin).
|
|
83
|
+
*
|
|
84
|
+
* EXECUTES the repo's ESLint in a child process — an own-repo / consented
|
|
85
|
+
* capability (see the file header). Returns null if ESLint isn't resolvable, no
|
|
86
|
+
* config applies, or the subprocess fails.
|
|
87
|
+
*/
|
|
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;
|
|
101
|
+
//# sourceMappingURL=rule-catalog.d.ts.map
|