@vertaaux/cli 0.2.2 → 0.3.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/auth/device-flow.d.ts.map +1 -1
  4. package/dist/auth/device-flow.js +46 -14
  5. package/dist/commands/audit.d.ts +2 -0
  6. package/dist/commands/audit.d.ts.map +1 -1
  7. package/dist/commands/audit.js +167 -8
  8. package/dist/commands/client.d.ts +14 -0
  9. package/dist/commands/client.d.ts.map +1 -0
  10. package/dist/commands/client.js +362 -0
  11. package/dist/commands/compare.d.ts +20 -0
  12. package/dist/commands/compare.d.ts.map +1 -0
  13. package/dist/commands/compare.js +335 -0
  14. package/dist/commands/doc.d.ts +18 -0
  15. package/dist/commands/doc.d.ts.map +1 -0
  16. package/dist/commands/doc.js +161 -0
  17. package/dist/commands/download.d.ts.map +1 -1
  18. package/dist/commands/download.js +9 -8
  19. package/dist/commands/drift.d.ts +15 -0
  20. package/dist/commands/drift.d.ts.map +1 -0
  21. package/dist/commands/drift.js +309 -0
  22. package/dist/commands/explain.d.ts +14 -33
  23. package/dist/commands/explain.d.ts.map +1 -1
  24. package/dist/commands/explain.js +277 -179
  25. package/dist/commands/fix-plan.d.ts +15 -0
  26. package/dist/commands/fix-plan.d.ts.map +1 -0
  27. package/dist/commands/fix-plan.js +182 -0
  28. package/dist/commands/patch-review.d.ts +14 -0
  29. package/dist/commands/patch-review.d.ts.map +1 -0
  30. package/dist/commands/patch-review.js +200 -0
  31. package/dist/commands/protect.d.ts +16 -0
  32. package/dist/commands/protect.d.ts.map +1 -0
  33. package/dist/commands/protect.js +323 -0
  34. package/dist/commands/release-notes.d.ts +17 -0
  35. package/dist/commands/release-notes.d.ts.map +1 -0
  36. package/dist/commands/release-notes.js +145 -0
  37. package/dist/commands/report.d.ts +15 -0
  38. package/dist/commands/report.d.ts.map +1 -0
  39. package/dist/commands/report.js +214 -0
  40. package/dist/commands/suggest.d.ts +18 -0
  41. package/dist/commands/suggest.d.ts.map +1 -0
  42. package/dist/commands/suggest.js +152 -0
  43. package/dist/commands/triage.d.ts +17 -0
  44. package/dist/commands/triage.d.ts.map +1 -0
  45. package/dist/commands/triage.js +205 -0
  46. package/dist/commands/upload.d.ts.map +1 -1
  47. package/dist/commands/upload.js +8 -7
  48. package/dist/index.js +62 -25
  49. package/dist/output/formats.d.ts.map +1 -1
  50. package/dist/output/formats.js +18 -2
  51. package/dist/output/human.d.ts +1 -10
  52. package/dist/output/human.d.ts.map +1 -1
  53. package/dist/output/human.js +26 -98
  54. package/dist/policy/sync.d.ts +67 -0
  55. package/dist/policy/sync.d.ts.map +1 -0
  56. package/dist/policy/sync.js +147 -0
  57. package/dist/prompts/command-catalog.d.ts +46 -0
  58. package/dist/prompts/command-catalog.d.ts.map +1 -0
  59. package/dist/prompts/command-catalog.js +187 -0
  60. package/dist/ui/spinner.d.ts +10 -35
  61. package/dist/ui/spinner.d.ts.map +1 -1
  62. package/dist/ui/spinner.js +11 -58
  63. package/dist/ui/table.d.ts +1 -18
  64. package/dist/ui/table.d.ts.map +1 -1
  65. package/dist/ui/table.js +56 -163
  66. package/dist/utils/ai-error.d.ts +48 -0
  67. package/dist/utils/ai-error.d.ts.map +1 -0
  68. package/dist/utils/ai-error.js +190 -0
  69. package/dist/utils/detect-env.d.ts +6 -8
  70. package/dist/utils/detect-env.d.ts.map +1 -1
  71. package/dist/utils/detect-env.js +6 -25
  72. package/dist/utils/stdin.d.ts +50 -0
  73. package/dist/utils/stdin.d.ts.map +1 -0
  74. package/dist/utils/stdin.js +93 -0
  75. package/package.json +11 -7
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Drift detection command for VertaaUX CLI.
3
+ *
4
+ * Compares current audit scores against a baseline and reports
5
+ * per-category regressions with delta magnitudes. Used in local
6
+ * workflows and CI pipelines to detect score regressions before merging.
7
+ *
8
+ * Implements DRIFT-01: CLI drift check command.
9
+ */
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import chalk from "chalk";
13
+ import { computeDrift, DEFAULT_DRIFT_CONFIG, isPolicyV2, } from "@vertaaux/quality-control";
14
+ import { loadBaseline } from "../baseline/manager.js";
15
+ import { ExitCode } from "../utils/exit-codes.js";
16
+ import { writeJsonOutput, writeOutput } from "../output/envelope.js";
17
+ import { resolveCommandFormat } from "../output/formats.js";
18
+ import { loadPolicy } from "../policy/index.js";
19
+ /**
20
+ * Extract numeric scores from an audit result object.
21
+ *
22
+ * Audit results store scores as Record<string, unknown>; this extracts
23
+ * only the numeric entries.
24
+ */
25
+ function extractNumericScores(scores) {
26
+ if (!scores)
27
+ return {};
28
+ const result = {};
29
+ for (const [key, value] of Object.entries(scores)) {
30
+ if (typeof value === "number" && Number.isFinite(value)) {
31
+ result[key] = value;
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ /**
37
+ * Format a signed delta value for display.
38
+ */
39
+ function formatDelta(delta) {
40
+ if (delta > 0)
41
+ return `+${delta}`;
42
+ if (delta < 0)
43
+ return `${delta}`;
44
+ return "0";
45
+ }
46
+ /**
47
+ * Pad a string to the right to a given width.
48
+ */
49
+ function padRight(str, width) {
50
+ return str.length >= width ? str : str + " ".repeat(width - str.length);
51
+ }
52
+ /**
53
+ * Pad a string to the left to a given width.
54
+ */
55
+ function padLeft(str, width) {
56
+ return str.length >= width ? str : " ".repeat(width - str.length) + str;
57
+ }
58
+ /**
59
+ * Build drift config from policy file if available.
60
+ *
61
+ * If the policy is v2 and has a delta config, extracts max_regression
62
+ * and per_analyzer settings. Otherwise falls back to DEFAULT_DRIFT_CONFIG.
63
+ */
64
+ function buildDriftConfigFromPolicy(policy) {
65
+ if (!policy)
66
+ return {};
67
+ // Only v2 policies have delta config
68
+ if (policy.version !== 2)
69
+ return {};
70
+ const v2 = policy;
71
+ if (!v2.delta)
72
+ return {};
73
+ const config = {};
74
+ if (v2.delta.max_regression !== undefined) {
75
+ config.max_regression = v2.delta.max_regression;
76
+ }
77
+ if (v2.delta.per_analyzer) {
78
+ config.per_analyzer = v2.delta.per_analyzer;
79
+ }
80
+ return config;
81
+ }
82
+ /**
83
+ * Format the human-readable drift report.
84
+ */
85
+ function formatDriftReport(result) {
86
+ const lines = [];
87
+ lines.push("");
88
+ lines.push(chalk.bold(`Drift Detection: ${result.url}`));
89
+ lines.push("");
90
+ // Build table data from both regressions and improvements, plus stable categories
91
+ const allCategories = new Set();
92
+ const categoryData = {};
93
+ for (const r of result.regressions) {
94
+ allCategories.add(r.category);
95
+ categoryData[r.category] = {
96
+ previous: r.previous,
97
+ current: r.current,
98
+ delta: -(r.delta), // delta in regressions is positive (previous - current), display as negative
99
+ status: r.exceeded ? "REGRESSION (exceeded)" : "Within tolerance",
100
+ };
101
+ }
102
+ for (const imp of result.improvements) {
103
+ allCategories.add(imp.category);
104
+ categoryData[imp.category] = {
105
+ previous: imp.previous,
106
+ current: imp.current,
107
+ delta: -(imp.delta), // delta in improvements is negative (previous - current), display as positive
108
+ status: "Improved",
109
+ };
110
+ }
111
+ // Table header
112
+ const catWidth = 22;
113
+ const numWidth = 10;
114
+ const deltaWidth = 8;
115
+ const statusWidth = 26;
116
+ const header = padRight("Category", catWidth) +
117
+ padLeft("Previous", numWidth) +
118
+ padLeft("Current", numWidth) +
119
+ padLeft("Delta", deltaWidth) +
120
+ " " +
121
+ padRight("Status", statusWidth);
122
+ lines.push(chalk.dim(header));
123
+ lines.push(chalk.dim("-".repeat(catWidth + numWidth * 2 + deltaWidth + 2 + statusWidth)));
124
+ // Sort categories alphabetically
125
+ const sortedCategories = [...allCategories].sort();
126
+ for (const category of sortedCategories) {
127
+ const data = categoryData[category];
128
+ if (!data)
129
+ continue;
130
+ const deltaStr = formatDelta(data.delta);
131
+ let statusStr;
132
+ let deltaDisplay;
133
+ if (data.status === "REGRESSION (exceeded)") {
134
+ statusStr = chalk.red(data.status);
135
+ deltaDisplay = chalk.red(deltaStr);
136
+ }
137
+ else if (data.status === "Within tolerance") {
138
+ statusStr = chalk.yellow(data.status);
139
+ deltaDisplay = chalk.yellow(deltaStr);
140
+ }
141
+ else {
142
+ statusStr = chalk.green(data.status);
143
+ deltaDisplay = chalk.green(deltaStr);
144
+ }
145
+ const row = padRight(category, catWidth) +
146
+ padLeft(String(data.previous), numWidth) +
147
+ padLeft(String(data.current), numWidth) +
148
+ padLeft(deltaDisplay, deltaWidth + 10) + // Extra for ANSI codes
149
+ " " +
150
+ statusStr;
151
+ lines.push(row);
152
+ }
153
+ if (sortedCategories.length === 0) {
154
+ lines.push(chalk.dim(" No category changes detected."));
155
+ }
156
+ lines.push("");
157
+ // Overall summary
158
+ const directionLabel = result.overall.direction === "improving"
159
+ ? chalk.green("Improving")
160
+ : result.overall.direction === "regressing"
161
+ ? chalk.red("Regressing")
162
+ : chalk.dim("Stable");
163
+ lines.push(`Overall: ${directionLabel} (magnitude: ${result.overall.magnitude})`);
164
+ // Alerts summary
165
+ if (result.alerts.length > 0) {
166
+ const criticalCount = result.alerts.filter((a) => a.severity === "critical").length;
167
+ const warningCount = result.alerts.filter((a) => a.severity === "warning").length;
168
+ const parts = [];
169
+ if (criticalCount > 0)
170
+ parts.push(chalk.red(`${criticalCount} critical`));
171
+ if (warningCount > 0)
172
+ parts.push(chalk.yellow(`${warningCount} warning`));
173
+ lines.push(`Alerts: ${parts.join(", ")} regression${result.alerts.length !== 1 ? "s" : ""} exceeded tolerance`);
174
+ }
175
+ else {
176
+ lines.push(chalk.green("Alerts: None -- all categories within tolerance"));
177
+ }
178
+ lines.push("");
179
+ return lines.join("\n");
180
+ }
181
+ /**
182
+ * Register the drift command with the Commander program.
183
+ */
184
+ export function registerDriftCommand(program) {
185
+ const drift = program
186
+ .command("drift")
187
+ .description("Drift detection and regression analysis");
188
+ drift
189
+ .command("check <url>")
190
+ .description("Compare current audit against baseline and report regressions")
191
+ .option("--baseline <path>", "Path to baseline audit results JSON")
192
+ .option("--input <path>", "Path to current audit results JSON (default: .vertaaux/latest-audit.json)")
193
+ .option("--policy <file>", "Policy file with drift thresholds (vertaa.policy.yml)")
194
+ .option("--format <type>", "Output format: human|json")
195
+ .option("--json", "Alias for --format json")
196
+ .action(async (url, options) => {
197
+ try {
198
+ // a. Resolve output format (--json is alias for --format json)
199
+ const explicitFormat = options.json ? "json" : options.format;
200
+ const format = resolveCommandFormat("drift", explicitFormat, false);
201
+ // b. Load baseline
202
+ let baselineData;
203
+ if (options.baseline) {
204
+ baselineData = await loadBaseline(options.baseline);
205
+ if (!baselineData) {
206
+ process.stderr.write(chalk.red(`Error: Baseline file not found: ${options.baseline}\n`));
207
+ process.exit(ExitCode.ERROR);
208
+ }
209
+ }
210
+ else {
211
+ baselineData = await loadBaseline();
212
+ }
213
+ if (!baselineData) {
214
+ process.stderr.write(chalk.red("Error: No baseline found. Create one first:\n"));
215
+ process.stderr.write(" vertaa baseline save <url>\n\n");
216
+ process.stderr.write("Or provide a baseline file:\n");
217
+ process.stderr.write(" vertaa drift check <url> --baseline path/to/baseline.json\n");
218
+ process.exit(ExitCode.ERROR);
219
+ }
220
+ // c. Extract previous scores from baseline
221
+ // BaselineFile doesn't have scores directly -- we need audit results with scores.
222
+ // The baseline contains issues, not scores. We need a separate source for previous scores.
223
+ // Look for scores in the baseline file (it may have been extended with scores field).
224
+ const baselineAny = baselineData;
225
+ const previousScores = extractNumericScores(baselineAny.scores ??
226
+ baselineAny.metadata?.scores);
227
+ if (Object.keys(previousScores).length === 0) {
228
+ process.stderr.write(chalk.red("Error: Baseline does not contain score data.\n"));
229
+ process.stderr.write("Re-save the baseline from an audit that includes scores:\n");
230
+ process.stderr.write(" vertaa baseline save <url>\n");
231
+ process.exit(ExitCode.ERROR);
232
+ }
233
+ // d. Obtain current audit scores
234
+ const inputPath = options.input ?? ".vertaaux/latest-audit.json";
235
+ const resolvedInput = path.resolve(process.cwd(), inputPath);
236
+ if (!fs.existsSync(resolvedInput)) {
237
+ if (options.input) {
238
+ process.stderr.write(chalk.red(`Error: Input file not found: ${resolvedInput}\n`));
239
+ }
240
+ else {
241
+ process.stderr.write(chalk.red("Error: No current audit results found.\n\n"));
242
+ process.stderr.write("Run an audit first:\n");
243
+ process.stderr.write(" vertaa audit <url>\n\n");
244
+ process.stderr.write("Or provide results explicitly:\n");
245
+ process.stderr.write(" vertaa drift check <url> --input path/to/audit-results.json\n");
246
+ }
247
+ process.exit(ExitCode.ERROR);
248
+ }
249
+ let currentAuditData;
250
+ try {
251
+ const raw = fs.readFileSync(resolvedInput, "utf-8");
252
+ currentAuditData = JSON.parse(raw);
253
+ }
254
+ catch (err) {
255
+ process.stderr.write(chalk.red(`Error: Failed to parse audit results: ${err instanceof Error ? err.message : String(err)}\n`));
256
+ process.exit(ExitCode.ERROR);
257
+ return; // TypeScript flow control
258
+ }
259
+ // e. Extract current scores
260
+ const currentScores = extractNumericScores(currentAuditData.scores);
261
+ if (Object.keys(currentScores).length === 0) {
262
+ process.stderr.write(chalk.red("Error: Current audit results do not contain scores.\n"));
263
+ process.exit(ExitCode.ERROR);
264
+ }
265
+ // f. Load policy for drift thresholds (optional)
266
+ let driftConfig = {};
267
+ try {
268
+ if (options.policy) {
269
+ const { loadPolicyFile } = await import("../policy/index.js");
270
+ const policy = await loadPolicyFile(options.policy);
271
+ if (isPolicyV2(policy)) {
272
+ driftConfig = buildDriftConfigFromPolicy(policy);
273
+ }
274
+ }
275
+ else {
276
+ const policyResult = await loadPolicy();
277
+ if (policyResult.policy && isPolicyV2(policyResult.policy)) {
278
+ driftConfig = buildDriftConfigFromPolicy(policyResult.policy);
279
+ }
280
+ }
281
+ }
282
+ catch {
283
+ // Policy loading is optional -- continue with defaults
284
+ }
285
+ // g. Compute drift
286
+ const driftResult = computeDrift(url, currentScores, previousScores, {
287
+ ...DEFAULT_DRIFT_CONFIG,
288
+ ...driftConfig,
289
+ });
290
+ // h. Output results
291
+ if (format === "json") {
292
+ writeJsonOutput(driftResult, "drift");
293
+ }
294
+ else {
295
+ const report = formatDriftReport(driftResult);
296
+ writeOutput(report);
297
+ }
298
+ // i. Exit codes
299
+ const hasExceededRegressions = driftResult.alerts.length > 0;
300
+ if (hasExceededRegressions) {
301
+ process.exitCode = ExitCode.ISSUES_FOUND;
302
+ }
303
+ }
304
+ catch (error) {
305
+ process.stderr.write(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}\n`));
306
+ process.exit(ExitCode.ERROR);
307
+ }
308
+ });
309
+ }
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * Explain command for VertaaUX CLI.
3
3
  *
4
- * Shows full evidence bundle for a specific finding:
5
- * - Description
6
- * - Selector
7
- * - WCAG reference
8
- * - Recommendation
9
- * - Related artifacts (screenshots, DOM)
4
+ * Two modes:
5
+ * 1. **AI mode** (no finding-id): Accepts full audit JSON via stdin, --file,
6
+ * or --job. Calls the LLM explain endpoint to produce a 3-bullet summary
7
+ * and per-issue explanations. Use --verbose for full evidence per issue.
10
8
  *
11
- * Supports loading issues from:
12
- * - Active job via --job flag
13
- * - Local JSON file via --file flag
14
- * - Recent audits from .vertaaux/recent.json
9
+ * 2. **Evidence mode** (with finding-id): Shows the full evidence bundle for
10
+ * a specific finding (backward compatible with existing usage).
11
+ *
12
+ * Examples:
13
+ * vertaa audit https://example.com --json | vertaa explain
14
+ * vertaa explain --job abc123
15
+ * vertaa explain --file audit.json --verbose
16
+ * vertaa explain color-contrast-001 --job abc123 (legacy mode)
15
17
  */
16
18
  import { Command } from "commander";
17
19
  import type { Issue } from "../baseline/hash.js";
@@ -19,44 +21,23 @@ import type { Issue } from "../baseline/hash.js";
19
21
  * Extended issue type with additional evidence fields.
20
22
  */
21
23
  export interface EvidenceIssue extends Issue {
22
- /** DOM snippet showing the issue */
23
24
  html?: string;
24
- /** Element HTML snippet */
25
25
  element?: string;
26
- /** Screenshot path or URL */
27
26
  screenshot?: string;
28
- /** Help URL for the rule */
29
27
  helpUrl?: string;
30
28
  }
31
29
  /**
32
30
  * Format a full evidence bundle for display.
33
- *
34
- * @param issue - Issue with evidence
35
- * @returns Formatted string for terminal display
36
31
  */
37
32
  export declare function formatEvidenceBundle(issue: EvidenceIssue): string;
38
- /**
39
- * Format evidence as JSON.
40
- */
41
33
  export declare function formatEvidenceJson(issue: EvidenceIssue): string;
42
- /**
43
- * Export for reuse in fix-wizard.
44
- *
45
- * @param issue - Issue to explain
46
- * @returns Formatted evidence string
47
- */
34
+ /** Exported for reuse in fix-wizard. */
48
35
  export declare function explainIssue(issue: EvidenceIssue): string;
49
- /**
50
- * Command options for explain.
51
- */
52
36
  export interface ExplainCommandOptions {
53
37
  job?: string;
54
38
  file?: string;
55
- format?: "json" | "human";
39
+ format?: string;
56
40
  base?: string;
57
41
  }
58
- /**
59
- * Register the explain command with the Commander program.
60
- */
61
42
  export declare function registerExplainCommand(program: Command): void;
62
43
  //# sourceMappingURL=explain.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"explain.d.ts","sourceRoot":"","sources":["../../src/commands/explain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK;IAC1C,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAyED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAyDjE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAgB/D;AAUD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAEzD;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgK7D"}
1
+ {"version":3,"file":"explain.d.ts","sourceRoot":"","sources":["../../src/commands/explain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAMjD;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiFD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAyCjE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAmB/D;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAEzD;AAqRD,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+C7D"}