opencode-swarm-plugin 0.12.24 → 0.12.26
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/.beads/issues.jsonl +17 -0
- package/README.md +105 -0
- package/examples/plugin-wrapper-template.ts +119 -0
- package/global-skills/agent-patterns/SKILL.md +682 -0
- package/global-skills/learning-systems/SKILL.md +644 -0
- package/global-skills/mcp-tool-authoring/SKILL.md +695 -0
- package/global-skills/resilience-patterns/SKILL.md +648 -0
- package/global-skills/tacit-knowledge-extraction/SKILL.md +387 -0
- package/global-skills/testing-strategies/SKILL.md +558 -0
- package/global-skills/zod-validation/SKILL.md +763 -0
- package/package.json +1 -1
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zod-validation
|
|
3
|
+
description: Schema validation patterns with Zod for runtime type safety. Use when defining data structures, validating tool arguments, parsing API responses, or creating type-safe schemas. Covers composition, refinements, error formatting, and TypeScript inference.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Zod Validation Patterns
|
|
7
|
+
|
|
8
|
+
Schema validation with Zod for runtime type safety. Zod provides parse-don't-validate semantics: schemas both validate AND transform data, with full TypeScript inference.
|
|
9
|
+
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// Define schema
|
|
16
|
+
const UserSchema = z.object({
|
|
17
|
+
name: z.string().min(1),
|
|
18
|
+
age: z.number().int().min(0),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Infer TypeScript type
|
|
22
|
+
type User = z.infer<typeof UserSchema>;
|
|
23
|
+
|
|
24
|
+
// Parse (throws on invalid)
|
|
25
|
+
const user = UserSchema.parse(data);
|
|
26
|
+
|
|
27
|
+
// Safe parse (returns result object)
|
|
28
|
+
const result = UserSchema.safeParse(data);
|
|
29
|
+
if (result.success) {
|
|
30
|
+
const user = result.data;
|
|
31
|
+
} else {
|
|
32
|
+
const errors = result.error;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Schema Basics
|
|
37
|
+
|
|
38
|
+
### Primitives
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
z.string(); // any string
|
|
42
|
+
z.string().min(1); // non-empty string
|
|
43
|
+
z.string().max(100); // max length
|
|
44
|
+
z.string().email(); // email validation
|
|
45
|
+
z.string().url(); // URL validation
|
|
46
|
+
z.string().uuid(); // UUID validation
|
|
47
|
+
z.string().regex(/^[a-z]+$/); // regex validation
|
|
48
|
+
|
|
49
|
+
z.number(); // any number
|
|
50
|
+
z.number().int(); // integer only
|
|
51
|
+
z.number().positive(); // > 0
|
|
52
|
+
z.number().nonnegative(); // >= 0
|
|
53
|
+
z.number().min(0).max(100); // range
|
|
54
|
+
|
|
55
|
+
z.boolean(); // true/false
|
|
56
|
+
z.date(); // Date object
|
|
57
|
+
z.null(); // null
|
|
58
|
+
z.undefined(); // undefined
|
|
59
|
+
z.unknown(); // any value (no validation)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Strings with Format Validation
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// ISO-8601 datetime with timezone offset
|
|
66
|
+
z.string().datetime({ offset: true });
|
|
67
|
+
// Examples: "2025-01-15T10:30:00Z", "2025-01-15T10:30:00-05:00"
|
|
68
|
+
|
|
69
|
+
// Custom format validation
|
|
70
|
+
z.string().regex(
|
|
71
|
+
/^[a-z0-9]+(-[a-z0-9]+)+(\.[\w-]+)?$/,
|
|
72
|
+
"Invalid bead ID format",
|
|
73
|
+
);
|
|
74
|
+
// Custom error message as second argument
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Enums
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Enum from array of literals
|
|
81
|
+
export const StatusSchema = z.enum([
|
|
82
|
+
"open",
|
|
83
|
+
"in_progress",
|
|
84
|
+
"blocked",
|
|
85
|
+
"closed",
|
|
86
|
+
]);
|
|
87
|
+
export type Status = z.infer<typeof StatusSchema>;
|
|
88
|
+
|
|
89
|
+
// Usage
|
|
90
|
+
StatusSchema.parse("open"); // ✓ "open"
|
|
91
|
+
StatusSchema.parse("invalid"); // ✗ throws ZodError
|
|
92
|
+
|
|
93
|
+
// Access enum values
|
|
94
|
+
StatusSchema.options; // ["open", "in_progress", "blocked", "closed"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Arrays
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// Array of strings
|
|
101
|
+
z.array(z.string());
|
|
102
|
+
|
|
103
|
+
// Non-empty array
|
|
104
|
+
z.array(z.string()).min(1);
|
|
105
|
+
|
|
106
|
+
// Array with length constraints
|
|
107
|
+
z.array(z.string()).min(1).max(10);
|
|
108
|
+
|
|
109
|
+
// Array of objects
|
|
110
|
+
z.array(
|
|
111
|
+
z.object({
|
|
112
|
+
id: z.string(),
|
|
113
|
+
value: z.number(),
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Objects
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Basic object
|
|
122
|
+
const PersonSchema = z.object({
|
|
123
|
+
name: z.string(),
|
|
124
|
+
age: z.number(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Optional properties
|
|
128
|
+
const UserSchema = z.object({
|
|
129
|
+
id: z.string(),
|
|
130
|
+
email: z.string().email().optional(), // email | undefined
|
|
131
|
+
bio: z.string().nullable(), // string | null
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Required vs optional
|
|
135
|
+
z.object({
|
|
136
|
+
required: z.string(),
|
|
137
|
+
optional: z.string().optional(),
|
|
138
|
+
withDefault: z.string().default("default value"),
|
|
139
|
+
nullable: z.string().nullable(),
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Records and Maps
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Record<string, unknown> - for metadata
|
|
147
|
+
z.record(z.string(), z.unknown());
|
|
148
|
+
|
|
149
|
+
// Record<string, specific type>
|
|
150
|
+
z.record(z.string(), z.number());
|
|
151
|
+
// Example: { "key1": 1, "key2": 2 }
|
|
152
|
+
|
|
153
|
+
// Record with typed keys and values
|
|
154
|
+
const CriteriaSchema = z.record(
|
|
155
|
+
z.string(), // criterion name
|
|
156
|
+
z.object({
|
|
157
|
+
passed: z.boolean(),
|
|
158
|
+
feedback: z.string(),
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
// Example: { "type_safe": { passed: true, feedback: "..." } }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Composition Patterns
|
|
165
|
+
|
|
166
|
+
### Union Types
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Either/or types
|
|
170
|
+
const SuccessSchema = z.object({
|
|
171
|
+
success: z.literal(true),
|
|
172
|
+
data: z.unknown(),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const ErrorSchema = z.object({
|
|
176
|
+
success: z.literal(false),
|
|
177
|
+
error: z.string(),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const ResultSchema = z.union([SuccessSchema, ErrorSchema]);
|
|
181
|
+
type Result = z.infer<typeof ResultSchema>;
|
|
182
|
+
// Result = { success: true, data: unknown } | { success: false, error: string }
|
|
183
|
+
|
|
184
|
+
// Discriminated unions (better inference)
|
|
185
|
+
const ResultSchema = z.discriminatedUnion("success", [
|
|
186
|
+
SuccessSchema,
|
|
187
|
+
ErrorSchema,
|
|
188
|
+
]);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Intersection Types
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// Combine multiple schemas
|
|
195
|
+
const NameSchema = z.object({ name: z.string() });
|
|
196
|
+
const AgeSchema = z.object({ age: z.number() });
|
|
197
|
+
|
|
198
|
+
const PersonSchema = z.intersection(NameSchema, AgeSchema);
|
|
199
|
+
// Equivalent to: z.object({ name: z.string(), age: z.number() })
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Extending Schemas
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Extend existing schema with new fields
|
|
206
|
+
const BaseSchema = z.object({
|
|
207
|
+
id: z.string(),
|
|
208
|
+
created_at: z.string().datetime({ offset: true }),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const ExtendedSchema = BaseSchema.extend({
|
|
212
|
+
updated_at: z.string().datetime({ offset: true }),
|
|
213
|
+
status: z.enum(["active", "inactive"]),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ExtendedSchema has: id, created_at, updated_at, status
|
|
217
|
+
|
|
218
|
+
// Common pattern: add metadata to base evaluation
|
|
219
|
+
const CriterionEvaluationSchema = z.object({
|
|
220
|
+
passed: z.boolean(),
|
|
221
|
+
feedback: z.string(),
|
|
222
|
+
score: z.number().min(0).max(1).optional(),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const WeightedCriterionEvaluationSchema = CriterionEvaluationSchema.extend({
|
|
226
|
+
weight: z.number().min(0).max(1).default(1),
|
|
227
|
+
weighted_score: z.number().min(0).max(1).optional(),
|
|
228
|
+
deprecated: z.boolean().default(false),
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Defaults and Transformations
|
|
233
|
+
|
|
234
|
+
### Default Values
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// .default() provides value if field is undefined
|
|
238
|
+
z.object({
|
|
239
|
+
status: z.enum(["open", "closed"]).default("open"),
|
|
240
|
+
priority: z.number().int().min(0).max(3).default(2),
|
|
241
|
+
tags: z.array(z.string()).default([]),
|
|
242
|
+
description: z.string().optional().default(""),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Input: { }
|
|
246
|
+
// Output: { status: "open", priority: 2, tags: [], description: "" }
|
|
247
|
+
|
|
248
|
+
// Input: { status: "closed" }
|
|
249
|
+
// Output: { status: "closed", priority: 2, tags: [], description: "" }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Optional vs Default
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// .optional() - field can be missing or undefined
|
|
256
|
+
z.string().optional(); // string | undefined
|
|
257
|
+
|
|
258
|
+
// .nullable() - field can be null
|
|
259
|
+
z.string().nullable(); // string | null
|
|
260
|
+
|
|
261
|
+
// .optional().default() - missing becomes default
|
|
262
|
+
z.string().optional().default("default"); // string (never undefined)
|
|
263
|
+
|
|
264
|
+
// .nullable().default() - null remains null
|
|
265
|
+
z.string().nullable().default("default"); // string | null
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Transform Data
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// .transform() - modify parsed data
|
|
272
|
+
const NumberFromString = z.string().transform((val) => parseInt(val, 10));
|
|
273
|
+
|
|
274
|
+
NumberFromString.parse("42"); // 42 (number)
|
|
275
|
+
|
|
276
|
+
// Chain transforms
|
|
277
|
+
const TrimmedString = z
|
|
278
|
+
.string()
|
|
279
|
+
.transform((val) => val.trim())
|
|
280
|
+
.transform((val) => val.toLowerCase());
|
|
281
|
+
|
|
282
|
+
TrimmedString.parse(" HELLO "); // "hello"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Refinements and Custom Validation
|
|
286
|
+
|
|
287
|
+
### .refine()
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Custom validation logic
|
|
291
|
+
const PositiveNumberSchema = z
|
|
292
|
+
.number()
|
|
293
|
+
.refine((val) => val > 0, { message: "Number must be positive" });
|
|
294
|
+
|
|
295
|
+
// Multiple refinements
|
|
296
|
+
const BeadIdSchema = z
|
|
297
|
+
.string()
|
|
298
|
+
.min(1, "ID required")
|
|
299
|
+
.refine((id) => id.includes("-"), {
|
|
300
|
+
message: "ID must contain project prefix",
|
|
301
|
+
})
|
|
302
|
+
.refine((id) => /^[a-z0-9]+(-[a-z0-9]+)+(\.[\w-]+)?$/.test(id), {
|
|
303
|
+
message: "Invalid ID format",
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Refinement with context
|
|
307
|
+
const SubtaskSchema = z
|
|
308
|
+
.object({
|
|
309
|
+
files: z.array(z.string()),
|
|
310
|
+
dependencies: z.array(z.number()),
|
|
311
|
+
})
|
|
312
|
+
.refine(
|
|
313
|
+
(data) => {
|
|
314
|
+
// Can't depend on yourself
|
|
315
|
+
return data.dependencies.every((dep) => dep >= 0);
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
message: "Invalid dependency index",
|
|
319
|
+
path: ["dependencies"], // Error path in object
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### .superRefine()
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// Advanced validation with multiple errors
|
|
328
|
+
const DecompositionSchema = z
|
|
329
|
+
.object({
|
|
330
|
+
subtasks: z.array(
|
|
331
|
+
z.object({
|
|
332
|
+
files: z.array(z.string()),
|
|
333
|
+
dependencies: z.array(z.number()),
|
|
334
|
+
}),
|
|
335
|
+
),
|
|
336
|
+
})
|
|
337
|
+
.superRefine((data, ctx) => {
|
|
338
|
+
const maxIndex = data.subtasks.length - 1;
|
|
339
|
+
|
|
340
|
+
data.subtasks.forEach((subtask, idx) => {
|
|
341
|
+
subtask.dependencies.forEach((dep) => {
|
|
342
|
+
if (dep > maxIndex) {
|
|
343
|
+
ctx.addIssue({
|
|
344
|
+
code: z.ZodIssueCode.custom,
|
|
345
|
+
message: `Subtask ${idx} depends on non-existent subtask ${dep}`,
|
|
346
|
+
path: ["subtasks", idx, "dependencies"],
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## TypeScript Inference
|
|
355
|
+
|
|
356
|
+
### Infer Types from Schemas
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
// Define schema first, infer type
|
|
360
|
+
export const BeadSchema = z.object({
|
|
361
|
+
id: z.string(),
|
|
362
|
+
title: z.string().min(1),
|
|
363
|
+
status: z.enum(["open", "in_progress", "closed"]).default("open"),
|
|
364
|
+
priority: z.number().int().min(0).max(3).default(2),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
export type Bead = z.infer<typeof BeadSchema>;
|
|
368
|
+
// Bead = {
|
|
369
|
+
// id: string;
|
|
370
|
+
// title: string;
|
|
371
|
+
// status: "open" | "in_progress" | "closed";
|
|
372
|
+
// priority: number;
|
|
373
|
+
// }
|
|
374
|
+
|
|
375
|
+
// Use in functions
|
|
376
|
+
function createBead(data: z.infer<typeof BeadSchema>): Bead {
|
|
377
|
+
return BeadSchema.parse(data);
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Input vs Output Types
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// Schema with defaults and transforms
|
|
385
|
+
const UserSchema = z.object({
|
|
386
|
+
name: z.string(),
|
|
387
|
+
age: z.string().transform((s) => parseInt(s, 10)),
|
|
388
|
+
role: z.enum(["user", "admin"]).default("user"),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Input type (before parsing)
|
|
392
|
+
type UserInput = z.input<typeof UserSchema>;
|
|
393
|
+
// { name: string; age: string; role?: "user" | "admin" }
|
|
394
|
+
|
|
395
|
+
// Output type (after parsing)
|
|
396
|
+
type UserOutput = z.output<typeof UserSchema>;
|
|
397
|
+
// { name: string; age: number; role: "user" | "admin" }
|
|
398
|
+
|
|
399
|
+
// Shorthand (equivalent to z.output)
|
|
400
|
+
type User = z.infer<typeof UserSchema>;
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Tool Argument Schemas
|
|
404
|
+
|
|
405
|
+
### MCP Tool Integration
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
import { tool } from "@opencode-ai/plugin";
|
|
409
|
+
|
|
410
|
+
// Define args schema first
|
|
411
|
+
export const BeadCreateArgsSchema = z.object({
|
|
412
|
+
title: z.string().min(1, "Title required"),
|
|
413
|
+
type: z.enum(["bug", "feature", "task", "epic", "chore"]).default("task"),
|
|
414
|
+
priority: z.number().int().min(0).max(3).default(2),
|
|
415
|
+
description: z.string().optional(),
|
|
416
|
+
parent_id: z.string().optional(),
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
export type BeadCreateArgs = z.infer<typeof BeadCreateArgsSchema>;
|
|
420
|
+
|
|
421
|
+
// Use in tool definition
|
|
422
|
+
export const beads_create = tool({
|
|
423
|
+
description: "Create a new bead with type-safe validation",
|
|
424
|
+
args: {
|
|
425
|
+
title: tool.schema.string().min(1),
|
|
426
|
+
type: tool.schema.enum(["bug", "feature", "task", "epic", "chore"]),
|
|
427
|
+
priority: tool.schema.number().int().min(0).max(3),
|
|
428
|
+
description: tool.schema.string().optional(),
|
|
429
|
+
parent_id: tool.schema.string().optional(),
|
|
430
|
+
},
|
|
431
|
+
async execute(args, ctx) {
|
|
432
|
+
// Validate with Zod schema
|
|
433
|
+
const validated = BeadCreateArgsSchema.parse(args);
|
|
434
|
+
|
|
435
|
+
// validated is now BeadCreateArgs with defaults applied
|
|
436
|
+
return createBead(validated);
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Validation in Tool Execution
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
// Pattern: separate schema from tool definition
|
|
445
|
+
const QueryArgsSchema = z.object({
|
|
446
|
+
status: z.enum(["open", "in_progress", "closed"]).optional(),
|
|
447
|
+
type: z.enum(["bug", "feature", "task"]).optional(),
|
|
448
|
+
ready: z.boolean().optional(),
|
|
449
|
+
limit: z.number().int().positive().default(20),
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
export const beads_query = tool({
|
|
453
|
+
description: "Query beads with filters",
|
|
454
|
+
args: {
|
|
455
|
+
/* tool.schema args */
|
|
456
|
+
},
|
|
457
|
+
async execute(args, ctx) {
|
|
458
|
+
// Step 1: Validate
|
|
459
|
+
const result = QueryArgsSchema.safeParse(args);
|
|
460
|
+
|
|
461
|
+
if (!result.success) {
|
|
462
|
+
throw new Error(formatZodErrors(result.error));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Step 2: Use validated data
|
|
466
|
+
const { status, type, ready, limit } = result.data;
|
|
467
|
+
|
|
468
|
+
// Step 3: Execute
|
|
469
|
+
return queryBeads({ status, type, ready, limit });
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Error Handling
|
|
475
|
+
|
|
476
|
+
### Parse vs SafeParse
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
// .parse() - throws ZodError on validation failure
|
|
480
|
+
try {
|
|
481
|
+
const data = MySchema.parse(input);
|
|
482
|
+
// Use data
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (error instanceof z.ZodError) {
|
|
485
|
+
// Handle validation errors
|
|
486
|
+
console.error(error.issues);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// .safeParse() - returns result object
|
|
491
|
+
const result = MySchema.safeParse(input);
|
|
492
|
+
|
|
493
|
+
if (result.success) {
|
|
494
|
+
const data = result.data;
|
|
495
|
+
// Use validated data
|
|
496
|
+
} else {
|
|
497
|
+
const errors = result.error;
|
|
498
|
+
// Handle validation errors
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Formatting Zod Errors
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
/**
|
|
506
|
+
* Format Zod validation errors as readable bullet points
|
|
507
|
+
*
|
|
508
|
+
* @param error - Zod error from schema validation
|
|
509
|
+
* @returns Array of error messages suitable for feedback
|
|
510
|
+
*/
|
|
511
|
+
function formatZodErrors(error: z.ZodError): string[] {
|
|
512
|
+
return error.issues.map((issue) => {
|
|
513
|
+
const path = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
514
|
+
return `${path}${issue.message}`;
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Usage
|
|
519
|
+
const result = BeadSchema.safeParse(input);
|
|
520
|
+
if (!result.success) {
|
|
521
|
+
const bullets = formatZodErrors(result.error);
|
|
522
|
+
console.error(bullets.join("\n"));
|
|
523
|
+
// Output:
|
|
524
|
+
// - title: String must contain at least 1 character(s)
|
|
525
|
+
// - priority: Number must be less than or equal to 3
|
|
526
|
+
// - status: Invalid enum value. Expected 'open' | 'in_progress' | 'closed'
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Custom Error Classes
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
/**
|
|
534
|
+
* Structured validation error with formatted feedback
|
|
535
|
+
*/
|
|
536
|
+
export class StructuredValidationError extends Error {
|
|
537
|
+
public readonly errorBullets: string[];
|
|
538
|
+
|
|
539
|
+
constructor(
|
|
540
|
+
message: string,
|
|
541
|
+
public readonly zodError: z.ZodError | null,
|
|
542
|
+
public readonly rawInput: string,
|
|
543
|
+
) {
|
|
544
|
+
super(message);
|
|
545
|
+
this.name = "StructuredValidationError";
|
|
546
|
+
this.errorBullets = zodError ? formatZodErrors(zodError) : [message];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Format errors as bullet list for retry prompts
|
|
551
|
+
*/
|
|
552
|
+
toFeedback(): string {
|
|
553
|
+
return this.errorBullets.map((e) => `- ${e}`).join("\n");
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Usage
|
|
558
|
+
try {
|
|
559
|
+
const validated = MySchema.parse(input);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (error instanceof z.ZodError) {
|
|
562
|
+
throw new StructuredValidationError(
|
|
563
|
+
"Validation failed",
|
|
564
|
+
error,
|
|
565
|
+
JSON.stringify(input),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
## JSON Extraction and Validation
|
|
573
|
+
|
|
574
|
+
### Multi-Strategy JSON Extraction
|
|
575
|
+
|
|
576
|
+
````typescript
|
|
577
|
+
/**
|
|
578
|
+
* Try to extract JSON from text using multiple strategies
|
|
579
|
+
*
|
|
580
|
+
* @param text - Raw text that may contain JSON
|
|
581
|
+
* @returns Tuple of [parsed object, extraction method used]
|
|
582
|
+
* @throws JsonExtractionError if no JSON can be extracted
|
|
583
|
+
*/
|
|
584
|
+
function extractJsonFromText(text: string): [unknown, string] {
|
|
585
|
+
const trimmed = text.trim();
|
|
586
|
+
|
|
587
|
+
// Strategy 1: Direct parse
|
|
588
|
+
try {
|
|
589
|
+
return [JSON.parse(trimmed), "direct_parse"];
|
|
590
|
+
} catch {}
|
|
591
|
+
|
|
592
|
+
// Strategy 2: Extract from ```json code blocks
|
|
593
|
+
const jsonBlockMatch = trimmed.match(/```json\s*([\s\S]*?)```/i);
|
|
594
|
+
if (jsonBlockMatch) {
|
|
595
|
+
try {
|
|
596
|
+
return [JSON.parse(jsonBlockMatch[1].trim()), "json_code_block"];
|
|
597
|
+
} catch {}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Strategy 3: Extract from any code block
|
|
601
|
+
const codeBlockMatch = trimmed.match(/```\s*([\s\S]*?)```/);
|
|
602
|
+
if (codeBlockMatch) {
|
|
603
|
+
try {
|
|
604
|
+
return [JSON.parse(codeBlockMatch[1].trim()), "any_code_block"];
|
|
605
|
+
} catch {}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Strategy 4: Find balanced {...} object
|
|
609
|
+
const objectJson = findBalancedBraces(trimmed, "{", "}");
|
|
610
|
+
if (objectJson) {
|
|
611
|
+
try {
|
|
612
|
+
return [JSON.parse(objectJson), "brace_match_object"];
|
|
613
|
+
} catch {}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
throw new JsonExtractionError(
|
|
617
|
+
"Could not extract valid JSON from response",
|
|
618
|
+
text,
|
|
619
|
+
["direct_parse", "json_code_block", "any_code_block", "brace_match_object"],
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
````
|
|
623
|
+
|
|
624
|
+
### Extract + Validate Pattern
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
/**
|
|
628
|
+
* Extract JSON from agent response and validate against schema
|
|
629
|
+
*/
|
|
630
|
+
async function parseAndValidate<T>(
|
|
631
|
+
response: string,
|
|
632
|
+
schema: z.ZodSchema<T>,
|
|
633
|
+
): Promise<T> {
|
|
634
|
+
// Step 1: Extract JSON
|
|
635
|
+
const [extracted, method] = extractJsonFromText(response);
|
|
636
|
+
|
|
637
|
+
// Step 2: Validate
|
|
638
|
+
const result = schema.safeParse(extracted);
|
|
639
|
+
|
|
640
|
+
if (!result.success) {
|
|
641
|
+
throw new StructuredValidationError(
|
|
642
|
+
"Validation failed",
|
|
643
|
+
result.error,
|
|
644
|
+
response,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return result.data;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Usage
|
|
652
|
+
const evaluation = await parseAndValidate(agentResponse, EvaluationSchema);
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Schema Registry Pattern
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
/**
|
|
659
|
+
* Schema registry for named schema lookups
|
|
660
|
+
*/
|
|
661
|
+
const SCHEMA_REGISTRY: Record<string, z.ZodSchema> = {
|
|
662
|
+
evaluation: EvaluationSchema,
|
|
663
|
+
task_decomposition: TaskDecompositionSchema,
|
|
664
|
+
bead_tree: BeadTreeSchema,
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Get schema by name from registry
|
|
669
|
+
*/
|
|
670
|
+
function getSchemaByName(name: string): z.ZodSchema {
|
|
671
|
+
const schema = SCHEMA_REGISTRY[name];
|
|
672
|
+
if (!schema) {
|
|
673
|
+
throw new Error(
|
|
674
|
+
`Unknown schema: ${name}. Available: ${Object.keys(SCHEMA_REGISTRY).join(", ")}`,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return schema;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Usage in tool
|
|
681
|
+
export const structured_validate = tool({
|
|
682
|
+
description: "Validate agent response against a schema",
|
|
683
|
+
args: {
|
|
684
|
+
response: tool.schema.string(),
|
|
685
|
+
schema_name: tool.schema.enum([
|
|
686
|
+
"evaluation",
|
|
687
|
+
"task_decomposition",
|
|
688
|
+
"bead_tree",
|
|
689
|
+
]),
|
|
690
|
+
},
|
|
691
|
+
async execute(args, ctx) {
|
|
692
|
+
const schema = getSchemaByName(args.schema_name);
|
|
693
|
+
const [extracted] = extractJsonFromText(args.response);
|
|
694
|
+
return schema.parse(extracted);
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
## Anti-Patterns
|
|
700
|
+
|
|
701
|
+
### Don't: Validate After the Fact
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
// ✗ BAD: TypeScript type with manual validation
|
|
705
|
+
type User = {
|
|
706
|
+
name: string;
|
|
707
|
+
age: number;
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
function validateUser(data: any): User {
|
|
711
|
+
if (typeof data.name !== "string") throw new Error("Invalid name");
|
|
712
|
+
if (typeof data.age !== "number") throw new Error("Invalid age");
|
|
713
|
+
return data as User;
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
// ✓ GOOD: Schema-first with inference
|
|
719
|
+
const UserSchema = z.object({
|
|
720
|
+
name: z.string(),
|
|
721
|
+
age: z.number(),
|
|
722
|
+
});
|
|
723
|
+
type User = z.infer<typeof UserSchema>;
|
|
724
|
+
|
|
725
|
+
const user = UserSchema.parse(data); // Validated and typed
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### Don't: Over-Constrain with Refinements
|
|
729
|
+
|
|
730
|
+
```typescript
|
|
731
|
+
// ✗ BAD: Too many refinements
|
|
732
|
+
const PasswordSchema = z
|
|
733
|
+
.string()
|
|
734
|
+
.refine((s) => s.length >= 8)
|
|
735
|
+
.refine((s) => /[A-Z]/.test(s))
|
|
736
|
+
.refine((s) => /[a-z]/.test(s))
|
|
737
|
+
.refine((s) => /[0-9]/.test(s))
|
|
738
|
+
.refine((s) => /[^A-Za-z0-9]/.test(s));
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
// ✓ GOOD: Single refinement with regex
|
|
743
|
+
const PasswordSchema = z
|
|
744
|
+
.string()
|
|
745
|
+
.min(8, "At least 8 characters")
|
|
746
|
+
.regex(
|
|
747
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z\d]).+$/,
|
|
748
|
+
"Must contain uppercase, lowercase, number, and special character",
|
|
749
|
+
);
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Don't: Parse in Loops
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
// ✗ BAD: Parse individual items
|
|
756
|
+
const items = rawItems.map((item) => ItemSchema.parse(item));
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
// ✓ GOOD: Parse array schema once
|
|
761
|
+
const ItemsSchema = z.array(ItemSchema);
|
|
762
|
+
const items = ItemsSchema.parse(rawItems);
|
|
763
|
+
```
|