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 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 (printed > 0) process.stdout.write("\n");
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, {