opencode-swarm-plugin 0.12.27 → 0.12.28

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.
@@ -1,763 +0,0 @@
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
- ```