ag-ui-validate 0.1.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/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/catalog-BglXBNbL.js +472 -0
- package/dist/catalog-BglXBNbL.js.map +1 -0
- package/dist/catalog-Ci9dqc1a.cjs +495 -0
- package/dist/catalog-Ci9dqc1a.cjs.map +1 -0
- package/dist/cli.js +2783 -0
- package/dist/cli.js.map +1 -0
- package/dist/index-Hmqj3r_r.d.cts +52 -0
- package/dist/index-oNG1kOp9.d.ts +52 -0
- package/dist/index.cjs +14 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/report.cjs +139 -0
- package/dist/report.cjs.map +1 -0
- package/dist/report.d.cts +85 -0
- package/dist/report.d.ts +85 -0
- package/dist/report.js +134 -0
- package/dist/report.js.map +1 -0
- package/dist/src-HmI-kxef.cjs +1596 -0
- package/dist/src-HmI-kxef.cjs.map +1 -0
- package/dist/src-rGZ2G4qA.js +1555 -0
- package/dist/src-rGZ2G4qA.js.map +1 -0
- package/dist/transport.cjs +329 -0
- package/dist/transport.cjs.map +1 -0
- package/dist/transport.d.cts +89 -0
- package/dist/transport.d.ts +89 -0
- package/dist/transport.js +323 -0
- package/dist/transport.js.map +1 -0
- package/dist/types-oH_QTnn2.d.cts +148 -0
- package/dist/types-oH_QTnn2.d.ts +148 -0
- package/dist/vitest.d.ts +28 -0
- package/dist/vitest.js +2089 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +127 -0
- package/src/cli-args.ts +202 -0
- package/src/cli.ts +147 -0
- package/src/index.ts +465 -0
- package/src/protocol/event-table.ts +316 -0
- package/src/protocol/jsonpatch.ts +220 -0
- package/src/report/index.ts +10 -0
- package/src/report/json.ts +20 -0
- package/src/report/junit.ts +56 -0
- package/src/report/pretty.ts +59 -0
- package/src/report/sarif.ts +109 -0
- package/src/rules/catalog.json +431 -0
- package/src/rules/catalog.ts +84 -0
- package/src/rules/checks/context.ts +117 -0
- package/src/rules/checks/lifecycle.ts +59 -0
- package/src/rules/checks/reasoning.ts +97 -0
- package/src/rules/checks/state.ts +72 -0
- package/src/rules/checks/text.ts +109 -0
- package/src/rules/checks/toolcalls.ts +167 -0
- package/src/rules/checks/transport.ts +17 -0
- package/src/transport/index.ts +331 -0
- package/src/transport/ndjson.ts +25 -0
- package/src/transport/sse.ts +126 -0
- package/src/types.ts +136 -0
- package/src/vitest/index.ts +19 -0
- package/src/vitest/matcher.ts +77 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// AUTO-GENERATED from @ag-ui/core v0.0.58 — do not edit by hand.
|
|
2
|
+
// Regenerate with: node scripts/generate-event-table.mjs
|
|
3
|
+
// test/protocol-drift.test.ts fails if this file drifts from the installed SDK.
|
|
4
|
+
|
|
5
|
+
export type FieldKind = "string" | "number" | "boolean" | "object" | "array" | "any"
|
|
6
|
+
|
|
7
|
+
export type EventCategory =
|
|
8
|
+
| "lifecycle"
|
|
9
|
+
| "text"
|
|
10
|
+
| "toolcall"
|
|
11
|
+
| "state"
|
|
12
|
+
| "activity"
|
|
13
|
+
| "reasoning"
|
|
14
|
+
| "thinking"
|
|
15
|
+
| "special"
|
|
16
|
+
|
|
17
|
+
export interface FieldSpec {
|
|
18
|
+
kind: FieldKind
|
|
19
|
+
required: boolean
|
|
20
|
+
enum?: readonly string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface EventSpec {
|
|
24
|
+
category: EventCategory
|
|
25
|
+
/** Present when the SDK marks the event @deprecated; names the replacement. */
|
|
26
|
+
deprecated?: string
|
|
27
|
+
/** Docs section governing this event. */
|
|
28
|
+
specUrl: string
|
|
29
|
+
/** Wire fields beyond the base-event fields (type, timestamp, rawEvent). */
|
|
30
|
+
fields: Readonly<Record<string, FieldSpec>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Version of @ag-ui/core this table was derived from. */
|
|
34
|
+
export const SDK_VERSION = "0.0.58"
|
|
35
|
+
|
|
36
|
+
/** Canonical wire event types and their schemas, derived from @ag-ui/core. */
|
|
37
|
+
export const EVENT_TABLE: Readonly<Record<string, EventSpec>> = {
|
|
38
|
+
"TEXT_MESSAGE_START": {
|
|
39
|
+
category: "text",
|
|
40
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#textmessagestart",
|
|
41
|
+
fields: {
|
|
42
|
+
"messageId": { kind: "string", required: true },
|
|
43
|
+
"role": { kind: "string", required: false, enum: ["developer","system","assistant","user"] },
|
|
44
|
+
"name": { kind: "string", required: false },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
"TEXT_MESSAGE_CONTENT": {
|
|
48
|
+
category: "text",
|
|
49
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#textmessagecontent",
|
|
50
|
+
fields: {
|
|
51
|
+
"messageId": { kind: "string", required: true },
|
|
52
|
+
"delta": { kind: "string", required: true },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
"TEXT_MESSAGE_END": {
|
|
56
|
+
category: "text",
|
|
57
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#textmessageend",
|
|
58
|
+
fields: {
|
|
59
|
+
"messageId": { kind: "string", required: true },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"TEXT_MESSAGE_CHUNK": {
|
|
63
|
+
category: "text",
|
|
64
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#textmessagechunk",
|
|
65
|
+
fields: {
|
|
66
|
+
"messageId": { kind: "string", required: false },
|
|
67
|
+
"role": { kind: "string", required: false, enum: ["developer","system","assistant","user"] },
|
|
68
|
+
"delta": { kind: "string", required: false },
|
|
69
|
+
"name": { kind: "string", required: false },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
"TOOL_CALL_START": {
|
|
73
|
+
category: "toolcall",
|
|
74
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallstart",
|
|
75
|
+
fields: {
|
|
76
|
+
"toolCallId": { kind: "string", required: true },
|
|
77
|
+
"toolCallName": { kind: "string", required: true },
|
|
78
|
+
"parentMessageId": { kind: "string", required: false },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"TOOL_CALL_ARGS": {
|
|
82
|
+
category: "toolcall",
|
|
83
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallargs",
|
|
84
|
+
fields: {
|
|
85
|
+
"toolCallId": { kind: "string", required: true },
|
|
86
|
+
"delta": { kind: "string", required: true },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"TOOL_CALL_END": {
|
|
90
|
+
category: "toolcall",
|
|
91
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallend",
|
|
92
|
+
fields: {
|
|
93
|
+
"toolCallId": { kind: "string", required: true },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
"TOOL_CALL_CHUNK": {
|
|
97
|
+
category: "toolcall",
|
|
98
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallchunk",
|
|
99
|
+
fields: {
|
|
100
|
+
"toolCallId": { kind: "string", required: false },
|
|
101
|
+
"toolCallName": { kind: "string", required: false },
|
|
102
|
+
"parentMessageId": { kind: "string", required: false },
|
|
103
|
+
"delta": { kind: "string", required: false },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
"TOOL_CALL_RESULT": {
|
|
107
|
+
category: "toolcall",
|
|
108
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#toolcallresult",
|
|
109
|
+
fields: {
|
|
110
|
+
"messageId": { kind: "string", required: true },
|
|
111
|
+
"toolCallId": { kind: "string", required: true },
|
|
112
|
+
"content": { kind: "string", required: true },
|
|
113
|
+
"role": { kind: "string", required: false, enum: ["tool"] },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"THINKING_START": {
|
|
117
|
+
category: "thinking",
|
|
118
|
+
deprecated: "REASONING_START",
|
|
119
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
120
|
+
fields: {
|
|
121
|
+
"title": { kind: "string", required: false },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
"THINKING_END": {
|
|
125
|
+
category: "thinking",
|
|
126
|
+
deprecated: "REASONING_END",
|
|
127
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
128
|
+
fields: {
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
"THINKING_TEXT_MESSAGE_START": {
|
|
132
|
+
category: "thinking",
|
|
133
|
+
deprecated: "REASONING_MESSAGE_START",
|
|
134
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
135
|
+
fields: {
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
"THINKING_TEXT_MESSAGE_CONTENT": {
|
|
139
|
+
category: "thinking",
|
|
140
|
+
deprecated: "REASONING_MESSAGE_CONTENT",
|
|
141
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
142
|
+
fields: {
|
|
143
|
+
"delta": { kind: "string", required: true },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
"THINKING_TEXT_MESSAGE_END": {
|
|
147
|
+
category: "thinking",
|
|
148
|
+
deprecated: "REASONING_MESSAGE_END",
|
|
149
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#thinking-events-deprecated",
|
|
150
|
+
fields: {
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
"STATE_SNAPSHOT": {
|
|
154
|
+
category: "state",
|
|
155
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#statesnapshot",
|
|
156
|
+
fields: {
|
|
157
|
+
"snapshot": { kind: "any", required: true },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
"STATE_DELTA": {
|
|
161
|
+
category: "state",
|
|
162
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#statedelta",
|
|
163
|
+
fields: {
|
|
164
|
+
"delta": { kind: "array", required: true },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
"MESSAGES_SNAPSHOT": {
|
|
168
|
+
category: "state",
|
|
169
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#messagessnapshot",
|
|
170
|
+
fields: {
|
|
171
|
+
"messages": { kind: "array", required: true },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
"ACTIVITY_SNAPSHOT": {
|
|
175
|
+
category: "activity",
|
|
176
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#activitysnapshot",
|
|
177
|
+
fields: {
|
|
178
|
+
"messageId": { kind: "string", required: true },
|
|
179
|
+
"activityType": { kind: "string", required: true },
|
|
180
|
+
"content": { kind: "object", required: true },
|
|
181
|
+
"replace": { kind: "boolean", required: false },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
"ACTIVITY_DELTA": {
|
|
185
|
+
category: "activity",
|
|
186
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#activitydelta",
|
|
187
|
+
fields: {
|
|
188
|
+
"messageId": { kind: "string", required: true },
|
|
189
|
+
"activityType": { kind: "string", required: true },
|
|
190
|
+
"patch": { kind: "array", required: true },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
"RAW": {
|
|
194
|
+
category: "special",
|
|
195
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#raw",
|
|
196
|
+
fields: {
|
|
197
|
+
"event": { kind: "any", required: true },
|
|
198
|
+
"source": { kind: "string", required: false },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
"CUSTOM": {
|
|
202
|
+
category: "special",
|
|
203
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#custom",
|
|
204
|
+
fields: {
|
|
205
|
+
"name": { kind: "string", required: true },
|
|
206
|
+
"value": { kind: "any", required: true },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
"RUN_STARTED": {
|
|
210
|
+
category: "lifecycle",
|
|
211
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#runstarted",
|
|
212
|
+
fields: {
|
|
213
|
+
"threadId": { kind: "string", required: true },
|
|
214
|
+
"runId": { kind: "string", required: true },
|
|
215
|
+
"parentRunId": { kind: "string", required: false },
|
|
216
|
+
"input": { kind: "object", required: false },
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
"RUN_FINISHED": {
|
|
220
|
+
category: "lifecycle",
|
|
221
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#runfinished",
|
|
222
|
+
fields: {
|
|
223
|
+
"threadId": { kind: "string", required: true },
|
|
224
|
+
"runId": { kind: "string", required: true },
|
|
225
|
+
"result": { kind: "any", required: false },
|
|
226
|
+
"outcome": { kind: "object", required: false },
|
|
227
|
+
"usage": { kind: "array", required: false },
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
"RUN_ERROR": {
|
|
231
|
+
category: "lifecycle",
|
|
232
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#runerror",
|
|
233
|
+
fields: {
|
|
234
|
+
"message": { kind: "string", required: true },
|
|
235
|
+
"code": { kind: "string", required: false },
|
|
236
|
+
"usage": { kind: "array", required: false },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
"STEP_STARTED": {
|
|
240
|
+
category: "lifecycle",
|
|
241
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#stepstarted",
|
|
242
|
+
fields: {
|
|
243
|
+
"stepName": { kind: "string", required: true },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
"STEP_FINISHED": {
|
|
247
|
+
category: "lifecycle",
|
|
248
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#stepfinished",
|
|
249
|
+
fields: {
|
|
250
|
+
"stepName": { kind: "string", required: true },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
"REASONING_START": {
|
|
254
|
+
category: "reasoning",
|
|
255
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningstart",
|
|
256
|
+
fields: {
|
|
257
|
+
"messageId": { kind: "string", required: true },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
"REASONING_MESSAGE_START": {
|
|
261
|
+
category: "reasoning",
|
|
262
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessagestart",
|
|
263
|
+
fields: {
|
|
264
|
+
"messageId": { kind: "string", required: true },
|
|
265
|
+
"role": { kind: "string", required: true, enum: ["reasoning"] },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
"REASONING_MESSAGE_CONTENT": {
|
|
269
|
+
category: "reasoning",
|
|
270
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessagecontent",
|
|
271
|
+
fields: {
|
|
272
|
+
"messageId": { kind: "string", required: true },
|
|
273
|
+
"delta": { kind: "string", required: true },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
"REASONING_MESSAGE_END": {
|
|
277
|
+
category: "reasoning",
|
|
278
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessageend",
|
|
279
|
+
fields: {
|
|
280
|
+
"messageId": { kind: "string", required: true },
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
"REASONING_MESSAGE_CHUNK": {
|
|
284
|
+
category: "reasoning",
|
|
285
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningmessagechunk",
|
|
286
|
+
fields: {
|
|
287
|
+
"messageId": { kind: "string", required: false },
|
|
288
|
+
"delta": { kind: "string", required: false },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
"REASONING_END": {
|
|
292
|
+
category: "reasoning",
|
|
293
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningend",
|
|
294
|
+
fields: {
|
|
295
|
+
"messageId": { kind: "string", required: true },
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
"REASONING_ENCRYPTED_VALUE": {
|
|
299
|
+
category: "reasoning",
|
|
300
|
+
specUrl: "https://docs.ag-ui.com/concepts/events#reasoningencryptedvalue",
|
|
301
|
+
fields: {
|
|
302
|
+
"subtype": { kind: "string", required: true, enum: ["tool-call","message"] },
|
|
303
|
+
"entityId": { kind: "string", required: true },
|
|
304
|
+
"encryptedValue": { kind: "string", required: true },
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** All canonical wire `type` values, in @ag-ui/core enum order. */
|
|
310
|
+
export const EVENT_TYPES: readonly string[] = Object.keys(EVENT_TABLE)
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Wire types documented as drafts (https://docs.ag-ui.com/drafts/overview) but
|
|
314
|
+
* not yet in @ag-ui/core. Not errors: reported at info severity.
|
|
315
|
+
*/
|
|
316
|
+
export const DRAFT_EVENT_TYPES: readonly string[] = ["META"]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Minimal RFC 6902 (JSON Patch) + RFC 6901 (JSON Pointer) implementation.
|
|
2
|
+
// Hand-rolled so the core keeps zero runtime dependencies. Immutable: the
|
|
3
|
+
// input document is never mutated; ops apply atomically (all or none).
|
|
4
|
+
|
|
5
|
+
export type PatchResult =
|
|
6
|
+
| { ok: true; result: unknown }
|
|
7
|
+
| { ok: false; error: string; opIndex: number }
|
|
8
|
+
|
|
9
|
+
export interface PatchShapeProblem {
|
|
10
|
+
/** Human-readable problem. */
|
|
11
|
+
error: string
|
|
12
|
+
/** Index of the offending op, or -1 when the document itself is not an array. */
|
|
13
|
+
opIndex: number
|
|
14
|
+
/** RFC 6901 pointer into the patch document, e.g. "/0/op". */
|
|
15
|
+
pointer: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const OPS = ["add", "remove", "replace", "move", "copy", "test"] as const
|
|
19
|
+
type OpName = (typeof OPS)[number]
|
|
20
|
+
|
|
21
|
+
/** RFC 6901: "" → whole document; "/a/b" → ["a", "b"]. Returns null when invalid. */
|
|
22
|
+
export function parsePointer(pointer: unknown): string[] | null {
|
|
23
|
+
if (typeof pointer !== "string") return null
|
|
24
|
+
if (pointer === "") return []
|
|
25
|
+
if (!pointer.startsWith("/")) return null
|
|
26
|
+
return pointer
|
|
27
|
+
.slice(1)
|
|
28
|
+
.split("/")
|
|
29
|
+
.map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Structural validation of a patch document, without applying it. */
|
|
33
|
+
export function validatePatchShape(patch: unknown): PatchShapeProblem | null {
|
|
34
|
+
if (!Array.isArray(patch)) {
|
|
35
|
+
return { error: "patch document must be an array of operations", opIndex: -1, pointer: "" }
|
|
36
|
+
}
|
|
37
|
+
for (let i = 0; i < patch.length; i++) {
|
|
38
|
+
const op = patch[i]
|
|
39
|
+
if (typeof op !== "object" || op === null || Array.isArray(op)) {
|
|
40
|
+
return { error: `operation ${i} is not an object`, opIndex: i, pointer: `/${i}` }
|
|
41
|
+
}
|
|
42
|
+
const o = op as Record<string, unknown>
|
|
43
|
+
if (!OPS.includes(o.op as OpName)) {
|
|
44
|
+
return { error: `operation ${i} has invalid op '${String(o.op)}'`, opIndex: i, pointer: `/${i}/op` }
|
|
45
|
+
}
|
|
46
|
+
if (parsePointer(o.path) === null) {
|
|
47
|
+
return { error: `operation ${i} (${String(o.op)}) has invalid path`, opIndex: i, pointer: `/${i}/path` }
|
|
48
|
+
}
|
|
49
|
+
if ((o.op === "add" || o.op === "replace" || o.op === "test") && !("value" in o)) {
|
|
50
|
+
return { error: `operation ${i} (${String(o.op)}) is missing value`, opIndex: i, pointer: `/${i}` }
|
|
51
|
+
}
|
|
52
|
+
if ((o.op === "move" || o.op === "copy") && parsePointer(o.from) === null) {
|
|
53
|
+
return { error: `operation ${i} (${String(o.op)}) is missing or has invalid from`, opIndex: i, pointer: `/${i}` }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clone(value: unknown): unknown {
|
|
60
|
+
if (Array.isArray(value)) return value.map(clone)
|
|
61
|
+
if (typeof value === "object" && value !== null) {
|
|
62
|
+
const out: Record<string, unknown> = {}
|
|
63
|
+
for (const [k, v] of Object.entries(value)) out[k] = clone(v)
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
return value
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
70
|
+
if (a === b) return true
|
|
71
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
72
|
+
return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]))
|
|
73
|
+
}
|
|
74
|
+
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null && !Array.isArray(a) && !Array.isArray(b)) {
|
|
75
|
+
const ka = Object.keys(a)
|
|
76
|
+
const kb = Object.keys(b as object)
|
|
77
|
+
return ka.length === kb.length && ka.every((k) => deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]))
|
|
78
|
+
}
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function arrayIndex(token: string, length: number, allowAppend: boolean): number | null {
|
|
83
|
+
if (allowAppend && token === "-") return length
|
|
84
|
+
if (!/^(0|[1-9]\d*)$/.test(token)) return null
|
|
85
|
+
const i = Number(token)
|
|
86
|
+
return i > length ? null : i
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface Located {
|
|
90
|
+
parent: Record<string, unknown> | unknown[] | null // null → the document root itself
|
|
91
|
+
key: string | number
|
|
92
|
+
exists: boolean
|
|
93
|
+
value: unknown
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function locate(doc: unknown, tokens: string[], forAdd: boolean): Located | string {
|
|
97
|
+
if (tokens.length === 0) return { parent: null, key: "", exists: true, value: doc }
|
|
98
|
+
let current = doc
|
|
99
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
100
|
+
const token = tokens[i]!
|
|
101
|
+
if (Array.isArray(current)) {
|
|
102
|
+
const idx = arrayIndex(token, current.length, false)
|
|
103
|
+
if (idx === null || idx >= current.length) return `path segment '/${token}' does not exist`
|
|
104
|
+
current = current[idx]
|
|
105
|
+
} else if (typeof current === "object" && current !== null) {
|
|
106
|
+
const obj = current as Record<string, unknown>
|
|
107
|
+
if (!(token in obj)) return `path segment '/${token}' does not exist`
|
|
108
|
+
current = obj[token]
|
|
109
|
+
} else {
|
|
110
|
+
return `path segment '/${token}' is not an object or array`
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const last = tokens[tokens.length - 1]!
|
|
114
|
+
if (Array.isArray(current)) {
|
|
115
|
+
const idx = arrayIndex(last, current.length, forAdd)
|
|
116
|
+
if (idx === null) return `'${last}' is not a valid index for an array of length ${current.length}`
|
|
117
|
+
return { parent: current, key: idx, exists: idx < current.length, value: current[idx] }
|
|
118
|
+
}
|
|
119
|
+
if (typeof current === "object" && current !== null) {
|
|
120
|
+
const obj = current as Record<string, unknown>
|
|
121
|
+
return { parent: obj, key: last, exists: last in obj, value: obj[last] }
|
|
122
|
+
}
|
|
123
|
+
return `target of '/${last}' is not an object or array`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Applies an RFC 6902 patch. Validates shape first; never throws. */
|
|
127
|
+
export function applyPatch(doc: unknown, patch: unknown): PatchResult {
|
|
128
|
+
const shape = validatePatchShape(patch)
|
|
129
|
+
if (shape !== null) return { ok: false, error: shape.error, opIndex: shape.opIndex }
|
|
130
|
+
|
|
131
|
+
let result = clone(doc)
|
|
132
|
+
const ops = patch as Record<string, unknown>[]
|
|
133
|
+
|
|
134
|
+
const getAt = (pointer: string): { ok: true; value: unknown } | { ok: false; error: string } => {
|
|
135
|
+
const loc = locate(result, parsePointer(pointer)!, false)
|
|
136
|
+
if (typeof loc === "string") return { ok: false, error: `${pointer}: ${loc}` }
|
|
137
|
+
if (!loc.exists) return { ok: false, error: `${pointer} does not exist` }
|
|
138
|
+
return { ok: true, value: loc.value }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const setAt = (pointer: string, value: unknown, mustExist: boolean): string | null => {
|
|
142
|
+
const tokens = parsePointer(pointer)!
|
|
143
|
+
if (tokens.length === 0) {
|
|
144
|
+
result = value
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
const loc = locate(result, tokens, !mustExist)
|
|
148
|
+
if (typeof loc === "string") return `${pointer}: ${loc}`
|
|
149
|
+
if (mustExist && !loc.exists) return `${pointer} does not exist`
|
|
150
|
+
if (Array.isArray(loc.parent)) {
|
|
151
|
+
if (mustExist) loc.parent[loc.key as number] = value
|
|
152
|
+
else loc.parent.splice(loc.key as number, 0, value)
|
|
153
|
+
} else {
|
|
154
|
+
loc.parent![loc.key as string] = value
|
|
155
|
+
}
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const removeAt = (pointer: string): { ok: true; removed: unknown } | { ok: false; error: string } => {
|
|
160
|
+
const tokens = parsePointer(pointer)!
|
|
161
|
+
if (tokens.length === 0) return { ok: false, error: "cannot remove the whole document" }
|
|
162
|
+
const loc = locate(result, tokens, false)
|
|
163
|
+
if (typeof loc === "string") return { ok: false, error: `${pointer}: ${loc}` }
|
|
164
|
+
if (!loc.exists) return { ok: false, error: `${pointer} does not exist` }
|
|
165
|
+
if (Array.isArray(loc.parent)) loc.parent.splice(loc.key as number, 1)
|
|
166
|
+
else delete loc.parent![loc.key as string]
|
|
167
|
+
return { ok: true, removed: loc.value }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < ops.length; i++) {
|
|
171
|
+
const op = ops[i]!
|
|
172
|
+
const path = op.path as string
|
|
173
|
+
let error: string | null = null
|
|
174
|
+
|
|
175
|
+
switch (op.op as OpName) {
|
|
176
|
+
case "add":
|
|
177
|
+
error = setAt(path, clone(op.value), false)
|
|
178
|
+
break
|
|
179
|
+
case "replace":
|
|
180
|
+
error = setAt(path, clone(op.value), true)
|
|
181
|
+
break
|
|
182
|
+
case "remove": {
|
|
183
|
+
const r = removeAt(path)
|
|
184
|
+
if (!r.ok) error = r.error
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
case "move": {
|
|
188
|
+
const from = op.from as string
|
|
189
|
+
if (path.startsWith(`${from}/`)) {
|
|
190
|
+
error = `cannot move ${from} into its own child ${path}`
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
const r = removeAt(from)
|
|
194
|
+
if (!r.ok) { error = r.error; break }
|
|
195
|
+
error = setAt(path, r.removed, false)
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
case "copy": {
|
|
199
|
+
const r = getAt(op.from as string)
|
|
200
|
+
if (!r.ok) { error = r.error; break }
|
|
201
|
+
error = setAt(path, clone(r.value), false)
|
|
202
|
+
break
|
|
203
|
+
}
|
|
204
|
+
case "test": {
|
|
205
|
+
const r = getAt(path)
|
|
206
|
+
if (!r.ok) error = r.error
|
|
207
|
+
else if (!deepEqual(r.value, op.value)) error = `test failed at ${path}`
|
|
208
|
+
break
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (error !== null) return { ok: false, error, opIndex: i }
|
|
213
|
+
}
|
|
214
|
+
return { ok: true, result }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** RFC 6901 token escaping, for building diagnostic pointers. */
|
|
218
|
+
export function escapePointerToken(token: string): string {
|
|
219
|
+
return token.replace(/~/g, "~0").replace(/\//g, "~1")
|
|
220
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// ag-ui-validate/report: pure reporters over a core Report. No I/O — callers
|
|
2
|
+
// decide where the strings go.
|
|
3
|
+
export { formatDiagnosticLine, formatReportSummary } from "./pretty.js"
|
|
4
|
+
export type { PrettyOptions } from "./pretty.js"
|
|
5
|
+
export { toJsonReport } from "./json.js"
|
|
6
|
+
export type { JsonReportDocument, JsonReportOptions } from "./json.js"
|
|
7
|
+
export { toSarif } from "./sarif.js"
|
|
8
|
+
export type { SarifLevel, SarifLog, SarifOptions, SarifResult } from "./sarif.js"
|
|
9
|
+
export { toJUnit } from "./junit.js"
|
|
10
|
+
export type { JUnitOptions } from "./junit.js"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Machine-readable report: the core Report plus tool identification, so a
|
|
2
|
+
// stored document is self-describing.
|
|
3
|
+
import type { Report } from "../types.js"
|
|
4
|
+
|
|
5
|
+
export interface JsonReportOptions {
|
|
6
|
+
tool: { name: string; version: string }
|
|
7
|
+
/** What was validated: URL, file path, or "stdin". */
|
|
8
|
+
target?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface JsonReportDocument extends Report {
|
|
12
|
+
tool: { name: string; version: string }
|
|
13
|
+
target?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function toJsonReport(report: Report, opts: JsonReportOptions): JsonReportDocument {
|
|
17
|
+
const doc: JsonReportDocument = { tool: opts.tool, ...report }
|
|
18
|
+
if (opts.target !== undefined) doc.target = opts.target
|
|
19
|
+
return doc
|
|
20
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// JUnit XML output for CI systems. One testcase per finding: errors are
|
|
2
|
+
// <failure>, warnings and info are <skipped> (visible but not build-breaking —
|
|
3
|
+
// the exit code, not the XML, decides pass/fail). A clean report is a single
|
|
4
|
+
// passing testcase so the suite is never empty.
|
|
5
|
+
import type { Report } from "../types.js"
|
|
6
|
+
|
|
7
|
+
export interface JUnitOptions {
|
|
8
|
+
/** Suite name — typically the validated target. */
|
|
9
|
+
name: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function esc(s: string): string {
|
|
13
|
+
return s
|
|
14
|
+
.replace(/&/g, "&")
|
|
15
|
+
.replace(/</g, "<")
|
|
16
|
+
.replace(/>/g, ">")
|
|
17
|
+
.replace(/"/g, """)
|
|
18
|
+
.replace(/'/g, "'")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function toJUnit(report: Report, opts: JUnitOptions): string {
|
|
22
|
+
const lines: string[] = ['<?xml version="1.0" encoding="UTF-8"?>']
|
|
23
|
+
const suite = esc(opts.name)
|
|
24
|
+
|
|
25
|
+
if (report.diagnostics.length === 0) {
|
|
26
|
+
lines.push('<testsuites name="ag-ui-validate" tests="1" failures="0" errors="0" skipped="0">')
|
|
27
|
+
lines.push(` <testsuite name="${suite}" tests="1" failures="0" errors="0" skipped="0">`)
|
|
28
|
+
lines.push(
|
|
29
|
+
` <testcase name="AG-UI conformance: no violations across ${report.eventCount} events" classname="${suite}"/>`,
|
|
30
|
+
)
|
|
31
|
+
lines.push(" </testsuite>")
|
|
32
|
+
lines.push("</testsuites>")
|
|
33
|
+
return `${lines.join("\n")}\n`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const failures = report.diagnostics.filter((d) => d.severity === "error").length
|
|
37
|
+
const skipped = report.diagnostics.length - failures
|
|
38
|
+
const counts = `tests="${report.diagnostics.length}" failures="${failures}" errors="0" skipped="${skipped}"`
|
|
39
|
+
lines.push(`<testsuites name="ag-ui-validate" ${counts}>`)
|
|
40
|
+
lines.push(` <testsuite name="${suite}" ${counts}>`)
|
|
41
|
+
for (const d of report.diagnostics) {
|
|
42
|
+
const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : "stream"
|
|
43
|
+
const name = esc(`${d.rule} (${where})`)
|
|
44
|
+
lines.push(` <testcase name="${name}" classname="${suite}">`)
|
|
45
|
+
const body = `${esc(d.message)}\n${esc(d.specUrl)}`
|
|
46
|
+
if (d.severity === "error") {
|
|
47
|
+
lines.push(` <failure message="${esc(d.message)}">${body}</failure>`)
|
|
48
|
+
} else {
|
|
49
|
+
lines.push(` <skipped message="${esc(`${d.severity}: ${d.message}`)}">${body}</skipped>`)
|
|
50
|
+
}
|
|
51
|
+
lines.push(" </testcase>")
|
|
52
|
+
}
|
|
53
|
+
lines.push(" </testsuite>")
|
|
54
|
+
lines.push("</testsuites>")
|
|
55
|
+
return `${lines.join("\n")}\n`
|
|
56
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Human-readable output. Pure string formatting — the CLI decides where it
|
|
2
|
+
// goes and whether a TTY wants color.
|
|
3
|
+
import type { Diagnostic, Report, Severity } from "../types.js"
|
|
4
|
+
|
|
5
|
+
export interface PrettyOptions {
|
|
6
|
+
color: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const SYMBOL: Record<Severity, string> = { error: "✖", warning: "⚠", info: "ℹ" }
|
|
10
|
+
const SGR: Record<Severity, string> = { error: "31", warning: "33", info: "36" }
|
|
11
|
+
|
|
12
|
+
function paint(code: string, s: string, on: boolean): string {
|
|
13
|
+
return on ? `\x1b[${code}m${s}\x1b[0m` : s
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatDiagnosticLine(d: Diagnostic, opts: PrettyOptions): string {
|
|
17
|
+
const where = d.eventIndex >= 0 ? `event ${d.eventIndex}` : "—"
|
|
18
|
+
const head = paint(SGR[d.severity], `${SYMBOL[d.severity]} ${d.rule}`, opts.color)
|
|
19
|
+
const meta = paint("2", `${d.severity.padEnd(7)} ${where.padEnd(10)}`, opts.color)
|
|
20
|
+
const cite = paint("2", ` ↳ ${d.specUrl}`, opts.color)
|
|
21
|
+
return `${head} ${meta} ${d.message}\n${cite}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function count(n: number, noun: string): string {
|
|
25
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatReportSummary(report: Report, opts: PrettyOptions): string {
|
|
29
|
+
const { errors, warnings, info } = report.summary
|
|
30
|
+
const lines: string[] = []
|
|
31
|
+
|
|
32
|
+
if (errors + warnings + info === 0) {
|
|
33
|
+
lines.push(
|
|
34
|
+
paint("32", `✔ no conformance violations across ${count(report.eventCount, "event")}`, opts.color),
|
|
35
|
+
)
|
|
36
|
+
} else {
|
|
37
|
+
lines.push(
|
|
38
|
+
`${count(errors, "error")}, ${count(warnings, "warning")}, ${info} info across ${count(report.eventCount, "event")}`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const features = Object.entries(report.features)
|
|
43
|
+
const exercised = features.filter(([, s]) => s === "exercised").map(([f]) => f)
|
|
44
|
+
const suffix = exercised.length > 0 ? `: ${exercised.join(", ")}` : ""
|
|
45
|
+
lines.push(`${exercised.length} of ${features.length} AG-UI features exercised${suffix}`)
|
|
46
|
+
|
|
47
|
+
if (report.skipped.length > 0) {
|
|
48
|
+
lines.push(`${count(report.skipped.length, "rule")} not evaluated:`)
|
|
49
|
+
for (const s of report.skipped) {
|
|
50
|
+
lines.push(paint("2", ` – ${s.rule}: ${s.reason}`, opts.color))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const e of report.internalErrors) {
|
|
55
|
+
lines.push(paint("31", `! internal validator error: ${e}`, opts.color))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return lines.join("\n")
|
|
59
|
+
}
|