ag-ui-validate 0.1.0 → 0.2.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 +7 -0
- package/dist/cli.js +105 -53
- package/dist/cli.js.map +1 -1
- package/dist/report.cjs +42 -0
- package/dist/report.cjs.map +1 -1
- package/dist/report.d.cts +7 -1
- package/dist/report.d.ts +7 -1
- package/dist/report.js +42 -1
- package/dist/report.js.map +1 -1
- package/dist/vitest.js.map +1 -1
- package/package.json +1 -1
- package/src/cli-args.ts +11 -0
- package/src/cli.ts +8 -3
- package/src/report/index.ts +1 -1
- package/src/report/pretty.ts +43 -0
package/src/cli-args.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface CliConfig {
|
|
|
20
20
|
sarifFile?: string
|
|
21
21
|
junitFile?: string
|
|
22
22
|
jsonFile?: string
|
|
23
|
+
/** Pretty output only: one line per rule with a count, not one per finding. */
|
|
24
|
+
group: boolean
|
|
23
25
|
help: boolean
|
|
24
26
|
version: boolean
|
|
25
27
|
}
|
|
@@ -40,6 +42,7 @@ Output (default: human-readable):
|
|
|
40
42
|
--json-file <path> additionally write the JSON report to a file
|
|
41
43
|
--sarif-file <path> additionally write a SARIF log to a file
|
|
42
44
|
--junit-file <path> additionally write JUnit XML to a file
|
|
45
|
+
--group one line per rule with a count (large streams)
|
|
43
46
|
--no-color disable ANSI colors
|
|
44
47
|
|
|
45
48
|
Rules:
|
|
@@ -67,6 +70,7 @@ const FLAGS: Record<string, Flag> = {
|
|
|
67
70
|
"--help": { takesValue: false, apply: (c) => ((c.help = true), null) },
|
|
68
71
|
"--version": { takesValue: false, apply: (c) => ((c.version = true), null) },
|
|
69
72
|
"--no-color": { takesValue: false, apply: (c) => ((c.color = false), null) },
|
|
73
|
+
"--group": { takesValue: false, apply: (c) => ((c.group = true), null) },
|
|
70
74
|
"--json": { takesValue: false, apply: (c) => setFormat(c, "json") },
|
|
71
75
|
"--sarif": { takesValue: false, apply: (c) => setFormat(c, "sarif") },
|
|
72
76
|
"--junit": { takesValue: false, apply: (c) => setFormat(c, "junit") },
|
|
@@ -150,6 +154,7 @@ export function parseCliArgs(argv: string[]): ParseResult {
|
|
|
150
154
|
color: null,
|
|
151
155
|
headers: {},
|
|
152
156
|
severityOverrides: {},
|
|
157
|
+
group: false,
|
|
153
158
|
help: false,
|
|
154
159
|
version: false,
|
|
155
160
|
}
|
|
@@ -182,6 +187,12 @@ export function parseCliArgs(argv: string[]): ParseResult {
|
|
|
182
187
|
if (error !== null) return { ok: false, error }
|
|
183
188
|
}
|
|
184
189
|
|
|
190
|
+
if (config.group && config.format !== "pretty") {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: "--group only applies to the default pretty output, not --json/--sarif/--junit",
|
|
194
|
+
}
|
|
195
|
+
}
|
|
185
196
|
if (targets.length > 1) {
|
|
186
197
|
return { ok: false, error: `expected exactly one target, got ${targets.length}` }
|
|
187
198
|
}
|
package/src/cli.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { createReadStream, readFileSync, writeFileSync } from "node:fs"
|
|
|
7
7
|
import process from "node:process"
|
|
8
8
|
import { decideExitCode, parseCliArgs, USAGE } from "./cli-args.js"
|
|
9
9
|
import type { CliConfig } from "./cli-args.js"
|
|
10
|
-
import { formatDiagnosticLine, formatReportSummary, toJsonReport, toJUnit, toSarif } from "./report/index.js"
|
|
10
|
+
import { formatDiagnosticLine, formatGroupedDiagnostics, formatReportSummary, toJsonReport, toJUnit, toSarif } from "./report/index.js"
|
|
11
11
|
import { TransportError, validateBody, validateEndpoint } from "./transport/index.js"
|
|
12
12
|
import type { TransportOptions, TransportResult } from "./transport/index.js"
|
|
13
13
|
import type { Diagnostic, ValidatorOptions } from "./types.js"
|
|
@@ -62,8 +62,9 @@ async function main(): Promise<number> {
|
|
|
62
62
|
|
|
63
63
|
// Pretty mode streams findings as they are detected; machine formats emit
|
|
64
64
|
// one document at the end, so nothing else may touch stdout before it.
|
|
65
|
+
// --group defers everything to one collapsed block after the run.
|
|
65
66
|
let printed = 0
|
|
66
|
-
const onDiagnostic = pretty
|
|
67
|
+
const onDiagnostic = pretty && !config.group
|
|
67
68
|
? (d: Diagnostic): void => {
|
|
68
69
|
printed += 1
|
|
69
70
|
process.stdout.write(`${formatDiagnosticLine(d, { color })}\n`)
|
|
@@ -116,7 +117,11 @@ async function main(): Promise<number> {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
if (pretty) {
|
|
119
|
-
if (
|
|
120
|
+
if (config.group && report.diagnostics.length > 0) {
|
|
121
|
+
process.stdout.write(`${formatGroupedDiagnostics(report.diagnostics, { color })}\n\n`)
|
|
122
|
+
} else if (printed > 0) {
|
|
123
|
+
process.stdout.write("\n")
|
|
124
|
+
}
|
|
120
125
|
process.stdout.write(`${formatReportSummary(report, { color })}\n`)
|
|
121
126
|
} else if (config.format === "json") {
|
|
122
127
|
const doc = toJsonReport(report, { tool: { name: TOOL_NAME, version }, target: targetLabel })
|
package/src/report/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ag-ui-validate/report: pure reporters over a core Report. No I/O — callers
|
|
2
2
|
// decide where the strings go.
|
|
3
|
-
export { formatDiagnosticLine, formatReportSummary } from "./pretty.js"
|
|
3
|
+
export { formatDiagnosticLine, formatGroupedDiagnostics, formatReportSummary } from "./pretty.js"
|
|
4
4
|
export type { PrettyOptions } from "./pretty.js"
|
|
5
5
|
export { toJsonReport } from "./json.js"
|
|
6
6
|
export type { JsonReportDocument, JsonReportOptions } from "./json.js"
|
package/src/report/pretty.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Human-readable output. Pure string formatting — the CLI decides where it
|
|
2
2
|
// goes and whether a TTY wants color.
|
|
3
|
+
import { RULES } from "../rules/catalog.js"
|
|
3
4
|
import type { Diagnostic, Report, Severity } from "../types.js"
|
|
4
5
|
|
|
5
6
|
export interface PrettyOptions {
|
|
@@ -25,6 +26,48 @@ function count(n: number, noun: string): string {
|
|
|
25
26
|
return `${n} ${noun}${n === 1 ? "" : "s"}`
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
const SEVERITY_RANK: Record<Severity, number> = { error: 0, warning: 1, info: 2 }
|
|
30
|
+
const SAMPLE_INDEXES = 3
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One line per rule instead of one per occurrence — for large streams where
|
|
34
|
+
* the same violation repeats. Totals stay honest in the summary; this only
|
|
35
|
+
* changes what is listed.
|
|
36
|
+
*/
|
|
37
|
+
export function formatGroupedDiagnostics(diagnostics: Diagnostic[], opts: PrettyOptions): string {
|
|
38
|
+
const groups = new Map<string, Diagnostic[]>()
|
|
39
|
+
for (const d of diagnostics) {
|
|
40
|
+
const list = groups.get(d.rule)
|
|
41
|
+
if (list === undefined) groups.set(d.rule, [d])
|
|
42
|
+
else list.push(d)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sorted = [...groups.values()].sort((a, b) => {
|
|
46
|
+
const bySeverity = SEVERITY_RANK[a[0]!.severity] - SEVERITY_RANK[b[0]!.severity]
|
|
47
|
+
return bySeverity !== 0 ? bySeverity : a[0]!.rule.localeCompare(b[0]!.rule)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const lines: string[] = []
|
|
51
|
+
for (const group of sorted) {
|
|
52
|
+
const first = group[0]!
|
|
53
|
+
const title = RULES.get(first.rule)?.title ?? first.message
|
|
54
|
+
const indexes = group.map((d) => d.eventIndex).filter((i) => i >= 0)
|
|
55
|
+
let where: string
|
|
56
|
+
if (indexes.length === 0) {
|
|
57
|
+
where = "stream-level"
|
|
58
|
+
} else {
|
|
59
|
+
const sample = indexes.slice(0, SAMPLE_INDEXES).join(", ")
|
|
60
|
+
const rest = indexes.length - SAMPLE_INDEXES
|
|
61
|
+
where = `events ${sample}${rest > 0 ? ` (+${rest} more)` : ""}`
|
|
62
|
+
}
|
|
63
|
+
const head = paint(SGR[first.severity], `${SYMBOL[first.severity]} ${first.rule}`, opts.color)
|
|
64
|
+
const meta = paint("2", `${first.severity.padEnd(7)} ×${group.length}`, opts.color)
|
|
65
|
+
const cite = paint("2", ` ↳ ${first.specUrl}`, opts.color)
|
|
66
|
+
lines.push(`${head} ${meta} ${title} — ${where}`, cite)
|
|
67
|
+
}
|
|
68
|
+
return lines.join("\n")
|
|
69
|
+
}
|
|
70
|
+
|
|
28
71
|
export function formatReportSummary(report: Report, opts: PrettyOptions): string {
|
|
29
72
|
const { errors, warnings, info } = report.summary
|
|
30
73
|
const lines: string[] = []
|