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 +49 -1
- package/README.md +11 -3
- package/dist/cjs/server/adapters/nestjs/module/ai-tools.service.d.ts +2 -2
- package/dist/cjs/server/core/index.d.ts +1 -1
- package/dist/cjs/server/core/zod-compact.d.ts +1 -1
- package/dist/cjs/server/core/zod-compact.js +78 -55
- package/dist/cjs/server/testing/edge-cases.js +52 -101
- package/dist/cjs/server/testing/schema-consistency.js +5 -26
- package/dist/cjs/server/testing/zod-introspect.d.ts +38 -0
- package/dist/cjs/server/testing/zod-introspect.js +124 -0
- package/dist/cjs/types.d.ts +1 -1
- package/dist/cjs/types.js +1 -1
- package/dist/server/adapters/nestjs/module/ai-tools.service.d.ts +2 -2
- package/dist/server/core/index.d.ts +1 -1
- package/dist/server/core/zod-compact.d.ts +1 -1
- package/dist/server/core/zod-compact.js +78 -55
- package/dist/server/core/zod-compact.js.map +1 -1
- package/dist/server/testing/edge-cases.js +46 -95
- package/dist/server/testing/edge-cases.js.map +1 -1
- package/dist/server/testing/schema-consistency.js +5 -26
- package/dist/server/testing/schema-consistency.js.map +1 -1
- package/dist/server/testing/zod-introspect.d.ts +38 -0
- package/dist/server/testing/zod-introspect.js +113 -0
- package/dist/server/testing/zod-introspect.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/skills/integrating-glirastes-nextjs/SKILL.md +9 -3
- package/skills/maintaining-glirastes-tools/SKILL.md +6 -2
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.
|
|
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
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
|
27
|
-
const
|
|
28
|
-
switch (
|
|
29
|
-
case '
|
|
30
|
+
const def = getDef(schema);
|
|
31
|
+
const type = def?.type;
|
|
32
|
+
switch (type) {
|
|
33
|
+
case 'string':
|
|
30
34
|
return 'string';
|
|
31
|
-
case '
|
|
35
|
+
case 'number':
|
|
36
|
+
case 'bigint':
|
|
32
37
|
return 'number';
|
|
33
|
-
case '
|
|
38
|
+
case 'boolean':
|
|
34
39
|
return 'boolean';
|
|
35
|
-
case '
|
|
40
|
+
case 'date':
|
|
36
41
|
return 'string'; // Dates serialize as strings in JSON
|
|
37
|
-
case '
|
|
38
|
-
|
|
39
|
-
|
|
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 '
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
case 'undefined':
|
|
50
|
+
case 'void':
|
|
51
|
+
return type;
|
|
52
|
+
case 'any':
|
|
44
53
|
return 'any';
|
|
45
|
-
case '
|
|
54
|
+
case 'unknown':
|
|
46
55
|
return 'unknown';
|
|
47
|
-
case '
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
case '
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
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 '
|
|
78
|
+
case 'record':
|
|
75
79
|
return `Record<string, ${describeSchema(def.valueType, depth + 1, maxDepth)}>`;
|
|
76
|
-
case '
|
|
77
|
-
|
|
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 '
|
|
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 '
|
|
91
|
+
case 'nullable':
|
|
88
92
|
return `${describeSchema(def.innerType, depth, maxDepth)} | null`;
|
|
89
|
-
case '
|
|
90
|
-
case '
|
|
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 '
|
|
93
|
-
|
|
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
|
|
110
|
+
/** Unwrap optional / nullable / default / pipe wrappers to the inner schema. */
|
|
99
111
|
function unwrap(schema) {
|
|
100
|
-
const def = schema
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
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 (
|
|
108
|
-
return unwrap(def.
|
|
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
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
129
|
+
const def = getDef(schema);
|
|
130
|
+
const type = def?.type;
|
|
131
|
+
if (type === 'optional' || type === 'default' || type === 'prefault') {
|
|
116
132
|
return true;
|
|
117
|
-
|
|
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
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
140
|
+
const def = getDef(schema);
|
|
141
|
+
const type = def?.type;
|
|
142
|
+
if (type === 'nullable')
|
|
125
143
|
return true;
|
|
126
|
-
if (
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
for (const val of
|
|
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 (
|
|
170
|
-
const
|
|
171
|
-
|
|
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 (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
167
|
+
const elementSchema = (0, zod_introspect_js_1.getArrayElement)(arraySchema);
|
|
206
168
|
if (elementSchema) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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 === '
|
|
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
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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 (
|
|
192
|
+
else if (type === 'number') {
|
|
238
193
|
input[name] = 1;
|
|
239
194
|
}
|
|
240
|
-
else if (
|
|
195
|
+
else if (type === 'boolean') {
|
|
241
196
|
input[name] = false;
|
|
242
197
|
}
|
|
243
|
-
else if (
|
|
244
|
-
|
|
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 (
|
|
202
|
+
else if (type === 'array') {
|
|
247
203
|
// Introspect array element type to generate valid placeholders
|
|
248
|
-
const elementSchema = inner
|
|
204
|
+
const elementSchema = (0, zod_introspect_js_1.getArrayElement)(inner);
|
|
249
205
|
if (elementSchema) {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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 === '
|
|
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 =
|
|
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 =
|
|
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 =
|
|
30
|
-
(0, vitest_1.expect)(['
|
|
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;
|