ag-ui-validate 0.1.0 → 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 (146) hide show
  1. package/README.md +77 -10
  2. package/dist/{catalog-Ci9dqc1a.cjs → catalog-AB5vuZ8j.cjs} +3 -3
  3. package/dist/catalog-AB5vuZ8j.cjs.map +1 -0
  4. package/dist/{catalog-BglXBNbL.js → catalog-DqTI4VLt.js} +3 -3
  5. package/dist/catalog-DqTI4VLt.js.map +1 -0
  6. package/dist/cli.js +107 -55
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.cjs +2 -2
  9. package/dist/index.js +2 -2
  10. package/dist/report.cjs +43 -1
  11. package/dist/report.cjs.map +1 -1
  12. package/dist/report.d.cts +7 -1
  13. package/dist/report.d.ts +7 -1
  14. package/dist/report.js +43 -2
  15. package/dist/report.js.map +1 -1
  16. package/dist/{src-HmI-kxef.cjs → src-D04i3Vso.cjs} +2 -2
  17. package/dist/src-D04i3Vso.cjs.map +1 -0
  18. package/dist/{src-rGZ2G4qA.js → src-Dcuqz2QC.js} +2 -2
  19. package/dist/src-Dcuqz2QC.js.map +1 -0
  20. package/dist/transport.cjs +1 -1
  21. package/dist/transport.cjs.map +1 -1
  22. package/dist/transport.js +1 -1
  23. package/dist/transport.js.map +1 -1
  24. package/dist/vitest.js +2 -2
  25. package/dist/vitest.js.map +1 -1
  26. package/{src → js/src}/cli-args.ts +11 -0
  27. package/{src → js/src}/cli.ts +8 -3
  28. package/{src → js/src}/report/index.ts +1 -1
  29. package/{src → js/src}/report/pretty.ts +43 -0
  30. package/{src → js/src}/rules/catalog.ts +1 -1
  31. package/{src → js/src}/transport/index.ts +1 -1
  32. package/package.json +14 -13
  33. package/{src/rules → spec}/catalog.json +1 -1
  34. package/spec/event-categories.json +56 -0
  35. package/spec/fixtures/README.md +91 -0
  36. package/spec/fixtures/invalid/AGUI001-first-event-not-run-started/expected.json +10 -0
  37. package/spec/fixtures/invalid/AGUI001-first-event-not-run-started/stream.jsonl +4 -0
  38. package/spec/fixtures/invalid/AGUI002-duplicate-run-started/expected.json +11 -0
  39. package/spec/fixtures/invalid/AGUI002-duplicate-run-started/stream.jsonl +3 -0
  40. package/spec/fixtures/invalid/AGUI003-run-never-terminated/expected.json +10 -0
  41. package/spec/fixtures/invalid/AGUI003-run-never-terminated/stream.jsonl +4 -0
  42. package/spec/fixtures/invalid/AGUI004-event-after-terminal/expected.json +11 -0
  43. package/spec/fixtures/invalid/AGUI004-event-after-terminal/stream.jsonl +3 -0
  44. package/spec/fixtures/invalid/AGUI005-finished-and-error/expected.json +11 -0
  45. package/spec/fixtures/invalid/AGUI005-finished-and-error/stream.jsonl +3 -0
  46. package/spec/fixtures/invalid/AGUI006-step-finished-unmatched/expected.json +11 -0
  47. package/spec/fixtures/invalid/AGUI006-step-finished-unmatched/stream.jsonl +3 -0
  48. package/spec/fixtures/invalid/AGUI007-step-unterminated/expected.json +10 -0
  49. package/spec/fixtures/invalid/AGUI007-step-unterminated/stream.jsonl +3 -0
  50. package/spec/fixtures/invalid/AGUI008-unstable-run-ids/expected.json +12 -0
  51. package/spec/fixtures/invalid/AGUI008-unstable-run-ids/stream.jsonl +2 -0
  52. package/spec/fixtures/invalid/AGUI101-content-without-start/expected.json +11 -0
  53. package/spec/fixtures/invalid/AGUI101-content-without-start/stream.jsonl +3 -0
  54. package/spec/fixtures/invalid/AGUI102-end-without-start/expected.json +11 -0
  55. package/spec/fixtures/invalid/AGUI102-end-without-start/stream.jsonl +3 -0
  56. package/spec/fixtures/invalid/AGUI103-message-unterminated/expected.json +10 -0
  57. package/spec/fixtures/invalid/AGUI103-message-unterminated/stream.jsonl +4 -0
  58. package/spec/fixtures/invalid/AGUI104-duplicate-message-id/expected.json +12 -0
  59. package/spec/fixtures/invalid/AGUI104-duplicate-message-id/stream.jsonl +6 -0
  60. package/spec/fixtures/invalid/AGUI105-empty-content-delta/expected.json +12 -0
  61. package/spec/fixtures/invalid/AGUI105-empty-content-delta/stream.jsonl +5 -0
  62. package/spec/fixtures/invalid/AGUI106-interleaved-same-message-id/expected.json +12 -0
  63. package/spec/fixtures/invalid/AGUI106-interleaved-same-message-id/stream.jsonl +5 -0
  64. package/spec/fixtures/invalid/AGUI201-args-without-start/expected.json +11 -0
  65. package/spec/fixtures/invalid/AGUI201-args-without-start/stream.jsonl +3 -0
  66. package/spec/fixtures/invalid/AGUI202-end-without-start/expected.json +11 -0
  67. package/spec/fixtures/invalid/AGUI202-end-without-start/stream.jsonl +3 -0
  68. package/spec/fixtures/invalid/AGUI203-unterminated-tool-call/expected.json +10 -0
  69. package/spec/fixtures/invalid/AGUI203-unterminated-tool-call/stream.jsonl +4 -0
  70. package/spec/fixtures/invalid/AGUI204-args-not-json/expected.json +10 -0
  71. package/spec/fixtures/invalid/AGUI204-args-not-json/stream.jsonl +6 -0
  72. package/spec/fixtures/invalid/AGUI205-duplicate-tool-call-id/expected.json +12 -0
  73. package/spec/fixtures/invalid/AGUI205-duplicate-tool-call-id/stream.jsonl +5 -0
  74. package/spec/fixtures/invalid/AGUI206-result-before-end/expected.json +12 -0
  75. package/spec/fixtures/invalid/AGUI206-result-before-end/stream.jsonl +5 -0
  76. package/spec/fixtures/invalid/AGUI207-result-unknown-id/expected.json +11 -0
  77. package/spec/fixtures/invalid/AGUI207-result-unknown-id/stream.jsonl +3 -0
  78. package/spec/fixtures/invalid/AGUI208-unknown-parent-message-id/expected.json +11 -0
  79. package/spec/fixtures/invalid/AGUI208-unknown-parent-message-id/stream.jsonl +4 -0
  80. package/spec/fixtures/invalid/AGUI301-delta-before-snapshot/expected.json +10 -0
  81. package/spec/fixtures/invalid/AGUI301-delta-before-snapshot/stream.jsonl +3 -0
  82. package/spec/fixtures/invalid/AGUI302-delta-failed-to-apply/expected.json +11 -0
  83. package/spec/fixtures/invalid/AGUI302-delta-failed-to-apply/stream.jsonl +4 -0
  84. package/spec/fixtures/invalid/AGUI303-invalid-patch-document/expected.json +11 -0
  85. package/spec/fixtures/invalid/AGUI303-invalid-patch-document/stream.jsonl +4 -0
  86. package/spec/fixtures/invalid/AGUI304-midrun-snapshot-discards-deltas/expected.json +10 -0
  87. package/spec/fixtures/invalid/AGUI304-midrun-snapshot-discards-deltas/stream.jsonl +5 -0
  88. package/spec/fixtures/invalid/AGUI305-shared-state-never-established/expected.json +9 -0
  89. package/spec/fixtures/invalid/AGUI305-shared-state-never-established/options.json +5 -0
  90. package/spec/fixtures/invalid/AGUI305-shared-state-never-established/stream.jsonl +5 -0
  91. package/spec/fixtures/invalid/AGUI401-reasoning-content-without-start/expected.json +11 -0
  92. package/spec/fixtures/invalid/AGUI401-reasoning-content-without-start/stream.jsonl +3 -0
  93. package/spec/fixtures/invalid/AGUI402-reasoning-unterminated/expected.json +10 -0
  94. package/spec/fixtures/invalid/AGUI402-reasoning-unterminated/stream.jsonl +3 -0
  95. package/spec/fixtures/invalid/AGUI501-missing-data-prefix/expected.json +9 -0
  96. package/spec/fixtures/invalid/AGUI501-missing-data-prefix/scenario.json +17 -0
  97. package/spec/fixtures/invalid/AGUI502-payload-not-json/expected.json +10 -0
  98. package/spec/fixtures/invalid/AGUI502-payload-not-json/stream.jsonl +3 -0
  99. package/spec/fixtures/invalid/AGUI503-unknown-event-type/expected.json +10 -0
  100. package/spec/fixtures/invalid/AGUI503-unknown-event-type/stream.jsonl +3 -0
  101. package/spec/fixtures/invalid/AGUI504-schema-violation/expected.json +11 -0
  102. package/spec/fixtures/invalid/AGUI504-schema-violation/stream.jsonl +4 -0
  103. package/spec/fixtures/invalid/AGUI505-unexpected-content-type/expected.json +9 -0
  104. package/spec/fixtures/invalid/AGUI505-unexpected-content-type/scenario.json +25 -0
  105. package/spec/fixtures/invalid/AGUI506-keepalive-gap/expected.json +9 -0
  106. package/spec/fixtures/invalid/AGUI506-keepalive-gap/scenario.json +25 -0
  107. package/spec/fixtures/invalid/AGUI507-buffered-response/expected.json +9 -0
  108. package/spec/fixtures/invalid/AGUI507-buffered-response/scenario.json +9 -0
  109. package/spec/fixtures/invalid/AGUI508-connection-dropped/expected.json +17 -0
  110. package/spec/fixtures/invalid/AGUI508-connection-dropped/scenario.json +10 -0
  111. package/spec/fixtures/invalid/AGUI901-raw-wraps-typed-event/expected.json +11 -0
  112. package/spec/fixtures/invalid/AGUI901-raw-wraps-typed-event/stream.jsonl +3 -0
  113. package/spec/fixtures/invalid/AGUI902-no-timestamps/expected.json +9 -0
  114. package/spec/fixtures/invalid/AGUI902-no-timestamps/stream.jsonl +2 -0
  115. package/spec/fixtures/invalid/AGUI903-custom-name-not-namespaced/expected.json +11 -0
  116. package/spec/fixtures/invalid/AGUI903-custom-name-not-namespaced/stream.jsonl +3 -0
  117. package/spec/fixtures/valid/agentic-chat.jsonl +14 -0
  118. package/spec/fixtures/valid/agentic-generative-ui.jsonl +13 -0
  119. package/spec/fixtures/valid/backend-tool-rendering.jsonl +14 -0
  120. package/spec/fixtures/valid/human-in-the-loop.jsonl +13 -0
  121. package/spec/fixtures/valid/predictive-state-updates.expected.json +11 -0
  122. package/spec/fixtures/valid/predictive-state-updates.jsonl +11 -0
  123. package/spec/fixtures/valid/shared-state.jsonl +8 -0
  124. package/spec/fixtures/valid/tool-based-generative-ui.jsonl +5 -0
  125. package/dist/catalog-BglXBNbL.js.map +0 -1
  126. package/dist/catalog-Ci9dqc1a.cjs.map +0 -1
  127. package/dist/src-HmI-kxef.cjs.map +0 -1
  128. package/dist/src-rGZ2G4qA.js.map +0 -1
  129. /package/{src → js/src}/index.ts +0 -0
  130. /package/{src → js/src}/protocol/event-table.ts +0 -0
  131. /package/{src → js/src}/protocol/jsonpatch.ts +0 -0
  132. /package/{src → js/src}/report/json.ts +0 -0
  133. /package/{src → js/src}/report/junit.ts +0 -0
  134. /package/{src → js/src}/report/sarif.ts +0 -0
  135. /package/{src → js/src}/rules/checks/context.ts +0 -0
  136. /package/{src → js/src}/rules/checks/lifecycle.ts +0 -0
  137. /package/{src → js/src}/rules/checks/reasoning.ts +0 -0
  138. /package/{src → js/src}/rules/checks/state.ts +0 -0
  139. /package/{src → js/src}/rules/checks/text.ts +0 -0
  140. /package/{src → js/src}/rules/checks/toolcalls.ts +0 -0
  141. /package/{src → js/src}/rules/checks/transport.ts +0 -0
  142. /package/{src → js/src}/transport/ndjson.ts +0 -0
  143. /package/{src → js/src}/transport/sse.ts +0 -0
  144. /package/{src → js/src}/types.ts +0 -0
  145. /package/{src → js/src}/vitest/index.ts +0 -0
  146. /package/{src → js/src}/vitest/matcher.ts +0 -0
package/README.md CHANGED
@@ -15,10 +15,11 @@ spec section.
15
15
  2 errors, 0 warnings, 1 info — 3 of 7 AG-UI features exercised
16
16
  ```
17
17
 
18
- > **Status: pre-release.** The core validator, the language-neutral fixture
19
- > corpus, and the transport layer are implemented and tested. The CLI
20
- > (`npx ag-ui-validate <url|->`), the Vitest matcher, and SARIF/JUnit
21
- > reporters are in progress.
18
+ > **Status:** the TypeScript implementation is released to npm
19
+ > (`npm install ag-ui-validate`). A native Python port same rule catalog,
20
+ > same fixture corpus, byte-identical CLI is fully implemented and tested
21
+ > but not yet published to PyPI as a functional release; see
22
+ > [Python](#python) below.
22
23
 
23
24
  ## Why
24
25
 
@@ -66,6 +67,7 @@ Useful flags (see `--help` for all):
66
67
  | Flag | Effect |
67
68
  | --- | --- |
68
69
  | `--json` / `--sarif` / `--junit` | machine-readable report on stdout (SARIF 2.1.0 for code scanning, JUnit XML for CI) |
70
+ | `--group` | one line per rule with a count — for large streams with repeated findings (totals stay exact) |
69
71
  | `--rule AGUI105=error`, `--off AGUI902` | per-rule severity overrides |
70
72
  | `--features shared-state,...` | declare exercised features (enables e.g. AGUI305) |
71
73
  | `--max-warnings 0` | fail CI on any warning |
@@ -123,7 +125,7 @@ const { diagnostics, summary, features, skipped } = v.report()
123
125
  ```
124
126
 
125
127
  The core is a pure function over an event sequence: zero I/O, zero runtime
126
- dependencies, isomorphic across Node 20+, browsers, Deno, and Workers.
128
+ dependencies, isomorphic across Node 22+, browsers, Deno, and Workers.
127
129
 
128
130
  ### Validate a live endpoint
129
131
 
@@ -169,12 +171,71 @@ import { formatReportSummary, toSarif, toJUnit } from "ag-ui-validate/report"
169
171
  }
170
172
  ```
171
173
 
174
+ ## Python
175
+
176
+ A native Python port ships in [py/](py/) — the same rule catalog, the same
177
+ fixture corpus, and a CLI with byte-identical flags, error messages, and
178
+ JSON/SARIF/JUnit output to the TypeScript one above. It's fully implemented
179
+ and tested, and checked against the TypeScript implementation on every PR
180
+ by Parity CI (see [docs/TESTING.md](docs/TESTING.md) for how, and
181
+ [docs/PYTHON-PORT-PLAN.md](docs/PYTHON-PORT-PLAN.md) for the port's full
182
+ milestone history). **Not yet published to PyPI as a functional release**
183
+ — only a name-reservation placeholder (`0.0.1`) is live today.
184
+ Install from source until a real release ships:
185
+
186
+ ```bash
187
+ git clone https://github.com/langport-dev/ag-ui-validate
188
+ cd ag-ui-validate/py
189
+ pip install -e ".[dev]" # or ".[transport]" for just the endpoint-validating extras
190
+ ```
191
+
192
+ ### CLI
193
+
194
+ ```bash
195
+ ag-ui-validate http://localhost:8000/agui # live endpoint (POSTs a RunAgentInput)
196
+ ag-ui-validate run.jsonl # recorded stream (NDJSON/JSONL or SSE capture)
197
+ cat run.jsonl | ag-ui-validate - # stdin
198
+ ```
199
+
200
+ Same exit codes and the same flags as the [CLI](#cli) above (see
201
+ `ag-ui-validate --help`) — the Python argument parser is a direct,
202
+ hand-rolled port of the TypeScript one rather than `argparse`/`click`,
203
+ specifically to keep both CLIs' invocation and error messages
204
+ byte-for-byte identical.
205
+
206
+ ### Test your agent in pytest
207
+
208
+ ```python
209
+ from ag_ui_validate.pytest_plugin import assert_valid_agui
210
+
211
+ def test_my_agent_stream(captured_events):
212
+ assert_valid_agui(captured_events, features=["shared-state"], max_warnings=0)
213
+ ```
214
+
215
+ `assert_valid_agui` accepts a list of events (dicts or JSON strings) or a
216
+ whole JSONL capture as one string, and raises `AssertionError` with every
217
+ finding's rule ID and spec link on failure. It registers automatically as a
218
+ pytest plugin on install — no `conftest.py` setup needed. An async
219
+ counterpart validates a live endpoint directly from a test:
220
+
221
+ ```python
222
+ from ag_ui_validate.pytest_plugin import assert_valid_agui_endpoint
223
+
224
+ async def test_my_live_agent():
225
+ await assert_valid_agui_endpoint("http://localhost:8000/agui", max_warnings=0)
226
+ ```
227
+
228
+ `assert_valid_agui` has zero runtime dependencies; only the
229
+ endpoint-validating helpers (`assert_valid_agui_endpoint`,
230
+ `validate_agui_endpoint`) pull in `httpx`
231
+ (`pip install ag-ui-validate[transport]`).
232
+
172
233
  ## The rule catalog
173
234
 
174
235
  40 rules, maintained as **data** in
175
- [src/rules/catalog.json](src/rules/catalog.json) so other implementations
176
- (Python, Go, ) can share them. Every rule has its own page — spec grounding,
177
- severity, and a violating example from the corpus in the
236
+ [spec/catalog.json](spec/catalog.json), shared by both implementations in
237
+ this repo ([TypeScript](.) and [Python](py/)) rather than duplicated.
238
+ Every rule has its own pagespec grounding,
178
239
  **[rule index](docs/rules/README.md)** (generated from the catalog,
179
240
  drift-checked in CI):
180
241
 
@@ -194,7 +255,7 @@ drift-tested against the installed SDK on every run.
194
255
 
195
256
  ## The fixture corpus
196
257
 
197
- [fixtures/](fixtures/README.md) is a language-neutral conformance corpus:
258
+ [spec/fixtures/](spec/fixtures/README.md) is a language-neutral conformance corpus:
198
259
  7 valid streams (one per canonical AG-UI feature — the false-positive guards)
199
260
  and 40 invalid fixtures (one per rule) with exact expected diagnostics. Any
200
261
  validator implementation that consumes the shared catalog can be tested
@@ -218,12 +279,18 @@ Component-by-component instructions live in
218
279
  rules are tracked in [docs/spec-questions.md](docs/spec-questions.md).
219
280
 
220
281
  Adding a rule: add the catalog entry (with its `specUrl`), add the fixture
221
- stream + intended findings to `scripts/build-fixtures.mjs`, and run
282
+ stream + intended findings to `js/scripts/build-fixtures.mjs`, and run
222
283
  `npm run fixtures:build` — the meta-tests fail until both exist. Rule
223
284
  *proposals* belong upstream as issues on
224
285
  [`ag-ui-protocol/ag-ui`](https://github.com/ag-ui-protocol/ag-ui) first; this
225
286
  project does not invent rules the spec doesn't support.
226
287
 
288
+ Releasing: merge the pending changesets (`npx changeset version`) via a PR,
289
+ then publish a GitHub release tagged `vX.Y.Z` (matching `package.json`) — the
290
+ [Publish workflow](.github/workflows/publish.yml) typechecks, builds, tests,
291
+ and publishes to npm with provenance via trusted publishing. The workflow
292
+ fails fast if the tag and `package.json` disagree.
293
+
227
294
  ## License
228
295
 
229
296
  MIT — maintained by [Faraz](https://langport.dev).
@@ -1,4 +1,4 @@
1
- //#region src/rules/catalog.json
1
+ //#region ../spec/catalog.json
2
2
  var catalog_default = {
3
3
  $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.",
4
4
  catalogVersion: "0.1.0",
@@ -342,7 +342,7 @@ var catalog_default = {
342
342
  "id": "AGUI503",
343
343
  "severity": "error",
344
344
  "title": "Unknown event type",
345
- "messageTemplate": "Unknown event type '{type}' (not in @ag-ui/core v{sdkVersion}, and not RAW or CUSTOM)",
345
+ "messageTemplate": "Unknown event type '{type}' (not in the installed AG-UI SDK v{sdkVersion}, and not RAW or CUSTOM)",
346
346
  "specUrl": "https://docs.ag-ui.com/concepts/events#event-types-overview",
347
347
  "specQuote": "Events in the protocol are categorized by their purpose.",
348
348
  "since": "0.x",
@@ -492,4 +492,4 @@ Object.defineProperty(exports, "validateCatalog", {
492
492
  }
493
493
  });
494
494
 
495
- //# sourceMappingURL=catalog-Ci9dqc1a.cjs.map
495
+ //# sourceMappingURL=catalog-AB5vuZ8j.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-AB5vuZ8j.cjs","names":["catalogJson"],"sources":["../spec/catalog.json","../js/src/rules/catalog.ts"],"sourcesContent":["","// Typed loader for the rule catalog. The catalog itself is data\n// (catalog.json) so other implementations can share it; this module gives it\n// types and validates its invariants once at load time.\n//\n// Note the asymmetry: the validator never throws on stream input, but a\n// malformed catalog is a programming error in this package, so the loader\n// throws loudly at import time.\n\nimport catalogJson from \"../../../spec/catalog.json\"\n\nexport type Severity = \"error\" | \"warning\" | \"info\"\nexport type SeverityOrOff = Severity | \"off\"\n\nconst SEVERITIES: readonly string[] = [\"error\", \"warning\", \"info\"]\nconst LAYERS: readonly string[] = [\"core\", \"transport\"]\n\nexport interface RuleDefinition {\n /** e.g. \"AGUI203\" */\n id: string\n severity: Severity\n title: string\n /** Human template with {placeholder} slots filled per diagnostic. */\n messageTemplate: string\n /** Governing spec section. Mandatory: rules that cannot cite one don't ship. */\n specUrl: string\n /** Exact sentence from the spec section, where one exists. */\n specQuote?: string\n since: string\n /** Canonical AG-UI feature this rule relates to, if any. */\n feature?: string\n /** True when the rule only fires if opts.features declares `feature`. */\n requiresFeature?: boolean\n /** Cross-reference into docs/spec-questions.md for downgraded/ambiguous rules. */\n specQuestion?: string\n /** Where the rule is evaluated. Transport rules are skipped (and the skip\n * reported) when validating recorded input with no transport in play. */\n checkedIn: \"core\" | \"transport\"\n}\n\nexport interface Catalog {\n catalogVersion: string\n spec: string\n rules: readonly RuleDefinition[]\n}\n\n/** Validates catalog data and returns it typed. Throws on structural problems. */\nexport function validateCatalog(data: unknown): Catalog {\n const problems: string[] = []\n const cat = data as Catalog\n if (typeof cat !== \"object\" || cat === null || !Array.isArray(cat.rules)) {\n throw new Error(\"rule catalog: expected an object with a rules array\")\n }\n const seen = new Set<string>()\n for (const rule of cat.rules) {\n const where = rule?.id ?? \"<missing id>\"\n if (!/^AGUI\\d{3}$/.test(rule.id ?? \"\")) problems.push(`${where}: id must match AGUI###`)\n if (seen.has(rule.id)) problems.push(`${where}: duplicate id`)\n seen.add(rule.id)\n if (!SEVERITIES.includes(rule.severity)) problems.push(`${where}: bad severity '${rule.severity}'`)\n if (!rule.title) problems.push(`${where}: missing title`)\n if (!rule.messageTemplate) problems.push(`${where}: missing messageTemplate`)\n if (!rule.specUrl?.startsWith(\"https://\")) problems.push(`${where}: specUrl must be an https URL`)\n if (!rule.since) problems.push(`${where}: missing since`)\n if (!LAYERS.includes(rule.checkedIn)) problems.push(`${where}: bad checkedIn '${rule.checkedIn}'`)\n if (rule.requiresFeature && !rule.feature) problems.push(`${where}: requiresFeature without feature`)\n }\n if (problems.length > 0) {\n throw new Error(`rule catalog is invalid:\\n ${problems.join(\"\\n \")}`)\n }\n return cat\n}\n\nexport const CATALOG: Catalog = validateCatalog(catalogJson)\n\nexport const RULES: ReadonlyMap<string, RuleDefinition> = new Map(\n CATALOG.rules.map((r) => [r.id, r]),\n)\n\n/** Fills a rule's messageTemplate. Unknown placeholders are left intact. */\nexport function formatMessage(rule: RuleDefinition, params: Record<string, unknown>): string {\n return rule.messageTemplate.replace(/\\{(\\w+)\\}/g, (whole, key: string) =>\n key in params ? String(params[key]) : whole,\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACaA,MAAM,aAAgC;CAAC;CAAS;CAAW;AAAM;AACjE,MAAM,SAA4B,CAAC,QAAQ,WAAW;;AAgCtD,SAAgB,gBAAgB,MAAwB;CACtD,MAAM,WAAqB,CAAC;CAC5B,MAAM,MAAM;CACZ,IAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC,MAAM,QAAQ,IAAI,KAAK,GACrE,MAAM,IAAI,MAAM,qDAAqD;CAEvE,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,QAAQ,IAAI,OAAO;EAC5B,MAAM,QAAQ,MAAM,MAAM;EAC1B,IAAI,CAAC,cAAc,KAAK,KAAK,MAAM,EAAE,GAAG,SAAS,KAAK,GAAG,MAAM,wBAAwB;EACvF,IAAI,KAAK,IAAI,KAAK,EAAE,GAAG,SAAS,KAAK,GAAG,MAAM,eAAe;EAC7D,KAAK,IAAI,KAAK,EAAE;EAChB,IAAI,CAAC,WAAW,SAAS,KAAK,QAAQ,GAAG,SAAS,KAAK,GAAG,MAAM,kBAAkB,KAAK,SAAS,EAAE;EAClG,IAAI,CAAC,KAAK,OAAO,SAAS,KAAK,GAAG,MAAM,gBAAgB;EACxD,IAAI,CAAC,KAAK,iBAAiB,SAAS,KAAK,GAAG,MAAM,0BAA0B;EAC5E,IAAI,CAAC,KAAK,SAAS,WAAW,UAAU,GAAG,SAAS,KAAK,GAAG,MAAM,+BAA+B;EACjG,IAAI,CAAC,KAAK,OAAO,SAAS,KAAK,GAAG,MAAM,gBAAgB;EACxD,IAAI,CAAC,OAAO,SAAS,KAAK,SAAS,GAAG,SAAS,KAAK,GAAG,MAAM,mBAAmB,KAAK,UAAU,EAAE;EACjG,IAAI,KAAK,mBAAmB,CAAC,KAAK,SAAS,SAAS,KAAK,GAAG,MAAM,kCAAkC;CACtG;CACA,IAAI,SAAS,SAAS,GACpB,MAAM,IAAI,MAAM,+BAA+B,SAAS,KAAK,MAAM,GAAG;CAExE,OAAO;AACT;AAEA,MAAa,UAAmB,gBAAgBA,eAAW;AAE3D,MAAa,QAA6C,IAAI,IAC5D,QAAQ,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CACpC;;AAGA,SAAgB,cAAc,MAAsB,QAAyC;CAC3F,OAAO,KAAK,gBAAgB,QAAQ,eAAe,OAAO,QACxD,OAAO,SAAS,OAAO,OAAO,IAAI,IAAI,KACxC;AACF"}
@@ -1,4 +1,4 @@
1
- //#region src/rules/catalog.json
1
+ //#region ../spec/catalog.json
2
2
  var catalog_default = {
3
3
  $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.",
4
4
  catalogVersion: "0.1.0",
@@ -342,7 +342,7 @@ var catalog_default = {
342
342
  "id": "AGUI503",
343
343
  "severity": "error",
344
344
  "title": "Unknown event type",
345
- "messageTemplate": "Unknown event type '{type}' (not in @ag-ui/core v{sdkVersion}, and not RAW or CUSTOM)",
345
+ "messageTemplate": "Unknown event type '{type}' (not in the installed AG-UI SDK v{sdkVersion}, and not RAW or CUSTOM)",
346
346
  "specUrl": "https://docs.ag-ui.com/concepts/events#event-types-overview",
347
347
  "specQuote": "Events in the protocol are categorized by their purpose.",
348
348
  "since": "0.x",
@@ -469,4 +469,4 @@ function formatMessage(rule, params) {
469
469
  //#endregion
470
470
  export { validateCatalog as i, RULES as n, formatMessage as r, CATALOG as t };
471
471
 
472
- //# sourceMappingURL=catalog-BglXBNbL.js.map
472
+ //# sourceMappingURL=catalog-DqTI4VLt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-DqTI4VLt.js","names":["catalogJson"],"sources":["../spec/catalog.json","../js/src/rules/catalog.ts"],"sourcesContent":["","// Typed loader for the rule catalog. The catalog itself is data\n// (catalog.json) so other implementations can share it; this module gives it\n// types and validates its invariants once at load time.\n//\n// Note the asymmetry: the validator never throws on stream input, but a\n// malformed catalog is a programming error in this package, so the loader\n// throws loudly at import time.\n\nimport catalogJson from \"../../../spec/catalog.json\"\n\nexport type Severity = \"error\" | \"warning\" | \"info\"\nexport type SeverityOrOff = Severity | \"off\"\n\nconst SEVERITIES: readonly string[] = [\"error\", \"warning\", \"info\"]\nconst LAYERS: readonly string[] = [\"core\", \"transport\"]\n\nexport interface RuleDefinition {\n /** e.g. \"AGUI203\" */\n id: string\n severity: Severity\n title: string\n /** Human template with {placeholder} slots filled per diagnostic. */\n messageTemplate: string\n /** Governing spec section. Mandatory: rules that cannot cite one don't ship. */\n specUrl: string\n /** Exact sentence from the spec section, where one exists. */\n specQuote?: string\n since: string\n /** Canonical AG-UI feature this rule relates to, if any. */\n feature?: string\n /** True when the rule only fires if opts.features declares `feature`. */\n requiresFeature?: boolean\n /** Cross-reference into docs/spec-questions.md for downgraded/ambiguous rules. */\n specQuestion?: string\n /** Where the rule is evaluated. Transport rules are skipped (and the skip\n * reported) when validating recorded input with no transport in play. */\n checkedIn: \"core\" | \"transport\"\n}\n\nexport interface Catalog {\n catalogVersion: string\n spec: string\n rules: readonly RuleDefinition[]\n}\n\n/** Validates catalog data and returns it typed. Throws on structural problems. */\nexport function validateCatalog(data: unknown): Catalog {\n const problems: string[] = []\n const cat = data as Catalog\n if (typeof cat !== \"object\" || cat === null || !Array.isArray(cat.rules)) {\n throw new Error(\"rule catalog: expected an object with a rules array\")\n }\n const seen = new Set<string>()\n for (const rule of cat.rules) {\n const where = rule?.id ?? \"<missing id>\"\n if (!/^AGUI\\d{3}$/.test(rule.id ?? \"\")) problems.push(`${where}: id must match AGUI###`)\n if (seen.has(rule.id)) problems.push(`${where}: duplicate id`)\n seen.add(rule.id)\n if (!SEVERITIES.includes(rule.severity)) problems.push(`${where}: bad severity '${rule.severity}'`)\n if (!rule.title) problems.push(`${where}: missing title`)\n if (!rule.messageTemplate) problems.push(`${where}: missing messageTemplate`)\n if (!rule.specUrl?.startsWith(\"https://\")) problems.push(`${where}: specUrl must be an https URL`)\n if (!rule.since) problems.push(`${where}: missing since`)\n if (!LAYERS.includes(rule.checkedIn)) problems.push(`${where}: bad checkedIn '${rule.checkedIn}'`)\n if (rule.requiresFeature && !rule.feature) problems.push(`${where}: requiresFeature without feature`)\n }\n if (problems.length > 0) {\n throw new Error(`rule catalog is invalid:\\n ${problems.join(\"\\n \")}`)\n }\n return cat\n}\n\nexport const CATALOG: Catalog = validateCatalog(catalogJson)\n\nexport const RULES: ReadonlyMap<string, RuleDefinition> = new Map(\n CATALOG.rules.map((r) => [r.id, r]),\n)\n\n/** Fills a rule's messageTemplate. Unknown placeholders are left intact. */\nexport function formatMessage(rule: RuleDefinition, params: Record<string, unknown>): string {\n return rule.messageTemplate.replace(/\\{(\\w+)\\}/g, (whole, key: string) =>\n key in params ? String(params[key]) : whole,\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACaA,MAAM,aAAgC;CAAC;CAAS;CAAW;AAAM;AACjE,MAAM,SAA4B,CAAC,QAAQ,WAAW;;AAgCtD,SAAgB,gBAAgB,MAAwB;CACtD,MAAM,WAAqB,CAAC;CAC5B,MAAM,MAAM;CACZ,IAAI,OAAO,QAAQ,YAAY,QAAQ,QAAQ,CAAC,MAAM,QAAQ,IAAI,KAAK,GACrE,MAAM,IAAI,MAAM,qDAAqD;CAEvE,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,QAAQ,IAAI,OAAO;EAC5B,MAAM,QAAQ,MAAM,MAAM;EAC1B,IAAI,CAAC,cAAc,KAAK,KAAK,MAAM,EAAE,GAAG,SAAS,KAAK,GAAG,MAAM,wBAAwB;EACvF,IAAI,KAAK,IAAI,KAAK,EAAE,GAAG,SAAS,KAAK,GAAG,MAAM,eAAe;EAC7D,KAAK,IAAI,KAAK,EAAE;EAChB,IAAI,CAAC,WAAW,SAAS,KAAK,QAAQ,GAAG,SAAS,KAAK,GAAG,MAAM,kBAAkB,KAAK,SAAS,EAAE;EAClG,IAAI,CAAC,KAAK,OAAO,SAAS,KAAK,GAAG,MAAM,gBAAgB;EACxD,IAAI,CAAC,KAAK,iBAAiB,SAAS,KAAK,GAAG,MAAM,0BAA0B;EAC5E,IAAI,CAAC,KAAK,SAAS,WAAW,UAAU,GAAG,SAAS,KAAK,GAAG,MAAM,+BAA+B;EACjG,IAAI,CAAC,KAAK,OAAO,SAAS,KAAK,GAAG,MAAM,gBAAgB;EACxD,IAAI,CAAC,OAAO,SAAS,KAAK,SAAS,GAAG,SAAS,KAAK,GAAG,MAAM,mBAAmB,KAAK,UAAU,EAAE;EACjG,IAAI,KAAK,mBAAmB,CAAC,KAAK,SAAS,SAAS,KAAK,GAAG,MAAM,kCAAkC;CACtG;CACA,IAAI,SAAS,SAAS,GACpB,MAAM,IAAI,MAAM,+BAA+B,SAAS,KAAK,MAAM,GAAG;CAExE,OAAO;AACT;AAEA,MAAa,UAAmB,gBAAgBA,eAAW;AAE3D,MAAa,QAA6C,IAAI,IAC5D,QAAQ,MAAM,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CACpC;;AAGA,SAAgB,cAAc,MAAsB,QAAyC;CAC3F,OAAO,KAAK,gBAAgB,QAAQ,eAAe,OAAO,QACxD,OAAO,SAAS,OAAO,OAAO,IAAI,IAAI,KACxC;AACF"}
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,58 +212,7 @@ 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
- //#region src/rules/catalog.json
215
+ //#region ../spec/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.",
259
218
  catalogVersion: "0.1.0",
@@ -597,7 +556,7 @@ var catalog_default = {
597
556
  "id": "AGUI503",
598
557
  "severity": "error",
599
558
  "title": "Unknown event type",
600
- "messageTemplate": "Unknown event type '{type}' (not in @ag-ui/core v{sdkVersion}, and not RAW or CUSTOM)",
559
+ "messageTemplate": "Unknown event type '{type}' (not in the installed AG-UI SDK v{sdkVersion}, and not RAW or CUSTOM)",
601
560
  "specUrl": "https://docs.ag-ui.com/concepts/events#event-types-overview",
602
561
  "specQuote": "Events in the protocol are categorized by their purpose.",
603
562
  "since": "0.x",
@@ -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, {