vigiles 8.0.0 → 9.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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Category scoring for `vigiles audit` — the Lighthouse rings.
3
+ *
4
+ * A single structural-health number (the leaderboard's `scoreReport`) ranks
5
+ * plugins, but it hides WHERE a harness is weak. This buckets the SAME
6
+ * deterministic findings into four categories — Truthfulness, Triggering,
7
+ * Structure, Tested — each a 0–100 ring, as a DIAGNOSTIC breakdown beneath one
8
+ * headline `overall` = `100 − Σ(all graded penalties)` (the SAME summed model as
9
+ * the leaderboard's single health number, via the shared `computeIntegrityScore`
10
+ * — so the two surfaces never disagree). Same detectors, no re-detection
11
+ * (one-detector-no-drift); all deterministic, no execution. (Safety — "do your
12
+ * hooks actually block?" — is NOT an `audit` ring:
13
+ * it requires executing your hooks, which needs cross-platform confinement
14
+ * that isn't shipped yet, so it lives in the `vigiles/testing` API via
15
+ * `guardrail-check`/`assertBlocksDisasters`, where you opt in explicitly.)
16
+ *
17
+ * A category that can't be assessed scores `null` (n/a) and is EXCLUDED from the
18
+ * overall — never a false 0. Pure over the `ScanReport`, so it's fully testable.
19
+ */
20
+ import { type PluginScore } from "./leaderboard.js";
21
+ import type { ScanReport } from "./scan.js";
22
+ export type CategoryKey = "Truthfulness" | "Triggering" | "Structure" | "Tested";
23
+ export interface CategoryScore {
24
+ readonly key: CategoryKey;
25
+ /** 0–100, or `null` when the category isn't assessable (n/a — excluded from overall). */
26
+ readonly score: number | null;
27
+ /** Relative weight in the overall (equal by default — tune later). */
28
+ readonly weight: number;
29
+ /**
30
+ * Advisory categories are shown but EXCLUDED from the overall grade. An untested
31
+ * surface (or any best-practice gap) is a HARDENING signal, not a broken harness
32
+ * — it must never drag the grade down, so `audit` doesn't read as F on a clean
33
+ * repo that simply hasn't written tests yet. The grade reflects what's BROKEN.
34
+ */
35
+ readonly advisory?: boolean;
36
+ /** Human-readable deductions / notes, worst first; empty when clean. */
37
+ readonly findings: readonly string[];
38
+ }
39
+ export interface AuditScore {
40
+ /**
41
+ * The headline score — `100 − Σ(all graded penalties)`, clamped to [0,100]
42
+ * (the SAME summed model as the leaderboard's single health number, computed by
43
+ * the shared {@link computeIntegrityScore}, so the two surfaces never disagree).
44
+ * The per-category rings below are a DIAGNOSTIC breakdown, not the headline: a
45
+ * plugin whose only issue is Structure −30 shows Structure 70 in the breakdown
46
+ * AND overall 70 (averaging the rings would dilute that to ~90). 0 when empty.
47
+ */
48
+ readonly overall: number;
49
+ readonly grade: PluginScore["grade"];
50
+ readonly categories: readonly CategoryScore[];
51
+ /** No loadable surface at all — overall 0, every category n/a. */
52
+ readonly empty: boolean;
53
+ }
54
+ /**
55
+ * Bucket a scan report into the four deterministic Lighthouse categories as a
56
+ * DIAGNOSTIC breakdown, with the headline `overall` = `100 − Σ(all graded
57
+ * penalties)` (the shared summed model — NOT the average of the rings — so it
58
+ * equals the leaderboard's single health number). The advisory Tested ring and
59
+ * any n/a ring are shown but excluded from the headline.
60
+ */
61
+ export declare function auditScore(report: ScanReport): AuditScore;
62
+ /** Render the category rings (diagnostic) + the summed overall for the terminal. */
63
+ export declare function formatAuditScore(s: AuditScore): string;
64
+ //# sourceMappingURL=audit-score.d.ts.map
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditScore = auditScore;
4
+ exports.formatAuditScore = formatAuditScore;
5
+ /**
6
+ * Category scoring for `vigiles audit` — the Lighthouse rings.
7
+ *
8
+ * A single structural-health number (the leaderboard's `scoreReport`) ranks
9
+ * plugins, but it hides WHERE a harness is weak. This buckets the SAME
10
+ * deterministic findings into four categories — Truthfulness, Triggering,
11
+ * Structure, Tested — each a 0–100 ring, as a DIAGNOSTIC breakdown beneath one
12
+ * headline `overall` = `100 − Σ(all graded penalties)` (the SAME summed model as
13
+ * the leaderboard's single health number, via the shared `computeIntegrityScore`
14
+ * — so the two surfaces never disagree). Same detectors, no re-detection
15
+ * (one-detector-no-drift); all deterministic, no execution. (Safety — "do your
16
+ * hooks actually block?" — is NOT an `audit` ring:
17
+ * it requires executing your hooks, which needs cross-platform confinement
18
+ * that isn't shipped yet, so it lives in the `vigiles/testing` API via
19
+ * `guardrail-check`/`assertBlocksDisasters`, where you opt in explicitly.)
20
+ *
21
+ * A category that can't be assessed scores `null` (n/a) and is EXCLUDED from the
22
+ * overall — never a false 0. Pure over the `ScanReport`, so it's fully testable.
23
+ */
24
+ const leaderboard_js_1 = require("./leaderboard.js");
25
+ // Per-item penalties are the SHARED leaderboard weights (imported above) so the
26
+ // category rings and the single health number can never drift. W_UNTESTED is
27
+ // audit-only — untested surfaces are advisory (shown, never scored into overall).
28
+ const W_UNTESTED = 3;
29
+ /** Apply deductions to a 100 base, clamped to [0,100], collecting non-zero labels. */
30
+ function scoreFrom(deductions) {
31
+ let penalty = 0;
32
+ const findings = [];
33
+ for (const d of deductions) {
34
+ if (d.n <= 0)
35
+ continue;
36
+ penalty += d.n * d.weight;
37
+ findings.push({ n: d.n, text: `${String(d.n)} ${d.label}` });
38
+ }
39
+ findings.sort((a, b) => b.n - a.n);
40
+ return {
41
+ score: Math.max(0, 100 - penalty),
42
+ findings: findings.map((f) => f.text),
43
+ };
44
+ }
45
+ function truthfulness(r) {
46
+ const missingHooks = r.hooks.filter((h) => h.status === "missing").length;
47
+ const { score, findings } = scoreFrom([
48
+ {
49
+ n: r.danglingRefs.length,
50
+ weight: leaderboard_js_1.W_DANGLING_REF,
51
+ label: "broken intra-plugin reference(s)",
52
+ },
53
+ {
54
+ n: missingHooks,
55
+ weight: leaderboard_js_1.W_MISSING_HOOK,
56
+ label: "hook script(s) missing (never run)",
57
+ },
58
+ ]);
59
+ return { key: "Truthfulness", score, weight: 1, findings };
60
+ }
61
+ function triggering(r) {
62
+ const noDesc = r.skills.filter((s) => !s.hasDescription).length;
63
+ const { score, findings } = scoreFrom([
64
+ {
65
+ n: noDesc,
66
+ weight: leaderboard_js_1.W_NO_DESCRIPTION,
67
+ label: "skill(s) with no usable description (can't trigger)",
68
+ },
69
+ {
70
+ n: r.descriptionOverlaps.length,
71
+ weight: leaderboard_js_1.W_OVERLAP,
72
+ label: "near-identical skill description(s) (wrong one fires)",
73
+ },
74
+ ]);
75
+ return { key: "Triggering", score, weight: 1, findings };
76
+ }
77
+ function structure(r) {
78
+ const noContract = r.agents.filter((a) => a.tools === null).length;
79
+ const deadTools = r.agents.reduce((n, a) => n + a.toolIssues.length, 0);
80
+ const deadMcpTools = r.agents.reduce((n, a) => n + a.mcpToolIssues.length, 0);
81
+ const deadDisallowed = r.agents.reduce((n, a) => n + a.disallowedToolIssues.length, 0);
82
+ const { score, findings } = scoreFrom([
83
+ {
84
+ n: deadTools,
85
+ weight: leaderboard_js_1.W_DANGLING_REF,
86
+ label: "agent tool(s) that don't exist (typo / never-available)",
87
+ },
88
+ {
89
+ n: deadMcpTools,
90
+ weight: leaderboard_js_1.W_DANGLING_REF,
91
+ label: "agent MCP tool(s) whose server isn't declared",
92
+ },
93
+ {
94
+ n: r.hookEventIssues.length,
95
+ weight: leaderboard_js_1.W_MISSING_HOOK,
96
+ label: "hook(s) on an unknown event (never fire)",
97
+ },
98
+ {
99
+ n: r.mcpIssues.length,
100
+ weight: leaderboard_js_1.W_DANGLING_REF,
101
+ label: "MCP server(s) that can't start (no command/url)",
102
+ },
103
+ {
104
+ n: r.mcpHookIssues.length,
105
+ weight: leaderboard_js_1.W_DANGLING_REF,
106
+ label: "mcp_tool hook(s) incomplete / undeclared server",
107
+ },
108
+ {
109
+ n: r.frontmatterIssues.length,
110
+ weight: leaderboard_js_1.W_NO_DESCRIPTION,
111
+ label: "surface(s) missing required frontmatter",
112
+ },
113
+ {
114
+ n: r.frontmatterValueIssues.length,
115
+ weight: leaderboard_js_1.W_NO_CONTRACT,
116
+ label: "agent(s) with an invalid model/color (silent fallback)",
117
+ },
118
+ {
119
+ n: deadDisallowed,
120
+ weight: leaderboard_js_1.W_NO_CONTRACT,
121
+ label: "disallowedTools typo(s) that block nothing",
122
+ },
123
+ {
124
+ n: noContract,
125
+ weight: leaderboard_js_1.W_NO_CONTRACT,
126
+ label: "agent(s) inherit all tools (no contract)",
127
+ },
128
+ ]);
129
+ return { key: "Structure", score, weight: 1, findings };
130
+ }
131
+ function tested(r) {
132
+ const { score, findings } = scoreFrom([
133
+ { n: r.untested, weight: W_UNTESTED, label: "untested surface(s)" },
134
+ ]);
135
+ // ADVISORY: untested surfaces are a hardening gap, not breakage — shown, but
136
+ // excluded from the overall grade (so a clean-but-untested repo isn't graded F).
137
+ return { key: "Tested", score, weight: 1, advisory: true, findings };
138
+ }
139
+ /**
140
+ * An instruction-only repo (just a CLAUDE.md/AGENTS.md, no plugin surface) is NOT
141
+ * empty — the scan records `instructions` precisely so it isn't graded F/0 "no
142
+ * loadable surface". Only a dir with NO instruction file AND no surface is empty.
143
+ * (The shared `isEmptyMachine` ignores `instructions`; audit additionally treats
144
+ * an instruction file as a surface.)
145
+ */
146
+ function isEmptyAudit(r) {
147
+ return (0, leaderboard_js_1.isEmptyMachine)(r) && !r.instructions;
148
+ }
149
+ /**
150
+ * Bucket a scan report into the four deterministic Lighthouse categories as a
151
+ * DIAGNOSTIC breakdown, with the headline `overall` = `100 − Σ(all graded
152
+ * penalties)` (the shared summed model — NOT the average of the rings — so it
153
+ * equals the leaderboard's single health number). The advisory Tested ring and
154
+ * any n/a ring are shown but excluded from the headline.
155
+ */
156
+ function auditScore(report) {
157
+ if (isEmptyAudit(report)) {
158
+ const categories = [
159
+ "Truthfulness",
160
+ "Triggering",
161
+ "Structure",
162
+ "Tested",
163
+ ];
164
+ return {
165
+ overall: 0,
166
+ grade: (0, leaderboard_js_1.gradeFor)(0),
167
+ categories: categories.map((key) => ({
168
+ key,
169
+ score: null,
170
+ weight: 1,
171
+ findings: ["no loadable plugin surface"],
172
+ })),
173
+ empty: true,
174
+ };
175
+ }
176
+ const categories = [
177
+ truthfulness(report),
178
+ triggering(report),
179
+ structure(report),
180
+ tested(report),
181
+ ];
182
+ // The headline is the SUMMED model (the shared integrity score), NOT the average
183
+ // of the rings — averaging would let a real problem in one category be diluted
184
+ // by clean siblings. The rings above stay a diagnostic breakdown; Tested
185
+ // (advisory) is never summed in (untested surfaces don't drag the grade).
186
+ const { score: overall } = (0, leaderboard_js_1.computeIntegrityScore)((0, leaderboard_js_1.reportDeductions)(report));
187
+ return { overall, grade: (0, leaderboard_js_1.gradeFor)(overall), categories, empty: false };
188
+ }
189
+ // A 22-cell bar gauge ("ring" in the terminal; the real rings are the HTML).
190
+ const BAR_CELLS = 22;
191
+ /** A glyph that signals the band at a glance (green/amber/red, no ANSI needed). */
192
+ function bandGlyph(score) {
193
+ if (score === null)
194
+ return "○";
195
+ if (score >= 90)
196
+ return "●";
197
+ if (score >= 70)
198
+ return "◑";
199
+ return "✗";
200
+ }
201
+ function bar(score) {
202
+ if (score === null)
203
+ return "n/a";
204
+ const filled = Math.round((score / 100) * BAR_CELLS);
205
+ return "█".repeat(filled) + "░".repeat(BAR_CELLS - filled);
206
+ }
207
+ /** Render the category rings (diagnostic) + the summed overall for the terminal. */
208
+ function formatAuditScore(s) {
209
+ const lines = ["Harness audit", ""];
210
+ for (const c of s.categories) {
211
+ const glyph = bandGlyph(c.score);
212
+ const label = c.key.padEnd(13);
213
+ const num = (c.score === null ? "n/a" : String(c.score)).padStart(4);
214
+ const tag = c.advisory ? " · advisory (not graded)" : "";
215
+ lines.push(` ${glyph} ${label} ${num} ${bar(c.score)}${tag}`);
216
+ if (c.findings.length > 0) {
217
+ lines.push(` └ ${c.findings.join("; ")}`);
218
+ }
219
+ }
220
+ lines.push("");
221
+ lines.push(`Harness health: ${s.grade} (${String(s.overall)}/100)`);
222
+ return lines.join("\n");
223
+ }
224
+ //# sourceMappingURL=audit-score.js.map
@@ -11,7 +11,7 @@
11
11
  * recognizes exactly these, so this list can't silently drift from the code.
12
12
  */
13
13
  /** Human-facing verbs (printed in help; typed by a human/agent/CI). */
14
- export declare const VERBS: readonly ["init", "compile", "eject", "lint", "test", "eval", "scan", "scaffold-test", "generate", "hook-runtime"];
14
+ export declare const VERBS: readonly ["init", "compile", "eject", "lint", "test", "eval", "audit", "scaffold-test", "generate", "hook-runtime"];
15
15
  /** Runtime entrypoint kinds under `vigiles hook-runtime <kind>` (emitted, not typed). */
16
16
  export declare const HOOK_RUNTIME_KINDS: readonly ["run-program", "agent", "agent-start", "agent-done", "skill", "skill-tool", "skill-start", "skill-done", "run-skill", "intercept-tool", "guard", "action", "refs", "effect-enter", "effect-exit"];
17
17
  export type Verb = (typeof VERBS)[number];
@@ -21,7 +21,7 @@ exports.VERBS = [
21
21
  "lint",
22
22
  "test",
23
23
  "eval",
24
- "scan",
24
+ "audit",
25
25
  "scaffold-test",
26
26
  "generate",
27
27
  "hook-runtime",