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/README.md
CHANGED
|
@@ -66,6 +66,7 @@ Useful flags (see `--help` for all):
|
|
|
66
66
|
| Flag | Effect |
|
|
67
67
|
| --- | --- |
|
|
68
68
|
| `--json` / `--sarif` / `--junit` | machine-readable report on stdout (SARIF 2.1.0 for code scanning, JUnit XML for CI) |
|
|
69
|
+
| `--group` | one line per rule with a count — for large streams with repeated findings (totals stay exact) |
|
|
69
70
|
| `--rule AGUI105=error`, `--off AGUI902` | per-rule severity overrides |
|
|
70
71
|
| `--features shared-state,...` | declare exercised features (enables e.g. AGUI305) |
|
|
71
72
|
| `--max-warnings 0` | fail CI on any warning |
|
|
@@ -224,6 +225,12 @@ stream + intended findings to `scripts/build-fixtures.mjs`, and run
|
|
|
224
225
|
[`ag-ui-protocol/ag-ui`](https://github.com/ag-ui-protocol/ag-ui) first; this
|
|
225
226
|
project does not invent rules the spec doesn't support.
|
|
226
227
|
|
|
228
|
+
Releasing: merge the pending changesets (`npx changeset version`) via a PR,
|
|
229
|
+
then publish a GitHub release tagged `vX.Y.Z` (matching `package.json`) — the
|
|
230
|
+
[Publish workflow](.github/workflows/publish.yml) typechecks, builds, tests,
|
|
231
|
+
and publishes to npm with provenance via trusted publishing. The workflow
|
|
232
|
+
fails fast if the tag and `package.json` disagree.
|
|
233
|
+
|
|
227
234
|
## License
|
|
228
235
|
|
|
229
236
|
MIT — maintained by [Faraz](https://langport.dev).
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ Output (default: human-readable):
|
|
|
16
16
|
--json-file <path> additionally write the JSON report to a file
|
|
17
17
|
--sarif-file <path> additionally write a SARIF log to a file
|
|
18
18
|
--junit-file <path> additionally write JUnit XML to a file
|
|
19
|
+
--group one line per rule with a count (large streams)
|
|
19
20
|
--no-color disable ANSI colors
|
|
20
21
|
|
|
21
22
|
Rules:
|
|
@@ -50,6 +51,10 @@ const FLAGS = {
|
|
|
50
51
|
takesValue: false,
|
|
51
52
|
apply: (c) => (c.color = false, null)
|
|
52
53
|
},
|
|
54
|
+
"--group": {
|
|
55
|
+
takesValue: false,
|
|
56
|
+
apply: (c) => (c.group = true, null)
|
|
57
|
+
},
|
|
53
58
|
"--json": {
|
|
54
59
|
takesValue: false,
|
|
55
60
|
apply: (c) => setFormat(c, "json")
|
|
@@ -142,6 +147,7 @@ function parseCliArgs(argv) {
|
|
|
142
147
|
color: null,
|
|
143
148
|
headers: {},
|
|
144
149
|
severityOverrides: {},
|
|
150
|
+
group: false,
|
|
145
151
|
help: false,
|
|
146
152
|
version: false
|
|
147
153
|
};
|
|
@@ -182,6 +188,10 @@ function parseCliArgs(argv) {
|
|
|
182
188
|
error
|
|
183
189
|
};
|
|
184
190
|
}
|
|
191
|
+
if (config.group && config.format !== "pretty") return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: "--group only applies to the default pretty output, not --json/--sarif/--junit"
|
|
194
|
+
};
|
|
185
195
|
if (targets.length > 1) return {
|
|
186
196
|
ok: false,
|
|
187
197
|
error: `expected exactly one target, got ${targets.length}`
|
|
@@ -202,57 +212,6 @@ function decideExitCode(summary, maxWarnings) {
|
|
|
202
212
|
return 0;
|
|
203
213
|
}
|
|
204
214
|
//#endregion
|
|
205
|
-
//#region src/report/pretty.ts
|
|
206
|
-
const SYMBOL = {
|
|
207
|
-
error: "✖",
|
|
208
|
-
warning: "⚠",
|
|
209
|
-
info: "ℹ"
|
|
210
|
-
};
|
|
211
|
-
const SGR = {
|
|
212
|
-
error: "31",
|
|
213
|
-
warning: "33",
|
|
214
|
-
info: "36"
|
|
215
|
-
};
|
|
216
|
-
function paint(code, s, on) {
|
|
217
|
-
return on ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
218
|
-
}
|
|
219
|
-
function formatDiagnosticLine(d, opts) {
|
|
220
|
-
const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : "—";
|
|
221
|
-
const head = paint(SGR[d.severity], `${SYMBOL[d.severity]} ${d.rule}`, opts.color);
|
|
222
|
-
const meta = paint("2", `${d.severity.padEnd(7)} ${where.padEnd(10)}`, opts.color);
|
|
223
|
-
const cite = paint("2", ` ↳ ${d.specUrl}`, opts.color);
|
|
224
|
-
return `${head} ${meta} ${d.message}\n${cite}`;
|
|
225
|
-
}
|
|
226
|
-
function count(n, noun) {
|
|
227
|
-
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
228
|
-
}
|
|
229
|
-
function formatReportSummary(report, opts) {
|
|
230
|
-
const { errors, warnings, info } = report.summary;
|
|
231
|
-
const lines = [];
|
|
232
|
-
if (errors + warnings + info === 0) lines.push(paint("32", `✔ no conformance violations across ${count(report.eventCount, "event")}`, opts.color));
|
|
233
|
-
else lines.push(`${count(errors, "error")}, ${count(warnings, "warning")}, ${info} info across ${count(report.eventCount, "event")}`);
|
|
234
|
-
const features = Object.entries(report.features);
|
|
235
|
-
const exercised = features.filter(([, s]) => s === "exercised").map(([f]) => f);
|
|
236
|
-
const suffix = exercised.length > 0 ? `: ${exercised.join(", ")}` : "";
|
|
237
|
-
lines.push(`${exercised.length} of ${features.length} AG-UI features exercised${suffix}`);
|
|
238
|
-
if (report.skipped.length > 0) {
|
|
239
|
-
lines.push(`${count(report.skipped.length, "rule")} not evaluated:`);
|
|
240
|
-
for (const s of report.skipped) lines.push(paint("2", ` – ${s.rule}: ${s.reason}`, opts.color));
|
|
241
|
-
}
|
|
242
|
-
for (const e of report.internalErrors) lines.push(paint("31", `! internal validator error: ${e}`, opts.color));
|
|
243
|
-
return lines.join("\n");
|
|
244
|
-
}
|
|
245
|
-
//#endregion
|
|
246
|
-
//#region src/report/json.ts
|
|
247
|
-
function toJsonReport(report, opts) {
|
|
248
|
-
const doc = {
|
|
249
|
-
tool: opts.tool,
|
|
250
|
-
...report
|
|
251
|
-
};
|
|
252
|
-
if (opts.target !== void 0) doc.target = opts.target;
|
|
253
|
-
return doc;
|
|
254
|
-
}
|
|
255
|
-
//#endregion
|
|
256
215
|
//#region src/rules/catalog.json
|
|
257
216
|
var catalog_default = {
|
|
258
217
|
$comment: "AG-UI conformance rule catalog. Data, not code: a Python/Go implementation shares these rules. Severities follow the working agreement that behaviour the spec does not clearly govern is at most 'info' — entries with 'specQuestion' were downgraded accordingly; see docs/spec-questions.md.",
|
|
@@ -722,6 +681,98 @@ function formatMessage(rule, params) {
|
|
|
722
681
|
return rule.messageTemplate.replace(/\{(\w+)\}/g, (whole, key) => key in params ? String(params[key]) : whole);
|
|
723
682
|
}
|
|
724
683
|
//#endregion
|
|
684
|
+
//#region src/report/pretty.ts
|
|
685
|
+
const SYMBOL = {
|
|
686
|
+
error: "✖",
|
|
687
|
+
warning: "⚠",
|
|
688
|
+
info: "ℹ"
|
|
689
|
+
};
|
|
690
|
+
const SGR = {
|
|
691
|
+
error: "31",
|
|
692
|
+
warning: "33",
|
|
693
|
+
info: "36"
|
|
694
|
+
};
|
|
695
|
+
function paint(code, s, on) {
|
|
696
|
+
return on ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
697
|
+
}
|
|
698
|
+
function formatDiagnosticLine(d, opts) {
|
|
699
|
+
const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : "—";
|
|
700
|
+
const head = paint(SGR[d.severity], `${SYMBOL[d.severity]} ${d.rule}`, opts.color);
|
|
701
|
+
const meta = paint("2", `${d.severity.padEnd(7)} ${where.padEnd(10)}`, opts.color);
|
|
702
|
+
const cite = paint("2", ` ↳ ${d.specUrl}`, opts.color);
|
|
703
|
+
return `${head} ${meta} ${d.message}\n${cite}`;
|
|
704
|
+
}
|
|
705
|
+
function count(n, noun) {
|
|
706
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
707
|
+
}
|
|
708
|
+
const SEVERITY_RANK = {
|
|
709
|
+
error: 0,
|
|
710
|
+
warning: 1,
|
|
711
|
+
info: 2
|
|
712
|
+
};
|
|
713
|
+
const SAMPLE_INDEXES = 3;
|
|
714
|
+
/**
|
|
715
|
+
* One line per rule instead of one per occurrence — for large streams where
|
|
716
|
+
* the same violation repeats. Totals stay honest in the summary; this only
|
|
717
|
+
* changes what is listed.
|
|
718
|
+
*/
|
|
719
|
+
function formatGroupedDiagnostics(diagnostics, opts) {
|
|
720
|
+
const groups = /* @__PURE__ */ new Map();
|
|
721
|
+
for (const d of diagnostics) {
|
|
722
|
+
const list = groups.get(d.rule);
|
|
723
|
+
if (list === void 0) groups.set(d.rule, [d]);
|
|
724
|
+
else list.push(d);
|
|
725
|
+
}
|
|
726
|
+
const sorted = [...groups.values()].sort((a, b) => {
|
|
727
|
+
const bySeverity = SEVERITY_RANK[a[0].severity] - SEVERITY_RANK[b[0].severity];
|
|
728
|
+
return bySeverity !== 0 ? bySeverity : a[0].rule.localeCompare(b[0].rule);
|
|
729
|
+
});
|
|
730
|
+
const lines = [];
|
|
731
|
+
for (const group of sorted) {
|
|
732
|
+
const first = group[0];
|
|
733
|
+
const title = RULES.get(first.rule)?.title ?? first.message;
|
|
734
|
+
const indexes = group.map((d) => d.eventIndex).filter((i) => i >= 0);
|
|
735
|
+
let where;
|
|
736
|
+
if (indexes.length === 0) where = "stream-level";
|
|
737
|
+
else {
|
|
738
|
+
const sample = indexes.slice(0, SAMPLE_INDEXES).join(", ");
|
|
739
|
+
const rest = indexes.length - SAMPLE_INDEXES;
|
|
740
|
+
where = `events ${sample}${rest > 0 ? ` (+${rest} more)` : ""}`;
|
|
741
|
+
}
|
|
742
|
+
const head = paint(SGR[first.severity], `${SYMBOL[first.severity]} ${first.rule}`, opts.color);
|
|
743
|
+
const meta = paint("2", `${first.severity.padEnd(7)} ×${group.length}`, opts.color);
|
|
744
|
+
const cite = paint("2", ` ↳ ${first.specUrl}`, opts.color);
|
|
745
|
+
lines.push(`${head} ${meta} ${title} — ${where}`, cite);
|
|
746
|
+
}
|
|
747
|
+
return lines.join("\n");
|
|
748
|
+
}
|
|
749
|
+
function formatReportSummary(report, opts) {
|
|
750
|
+
const { errors, warnings, info } = report.summary;
|
|
751
|
+
const lines = [];
|
|
752
|
+
if (errors + warnings + info === 0) lines.push(paint("32", `✔ no conformance violations across ${count(report.eventCount, "event")}`, opts.color));
|
|
753
|
+
else lines.push(`${count(errors, "error")}, ${count(warnings, "warning")}, ${info} info across ${count(report.eventCount, "event")}`);
|
|
754
|
+
const features = Object.entries(report.features);
|
|
755
|
+
const exercised = features.filter(([, s]) => s === "exercised").map(([f]) => f);
|
|
756
|
+
const suffix = exercised.length > 0 ? `: ${exercised.join(", ")}` : "";
|
|
757
|
+
lines.push(`${exercised.length} of ${features.length} AG-UI features exercised${suffix}`);
|
|
758
|
+
if (report.skipped.length > 0) {
|
|
759
|
+
lines.push(`${count(report.skipped.length, "rule")} not evaluated:`);
|
|
760
|
+
for (const s of report.skipped) lines.push(paint("2", ` – ${s.rule}: ${s.reason}`, opts.color));
|
|
761
|
+
}
|
|
762
|
+
for (const e of report.internalErrors) lines.push(paint("31", `! internal validator error: ${e}`, opts.color));
|
|
763
|
+
return lines.join("\n");
|
|
764
|
+
}
|
|
765
|
+
//#endregion
|
|
766
|
+
//#region src/report/json.ts
|
|
767
|
+
function toJsonReport(report, opts) {
|
|
768
|
+
const doc = {
|
|
769
|
+
tool: opts.tool,
|
|
770
|
+
...report
|
|
771
|
+
};
|
|
772
|
+
if (opts.target !== void 0) doc.target = opts.target;
|
|
773
|
+
return doc;
|
|
774
|
+
}
|
|
775
|
+
//#endregion
|
|
725
776
|
//#region src/report/sarif.ts
|
|
726
777
|
const LEVEL = {
|
|
727
778
|
error: "error",
|
|
@@ -2707,7 +2758,7 @@ async function main() {
|
|
|
2707
2758
|
const color = config.color ?? (process.stdout.isTTY === true && process.env.NO_COLOR === void 0);
|
|
2708
2759
|
const pretty = config.format === "pretty";
|
|
2709
2760
|
let printed = 0;
|
|
2710
|
-
const onDiagnostic = pretty ? (d) => {
|
|
2761
|
+
const onDiagnostic = pretty && !config.group ? (d) => {
|
|
2711
2762
|
printed += 1;
|
|
2712
2763
|
process.stdout.write(`${formatDiagnosticLine(d, { color })}\n`);
|
|
2713
2764
|
} : void 0;
|
|
@@ -2755,7 +2806,8 @@ async function main() {
|
|
|
2755
2806
|
if (config.sarifFile !== void 0) writeFileSync(config.sarifFile, `${JSON.stringify(toSarif(report, sarifOptions), null, 2)}\n`);
|
|
2756
2807
|
if (config.junitFile !== void 0) writeFileSync(config.junitFile, toJUnit(report, { name: targetLabel }));
|
|
2757
2808
|
if (pretty) {
|
|
2758
|
-
if (
|
|
2809
|
+
if (config.group && report.diagnostics.length > 0) process.stdout.write(`${formatGroupedDiagnostics(report.diagnostics, { color })}\n\n`);
|
|
2810
|
+
else if (printed > 0) process.stdout.write("\n");
|
|
2759
2811
|
process.stdout.write(`${formatReportSummary(report, { color })}\n`);
|
|
2760
2812
|
} else if (config.format === "json") {
|
|
2761
2813
|
const doc = toJsonReport(report, {
|