@kindgi/schema 0.0.0-bootstrap.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +52 -2
- package/SPEC.md +120 -0
- package/dist/canonical.d.ts +27 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +55 -0
- package/dist/canonical.js.map +1 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +86 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +261 -0
- package/dist/registry.js.map +1 -0
- package/dist/version.d.ts +30 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +70 -0
- package/dist/version.js.map +1 -0
- package/dist/zod.d.ts +154 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +216 -0
- package/dist/zod.js.map +1 -0
- package/package.json +61 -4
- package/src/canonical.ts +52 -0
- package/src/errors.ts +82 -0
- package/src/index.ts +36 -0
- package/src/registry.test.ts +221 -0
- package/src/registry.ts +347 -0
- package/src/version.test.ts +114 -0
- package/src/version.ts +93 -0
- package/src/zod.ts +313 -0
- package/type-manifest.json +101 -0
package/dist/zod.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
// `createRequire` is Node's ESM-side bridge to CJS-style synchronous
|
|
5
|
+
// resolution. We only use it to resolve the `zod` peer dependency
|
|
6
|
+
// synchronously — the actual JSON conversion function works ESM or CJS.
|
|
7
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
8
|
+
/**
|
|
9
|
+
* Structural detection. Returns true iff `value` looks like a Zod v4
|
|
10
|
+
* schema — `_zod` property present AND `~standard` present. Purely
|
|
11
|
+
* structural: never imports from `zod` at runtime, so the check is safe
|
|
12
|
+
* even when zod is not installed at all.
|
|
13
|
+
*
|
|
14
|
+
* The check is deliberately narrow — a JSON Schema object never has a
|
|
15
|
+
* `_zod` property, so the two authoring surfaces cannot collide.
|
|
16
|
+
*/
|
|
17
|
+
export function isZodSchema(value) {
|
|
18
|
+
if (value === null || typeof value !== 'object')
|
|
19
|
+
return false;
|
|
20
|
+
const rec = value;
|
|
21
|
+
return '_zod' in rec && '~standard' in rec && typeof rec._zod === 'object';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Structural detection for a JSON Schema object. Cheap heuristic — accepts
|
|
25
|
+
* any object that isn't a Zod schema. JSON Schema validity (Draft 2020-12
|
|
26
|
+
* compilability) is checked separately by each consumer via Ajv.
|
|
27
|
+
*/
|
|
28
|
+
export function isJsonSchemaObject(value) {
|
|
29
|
+
if (value === null || typeof value !== 'object')
|
|
30
|
+
return false;
|
|
31
|
+
if (isZodSchema(value))
|
|
32
|
+
return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/** Return `'zod'` for Zod schemas, `'json-schema'` for JSON Schema objects. */
|
|
36
|
+
export function schemaKindOf(value) {
|
|
37
|
+
return isZodSchema(value) ? 'zod' : 'json-schema';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Convert an `AnySchema` to its authoritative JSON Schema wire form.
|
|
41
|
+
*
|
|
42
|
+
* - Zod schema → calls `z.toJSONSchema(schema, { io })` (Zod v4 native —
|
|
43
|
+
* no third-party `zod-to-json-schema` dependency).
|
|
44
|
+
* - JSON Schema object → returned as-is.
|
|
45
|
+
* - Anything else → `err`.
|
|
46
|
+
*
|
|
47
|
+
* Conversion failures (unrepresentable Zod types like `.transform()`,
|
|
48
|
+
* cycles without a registry, function schemas) surface as
|
|
49
|
+
* `InvalidZodConversionError` with the underlying cause preserved. No
|
|
50
|
+
* throws escape.
|
|
51
|
+
*
|
|
52
|
+
* Zod is imported dynamically the first time a Zod schema flows through:
|
|
53
|
+
* a workspace that never uses Zod never pays the import cost, and one
|
|
54
|
+
* that hasn't installed the peer dep gets a clean typed error instead of
|
|
55
|
+
* a module-resolution crash.
|
|
56
|
+
*/
|
|
57
|
+
export async function toJSONSchema(schema, io) {
|
|
58
|
+
if (!isZodSchema(schema)) {
|
|
59
|
+
return { kind: 'ok', value: schema };
|
|
60
|
+
}
|
|
61
|
+
const converterResult = await loadZodToJSONSchema();
|
|
62
|
+
if (converterResult.kind === 'err')
|
|
63
|
+
return converterResult;
|
|
64
|
+
try {
|
|
65
|
+
const converted = converterResult.value(schema, { io });
|
|
66
|
+
return { kind: 'ok', value: converted };
|
|
67
|
+
}
|
|
68
|
+
catch (cause) {
|
|
69
|
+
return {
|
|
70
|
+
kind: 'err',
|
|
71
|
+
error: {
|
|
72
|
+
code: 'invalid-zod-conversion',
|
|
73
|
+
message: `z.toJSONSchema() failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
74
|
+
cause,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Synchronous variant — for defineTool / defineGuardrail sites that must
|
|
81
|
+
* stay sync. Requires the caller to have already resolved the converter
|
|
82
|
+
* (via `loadZodConverter()`) at package init. When Zod is not installed
|
|
83
|
+
* but a Zod schema is passed, returns `err` with `peer-dep-missing`
|
|
84
|
+
* distinguishable by its `cause`.
|
|
85
|
+
*/
|
|
86
|
+
export function toJSONSchemaSync(schema, converter, io) {
|
|
87
|
+
if (!isZodSchema(schema)) {
|
|
88
|
+
return { kind: 'ok', value: schema };
|
|
89
|
+
}
|
|
90
|
+
if (converter === undefined) {
|
|
91
|
+
return {
|
|
92
|
+
kind: 'err',
|
|
93
|
+
error: {
|
|
94
|
+
code: 'invalid-zod-conversion',
|
|
95
|
+
message: 'Zod schema passed but the `zod` peer dependency is not installed. ' +
|
|
96
|
+
'Install `zod` (>=4.0.0) or author with a JSON Schema object.',
|
|
97
|
+
cause: new Error('zod peer dep not installed'),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const converted = converter(schema, { io });
|
|
103
|
+
return { kind: 'ok', value: converted };
|
|
104
|
+
}
|
|
105
|
+
catch (cause) {
|
|
106
|
+
return {
|
|
107
|
+
kind: 'err',
|
|
108
|
+
error: {
|
|
109
|
+
code: 'invalid-zod-conversion',
|
|
110
|
+
message: `z.toJSONSchema() failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
111
|
+
cause,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Parse a value with a Zod schema through its Standard Schema interface
|
|
118
|
+
* (`'~standard'.validate`): the parsed value — defaults filled,
|
|
119
|
+
* transforms and refinements applied — or the issues. What a handler
|
|
120
|
+
* typed with `z.infer` of the schema expects to receive.
|
|
121
|
+
*/
|
|
122
|
+
export async function parseWithSchema(schema, value) {
|
|
123
|
+
const standard = schema['~standard'];
|
|
124
|
+
const result = await standard.validate(value);
|
|
125
|
+
return result.issues !== undefined
|
|
126
|
+
? {
|
|
127
|
+
kind: 'err',
|
|
128
|
+
issues: result.issues.map((i) => ({
|
|
129
|
+
message: i.message,
|
|
130
|
+
...(i.path !== undefined && { path: [...i.path] }),
|
|
131
|
+
})),
|
|
132
|
+
}
|
|
133
|
+
: { kind: 'ok', value: result.value };
|
|
134
|
+
}
|
|
135
|
+
let cachedConverter = null;
|
|
136
|
+
/**
|
|
137
|
+
* Load `z.toJSONSchema` from the `zod` peer dependency. Returns
|
|
138
|
+
* `undefined` when zod is not installed — callers decide whether that's
|
|
139
|
+
* an error or a silent no-op (JSON-Schema-only workspaces are legitimate).
|
|
140
|
+
*
|
|
141
|
+
* The result is cached — repeat calls resolve synchronously after the
|
|
142
|
+
* first.
|
|
143
|
+
*/
|
|
144
|
+
export async function loadZodConverter() {
|
|
145
|
+
if (cachedConverter !== null)
|
|
146
|
+
return cachedConverter;
|
|
147
|
+
try {
|
|
148
|
+
const zod = (await import('zod'));
|
|
149
|
+
if (typeof zod.toJSONSchema !== 'function') {
|
|
150
|
+
cachedConverter = undefined;
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
cachedConverter = zod.toJSONSchema;
|
|
154
|
+
return cachedConverter;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
cachedConverter = undefined;
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Synchronous variant of `loadZodConverter`. Uses Node's `createRequire`
|
|
163
|
+
* so sync authoring surfaces (`defineTool`, `defineGuardrail`) can stay
|
|
164
|
+
* sync even on their first Zod-authored call. Falls back cleanly when
|
|
165
|
+
* `zod` isn't installed: returns `undefined` and callers surface the
|
|
166
|
+
* missing peer dep as a typed error at the boundary.
|
|
167
|
+
*
|
|
168
|
+
* Runtime: Node ≥18 (both CJS and ESM entry points to zod resolve
|
|
169
|
+
* through the same `createRequire` handle). No behavior change for
|
|
170
|
+
* workspaces that never author with Zod — the first sync call to
|
|
171
|
+
* `loadZodConverterSync` returns `undefined` cheaply.
|
|
172
|
+
*/
|
|
173
|
+
export function loadZodConverterSync() {
|
|
174
|
+
if (cachedConverter !== null)
|
|
175
|
+
return cachedConverter;
|
|
176
|
+
try {
|
|
177
|
+
const zod = nodeRequire('zod');
|
|
178
|
+
if (typeof zod.toJSONSchema !== 'function') {
|
|
179
|
+
cachedConverter = undefined;
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
cachedConverter = zod.toJSONSchema;
|
|
183
|
+
return cachedConverter;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
cachedConverter = undefined;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function loadZodToJSONSchema() {
|
|
191
|
+
const converter = await loadZodConverter();
|
|
192
|
+
if (converter === undefined) {
|
|
193
|
+
return {
|
|
194
|
+
kind: 'err',
|
|
195
|
+
error: {
|
|
196
|
+
code: 'invalid-zod-conversion',
|
|
197
|
+
message: 'Zod schema passed but the `zod` peer dependency is not installed. ' +
|
|
198
|
+
'Install `zod` (>=4.0.0) or author with a JSON Schema object.',
|
|
199
|
+
cause: new Error('zod peer dep not installed'),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return { kind: 'ok', value: converter };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Test-only: reset the cached converter so tests can exercise both the
|
|
207
|
+
* peer-dep-present and peer-dep-missing branches. Not part of the public
|
|
208
|
+
* API — callers outside this package must not depend on this being
|
|
209
|
+
* present.
|
|
210
|
+
*
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
export function __resetZodConverterCacheForTests() {
|
|
214
|
+
cachedConverter = null;
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=zod.js.map
|
package/dist/zod.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.js","sourceRoot":"","sources":["../src/zod.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kindgi/schema",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JSON Schema utilities for Kindgi. Loads specs, provides Ajv-based validation, extracts schema versions. Consumers validate incoming data against the wire contracts in @kindgi/specs.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"repository": {
|
|
7
|
-
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kindgi/kindgi-sdk.git",
|
|
9
|
+
"directory": "packages/schema"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/kindgi/kindgi-sdk/tree/main/packages/schema#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kindgi/kindgi-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md",
|
|
28
|
+
"SPEC.md",
|
|
29
|
+
"type-manifest.json"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@kindgi/types": "0.1.0",
|
|
33
|
+
"ajv": "^8.17.1",
|
|
34
|
+
"ajv-formats": "^3.0.1"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"zod": "^4.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"zod": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@kindgi/specs": "0.1.0",
|
|
46
|
+
"@types/node": "^22.10.5",
|
|
47
|
+
"typescript": "^5.7.3",
|
|
48
|
+
"vitest": "^2.1.8",
|
|
49
|
+
"zod": "^4.6.5"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22.0.0"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public",
|
|
56
|
+
"provenance": true
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.build.json",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/canonical.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canonical JSON serialization — deterministic, reproducible byte-shape
|
|
6
|
+
* suitable for signing.
|
|
7
|
+
*
|
|
8
|
+
* A simplified RFC 8785 (JSON Canonicalization Scheme, JCS) implementation
|
|
9
|
+
* covering the subset of JSON the framework emits: primitives, arrays, and
|
|
10
|
+
* objects with sorted string keys.
|
|
11
|
+
*
|
|
12
|
+
* Rules:
|
|
13
|
+
* - Object keys are sorted lexicographically.
|
|
14
|
+
* - `undefined` values are omitted entirely (they're never signed).
|
|
15
|
+
* - `null` is preserved.
|
|
16
|
+
* - Numbers are serialized via `JSON.stringify` — sufficient for the
|
|
17
|
+
* integer + normal-float range the framework emits; if callers ever
|
|
18
|
+
* emit denormalized doubles or high-precision values, revisit.
|
|
19
|
+
* - Strings use JSON escaping (again via `JSON.stringify`).
|
|
20
|
+
* - No whitespace between tokens.
|
|
21
|
+
*
|
|
22
|
+
* This lives in the public `@kindgi/schema` package because:
|
|
23
|
+
* - Signature verifiers on the SDK side must reproduce the exact
|
|
24
|
+
* same bytes to check signatures — moving this to closed code
|
|
25
|
+
* would break client-side verify.
|
|
26
|
+
* - The algorithm is a public commitment: existing signatures become
|
|
27
|
+
* unverifiable if this changes.
|
|
28
|
+
*/
|
|
29
|
+
export function canonicalize(value: unknown): string {
|
|
30
|
+
if (value === null) return 'null';
|
|
31
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
32
|
+
if (typeof value === 'number') {
|
|
33
|
+
if (!Number.isFinite(value)) {
|
|
34
|
+
throw new Error(`Cannot canonicalize non-finite number: ${String(value)}`);
|
|
35
|
+
}
|
|
36
|
+
return JSON.stringify(value);
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
const parts = value.map((item) => canonicalize(item));
|
|
41
|
+
return `[${parts.join(',')}]`;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === 'object') {
|
|
44
|
+
const obj = value as Readonly<Record<string, unknown>>;
|
|
45
|
+
const keys = Object.keys(obj)
|
|
46
|
+
.filter((k) => obj[k] !== undefined)
|
|
47
|
+
.sort();
|
|
48
|
+
const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`);
|
|
49
|
+
return `{${parts.join(',')}}`;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Cannot canonicalize value of type ${typeof value}`);
|
|
52
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed errors emitted by @kindgi/schema.
|
|
6
|
+
*
|
|
7
|
+
* Every error carries:
|
|
8
|
+
* - `code` — machine-readable discriminator for `if (err.code === ...)` matching.
|
|
9
|
+
* - `message` — human-readable summary.
|
|
10
|
+
* - Case-specific fields (path, id, cause, errors[]).
|
|
11
|
+
*
|
|
12
|
+
* Consumers should switch on `code`, not on `message`. Message strings are
|
|
13
|
+
* documentation-grade but not part of the API contract.
|
|
14
|
+
*/
|
|
15
|
+
export type SchemaError =
|
|
16
|
+
| SchemaLoadError
|
|
17
|
+
| SchemaParseError
|
|
18
|
+
| SchemaCompileError
|
|
19
|
+
| SchemaNotFoundError
|
|
20
|
+
| ValidationError
|
|
21
|
+
| InvalidZodConversionError;
|
|
22
|
+
|
|
23
|
+
/** Filesystem error while reading a schema file or specs directory. */
|
|
24
|
+
export interface SchemaLoadError {
|
|
25
|
+
readonly code: 'schema-load-error';
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly path: string;
|
|
28
|
+
readonly cause: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** JSON parse error while reading a schema file. */
|
|
32
|
+
export interface SchemaParseError {
|
|
33
|
+
readonly code: 'schema-parse-error';
|
|
34
|
+
readonly message: string;
|
|
35
|
+
readonly path: string;
|
|
36
|
+
readonly cause: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ajv rejected the schema during registration/compilation.
|
|
41
|
+
* Also used when a schema is structurally invalid (missing `$id`, etc).
|
|
42
|
+
*/
|
|
43
|
+
export interface SchemaCompileError {
|
|
44
|
+
readonly code: 'schema-compile-error';
|
|
45
|
+
readonly message: string;
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly cause: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Requested a schema by `$id` that isn't registered. */
|
|
51
|
+
export interface SchemaNotFoundError {
|
|
52
|
+
readonly code: 'schema-not-found';
|
|
53
|
+
readonly message: string;
|
|
54
|
+
readonly id: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Data did not validate against the schema. `errors` holds Ajv's error report,
|
|
59
|
+
* intentionally typed as `unknown[]` — consumers must cast to `AjvError[]` if
|
|
60
|
+
* they want to inspect fields, so this package doesn't leak the Ajv API shape.
|
|
61
|
+
*/
|
|
62
|
+
export interface ValidationError {
|
|
63
|
+
readonly code: 'validation-error';
|
|
64
|
+
readonly message: string;
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly errors: readonly unknown[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A Zod schema was passed at an authoring boundary and either
|
|
71
|
+
* - the `zod` peer dependency is not installed, or
|
|
72
|
+
* - `z.toJSONSchema(schema)` threw (unrepresentable Zod construct —
|
|
73
|
+
* e.g. `.transform()` targeting a runtime side-effect, function
|
|
74
|
+
* schemas, uncontained cycles).
|
|
75
|
+
*
|
|
76
|
+
* `cause` is the underlying Error, preserved for surfacing to the author.
|
|
77
|
+
*/
|
|
78
|
+
export interface InvalidZodConversionError {
|
|
79
|
+
readonly code: 'invalid-zod-conversion';
|
|
80
|
+
readonly message: string;
|
|
81
|
+
readonly cause: unknown;
|
|
82
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
export { canonicalize } from './canonical.js';
|
|
5
|
+
export type {
|
|
6
|
+
InvalidZodConversionError,
|
|
7
|
+
SchemaCompileError,
|
|
8
|
+
SchemaError,
|
|
9
|
+
SchemaLoadError,
|
|
10
|
+
SchemaNotFoundError,
|
|
11
|
+
SchemaParseError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
} from './errors.js';
|
|
14
|
+
export { compileInlineSchema, createSpecRegistry, loadSpecRegistry } from './registry.js';
|
|
15
|
+
export type { CompiledInlineSchema, SpecRegistry, ValidationErrorLike } from './registry.js';
|
|
16
|
+
export { versionOf } from './version.js';
|
|
17
|
+
export type { SchemaVersion } from './version.js';
|
|
18
|
+
export {
|
|
19
|
+
isJsonSchemaObject,
|
|
20
|
+
isZodSchema,
|
|
21
|
+
loadZodConverter,
|
|
22
|
+
loadZodConverterSync,
|
|
23
|
+
parseWithSchema,
|
|
24
|
+
schemaKindOf,
|
|
25
|
+
toJSONSchema,
|
|
26
|
+
toJSONSchemaSync,
|
|
27
|
+
} from './zod.js';
|
|
28
|
+
export type {
|
|
29
|
+
AnySchema,
|
|
30
|
+
JSONSchemaObject,
|
|
31
|
+
SchemaIo,
|
|
32
|
+
SchemaIssue,
|
|
33
|
+
SchemaKind,
|
|
34
|
+
ZodConverter,
|
|
35
|
+
ZodLikeSchema,
|
|
36
|
+
} from './zod.js';
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { compileInlineSchema, createSpecRegistry } from './registry.js';
|
|
7
|
+
|
|
8
|
+
const flowSchema = {
|
|
9
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
10
|
+
$id: 'https://kindgi.com/schemas/v1/test-flow.schema.json',
|
|
11
|
+
$comment: 'schema-version: 1.0.0',
|
|
12
|
+
title: 'TestGraph',
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
required: ['id', 'nodes'],
|
|
16
|
+
properties: {
|
|
17
|
+
id: { type: 'string', minLength: 1 },
|
|
18
|
+
nodes: { type: 'array', items: { type: 'string' } },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const toolSchema = {
|
|
23
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
24
|
+
$id: 'https://kindgi.com/schemas/v1/test-tool.schema.json',
|
|
25
|
+
$comment: 'schema-version: 1.0.0',
|
|
26
|
+
title: 'TestTool',
|
|
27
|
+
type: 'object',
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
required: ['id'],
|
|
30
|
+
properties: {
|
|
31
|
+
id: { type: 'string' },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// A pack schema that references the tool schema by $id — this exercises
|
|
36
|
+
// cross-schema $ref resolution, the whole reason for two-pass registration.
|
|
37
|
+
const packSchema = {
|
|
38
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
39
|
+
$id: 'https://kindgi.com/schemas/v1/test-pack.schema.json',
|
|
40
|
+
$comment: 'schema-version: 1.0.0',
|
|
41
|
+
title: 'TestPack',
|
|
42
|
+
type: 'object',
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
required: ['id', 'tools'],
|
|
45
|
+
properties: {
|
|
46
|
+
id: { type: 'string' },
|
|
47
|
+
tools: {
|
|
48
|
+
type: 'array',
|
|
49
|
+
items: { $ref: 'https://kindgi.com/schemas/v1/test-tool.schema.json' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('createSpecRegistry', () => {
|
|
55
|
+
test('empty registry succeeds with no schemas', () => {
|
|
56
|
+
const result = createSpecRegistry([]);
|
|
57
|
+
expect(result.kind).toBe('ok');
|
|
58
|
+
if (result.kind === 'ok') {
|
|
59
|
+
expect(result.value.ids()).toEqual([]);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('registers schemas and exposes their $ids', () => {
|
|
64
|
+
const result = createSpecRegistry([flowSchema, toolSchema]);
|
|
65
|
+
expect(result.kind).toBe('ok');
|
|
66
|
+
if (result.kind === 'ok') {
|
|
67
|
+
expect(result.value.ids()).toEqual([
|
|
68
|
+
'https://kindgi.com/schemas/v1/test-flow.schema.json',
|
|
69
|
+
'https://kindgi.com/schemas/v1/test-tool.schema.json',
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('rejects a schema missing $id', () => {
|
|
75
|
+
const result = createSpecRegistry([{ type: 'object' }]);
|
|
76
|
+
expect(result.kind).toBe('err');
|
|
77
|
+
if (result.kind === 'err') {
|
|
78
|
+
expect(result.error.code).toBe('schema-compile-error');
|
|
79
|
+
expect(result.error.message).toContain('$id');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('rejects a non-object schema', () => {
|
|
84
|
+
const result = createSpecRegistry(['not-a-schema']);
|
|
85
|
+
expect(result.kind).toBe('err');
|
|
86
|
+
if (result.kind === 'err') {
|
|
87
|
+
expect(result.error.code).toBe('schema-compile-error');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('resolves cross-schema $refs when all schemas are registered together', () => {
|
|
92
|
+
const result = createSpecRegistry([toolSchema, packSchema]);
|
|
93
|
+
expect(result.kind).toBe('ok');
|
|
94
|
+
if (result.kind === 'ok') {
|
|
95
|
+
const packOk = result.value.validate('https://kindgi.com/schemas/v1/test-pack.schema.json', {
|
|
96
|
+
id: 'p',
|
|
97
|
+
tools: [{ id: 't1' }],
|
|
98
|
+
});
|
|
99
|
+
expect(packOk.kind).toBe('ok');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('compileInlineSchema', () => {
|
|
105
|
+
test('compiles a minimal valid schema', () => {
|
|
106
|
+
const r = compileInlineSchema({ type: 'object' });
|
|
107
|
+
expect(r.kind).toBe('ok');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('returns err on a schema with an unknown keyword (strict mode)', () => {
|
|
111
|
+
const r = compileInlineSchema({ thisIsNotAJsonSchemaKeyword: true });
|
|
112
|
+
expect(r.kind).toBe('err');
|
|
113
|
+
if (r.kind === 'err') expect(r.error.code).toBe('schema-compile-error');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('returns err on a non-object schema', () => {
|
|
117
|
+
const r = compileInlineSchema('not-a-schema');
|
|
118
|
+
expect(r.kind).toBe('err');
|
|
119
|
+
if (r.kind === 'err') expect(r.error.code).toBe('schema-compile-error');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('validate() accepts conforming data', () => {
|
|
123
|
+
const r = compileInlineSchema({
|
|
124
|
+
type: 'object',
|
|
125
|
+
required: ['name'],
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
properties: { name: { type: 'string' } },
|
|
128
|
+
});
|
|
129
|
+
expect(r.kind).toBe('ok');
|
|
130
|
+
if (r.kind !== 'ok') return;
|
|
131
|
+
const ok = r.value.validate({ name: 'alice' });
|
|
132
|
+
expect(ok.kind).toBe('ok');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('validate() rejects data that violates the schema', () => {
|
|
136
|
+
const r = compileInlineSchema({
|
|
137
|
+
type: 'object',
|
|
138
|
+
required: ['name'],
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
properties: { name: { type: 'string' } },
|
|
141
|
+
});
|
|
142
|
+
if (r.kind !== 'ok') throw new Error('compile failed');
|
|
143
|
+
const bad = r.value.validate({});
|
|
144
|
+
expect(bad.kind).toBe('err');
|
|
145
|
+
if (bad.kind === 'err') expect(bad.error.code).toBe('validation-error');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('validate() returns the input value on success (identity, cast to T)', () => {
|
|
149
|
+
const r = compileInlineSchema({ type: 'object' });
|
|
150
|
+
if (r.kind !== 'ok') throw new Error('compile failed');
|
|
151
|
+
const input = { hello: 'world' };
|
|
152
|
+
const ok = r.value.validate<{ hello: string }>(input);
|
|
153
|
+
if (ok.kind === 'ok') {
|
|
154
|
+
expect(ok.value).toBe(input);
|
|
155
|
+
expect(ok.value.hello).toBe('world');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('source is round-trippable', () => {
|
|
160
|
+
const schema = { type: 'object' };
|
|
161
|
+
const r = compileInlineSchema(schema);
|
|
162
|
+
if (r.kind !== 'ok') throw new Error('compile failed');
|
|
163
|
+
expect(r.value.source).toBe(schema);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('SpecRegistry.validate', () => {
|
|
168
|
+
test('accepts data that matches the schema', () => {
|
|
169
|
+
const setup = createSpecRegistry([flowSchema]);
|
|
170
|
+
if (setup.kind === 'err') throw new Error('setup failed');
|
|
171
|
+
const result = setup.value.validate('https://kindgi.com/schemas/v1/test-flow.schema.json', {
|
|
172
|
+
id: 'g1',
|
|
173
|
+
nodes: ['a', 'b'],
|
|
174
|
+
});
|
|
175
|
+
expect(result.kind).toBe('ok');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('rejects data that fails the schema with a validation-error', () => {
|
|
179
|
+
const setup = createSpecRegistry([flowSchema]);
|
|
180
|
+
if (setup.kind === 'err') throw new Error('setup failed');
|
|
181
|
+
const result = setup.value.validate('https://kindgi.com/schemas/v1/test-flow.schema.json', {
|
|
182
|
+
id: '',
|
|
183
|
+
nodes: 'not-an-array',
|
|
184
|
+
});
|
|
185
|
+
expect(result.kind).toBe('err');
|
|
186
|
+
if (result.kind === 'err' && result.error.code === 'validation-error') {
|
|
187
|
+
expect(result.error.errors.length).toBeGreaterThan(0);
|
|
188
|
+
} else {
|
|
189
|
+
throw new Error('expected validation-error');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('returns schema-not-found for an unknown $id', () => {
|
|
194
|
+
const setup = createSpecRegistry([flowSchema]);
|
|
195
|
+
if (setup.kind === 'err') throw new Error('setup failed');
|
|
196
|
+
const result = setup.value.validate('https://kindgi.com/schemas/v1/nope.schema.json', {});
|
|
197
|
+
expect(result.kind).toBe('err');
|
|
198
|
+
if (result.kind === 'err') {
|
|
199
|
+
expect(result.error.code).toBe('schema-not-found');
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('SpecRegistry.getSchema', () => {
|
|
205
|
+
test('returns the raw schema for a known $id', () => {
|
|
206
|
+
const setup = createSpecRegistry([flowSchema]);
|
|
207
|
+
if (setup.kind === 'err') throw new Error('setup failed');
|
|
208
|
+
const result = setup.value.getSchema('https://kindgi.com/schemas/v1/test-flow.schema.json');
|
|
209
|
+
expect(result.kind).toBe('ok');
|
|
210
|
+
if (result.kind === 'ok') {
|
|
211
|
+
expect(result.value).toBe(flowSchema);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('returns schema-not-found for an unknown $id', () => {
|
|
216
|
+
const setup = createSpecRegistry([]);
|
|
217
|
+
if (setup.kind === 'err') throw new Error('setup failed');
|
|
218
|
+
const result = setup.value.getSchema('https://kindgi.com/schemas/v1/nope.schema.json');
|
|
219
|
+
expect(result.kind).toBe('err');
|
|
220
|
+
});
|
|
221
|
+
});
|