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/dist/report.cjs CHANGED
@@ -24,6 +24,47 @@ function formatDiagnosticLine(d, opts) {
24
24
  function count(n, noun) {
25
25
  return `${n} ${noun}${n === 1 ? "" : "s"}`;
26
26
  }
27
+ const SEVERITY_RANK = {
28
+ error: 0,
29
+ warning: 1,
30
+ info: 2
31
+ };
32
+ const SAMPLE_INDEXES = 3;
33
+ /**
34
+ * One line per rule instead of one per occurrence — for large streams where
35
+ * the same violation repeats. Totals stay honest in the summary; this only
36
+ * changes what is listed.
37
+ */
38
+ function formatGroupedDiagnostics(diagnostics, opts) {
39
+ const groups = /* @__PURE__ */ new Map();
40
+ for (const d of diagnostics) {
41
+ const list = groups.get(d.rule);
42
+ if (list === void 0) groups.set(d.rule, [d]);
43
+ else list.push(d);
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
+ const lines = [];
50
+ for (const group of sorted) {
51
+ const first = group[0];
52
+ const title = require_catalog.RULES.get(first.rule)?.title ?? first.message;
53
+ const indexes = group.map((d) => d.eventIndex).filter((i) => i >= 0);
54
+ let where;
55
+ if (indexes.length === 0) where = "stream-level";
56
+ else {
57
+ const sample = indexes.slice(0, SAMPLE_INDEXES).join(", ");
58
+ const rest = indexes.length - SAMPLE_INDEXES;
59
+ where = `events ${sample}${rest > 0 ? ` (+${rest} more)` : ""}`;
60
+ }
61
+ const head = paint(SGR[first.severity], `${SYMBOL[first.severity]} ${first.rule}`, opts.color);
62
+ const meta = paint("2", `${first.severity.padEnd(7)} ×${group.length}`, opts.color);
63
+ const cite = paint("2", ` ↳ ${first.specUrl}`, opts.color);
64
+ lines.push(`${head} ${meta} ${title} — ${where}`, cite);
65
+ }
66
+ return lines.join("\n");
67
+ }
27
68
  function formatReportSummary(report, opts) {
28
69
  const { errors, warnings, info } = report.summary;
29
70
  const lines = [];
@@ -131,6 +172,7 @@ function toJUnit(report, opts) {
131
172
  }
132
173
  //#endregion
133
174
  exports.formatDiagnosticLine = formatDiagnosticLine;
175
+ exports.formatGroupedDiagnostics = formatGroupedDiagnostics;
134
176
  exports.formatReportSummary = formatReportSummary;
135
177
  exports.toJUnit = toJUnit;
136
178
  exports.toJsonReport = toJsonReport;
@@ -1 +1 @@
1
- {"version":3,"file":"report.cjs","names":["RULES"],"sources":["../src/report/pretty.ts","../src/report/json.ts","../src/report/sarif.ts","../src/report/junit.ts"],"sourcesContent":["// Human-readable output. Pure string formatting — the CLI decides where it\n// goes and whether a TTY wants color.\nimport type { Diagnostic, Report, Severity } from \"../types.js\"\n\nexport interface PrettyOptions {\n color: boolean\n}\n\nconst SYMBOL: Record<Severity, string> = { error: \"✖\", warning: \"⚠\", info: \"ℹ\" }\nconst SGR: Record<Severity, string> = { error: \"31\", warning: \"33\", info: \"36\" }\n\nfunction paint(code: string, s: string, on: boolean): string {\n return on ? `\\x1b[${code}m${s}\\x1b[0m` : s\n}\n\nexport function formatDiagnosticLine(d: Diagnostic, opts: PrettyOptions): string {\n const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : \"—\"\n const head = paint(SGR[d.severity], `${SYMBOL[d.severity]} ${d.rule}`, opts.color)\n const meta = paint(\"2\", `${d.severity.padEnd(7)} ${where.padEnd(10)}`, opts.color)\n const cite = paint(\"2\", ` ↳ ${d.specUrl}`, opts.color)\n return `${head} ${meta} ${d.message}\\n${cite}`\n}\n\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`\n}\n\nexport function formatReportSummary(report: Report, opts: PrettyOptions): string {\n const { errors, warnings, info } = report.summary\n const lines: string[] = []\n\n if (errors + warnings + info === 0) {\n lines.push(\n paint(\"32\", `✔ no conformance violations across ${count(report.eventCount, \"event\")}`, opts.color),\n )\n } else {\n lines.push(\n `${count(errors, \"error\")}, ${count(warnings, \"warning\")}, ${info} info across ${count(report.eventCount, \"event\")}`,\n )\n }\n\n const features = Object.entries(report.features)\n const exercised = features.filter(([, s]) => s === \"exercised\").map(([f]) => f)\n const suffix = exercised.length > 0 ? `: ${exercised.join(\", \")}` : \"\"\n lines.push(`${exercised.length} of ${features.length} AG-UI features exercised${suffix}`)\n\n if (report.skipped.length > 0) {\n lines.push(`${count(report.skipped.length, \"rule\")} not evaluated:`)\n for (const s of report.skipped) {\n lines.push(paint(\"2\", ` – ${s.rule}: ${s.reason}`, opts.color))\n }\n }\n\n for (const e of report.internalErrors) {\n lines.push(paint(\"31\", `! internal validator error: ${e}`, opts.color))\n }\n\n return lines.join(\"\\n\")\n}\n","// Machine-readable report: the core Report plus tool identification, so a\n// stored document is self-describing.\nimport type { Report } from \"../types.js\"\n\nexport interface JsonReportOptions {\n tool: { name: string; version: string }\n /** What was validated: URL, file path, or \"stdin\". */\n target?: string\n}\n\nexport interface JsonReportDocument extends Report {\n tool: { name: string; version: string }\n target?: string\n}\n\nexport function toJsonReport(report: Report, opts: JsonReportOptions): JsonReportDocument {\n const doc: JsonReportDocument = { tool: opts.tool, ...report }\n if (opts.target !== undefined) doc.target = opts.target\n return doc\n}\n","// SARIF 2.1.0 output for code-scanning integrations (e.g. GitHub).\n// Level mapping: error→error, warning→warning, info→note. When the input was a\n// line-oriented file (JSONL/captured SSE fed line-per-event is not guaranteed,\n// so only the caller knows), event N is reported at line N+1 via artifactUri.\nimport { RULES } from \"../rules/catalog.js\"\nimport type { Diagnostic, Report } from \"../types.js\"\n\nexport interface SarifOptions {\n toolVersion: string\n /** URI of the validated artifact; enables line-based locations. */\n artifactUri?: string\n}\n\nexport type SarifLevel = \"error\" | \"warning\" | \"note\"\n\nexport interface SarifResult {\n ruleId: string\n level: SarifLevel\n message: { text: string }\n locations: Array<{\n physicalLocation: {\n artifactLocation: { uri: string }\n region: { startLine: number }\n }\n }>\n}\n\nexport interface SarifLog {\n $schema: string\n version: \"2.1.0\"\n runs: Array<{\n tool: {\n driver: {\n name: string\n version: string\n informationUri: string\n rules: Array<{\n id: string\n helpUri: string\n shortDescription?: { text: string }\n defaultConfiguration?: { level: SarifLevel }\n }>\n }\n }\n results: SarifResult[]\n }>\n}\n\nconst LEVEL: Record<Diagnostic[\"severity\"], SarifLevel> = {\n error: \"error\",\n warning: \"warning\",\n info: \"note\",\n}\n\ntype SarifRule = {\n id: string\n helpUri: string\n shortDescription?: { text: string }\n defaultConfiguration?: { level: SarifLevel }\n}\n\nexport function toSarif(report: Report, opts: SarifOptions): SarifLog {\n const rules = new Map<string, SarifRule>()\n for (const d of report.diagnostics) {\n if (rules.has(d.rule)) continue\n const entry: SarifRule = { id: d.rule, helpUri: d.specUrl }\n const catalog = RULES.get(d.rule)\n if (catalog !== undefined) {\n entry.shortDescription = { text: catalog.title }\n entry.defaultConfiguration = { level: LEVEL[catalog.severity] }\n }\n rules.set(d.rule, entry)\n }\n\n const results: SarifResult[] = report.diagnostics.map((d) => ({\n ruleId: d.rule,\n level: LEVEL[d.severity],\n message: { text: d.message },\n locations:\n opts.artifactUri !== undefined && d.eventIndex >= 0\n ? [\n {\n physicalLocation: {\n artifactLocation: { uri: opts.artifactUri },\n region: { startLine: d.eventIndex + 1 },\n },\n },\n ]\n : [],\n }))\n\n return {\n $schema: \"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"ag-ui-validate\",\n version: opts.toolVersion,\n informationUri: \"https://github.com/langport-dev/ag-ui-validate\",\n rules: [...rules.values()],\n },\n },\n results,\n },\n ],\n }\n}\n","// JUnit XML output for CI systems. One testcase per finding: errors are\n// <failure>, warnings and info are <skipped> (visible but not build-breaking —\n// the exit code, not the XML, decides pass/fail). A clean report is a single\n// passing testcase so the suite is never empty.\nimport type { Report } from \"../types.js\"\n\nexport interface JUnitOptions {\n /** Suite name — typically the validated target. */\n name: string\n}\n\nfunction esc(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\n}\n\nexport function toJUnit(report: Report, opts: JUnitOptions): string {\n const lines: string[] = ['<?xml version=\"1.0\" encoding=\"UTF-8\"?>']\n const suite = esc(opts.name)\n\n if (report.diagnostics.length === 0) {\n lines.push('<testsuites name=\"ag-ui-validate\" tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\">')\n lines.push(` <testsuite name=\"${suite}\" tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\">`)\n lines.push(\n ` <testcase name=\"AG-UI conformance: no violations across ${report.eventCount} events\" classname=\"${suite}\"/>`,\n )\n lines.push(\" </testsuite>\")\n lines.push(\"</testsuites>\")\n return `${lines.join(\"\\n\")}\\n`\n }\n\n const failures = report.diagnostics.filter((d) => d.severity === \"error\").length\n const skipped = report.diagnostics.length - failures\n const counts = `tests=\"${report.diagnostics.length}\" failures=\"${failures}\" errors=\"0\" skipped=\"${skipped}\"`\n lines.push(`<testsuites name=\"ag-ui-validate\" ${counts}>`)\n lines.push(` <testsuite name=\"${suite}\" ${counts}>`)\n for (const d of report.diagnostics) {\n const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : \"stream\"\n const name = esc(`${d.rule} (${where})`)\n lines.push(` <testcase name=\"${name}\" classname=\"${suite}\">`)\n const body = `${esc(d.message)}\\n${esc(d.specUrl)}`\n if (d.severity === \"error\") {\n lines.push(` <failure message=\"${esc(d.message)}\">${body}</failure>`)\n } else {\n lines.push(` <skipped message=\"${esc(`${d.severity}: ${d.message}`)}\">${body}</skipped>`)\n }\n lines.push(\" </testcase>\")\n }\n lines.push(\" </testsuite>\")\n lines.push(\"</testsuites>\")\n return `${lines.join(\"\\n\")}\\n`\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"report.cjs","names":["RULES","RULES"],"sources":["../src/report/pretty.ts","../src/report/json.ts","../src/report/sarif.ts","../src/report/junit.ts"],"sourcesContent":["// Human-readable output. Pure string formatting — the CLI decides where it\n// goes and whether a TTY wants color.\nimport { RULES } from \"../rules/catalog.js\"\nimport type { Diagnostic, Report, Severity } from \"../types.js\"\n\nexport interface PrettyOptions {\n color: boolean\n}\n\nconst SYMBOL: Record<Severity, string> = { error: \"✖\", warning: \"⚠\", info: \"ℹ\" }\nconst SGR: Record<Severity, string> = { error: \"31\", warning: \"33\", info: \"36\" }\n\nfunction paint(code: string, s: string, on: boolean): string {\n return on ? `\\x1b[${code}m${s}\\x1b[0m` : s\n}\n\nexport function formatDiagnosticLine(d: Diagnostic, opts: PrettyOptions): string {\n const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : \"—\"\n const head = paint(SGR[d.severity], `${SYMBOL[d.severity]} ${d.rule}`, opts.color)\n const meta = paint(\"2\", `${d.severity.padEnd(7)} ${where.padEnd(10)}`, opts.color)\n const cite = paint(\"2\", ` ↳ ${d.specUrl}`, opts.color)\n return `${head} ${meta} ${d.message}\\n${cite}`\n}\n\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`\n}\n\nconst SEVERITY_RANK: Record<Severity, number> = { error: 0, warning: 1, info: 2 }\nconst SAMPLE_INDEXES = 3\n\n/**\n * One line per rule instead of one per occurrence — for large streams where\n * the same violation repeats. Totals stay honest in the summary; this only\n * changes what is listed.\n */\nexport function formatGroupedDiagnostics(diagnostics: Diagnostic[], opts: PrettyOptions): string {\n const groups = new Map<string, Diagnostic[]>()\n for (const d of diagnostics) {\n const list = groups.get(d.rule)\n if (list === undefined) groups.set(d.rule, [d])\n else list.push(d)\n }\n\n const sorted = [...groups.values()].sort((a, b) => {\n const bySeverity = SEVERITY_RANK[a[0]!.severity] - SEVERITY_RANK[b[0]!.severity]\n return bySeverity !== 0 ? bySeverity : a[0]!.rule.localeCompare(b[0]!.rule)\n })\n\n const lines: string[] = []\n for (const group of sorted) {\n const first = group[0]!\n const title = RULES.get(first.rule)?.title ?? first.message\n const indexes = group.map((d) => d.eventIndex).filter((i) => i >= 0)\n let where: string\n if (indexes.length === 0) {\n where = \"stream-level\"\n } else {\n const sample = indexes.slice(0, SAMPLE_INDEXES).join(\", \")\n const rest = indexes.length - SAMPLE_INDEXES\n where = `events ${sample}${rest > 0 ? ` (+${rest} more)` : \"\"}`\n }\n const head = paint(SGR[first.severity], `${SYMBOL[first.severity]} ${first.rule}`, opts.color)\n const meta = paint(\"2\", `${first.severity.padEnd(7)} ×${group.length}`, opts.color)\n const cite = paint(\"2\", ` ↳ ${first.specUrl}`, opts.color)\n lines.push(`${head} ${meta} ${title} — ${where}`, cite)\n }\n return lines.join(\"\\n\")\n}\n\nexport function formatReportSummary(report: Report, opts: PrettyOptions): string {\n const { errors, warnings, info } = report.summary\n const lines: string[] = []\n\n if (errors + warnings + info === 0) {\n lines.push(\n paint(\"32\", `✔ no conformance violations across ${count(report.eventCount, \"event\")}`, opts.color),\n )\n } else {\n lines.push(\n `${count(errors, \"error\")}, ${count(warnings, \"warning\")}, ${info} info across ${count(report.eventCount, \"event\")}`,\n )\n }\n\n const features = Object.entries(report.features)\n const exercised = features.filter(([, s]) => s === \"exercised\").map(([f]) => f)\n const suffix = exercised.length > 0 ? `: ${exercised.join(\", \")}` : \"\"\n lines.push(`${exercised.length} of ${features.length} AG-UI features exercised${suffix}`)\n\n if (report.skipped.length > 0) {\n lines.push(`${count(report.skipped.length, \"rule\")} not evaluated:`)\n for (const s of report.skipped) {\n lines.push(paint(\"2\", ` – ${s.rule}: ${s.reason}`, opts.color))\n }\n }\n\n for (const e of report.internalErrors) {\n lines.push(paint(\"31\", `! internal validator error: ${e}`, opts.color))\n }\n\n return lines.join(\"\\n\")\n}\n","// Machine-readable report: the core Report plus tool identification, so a\n// stored document is self-describing.\nimport type { Report } from \"../types.js\"\n\nexport interface JsonReportOptions {\n tool: { name: string; version: string }\n /** What was validated: URL, file path, or \"stdin\". */\n target?: string\n}\n\nexport interface JsonReportDocument extends Report {\n tool: { name: string; version: string }\n target?: string\n}\n\nexport function toJsonReport(report: Report, opts: JsonReportOptions): JsonReportDocument {\n const doc: JsonReportDocument = { tool: opts.tool, ...report }\n if (opts.target !== undefined) doc.target = opts.target\n return doc\n}\n","// SARIF 2.1.0 output for code-scanning integrations (e.g. GitHub).\n// Level mapping: error→error, warning→warning, info→note. When the input was a\n// line-oriented file (JSONL/captured SSE fed line-per-event is not guaranteed,\n// so only the caller knows), event N is reported at line N+1 via artifactUri.\nimport { RULES } from \"../rules/catalog.js\"\nimport type { Diagnostic, Report } from \"../types.js\"\n\nexport interface SarifOptions {\n toolVersion: string\n /** URI of the validated artifact; enables line-based locations. */\n artifactUri?: string\n}\n\nexport type SarifLevel = \"error\" | \"warning\" | \"note\"\n\nexport interface SarifResult {\n ruleId: string\n level: SarifLevel\n message: { text: string }\n locations: Array<{\n physicalLocation: {\n artifactLocation: { uri: string }\n region: { startLine: number }\n }\n }>\n}\n\nexport interface SarifLog {\n $schema: string\n version: \"2.1.0\"\n runs: Array<{\n tool: {\n driver: {\n name: string\n version: string\n informationUri: string\n rules: Array<{\n id: string\n helpUri: string\n shortDescription?: { text: string }\n defaultConfiguration?: { level: SarifLevel }\n }>\n }\n }\n results: SarifResult[]\n }>\n}\n\nconst LEVEL: Record<Diagnostic[\"severity\"], SarifLevel> = {\n error: \"error\",\n warning: \"warning\",\n info: \"note\",\n}\n\ntype SarifRule = {\n id: string\n helpUri: string\n shortDescription?: { text: string }\n defaultConfiguration?: { level: SarifLevel }\n}\n\nexport function toSarif(report: Report, opts: SarifOptions): SarifLog {\n const rules = new Map<string, SarifRule>()\n for (const d of report.diagnostics) {\n if (rules.has(d.rule)) continue\n const entry: SarifRule = { id: d.rule, helpUri: d.specUrl }\n const catalog = RULES.get(d.rule)\n if (catalog !== undefined) {\n entry.shortDescription = { text: catalog.title }\n entry.defaultConfiguration = { level: LEVEL[catalog.severity] }\n }\n rules.set(d.rule, entry)\n }\n\n const results: SarifResult[] = report.diagnostics.map((d) => ({\n ruleId: d.rule,\n level: LEVEL[d.severity],\n message: { text: d.message },\n locations:\n opts.artifactUri !== undefined && d.eventIndex >= 0\n ? [\n {\n physicalLocation: {\n artifactLocation: { uri: opts.artifactUri },\n region: { startLine: d.eventIndex + 1 },\n },\n },\n ]\n : [],\n }))\n\n return {\n $schema: \"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"ag-ui-validate\",\n version: opts.toolVersion,\n informationUri: \"https://github.com/langport-dev/ag-ui-validate\",\n rules: [...rules.values()],\n },\n },\n results,\n },\n ],\n }\n}\n","// JUnit XML output for CI systems. One testcase per finding: errors are\n// <failure>, warnings and info are <skipped> (visible but not build-breaking —\n// the exit code, not the XML, decides pass/fail). A clean report is a single\n// passing testcase so the suite is never empty.\nimport type { Report } from \"../types.js\"\n\nexport interface JUnitOptions {\n /** Suite name — typically the validated target. */\n name: string\n}\n\nfunction esc(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\n}\n\nexport function toJUnit(report: Report, opts: JUnitOptions): string {\n const lines: string[] = ['<?xml version=\"1.0\" encoding=\"UTF-8\"?>']\n const suite = esc(opts.name)\n\n if (report.diagnostics.length === 0) {\n lines.push('<testsuites name=\"ag-ui-validate\" tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\">')\n lines.push(` <testsuite name=\"${suite}\" tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\">`)\n lines.push(\n ` <testcase name=\"AG-UI conformance: no violations across ${report.eventCount} events\" classname=\"${suite}\"/>`,\n )\n lines.push(\" </testsuite>\")\n lines.push(\"</testsuites>\")\n return `${lines.join(\"\\n\")}\\n`\n }\n\n const failures = report.diagnostics.filter((d) => d.severity === \"error\").length\n const skipped = report.diagnostics.length - failures\n const counts = `tests=\"${report.diagnostics.length}\" failures=\"${failures}\" errors=\"0\" skipped=\"${skipped}\"`\n lines.push(`<testsuites name=\"ag-ui-validate\" ${counts}>`)\n lines.push(` <testsuite name=\"${suite}\" ${counts}>`)\n for (const d of report.diagnostics) {\n const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : \"stream\"\n const name = esc(`${d.rule} (${where})`)\n lines.push(` <testcase name=\"${name}\" classname=\"${suite}\">`)\n const body = `${esc(d.message)}\\n${esc(d.specUrl)}`\n if (d.severity === \"error\") {\n lines.push(` <failure message=\"${esc(d.message)}\">${body}</failure>`)\n } else {\n lines.push(` <skipped message=\"${esc(`${d.severity}: ${d.message}`)}\">${body}</skipped>`)\n }\n lines.push(\" </testcase>\")\n }\n lines.push(\" </testsuite>\")\n lines.push(\"</testsuites>\")\n return `${lines.join(\"\\n\")}\\n`\n}\n"],"mappings":"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"}
package/dist/report.d.cts CHANGED
@@ -4,6 +4,12 @@ interface PrettyOptions {
4
4
  color: boolean;
5
5
  }
6
6
  declare function formatDiagnosticLine(d: Diagnostic, opts: PrettyOptions): string;
7
+ /**
8
+ * One line per rule instead of one per occurrence — for large streams where
9
+ * the same violation repeats. Totals stay honest in the summary; this only
10
+ * changes what is listed.
11
+ */
12
+ declare function formatGroupedDiagnostics(diagnostics: Diagnostic[], opts: PrettyOptions): string;
7
13
  declare function formatReportSummary(report: Report, opts: PrettyOptions): string;
8
14
  //#endregion
9
15
  //#region src/report/json.d.ts
@@ -81,5 +87,5 @@ interface JUnitOptions {
81
87
  }
82
88
  declare function toJUnit(report: Report, opts: JUnitOptions): string;
83
89
  //#endregion
84
- export { type JUnitOptions, type JsonReportDocument, type JsonReportOptions, type PrettyOptions, type SarifLevel, type SarifLog, type SarifOptions, type SarifResult, formatDiagnosticLine, formatReportSummary, toJUnit, toJsonReport, toSarif };
90
+ export { type JUnitOptions, type JsonReportDocument, type JsonReportOptions, type PrettyOptions, type SarifLevel, type SarifLog, type SarifOptions, type SarifResult, formatDiagnosticLine, formatGroupedDiagnostics, formatReportSummary, toJUnit, toJsonReport, toSarif };
85
91
  //# sourceMappingURL=report.d.cts.map
package/dist/report.d.ts CHANGED
@@ -4,6 +4,12 @@ interface PrettyOptions {
4
4
  color: boolean;
5
5
  }
6
6
  declare function formatDiagnosticLine(d: Diagnostic, opts: PrettyOptions): string;
7
+ /**
8
+ * One line per rule instead of one per occurrence — for large streams where
9
+ * the same violation repeats. Totals stay honest in the summary; this only
10
+ * changes what is listed.
11
+ */
12
+ declare function formatGroupedDiagnostics(diagnostics: Diagnostic[], opts: PrettyOptions): string;
7
13
  declare function formatReportSummary(report: Report, opts: PrettyOptions): string;
8
14
  //#endregion
9
15
  //#region src/report/json.d.ts
@@ -81,5 +87,5 @@ interface JUnitOptions {
81
87
  }
82
88
  declare function toJUnit(report: Report, opts: JUnitOptions): string;
83
89
  //#endregion
84
- export { type JUnitOptions, type JsonReportDocument, type JsonReportOptions, type PrettyOptions, type SarifLevel, type SarifLog, type SarifOptions, type SarifResult, formatDiagnosticLine, formatReportSummary, toJUnit, toJsonReport, toSarif };
90
+ export { type JUnitOptions, type JsonReportDocument, type JsonReportOptions, type PrettyOptions, type SarifLevel, type SarifLog, type SarifOptions, type SarifResult, formatDiagnosticLine, formatGroupedDiagnostics, formatReportSummary, toJUnit, toJsonReport, toSarif };
85
91
  //# sourceMappingURL=report.d.ts.map
package/dist/report.js CHANGED
@@ -23,6 +23,47 @@ function formatDiagnosticLine(d, opts) {
23
23
  function count(n, noun) {
24
24
  return `${n} ${noun}${n === 1 ? "" : "s"}`;
25
25
  }
26
+ const SEVERITY_RANK = {
27
+ error: 0,
28
+ warning: 1,
29
+ info: 2
30
+ };
31
+ const SAMPLE_INDEXES = 3;
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
+ function formatGroupedDiagnostics(diagnostics, opts) {
38
+ const groups = /* @__PURE__ */ new Map();
39
+ for (const d of diagnostics) {
40
+ const list = groups.get(d.rule);
41
+ if (list === void 0) groups.set(d.rule, [d]);
42
+ else list.push(d);
43
+ }
44
+ const sorted = [...groups.values()].sort((a, b) => {
45
+ const bySeverity = SEVERITY_RANK[a[0].severity] - SEVERITY_RANK[b[0].severity];
46
+ return bySeverity !== 0 ? bySeverity : a[0].rule.localeCompare(b[0].rule);
47
+ });
48
+ const lines = [];
49
+ for (const group of sorted) {
50
+ const first = group[0];
51
+ const title = RULES.get(first.rule)?.title ?? first.message;
52
+ const indexes = group.map((d) => d.eventIndex).filter((i) => i >= 0);
53
+ let where;
54
+ if (indexes.length === 0) where = "stream-level";
55
+ else {
56
+ const sample = indexes.slice(0, SAMPLE_INDEXES).join(", ");
57
+ const rest = indexes.length - SAMPLE_INDEXES;
58
+ where = `events ${sample}${rest > 0 ? ` (+${rest} more)` : ""}`;
59
+ }
60
+ const head = paint(SGR[first.severity], `${SYMBOL[first.severity]} ${first.rule}`, opts.color);
61
+ const meta = paint("2", `${first.severity.padEnd(7)} ×${group.length}`, opts.color);
62
+ const cite = paint("2", ` ↳ ${first.specUrl}`, opts.color);
63
+ lines.push(`${head} ${meta} ${title} — ${where}`, cite);
64
+ }
65
+ return lines.join("\n");
66
+ }
26
67
  function formatReportSummary(report, opts) {
27
68
  const { errors, warnings, info } = report.summary;
28
69
  const lines = [];
@@ -129,6 +170,6 @@ function toJUnit(report, opts) {
129
170
  return `${lines.join("\n")}\n`;
130
171
  }
131
172
  //#endregion
132
- export { formatDiagnosticLine, formatReportSummary, toJUnit, toJsonReport, toSarif };
173
+ export { formatDiagnosticLine, formatGroupedDiagnostics, formatReportSummary, toJUnit, toJsonReport, toSarif };
133
174
 
134
175
  //# sourceMappingURL=report.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","names":[],"sources":["../src/report/pretty.ts","../src/report/json.ts","../src/report/sarif.ts","../src/report/junit.ts"],"sourcesContent":["// Human-readable output. Pure string formatting — the CLI decides where it\n// goes and whether a TTY wants color.\nimport type { Diagnostic, Report, Severity } from \"../types.js\"\n\nexport interface PrettyOptions {\n color: boolean\n}\n\nconst SYMBOL: Record<Severity, string> = { error: \"✖\", warning: \"⚠\", info: \"ℹ\" }\nconst SGR: Record<Severity, string> = { error: \"31\", warning: \"33\", info: \"36\" }\n\nfunction paint(code: string, s: string, on: boolean): string {\n return on ? `\\x1b[${code}m${s}\\x1b[0m` : s\n}\n\nexport function formatDiagnosticLine(d: Diagnostic, opts: PrettyOptions): string {\n const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : \"—\"\n const head = paint(SGR[d.severity], `${SYMBOL[d.severity]} ${d.rule}`, opts.color)\n const meta = paint(\"2\", `${d.severity.padEnd(7)} ${where.padEnd(10)}`, opts.color)\n const cite = paint(\"2\", ` ↳ ${d.specUrl}`, opts.color)\n return `${head} ${meta} ${d.message}\\n${cite}`\n}\n\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`\n}\n\nexport function formatReportSummary(report: Report, opts: PrettyOptions): string {\n const { errors, warnings, info } = report.summary\n const lines: string[] = []\n\n if (errors + warnings + info === 0) {\n lines.push(\n paint(\"32\", `✔ no conformance violations across ${count(report.eventCount, \"event\")}`, opts.color),\n )\n } else {\n lines.push(\n `${count(errors, \"error\")}, ${count(warnings, \"warning\")}, ${info} info across ${count(report.eventCount, \"event\")}`,\n )\n }\n\n const features = Object.entries(report.features)\n const exercised = features.filter(([, s]) => s === \"exercised\").map(([f]) => f)\n const suffix = exercised.length > 0 ? `: ${exercised.join(\", \")}` : \"\"\n lines.push(`${exercised.length} of ${features.length} AG-UI features exercised${suffix}`)\n\n if (report.skipped.length > 0) {\n lines.push(`${count(report.skipped.length, \"rule\")} not evaluated:`)\n for (const s of report.skipped) {\n lines.push(paint(\"2\", ` – ${s.rule}: ${s.reason}`, opts.color))\n }\n }\n\n for (const e of report.internalErrors) {\n lines.push(paint(\"31\", `! internal validator error: ${e}`, opts.color))\n }\n\n return lines.join(\"\\n\")\n}\n","// Machine-readable report: the core Report plus tool identification, so a\n// stored document is self-describing.\nimport type { Report } from \"../types.js\"\n\nexport interface JsonReportOptions {\n tool: { name: string; version: string }\n /** What was validated: URL, file path, or \"stdin\". */\n target?: string\n}\n\nexport interface JsonReportDocument extends Report {\n tool: { name: string; version: string }\n target?: string\n}\n\nexport function toJsonReport(report: Report, opts: JsonReportOptions): JsonReportDocument {\n const doc: JsonReportDocument = { tool: opts.tool, ...report }\n if (opts.target !== undefined) doc.target = opts.target\n return doc\n}\n","// SARIF 2.1.0 output for code-scanning integrations (e.g. GitHub).\n// Level mapping: error→error, warning→warning, info→note. When the input was a\n// line-oriented file (JSONL/captured SSE fed line-per-event is not guaranteed,\n// so only the caller knows), event N is reported at line N+1 via artifactUri.\nimport { RULES } from \"../rules/catalog.js\"\nimport type { Diagnostic, Report } from \"../types.js\"\n\nexport interface SarifOptions {\n toolVersion: string\n /** URI of the validated artifact; enables line-based locations. */\n artifactUri?: string\n}\n\nexport type SarifLevel = \"error\" | \"warning\" | \"note\"\n\nexport interface SarifResult {\n ruleId: string\n level: SarifLevel\n message: { text: string }\n locations: Array<{\n physicalLocation: {\n artifactLocation: { uri: string }\n region: { startLine: number }\n }\n }>\n}\n\nexport interface SarifLog {\n $schema: string\n version: \"2.1.0\"\n runs: Array<{\n tool: {\n driver: {\n name: string\n version: string\n informationUri: string\n rules: Array<{\n id: string\n helpUri: string\n shortDescription?: { text: string }\n defaultConfiguration?: { level: SarifLevel }\n }>\n }\n }\n results: SarifResult[]\n }>\n}\n\nconst LEVEL: Record<Diagnostic[\"severity\"], SarifLevel> = {\n error: \"error\",\n warning: \"warning\",\n info: \"note\",\n}\n\ntype SarifRule = {\n id: string\n helpUri: string\n shortDescription?: { text: string }\n defaultConfiguration?: { level: SarifLevel }\n}\n\nexport function toSarif(report: Report, opts: SarifOptions): SarifLog {\n const rules = new Map<string, SarifRule>()\n for (const d of report.diagnostics) {\n if (rules.has(d.rule)) continue\n const entry: SarifRule = { id: d.rule, helpUri: d.specUrl }\n const catalog = RULES.get(d.rule)\n if (catalog !== undefined) {\n entry.shortDescription = { text: catalog.title }\n entry.defaultConfiguration = { level: LEVEL[catalog.severity] }\n }\n rules.set(d.rule, entry)\n }\n\n const results: SarifResult[] = report.diagnostics.map((d) => ({\n ruleId: d.rule,\n level: LEVEL[d.severity],\n message: { text: d.message },\n locations:\n opts.artifactUri !== undefined && d.eventIndex >= 0\n ? [\n {\n physicalLocation: {\n artifactLocation: { uri: opts.artifactUri },\n region: { startLine: d.eventIndex + 1 },\n },\n },\n ]\n : [],\n }))\n\n return {\n $schema: \"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"ag-ui-validate\",\n version: opts.toolVersion,\n informationUri: \"https://github.com/langport-dev/ag-ui-validate\",\n rules: [...rules.values()],\n },\n },\n results,\n },\n ],\n }\n}\n","// JUnit XML output for CI systems. One testcase per finding: errors are\n// <failure>, warnings and info are <skipped> (visible but not build-breaking —\n// the exit code, not the XML, decides pass/fail). A clean report is a single\n// passing testcase so the suite is never empty.\nimport type { Report } from \"../types.js\"\n\nexport interface JUnitOptions {\n /** Suite name — typically the validated target. */\n name: string\n}\n\nfunction esc(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\n}\n\nexport function toJUnit(report: Report, opts: JUnitOptions): string {\n const lines: string[] = ['<?xml version=\"1.0\" encoding=\"UTF-8\"?>']\n const suite = esc(opts.name)\n\n if (report.diagnostics.length === 0) {\n lines.push('<testsuites name=\"ag-ui-validate\" tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\">')\n lines.push(` <testsuite name=\"${suite}\" tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\">`)\n lines.push(\n ` <testcase name=\"AG-UI conformance: no violations across ${report.eventCount} events\" classname=\"${suite}\"/>`,\n )\n lines.push(\" </testsuite>\")\n lines.push(\"</testsuites>\")\n return `${lines.join(\"\\n\")}\\n`\n }\n\n const failures = report.diagnostics.filter((d) => d.severity === \"error\").length\n const skipped = report.diagnostics.length - failures\n const counts = `tests=\"${report.diagnostics.length}\" failures=\"${failures}\" errors=\"0\" skipped=\"${skipped}\"`\n lines.push(`<testsuites name=\"ag-ui-validate\" ${counts}>`)\n lines.push(` <testsuite name=\"${suite}\" ${counts}>`)\n for (const d of report.diagnostics) {\n const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : \"stream\"\n const name = esc(`${d.rule} (${where})`)\n lines.push(` <testcase name=\"${name}\" classname=\"${suite}\">`)\n const body = `${esc(d.message)}\\n${esc(d.specUrl)}`\n if (d.severity === \"error\") {\n lines.push(` <failure message=\"${esc(d.message)}\">${body}</failure>`)\n } else {\n lines.push(` <skipped message=\"${esc(`${d.severity}: ${d.message}`)}\">${body}</skipped>`)\n }\n lines.push(\" </testcase>\")\n }\n lines.push(\" </testsuite>\")\n lines.push(\"</testsuites>\")\n return `${lines.join(\"\\n\")}\\n`\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"report.js","names":[],"sources":["../src/report/pretty.ts","../src/report/json.ts","../src/report/sarif.ts","../src/report/junit.ts"],"sourcesContent":["// Human-readable output. Pure string formatting — the CLI decides where it\n// goes and whether a TTY wants color.\nimport { RULES } from \"../rules/catalog.js\"\nimport type { Diagnostic, Report, Severity } from \"../types.js\"\n\nexport interface PrettyOptions {\n color: boolean\n}\n\nconst SYMBOL: Record<Severity, string> = { error: \"✖\", warning: \"⚠\", info: \"ℹ\" }\nconst SGR: Record<Severity, string> = { error: \"31\", warning: \"33\", info: \"36\" }\n\nfunction paint(code: string, s: string, on: boolean): string {\n return on ? `\\x1b[${code}m${s}\\x1b[0m` : s\n}\n\nexport function formatDiagnosticLine(d: Diagnostic, opts: PrettyOptions): string {\n const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : \"—\"\n const head = paint(SGR[d.severity], `${SYMBOL[d.severity]} ${d.rule}`, opts.color)\n const meta = paint(\"2\", `${d.severity.padEnd(7)} ${where.padEnd(10)}`, opts.color)\n const cite = paint(\"2\", ` ↳ ${d.specUrl}`, opts.color)\n return `${head} ${meta} ${d.message}\\n${cite}`\n}\n\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`\n}\n\nconst SEVERITY_RANK: Record<Severity, number> = { error: 0, warning: 1, info: 2 }\nconst SAMPLE_INDEXES = 3\n\n/**\n * One line per rule instead of one per occurrence — for large streams where\n * the same violation repeats. Totals stay honest in the summary; this only\n * changes what is listed.\n */\nexport function formatGroupedDiagnostics(diagnostics: Diagnostic[], opts: PrettyOptions): string {\n const groups = new Map<string, Diagnostic[]>()\n for (const d of diagnostics) {\n const list = groups.get(d.rule)\n if (list === undefined) groups.set(d.rule, [d])\n else list.push(d)\n }\n\n const sorted = [...groups.values()].sort((a, b) => {\n const bySeverity = SEVERITY_RANK[a[0]!.severity] - SEVERITY_RANK[b[0]!.severity]\n return bySeverity !== 0 ? bySeverity : a[0]!.rule.localeCompare(b[0]!.rule)\n })\n\n const lines: string[] = []\n for (const group of sorted) {\n const first = group[0]!\n const title = RULES.get(first.rule)?.title ?? first.message\n const indexes = group.map((d) => d.eventIndex).filter((i) => i >= 0)\n let where: string\n if (indexes.length === 0) {\n where = \"stream-level\"\n } else {\n const sample = indexes.slice(0, SAMPLE_INDEXES).join(\", \")\n const rest = indexes.length - SAMPLE_INDEXES\n where = `events ${sample}${rest > 0 ? ` (+${rest} more)` : \"\"}`\n }\n const head = paint(SGR[first.severity], `${SYMBOL[first.severity]} ${first.rule}`, opts.color)\n const meta = paint(\"2\", `${first.severity.padEnd(7)} ×${group.length}`, opts.color)\n const cite = paint(\"2\", ` ↳ ${first.specUrl}`, opts.color)\n lines.push(`${head} ${meta} ${title} — ${where}`, cite)\n }\n return lines.join(\"\\n\")\n}\n\nexport function formatReportSummary(report: Report, opts: PrettyOptions): string {\n const { errors, warnings, info } = report.summary\n const lines: string[] = []\n\n if (errors + warnings + info === 0) {\n lines.push(\n paint(\"32\", `✔ no conformance violations across ${count(report.eventCount, \"event\")}`, opts.color),\n )\n } else {\n lines.push(\n `${count(errors, \"error\")}, ${count(warnings, \"warning\")}, ${info} info across ${count(report.eventCount, \"event\")}`,\n )\n }\n\n const features = Object.entries(report.features)\n const exercised = features.filter(([, s]) => s === \"exercised\").map(([f]) => f)\n const suffix = exercised.length > 0 ? `: ${exercised.join(\", \")}` : \"\"\n lines.push(`${exercised.length} of ${features.length} AG-UI features exercised${suffix}`)\n\n if (report.skipped.length > 0) {\n lines.push(`${count(report.skipped.length, \"rule\")} not evaluated:`)\n for (const s of report.skipped) {\n lines.push(paint(\"2\", ` – ${s.rule}: ${s.reason}`, opts.color))\n }\n }\n\n for (const e of report.internalErrors) {\n lines.push(paint(\"31\", `! internal validator error: ${e}`, opts.color))\n }\n\n return lines.join(\"\\n\")\n}\n","// Machine-readable report: the core Report plus tool identification, so a\n// stored document is self-describing.\nimport type { Report } from \"../types.js\"\n\nexport interface JsonReportOptions {\n tool: { name: string; version: string }\n /** What was validated: URL, file path, or \"stdin\". */\n target?: string\n}\n\nexport interface JsonReportDocument extends Report {\n tool: { name: string; version: string }\n target?: string\n}\n\nexport function toJsonReport(report: Report, opts: JsonReportOptions): JsonReportDocument {\n const doc: JsonReportDocument = { tool: opts.tool, ...report }\n if (opts.target !== undefined) doc.target = opts.target\n return doc\n}\n","// SARIF 2.1.0 output for code-scanning integrations (e.g. GitHub).\n// Level mapping: error→error, warning→warning, info→note. When the input was a\n// line-oriented file (JSONL/captured SSE fed line-per-event is not guaranteed,\n// so only the caller knows), event N is reported at line N+1 via artifactUri.\nimport { RULES } from \"../rules/catalog.js\"\nimport type { Diagnostic, Report } from \"../types.js\"\n\nexport interface SarifOptions {\n toolVersion: string\n /** URI of the validated artifact; enables line-based locations. */\n artifactUri?: string\n}\n\nexport type SarifLevel = \"error\" | \"warning\" | \"note\"\n\nexport interface SarifResult {\n ruleId: string\n level: SarifLevel\n message: { text: string }\n locations: Array<{\n physicalLocation: {\n artifactLocation: { uri: string }\n region: { startLine: number }\n }\n }>\n}\n\nexport interface SarifLog {\n $schema: string\n version: \"2.1.0\"\n runs: Array<{\n tool: {\n driver: {\n name: string\n version: string\n informationUri: string\n rules: Array<{\n id: string\n helpUri: string\n shortDescription?: { text: string }\n defaultConfiguration?: { level: SarifLevel }\n }>\n }\n }\n results: SarifResult[]\n }>\n}\n\nconst LEVEL: Record<Diagnostic[\"severity\"], SarifLevel> = {\n error: \"error\",\n warning: \"warning\",\n info: \"note\",\n}\n\ntype SarifRule = {\n id: string\n helpUri: string\n shortDescription?: { text: string }\n defaultConfiguration?: { level: SarifLevel }\n}\n\nexport function toSarif(report: Report, opts: SarifOptions): SarifLog {\n const rules = new Map<string, SarifRule>()\n for (const d of report.diagnostics) {\n if (rules.has(d.rule)) continue\n const entry: SarifRule = { id: d.rule, helpUri: d.specUrl }\n const catalog = RULES.get(d.rule)\n if (catalog !== undefined) {\n entry.shortDescription = { text: catalog.title }\n entry.defaultConfiguration = { level: LEVEL[catalog.severity] }\n }\n rules.set(d.rule, entry)\n }\n\n const results: SarifResult[] = report.diagnostics.map((d) => ({\n ruleId: d.rule,\n level: LEVEL[d.severity],\n message: { text: d.message },\n locations:\n opts.artifactUri !== undefined && d.eventIndex >= 0\n ? [\n {\n physicalLocation: {\n artifactLocation: { uri: opts.artifactUri },\n region: { startLine: d.eventIndex + 1 },\n },\n },\n ]\n : [],\n }))\n\n return {\n $schema: \"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"ag-ui-validate\",\n version: opts.toolVersion,\n informationUri: \"https://github.com/langport-dev/ag-ui-validate\",\n rules: [...rules.values()],\n },\n },\n results,\n },\n ],\n }\n}\n","// JUnit XML output for CI systems. One testcase per finding: errors are\n// <failure>, warnings and info are <skipped> (visible but not build-breaking —\n// the exit code, not the XML, decides pass/fail). A clean report is a single\n// passing testcase so the suite is never empty.\nimport type { Report } from \"../types.js\"\n\nexport interface JUnitOptions {\n /** Suite name — typically the validated target. */\n name: string\n}\n\nfunction esc(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\")\n}\n\nexport function toJUnit(report: Report, opts: JUnitOptions): string {\n const lines: string[] = ['<?xml version=\"1.0\" encoding=\"UTF-8\"?>']\n const suite = esc(opts.name)\n\n if (report.diagnostics.length === 0) {\n lines.push('<testsuites name=\"ag-ui-validate\" tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\">')\n lines.push(` <testsuite name=\"${suite}\" tests=\"1\" failures=\"0\" errors=\"0\" skipped=\"0\">`)\n lines.push(\n ` <testcase name=\"AG-UI conformance: no violations across ${report.eventCount} events\" classname=\"${suite}\"/>`,\n )\n lines.push(\" </testsuite>\")\n lines.push(\"</testsuites>\")\n return `${lines.join(\"\\n\")}\\n`\n }\n\n const failures = report.diagnostics.filter((d) => d.severity === \"error\").length\n const skipped = report.diagnostics.length - failures\n const counts = `tests=\"${report.diagnostics.length}\" failures=\"${failures}\" errors=\"0\" skipped=\"${skipped}\"`\n lines.push(`<testsuites name=\"ag-ui-validate\" ${counts}>`)\n lines.push(` <testsuite name=\"${suite}\" ${counts}>`)\n for (const d of report.diagnostics) {\n const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : \"stream\"\n const name = esc(`${d.rule} (${where})`)\n lines.push(` <testcase name=\"${name}\" classname=\"${suite}\">`)\n const body = `${esc(d.message)}\\n${esc(d.specUrl)}`\n if (d.severity === \"error\") {\n lines.push(` <failure message=\"${esc(d.message)}\">${body}</failure>`)\n } else {\n lines.push(` <skipped message=\"${esc(`${d.severity}: ${d.message}`)}\">${body}</skipped>`)\n }\n lines.push(\" </testcase>\")\n }\n lines.push(\" </testsuite>\")\n lines.push(\"</testsuites>\")\n return `${lines.join(\"\\n\")}\\n`\n}\n"],"mappings":"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"}