glirastes 0.3.1 → 0.4.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/CHANGELOG.md CHANGED
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-06-25
11
+
12
+ ### Changed
13
+
14
+ - **Upgraded Zod from v3 to v4** (`zod@^4`). The public schema-authoring API
15
+ is unchanged for consumers, but Zod 4 is now installed transitively — apps
16
+ that import `zod` directly alongside glirastes will resolve to Zod 4.
17
+ - Rewrote the internal Zod-introspection code that depended on Zod 3's
18
+ private representation (`_def.typeName`, `def.shape()`, `_def.checks`).
19
+ Most critically, `zodToCompactDescription()` (the `Returns: …` suffix
20
+ generator for AI tool descriptions) silently degraded to `"unknown"`
21
+ under Zod 4 — it now reads Zod 4's `_zod.def` correctly.
22
+ - Centralized all Zod-internals access for the test-suite generators
23
+ (`runEdgeCaseTest`, `runSchemaConsistencyTest`) into a single
24
+ `zod-introspect` module with full unit coverage, so a future Zod upgrade
25
+ fails loudly instead of generating empty test suites.
26
+ - `z.record(z.unknown())` updated to the Zod 4 two-argument form
27
+ `z.record(z.string(), z.unknown())`.
28
+
29
+ ### Added
30
+
31
+ - Regression tests pinning `zodToCompactDescription` output and the Zod
32
+ introspection helpers, closing the silent-failure gap the upgrade exposed.
33
+
34
+ ## [0.3.2] - 2026-04-29
35
+
36
+ ### Changed
37
+
38
+ - Documentation pass that makes the **CLI codegen step explicit as a
39
+ required part of the Next.js setup** for any non-trivial app. The
40
+ earlier framing implied codegen was only "for scaling up" — in
41
+ practice the manual `endpointToolsToRegistry([…])` import list breaks
42
+ down past a handful of tools, and codegen is the canonical wiring.
43
+ - README quickstart Step 4 now points readers at the codegen workflow
44
+ immediately after the demo manual-import example.
45
+ - The "Scaling up" section is renamed to **"Scaling up — required
46
+ codegen workflow for Next.js"** and explicitly states NestJS users
47
+ skip it (runtime DI scan handles aggregation).
48
+ - The `integrating-glirastes-nextjs` and `maintaining-glirastes-tools`
49
+ skills mirror the same framing so coding agents pick the codegen
50
+ path by default for non-trivial apps.
51
+ - Forward-looking note in README and skills: a `buildToolRegistry`
52
+ helper based on `import.meta.glob` / `require.context` is planned
53
+ for **0.4.0** and will let Next.js consumers skip the codegen step.
54
+
55
+ No code changes — patch-level docs release.
56
+
10
57
  ## [0.3.1] - 2026-04-28
11
58
 
12
59
  ### Added
@@ -140,6 +187,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
140
187
  these libraries on install. `react-markdown` is loaded via `React.lazy`
141
188
  with a plain-text fallback when the peer dep is not installed.
142
189
 
143
- [Unreleased]: https://github.com/chainmatics/glirastes/compare/v0.2.1...HEAD
190
+ [Unreleased]: https://github.com/chainmatics/glirastes/compare/v0.4.0...HEAD
191
+ [0.4.0]: https://github.com/chainmatics/glirastes/compare/v0.3.2...v0.4.0
144
192
  [0.2.1]: https://github.com/chainmatics/glirastes/compare/v0.2.0...v0.2.1
145
193
  [0.2.0]: https://github.com/chainmatics/glirastes/releases/tag/v0.2.0
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  <a href="#core-concepts"><strong>Concepts</strong></a> ·
15
15
  <a href="#examples"><strong>Examples</strong></a> ·
16
16
  <a href="#advanced"><strong>Advanced</strong></a> ·
17
- <a href="#scaling-up--codegen-workflow"><strong>Scaling up</strong></a> ·
17
+ <a href="#scaling-up--required-codegen-workflow-for-nextjs"><strong>Scaling up</strong></a> ·
18
18
  <a href="#cli"><strong>CLI</strong></a> ·
19
19
  <a href="#agent-skills"><strong>Skills</strong></a>
20
20
  </p>
@@ -246,6 +246,10 @@ const registry = endpointToolsToRegistry(scan.tools);
246
246
 
247
247
  Done. AI chat is live, streaming, and your `list_tasks` tool is callable. Add more tools the same way — pick the style that matches your stack. See [NestJS with decorators](#nestjs-with-decorators) below for DTOs, `@AiParam`, and multi-controller scanning.
248
248
 
249
+ > **One more step for Next.js once you have more than a handful of tools.** The manual `endpointToolsToRegistry([listTasks, …])` import list above stops scaling around 5–10 tools. Today's required pattern: write each tool in a co-located `*.ai-tool.ts` file and run the CLI codegen as a `predev` / `prebuild` hook to aggregate them into a generated registry your chat route imports. See [Scaling up — required codegen workflow for Next.js](#scaling-up--required-codegen-workflow-for-nextjs) below for the full setup. **NestJS users skip this entirely** — the runtime decorator scan handles aggregation automatically.
250
+ >
251
+ > *Coming in 0.4.0:* a `buildToolRegistry` helper backed by `import.meta.glob` / `require.context` that drops the codegen step for Next.js too. Track at [chainmatics/glirastes#issues](https://github.com/chainmatics/glirastes/issues).
252
+
249
253
  ### Setup notes
250
254
 
251
255
  #### tsconfig — modern is best, legacy still works
@@ -772,9 +776,13 @@ The voice button streams audio to your backend, which proxies it to a speech-to-
772
776
 
773
777
  <br/>
774
778
 
775
- ## Scaling up — codegen workflow
779
+ ## Scaling up — required codegen workflow for Next.js
776
780
 
777
- Hand-importing 50 tool definitions into your chat route doesn't scale. The CLI generates a registry from co-located `*.ai-tool.ts` files so you can stay co-located while still wiring everything centrally.
781
+ Hand-importing every tool definition into your chat route doesn't scale past a handful. **In Glirastes 0.3.x the canonical pattern for Next.js consumers is to run the CLI codegen as a build hook** — it generates a registry from co-located `*.ai-tool.ts` files. The chat route imports the generated registry instead of maintaining a manual import list.
782
+
783
+ > NestJS users **skip this section entirely.** `AiChatModule.forRoot()` discovers `@AiTool`-decorated controllers at runtime via the NestJS `DiscoveryService` — no codegen step is ever needed.
784
+ >
785
+ > **Coming in 0.4.0:** a `buildToolRegistry` helper based on `import.meta.glob` / `require.context` will let Next.js consumers skip the codegen step too. Until then the steps below are required for any non-trivial Next.js app.
778
786
 
779
787
  ### 1. Bootstrap a tool from an existing route
780
788
 
@@ -14,14 +14,14 @@ export declare class AiToolsService {
14
14
  buildAiTools(context: ToolContext, options?: ToolToAiToolOptions): Promise<Record<string, {
15
15
  _toolId: string;
16
16
  description: string;
17
- parameters: import("zod").ZodType<any, import("zod").ZodTypeDef, any>;
17
+ parameters: import("zod").ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
18
18
  needsApproval: ((input: unknown) => Promise<boolean>) | undefined;
19
19
  execute: (input: unknown) => Promise<unknown>;
20
20
  }>>;
21
21
  buildAiToolsForIntent(intent: string, context: ToolContext, options?: ToolToAiToolOptions): Promise<Record<string, {
22
22
  _toolId: string;
23
23
  description: string;
24
- parameters: import("zod").ZodType<any, import("zod").ZodTypeDef, any>;
24
+ parameters: import("zod").ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
25
25
  needsApproval: ((input: unknown) => Promise<boolean>) | undefined;
26
26
  execute: (input: unknown) => Promise<unknown>;
27
27
  }>>;
@@ -117,7 +117,7 @@ export interface ToolToAiToolOptions {
117
117
  export declare function toolToAiTool(tool: Tool, context: ToolContext, toolName?: string, options?: ToolToAiToolOptions): {
118
118
  _toolId: string;
119
119
  description: string;
120
- parameters: z.ZodType<any, z.ZodTypeDef, any>;
120
+ parameters: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
121
121
  needsApproval: ((input: unknown) => Promise<boolean>) | undefined;
122
122
  execute: (input: unknown) => Promise<unknown>;
123
123
  };
@@ -9,7 +9,7 @@ import { z } from 'zod';
9
9
  * zodToCompactDescription(z.object({
10
10
  * tasks: z.array(z.object({ id: z.string(), title: z.string() })),
11
11
  * total: z.number(),
12
- * }))
12
+ * }), 5)
13
13
  * // → '{ tasks: Array<{ id: string, title: string }>, total: number }'
14
14
  *
15
15
  * @param schema - Any Zod schema
@@ -11,7 +11,7 @@ exports.zodToCompactDescription = zodToCompactDescription;
11
11
  * zodToCompactDescription(z.object({
12
12
  * tasks: z.array(z.object({ id: z.string(), title: z.string() })),
13
13
  * total: z.number(),
14
- * }))
14
+ * }), 5)
15
15
  * // → '{ tasks: Array<{ id: string, title: string }>, total: number }'
16
16
  *
17
17
  * @param schema - Any Zod schema
@@ -20,45 +20,49 @@ exports.zodToCompactDescription = zodToCompactDescription;
20
20
  function zodToCompactDescription(schema, maxDepth = 3) {
21
21
  return describeSchema(schema, 0, maxDepth);
22
22
  }
23
+ /** Read Zod v4's internal schema definition (`schema._zod.def`). */
24
+ function getDef(schema) {
25
+ return schema?._zod?.def;
26
+ }
23
27
  function describeSchema(schema, depth, maxDepth) {
24
28
  if (depth >= maxDepth)
25
29
  return '...';
26
- const def = schema._def;
27
- const typeName = def?.typeName;
28
- switch (typeName) {
29
- case 'ZodString':
30
+ const def = getDef(schema);
31
+ const type = def?.type;
32
+ switch (type) {
33
+ case 'string':
30
34
  return 'string';
31
- case 'ZodNumber':
35
+ case 'number':
36
+ case 'bigint':
32
37
  return 'number';
33
- case 'ZodBoolean':
38
+ case 'boolean':
34
39
  return 'boolean';
35
- case 'ZodDate':
40
+ case 'date':
36
41
  return 'string'; // Dates serialize as strings in JSON
37
- case 'ZodLiteral':
38
- return JSON.stringify(def.value);
39
- case 'ZodNull':
42
+ case 'literal':
43
+ // Zod v4 literals hold an array of allowed values.
44
+ return def.values
45
+ .map((v) => JSON.stringify(v))
46
+ .join(' | ');
47
+ case 'null':
40
48
  return 'null';
41
- case 'ZodUndefined':
42
- return 'undefined';
43
- case 'ZodAny':
49
+ case 'undefined':
50
+ case 'void':
51
+ return type;
52
+ case 'any':
44
53
  return 'any';
45
- case 'ZodUnknown':
54
+ case 'unknown':
46
55
  return 'unknown';
47
- case 'ZodVoid':
48
- return 'void';
49
- case 'ZodEnum':
50
- return def.values
51
- .map((v) => `"${v}"`)
52
- .join(' | ');
53
- case 'ZodNativeEnum':
54
- return Object.values(def.values)
55
- .filter((v) => typeof v === 'string')
56
- .map((v) => `"${v}"`)
57
- .join(' | ');
58
- case 'ZodArray':
59
- return `Array<${describeSchema(def.type, depth + 1, maxDepth)}>`;
60
- case 'ZodObject': {
61
- const shape = def.shape();
56
+ case 'enum': {
57
+ // Zod v4 stores enum members in an `entries` record (covers both string
58
+ // enums and native enums). Emit the string values only.
59
+ const values = Object.values(def.entries).filter((v) => typeof v === 'string');
60
+ return values.map((v) => `"${v}"`).join(' | ');
61
+ }
62
+ case 'array':
63
+ return `Array<${describeSchema(def.element, depth + 1, maxDepth)}>`;
64
+ case 'object': {
65
+ const shape = def.shape;
62
66
  const entries = Object.entries(shape);
63
67
  if (entries.length === 0)
64
68
  return '{}';
@@ -71,59 +75,78 @@ function describeSchema(schema, depth, maxDepth) {
71
75
  });
72
76
  return `{ ${fields.join(', ')} }`;
73
77
  }
74
- case 'ZodRecord':
78
+ case 'record':
75
79
  return `Record<string, ${describeSchema(def.valueType, depth + 1, maxDepth)}>`;
76
- case 'ZodUnion':
77
- case 'ZodDiscriminatedUnion': {
80
+ case 'union': {
81
+ // Covers both unions and discriminated unions in Zod v4.
78
82
  const options = def.options;
79
83
  return options
80
84
  .map((o) => describeSchema(o, depth + 1, maxDepth))
81
85
  .join(' | ');
82
86
  }
83
- case 'ZodTuple': {
87
+ case 'tuple': {
84
88
  const items = def.items;
85
89
  return `[${items.map((i) => describeSchema(i, depth + 1, maxDepth)).join(', ')}]`;
86
90
  }
87
- case 'ZodNullable':
91
+ case 'nullable':
88
92
  return `${describeSchema(def.innerType, depth, maxDepth)} | null`;
89
- case 'ZodOptional':
90
- case 'ZodDefault':
93
+ case 'optional':
94
+ case 'default':
95
+ case 'prefault':
96
+ case 'catch':
97
+ case 'readonly':
98
+ case 'nonoptional':
91
99
  return describeSchema(def.innerType, depth, maxDepth);
92
- case 'ZodEffects':
93
- return describeSchema(def.schema, depth, maxDepth);
100
+ case 'pipe':
101
+ // Transforms / refinements become pipes in Zod v4; describe the input
102
+ // side, matching the pre-v4 behaviour of describing the base schema.
103
+ return describeSchema(def.in, depth, maxDepth);
104
+ case 'lazy':
105
+ return describeSchema(def.getter(), depth, maxDepth);
94
106
  default:
95
107
  return 'unknown';
96
108
  }
97
109
  }
98
- /** Unwrap ZodOptional / ZodNullable / ZodDefault / ZodEffects to the inner schema. */
110
+ /** Unwrap optional / nullable / default / pipe wrappers to the inner schema. */
99
111
  function unwrap(schema) {
100
- const def = schema._def;
101
- const typeName = def?.typeName;
102
- if (typeName === 'ZodOptional' ||
103
- typeName === 'ZodNullable' ||
104
- typeName === 'ZodDefault') {
112
+ const def = getDef(schema);
113
+ const type = def?.type;
114
+ if (type === 'optional' ||
115
+ type === 'nullable' ||
116
+ type === 'default' ||
117
+ type === 'prefault' ||
118
+ type === 'catch' ||
119
+ type === 'readonly' ||
120
+ type === 'nonoptional') {
105
121
  return unwrap(def.innerType);
106
122
  }
107
- if (typeName === 'ZodEffects') {
108
- return unwrap(def.schema);
123
+ if (type === 'pipe') {
124
+ return unwrap(def.in);
109
125
  }
110
126
  return schema;
111
127
  }
112
128
  function isOptional(schema) {
113
- const def = schema._def;
114
- const typeName = def?.typeName;
115
- if (typeName === 'ZodOptional' || typeName === 'ZodDefault')
129
+ const def = getDef(schema);
130
+ const type = def?.type;
131
+ if (type === 'optional' || type === 'default' || type === 'prefault') {
116
132
  return true;
117
- if (typeName === 'ZodNullable')
133
+ }
134
+ if (type === 'nullable' || type === 'readonly' || type === 'catch') {
118
135
  return isOptional(def.innerType);
136
+ }
119
137
  return false;
120
138
  }
121
139
  function isNullable(schema) {
122
- const def = schema._def;
123
- const typeName = def?.typeName;
124
- if (typeName === 'ZodNullable')
140
+ const def = getDef(schema);
141
+ const type = def?.type;
142
+ if (type === 'nullable')
125
143
  return true;
126
- if (typeName === 'ZodOptional' || typeName === 'ZodDefault')
144
+ if (type === 'optional' ||
145
+ type === 'default' ||
146
+ type === 'prefault' ||
147
+ type === 'readonly' ||
148
+ type === 'catch') {
127
149
  return isNullable(def.innerType);
150
+ }
128
151
  return false;
129
152
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runEdgeCaseTest = runEdgeCaseTest;
4
4
  const vitest_1 = require("vitest");
5
+ const zod_introspect_js_1 = require("./zod-introspect.js");
5
6
  /**
6
7
  * Auto-generates edge case tests from tool Zod schemas + custom cases.
7
8
  *
@@ -23,7 +24,7 @@ function runEdgeCaseTest(toolRegistry, options) {
23
24
  (0, vitest_1.describe)('AI Tool Edge Cases', () => {
24
25
  for (const [toolName, toolDef] of toolsWithSchemas) {
25
26
  const schema = toolDef.inputSchema;
26
- const shape = getObjectShape(schema);
27
+ const shape = (0, zod_introspect_js_1.getObjectShape)(schema);
27
28
  const hasShapeTests = shape && hasTestableFields(shape);
28
29
  const custom = customCases[toolName];
29
30
  const hasCustom = custom && custom.length > 0;
@@ -89,59 +90,26 @@ function runEdgeCaseTest(toolRegistry, options) {
89
90
  */
90
91
  function hasTestableFields(shape) {
91
92
  for (const [, fieldSchema] of Object.entries(shape)) {
92
- const optional = isOptional(fieldSchema);
93
- if (!optional)
94
- return true; // Required field → generates "rejects missing" test
95
- const inner = unwrap(fieldSchema);
96
- const typeName = inner._def.typeName;
97
- if (typeName === 'ZodEnum' && inner._def.values)
93
+ if (!(0, zod_introspect_js_1.isOptional)(fieldSchema))
94
+ return true; // Required field → "rejects missing" test
95
+ const inner = (0, zod_introspect_js_1.unwrap)(fieldSchema);
96
+ const type = (0, zod_introspect_js_1.getBaseType)(inner);
97
+ if (type === 'enum' && (0, zod_introspect_js_1.getEnumValues)(inner).length > 0)
98
+ return true;
99
+ if (type === 'array' && (0, zod_introspect_js_1.getArrayMax)(inner) !== null)
100
+ return true;
101
+ if (type === 'string' && (0, zod_introspect_js_1.isUuidString)(inner))
98
102
  return true;
99
- if (typeName === 'ZodArray') {
100
- const checks = inner._def.checks ?? [];
101
- if (checks.some((c) => c.kind === 'max'))
102
- return true;
103
- }
104
- if (typeName === 'ZodString') {
105
- const checks = inner._def.checks ?? [];
106
- if (checks.some((c) => c.kind === 'uuid'))
107
- return true;
108
- }
109
103
  }
110
104
  return false;
111
105
  }
112
- function getObjectShape(schema) {
113
- const def = schema._def;
114
- if (def.typeName === 'ZodObject' && typeof def.shape === 'function') {
115
- return def.shape();
116
- }
117
- // Unwrap ZodEffects (e.g. .refine(), .transform())
118
- if (def.typeName === 'ZodEffects' && def.innerType) {
119
- return getObjectShape(def.innerType);
120
- }
121
- return null;
122
- }
123
- function isOptional(schema) {
124
- const name = schema._def.typeName;
125
- return name === 'ZodOptional' || name === 'ZodNullable' || name === 'ZodDefault';
126
- }
127
- function unwrap(schema) {
128
- const name = schema._def.typeName;
129
- if ((name === 'ZodOptional' || name === 'ZodNullable' || name === 'ZodDefault') &&
130
- schema._def.innerType) {
131
- return unwrap(schema._def.innerType);
132
- }
133
- if (name === 'ZodEffects' && schema._def.innerType) {
134
- return unwrap(schema._def.innerType);
135
- }
136
- return schema;
137
- }
138
106
  function generateShapeTests(shape, parentSchema) {
139
107
  // Build a valid base object with placeholder values
140
108
  const baseInput = buildMinimalValidInput(shape);
141
109
  for (const [fieldName, fieldSchema] of Object.entries(shape)) {
142
- const optional = isOptional(fieldSchema);
143
- const inner = unwrap(fieldSchema);
144
- const typeName = inner._def.typeName;
110
+ const optional = (0, zod_introspect_js_1.isOptional)(fieldSchema);
111
+ const inner = (0, zod_introspect_js_1.unwrap)(fieldSchema);
112
+ const type = (0, zod_introspect_js_1.getBaseType)(inner);
145
113
  // Required field: test missing
146
114
  if (!optional) {
147
115
  (0, vitest_1.it)(`rejects missing ${fieldName} (required)`, () => {
@@ -152,9 +120,9 @@ function generateShapeTests(shape, parentSchema) {
152
120
  });
153
121
  }
154
122
  // Enum: test each valid value + invalid
155
- if (typeName === 'ZodEnum' && inner._def.values) {
156
- const values = inner._def.values;
157
- for (const val of values) {
123
+ const enumValues = type === 'enum' ? (0, zod_introspect_js_1.getEnumValues)(inner) : [];
124
+ if (enumValues.length > 0) {
125
+ for (const val of enumValues) {
158
126
  (0, vitest_1.it)(`accepts valid ${fieldName} enum: ${val}`, () => {
159
127
  const result = parentSchema.safeParse({ ...baseInput, [fieldName]: val });
160
128
  (0, vitest_1.expect)(result.success).toBe(true);
@@ -166,11 +134,9 @@ function generateShapeTests(shape, parentSchema) {
166
134
  });
167
135
  }
168
136
  // Array with max: test at limit and over limit
169
- if (typeName === 'ZodArray') {
170
- const checks = inner._def.checks ?? [];
171
- const maxCheck = checks.find((c) => c.kind === 'max');
172
- if (maxCheck && typeof maxCheck.value === 'number') {
173
- const max = maxCheck.value;
137
+ if (type === 'array') {
138
+ const max = (0, zod_introspect_js_1.getArrayMax)(inner);
139
+ if (max !== null) {
174
140
  const arrayFillValue = getArrayElementPlaceholder(inner);
175
141
  (0, vitest_1.it)(`accepts ${fieldName} at max(${max})`, () => {
176
142
  const result = parentSchema.safeParse({
@@ -189,31 +155,24 @@ function generateShapeTests(shape, parentSchema) {
189
155
  }
190
156
  }
191
157
  // String with UUID check
192
- if (typeName === 'ZodString') {
193
- const checks = inner._def.checks ?? [];
194
- const hasUuid = checks.some((c) => c.kind === 'uuid');
195
- if (hasUuid) {
196
- (0, vitest_1.it)(`rejects invalid ${fieldName} UUID: "not-a-uuid"`, () => {
197
- const result = parentSchema.safeParse({ ...baseInput, [fieldName]: 'not-a-uuid' });
198
- (0, vitest_1.expect)(result.success).toBe(false);
199
- });
200
- }
158
+ if (type === 'string' && (0, zod_introspect_js_1.isUuidString)(inner)) {
159
+ (0, vitest_1.it)(`rejects invalid ${fieldName} UUID: "not-a-uuid"`, () => {
160
+ const result = parentSchema.safeParse({ ...baseInput, [fieldName]: 'not-a-uuid' });
161
+ (0, vitest_1.expect)(result.success).toBe(false);
162
+ });
201
163
  }
202
164
  }
203
165
  }
204
166
  function getArrayElementPlaceholder(arraySchema) {
205
- const elementSchema = arraySchema._def.type;
167
+ const elementSchema = (0, zod_introspect_js_1.getArrayElement)(arraySchema);
206
168
  if (elementSchema) {
207
- const elInner = unwrap(elementSchema);
208
- const elType = elInner._def.typeName;
209
- if (elType === 'ZodString') {
210
- const elChecks = elInner._def.checks ?? [];
211
- if (elChecks.some((c) => c.kind === 'uuid')) {
212
- return '00000000-0000-0000-0000-000000000000';
213
- }
214
- return 'test-id';
169
+ const elType = (0, zod_introspect_js_1.getBaseType)(elementSchema);
170
+ if (elType === 'string') {
171
+ return (0, zod_introspect_js_1.isUuidString)(elementSchema)
172
+ ? '00000000-0000-0000-0000-000000000000'
173
+ : 'test-id';
215
174
  }
216
- if (elType === 'ZodNumber')
175
+ if (elType === 'number')
217
176
  return 1;
218
177
  }
219
178
  return 'test-id';
@@ -221,44 +180,36 @@ function getArrayElementPlaceholder(arraySchema) {
221
180
  function buildMinimalValidInput(shape) {
222
181
  const input = {};
223
182
  for (const [name, schema] of Object.entries(shape)) {
224
- if (isOptional(schema))
183
+ if ((0, zod_introspect_js_1.isOptional)(schema))
225
184
  continue;
226
- const inner = unwrap(schema);
227
- const typeName = inner._def.typeName;
228
- if (typeName === 'ZodString') {
229
- const checks = inner._def.checks ?? [];
230
- if (checks.some((c) => c.kind === 'uuid')) {
231
- input[name] = '00000000-0000-0000-0000-000000000000';
232
- }
233
- else {
234
- input[name] = 'test-value';
235
- }
185
+ const inner = (0, zod_introspect_js_1.unwrap)(schema);
186
+ const type = (0, zod_introspect_js_1.getBaseType)(inner);
187
+ if (type === 'string') {
188
+ input[name] = (0, zod_introspect_js_1.isUuidString)(inner)
189
+ ? '00000000-0000-0000-0000-000000000000'
190
+ : 'test-value';
236
191
  }
237
- else if (typeName === 'ZodNumber') {
192
+ else if (type === 'number') {
238
193
  input[name] = 1;
239
194
  }
240
- else if (typeName === 'ZodBoolean') {
195
+ else if (type === 'boolean') {
241
196
  input[name] = false;
242
197
  }
243
- else if (typeName === 'ZodEnum' && inner._def.values) {
244
- input[name] = inner._def.values[0];
198
+ else if (type === 'enum') {
199
+ const values = (0, zod_introspect_js_1.getEnumValues)(inner);
200
+ input[name] = values.length > 0 ? values[0] : 'test';
245
201
  }
246
- else if (typeName === 'ZodArray') {
202
+ else if (type === 'array') {
247
203
  // Introspect array element type to generate valid placeholders
248
- const elementSchema = inner._def.type;
204
+ const elementSchema = (0, zod_introspect_js_1.getArrayElement)(inner);
249
205
  if (elementSchema) {
250
- const elInner = unwrap(elementSchema);
251
- const elType = elInner._def.typeName;
252
- if (elType === 'ZodString') {
253
- const elChecks = elInner._def.checks ?? [];
254
- if (elChecks.some((c) => c.kind === 'uuid')) {
255
- input[name] = ['00000000-0000-0000-0000-000000000000'];
256
- }
257
- else {
258
- input[name] = ['test-id'];
259
- }
206
+ const elType = (0, zod_introspect_js_1.getBaseType)(elementSchema);
207
+ if (elType === 'string') {
208
+ input[name] = (0, zod_introspect_js_1.isUuidString)(elementSchema)
209
+ ? ['00000000-0000-0000-0000-000000000000']
210
+ : ['test-id'];
260
211
  }
261
- else if (elType === 'ZodNumber') {
212
+ else if (elType === 'number') {
262
213
  input[name] = [1];
263
214
  }
264
215
  else {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runSchemaConsistencyTest = runSchemaConsistencyTest;
4
4
  const vitest_1 = require("vitest");
5
+ const zod_introspect_js_1 = require("./zod-introspect.js");
5
6
  /**
6
7
  * Verifies that tool output field types match the input field types
7
8
  * of downstream tools in declared workflow chains.
@@ -21,36 +22,14 @@ function runSchemaConsistencyTest(toolRegistry, options) {
21
22
  // Target tool must have inputSchema with the target field
22
23
  const toSchema = toTool.inputSchema;
23
24
  (0, vitest_1.expect)(toSchema, `Target tool "${chain.to}" has no inputSchema`).toBeDefined();
24
- const toShape = getShape(toSchema);
25
+ const toShape = (0, zod_introspect_js_1.getObjectShape)(toSchema);
25
26
  (0, vitest_1.expect)(toShape, `Target tool "${chain.to}" inputSchema is not an object`).not.toBeNull();
26
- const targetField = resolveField(toShape, chain.targetField);
27
+ const targetField = toShape[chain.targetField] ?? null;
27
28
  (0, vitest_1.expect)(targetField, `Target field "${chain.targetField}" not found in ${chain.to} inputSchema`).not.toBeNull();
28
29
  // Verify type compatibility
29
- const targetType = getBaseTypeName(targetField);
30
- (0, vitest_1.expect)(['ZodString', 'ZodNumber', 'ZodArray', 'ZodEnum', 'ZodBoolean'].includes(targetType), `Target field "${chain.targetField}" in ${chain.to} has unrecognized type: ${targetType}`).toBe(true);
30
+ const targetType = (0, zod_introspect_js_1.getBaseType)(targetField);
31
+ (0, vitest_1.expect)(['string', 'number', 'array', 'enum', 'boolean'].includes(targetType ?? ''), `Target field "${chain.targetField}" in ${chain.to} has unrecognized type: ${targetType}`).toBe(true);
31
32
  });
32
33
  }
33
34
  });
34
35
  }
35
- function getShape(schema) {
36
- const def = schema._def;
37
- if (def.typeName === 'ZodObject' && typeof def.shape === 'function') {
38
- return def.shape();
39
- }
40
- if ((def.typeName === 'ZodEffects' || def.typeName === 'ZodOptional') && def.innerType) {
41
- return getShape(def.innerType);
42
- }
43
- return null;
44
- }
45
- function resolveField(shape, path) {
46
- if (shape[path])
47
- return shape[path];
48
- return null;
49
- }
50
- function getBaseTypeName(schema) {
51
- const def = schema._def;
52
- if (def.typeName === 'ZodOptional' || def.typeName === 'ZodNullable' || def.typeName === 'ZodDefault') {
53
- return def.innerType ? getBaseTypeName(def.innerType) : def.typeName;
54
- }
55
- return def.typeName;
56
- }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Zod schema introspection helpers (Zod v4).
3
+ *
4
+ * Centralizes all access to Zod's internal `_zod.def` representation so the
5
+ * test-suite generators (`edge-cases.ts`, `schema-consistency.ts`) don't each
6
+ * reach into internals independently. If a future Zod version changes its
7
+ * internal shape, only this file — covered by `zod-introspect.test.ts` — needs
8
+ * updating, and the unit tests will catch a silent regression (introspection
9
+ * returning nothing) instead of letting it ship as a green-but-empty suite.
10
+ */
11
+ /** The normalized Zod v4 type tag (e.g. 'object', 'string', 'array', 'enum'). */
12
+ export declare function getSchemaType(schema: unknown): string | undefined;
13
+ /**
14
+ * True if the field is optional, nullable, or has a default — i.e. an input
15
+ * is not strictly required to provide a concrete value. Matches the historical
16
+ * behaviour of the edge-case generator, which skips the "rejects missing"
17
+ * boundary test for any of these.
18
+ */
19
+ export declare function isOptional(schema: unknown): boolean;
20
+ /** Strip optional/nullable/default/pipe wrappers to the base schema. */
21
+ export declare function unwrap<T = unknown>(schema: T): T;
22
+ /** The base Zod type tag after unwrapping (e.g. an optional string → 'string'). */
23
+ export declare function getBaseType(schema: unknown): string | undefined;
24
+ /**
25
+ * The field map of an object schema, unwrapping optional/default/pipe wrappers.
26
+ * Returns null for non-object schemas.
27
+ */
28
+ export declare function getObjectShape(schema: unknown): Record<string, unknown> | null;
29
+ /** The string members of an enum schema (after unwrapping). */
30
+ export declare function getEnumValues(schema: unknown): string[];
31
+ /** The element schema of an array (after unwrapping), or null. */
32
+ export declare function getArrayElement(schema: unknown): unknown | null;
33
+ /** The `.max(n)` length constraint on an array schema, or null. */
34
+ export declare function getArrayMax(schema: unknown): number | null;
35
+ /** True if a string schema carries the given format check (e.g. 'uuid'). */
36
+ export declare function hasStringFormat(schema: unknown, format: string): boolean;
37
+ /** True if a string schema is a UUID. */
38
+ export declare function isUuidString(schema: unknown): boolean;