@qilin-agent/tools 0.0.0-stage → 3.1.3
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 +21 -0
- package/README.i18n.yaml +103 -0
- package/README.md +239 -2
- package/README.zh.md +240 -0
- package/lib/index.js +3705 -0
- package/lib/types/index.d.ts +844 -0
- package/lib/types/index.js +1334 -0
- package/lib/types/json-schema.d.ts +106 -0
- package/lib/types/json-schema.js +577 -0
- package/lib/types/presentation.d.ts +367 -0
- package/lib/types/presentation.js +8 -0
- package/lib/types/ptc.d.ts +80 -0
- package/lib/types/ptc.js +660 -0
- package/lib/types/py-types.d.ts +49 -0
- package/lib/types/py-types.js +788 -0
- package/lib/types/schema.d.ts +243 -0
- package/lib/types/schema.js +350 -0
- package/lib/types/testing.d.ts +25 -0
- package/lib/types/testing.js +25 -0
- package/lib/types/ts-types.d.ts +37 -0
- package/lib/types/ts-types.js +291 -0
- package/lib/types/types.d.ts +61 -0
- package/lib/types/types.js +7 -0
- package/package.json +68 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforced JSON Schema subset shared by tool outputs, generated PTC mode
|
|
3
|
+
* types, subagents, and workflows. The subset accepts any JSON root, an
|
|
4
|
+
* annotation-only schema for unconstrained JSON, one scalar `type`, object
|
|
5
|
+
* `properties`/`required`/boolean `additionalProperties`, array `items`,
|
|
6
|
+
* type-correct scalar `enum`/`const`, and exact-one `oneOf`.
|
|
7
|
+
*
|
|
8
|
+
* Unsupported or misplaced keywords reject rather than being accepted without
|
|
9
|
+
* enforcement. Consumers that require an object root apply
|
|
10
|
+
* {@link assertObjectJsonSchema} before accepting input.
|
|
11
|
+
* @module qilin-tools/json-schema
|
|
12
|
+
*/
|
|
13
|
+
import { HarnessError } from '@qilin-agent/llm';
|
|
14
|
+
import { type JsonValue } from '@qilin-agent/util-values';
|
|
15
|
+
/** Scalar JSON values supported by `enum` and `const`. */
|
|
16
|
+
export type JsonSchemaScalar = string | number | boolean | null;
|
|
17
|
+
/** Single-type keywords accepted by the enforced subset. */
|
|
18
|
+
export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';
|
|
19
|
+
/**
|
|
20
|
+
* One raw JSON Schema node in the enforced subset. The optional fields express
|
|
21
|
+
* the external wire schema; {@link assertSupportedJsonSchema} rejects invalid
|
|
22
|
+
* combinations before a caller treats the node as trusted.
|
|
23
|
+
*/
|
|
24
|
+
export interface JsonSchemaNode {
|
|
25
|
+
/** Omit with no constraints for any JSON value, or use `oneOf`. */
|
|
26
|
+
type?: JsonSchemaType;
|
|
27
|
+
/** Exactly one branch must validate; at least two branches are required. */
|
|
28
|
+
oneOf?: JsonSchemaNode[];
|
|
29
|
+
/** Nested property schemas (`type: 'object'` only). */
|
|
30
|
+
properties?: Record<string, JsonSchemaNode>;
|
|
31
|
+
/** Required property names; each must appear in `properties`. */
|
|
32
|
+
required?: string[];
|
|
33
|
+
/** `false` rejects undeclared keys; absent/`true` follows JSON Schema's open default. */
|
|
34
|
+
additionalProperties?: boolean;
|
|
35
|
+
/** Item schema (`type: 'array'` only); absent accepts any JSON item. */
|
|
36
|
+
items?: JsonSchemaNode;
|
|
37
|
+
/** Allowed values for a scalar node. */
|
|
38
|
+
enum?: JsonSchemaScalar[];
|
|
39
|
+
/** The single allowed value for a scalar node. */
|
|
40
|
+
const?: JsonSchemaScalar;
|
|
41
|
+
/** Annotation, ignored for validation. */
|
|
42
|
+
description?: string;
|
|
43
|
+
/** Annotation, ignored for validation. */
|
|
44
|
+
title?: string;
|
|
45
|
+
/** Annotation, ignored for validation but required to be lossless JSON. */
|
|
46
|
+
default?: JsonValue;
|
|
47
|
+
/** Annotation, ignored for validation but required to be lossless JSON. */
|
|
48
|
+
examples?: JsonValue;
|
|
49
|
+
}
|
|
50
|
+
/** A consumer-constrained object-rooted schema. */
|
|
51
|
+
export type ObjectJsonSchema = JsonSchemaNode & {
|
|
52
|
+
type: 'object';
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Thrown when a raw schema falls outside the enforced subset. `violations`
|
|
56
|
+
* lists every offending path instead of stopping at the first author error.
|
|
57
|
+
*/
|
|
58
|
+
export declare class JsonSchemaError extends HarnessError {
|
|
59
|
+
/** Individual schema violations in walk order. */
|
|
60
|
+
readonly violations: string[];
|
|
61
|
+
constructor(violations: string[]);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Test for a realm-agnostic plain JSON record without accepting arrays or
|
|
65
|
+
* exotic objects.
|
|
66
|
+
* @param value - candidate record from any JavaScript realm.
|
|
67
|
+
* @returns Whether the value has a plain-object prototype chain.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isPlainJsonRecord(value: unknown): value is Record<string, unknown>;
|
|
70
|
+
/**
|
|
71
|
+
* Test for an ordinary schema record whose keys survive JSON projection.
|
|
72
|
+
* @param value - candidate record from any JavaScript realm.
|
|
73
|
+
* @returns Whether the record has an intrinsic prototype and only own enumerable string keys.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isJsonSchemaRecord(value: unknown): value is Record<string, unknown>;
|
|
76
|
+
/**
|
|
77
|
+
* Test for a dense ordinary array with no JSON-invisible decorations.
|
|
78
|
+
* @param value - candidate array from any JavaScript realm.
|
|
79
|
+
* @returns Whether the array is intrinsic, dense, and undecorated.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isPlainJsonArray(value: unknown): value is unknown[];
|
|
82
|
+
/**
|
|
83
|
+
* Assert that an arbitrary raw schema uses only the enforced subset.
|
|
84
|
+
* Annotation-only schemas are accepted as the standard unconstrained-JSON
|
|
85
|
+
* form; callers that require an object root use {@link assertObjectJsonSchema}.
|
|
86
|
+
* @param schema - untrusted raw JSON Schema.
|
|
87
|
+
* @returns Assertion that the schema belongs to the supported subset.
|
|
88
|
+
*/
|
|
89
|
+
export declare function assertSupportedJsonSchema(schema: unknown): asserts schema is JsonSchemaNode;
|
|
90
|
+
/**
|
|
91
|
+
* Assert the enforced subset plus the object-root constraint retained by
|
|
92
|
+
* subagent and workflow structured outputs.
|
|
93
|
+
* @param schema - untrusted caller-supplied schema.
|
|
94
|
+
* @returns Assertion that the schema belongs to the supported subset and has an object root.
|
|
95
|
+
*/
|
|
96
|
+
export declare function assertObjectJsonSchema(schema: unknown): asserts schema is ObjectJsonSchema;
|
|
97
|
+
/**
|
|
98
|
+
* Validate a candidate value against an asserted raw schema. The function is
|
|
99
|
+
* total for arbitrary values and returns path-qualified violations.
|
|
100
|
+
* @param schema - a schema accepted by {@link assertSupportedJsonSchema}.
|
|
101
|
+
* @param value - the candidate JSON value.
|
|
102
|
+
* @param path - root label used in diagnostics.
|
|
103
|
+
* @returns All violations in walk order; empty means valid.
|
|
104
|
+
*/
|
|
105
|
+
export declare function validateJsonSchemaValue(schema: JsonSchemaNode, value: unknown, path?: string): string[];
|
|
106
|
+
//# sourceMappingURL=json-schema.d.ts.map
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforced JSON Schema subset shared by tool outputs, generated PTC mode
|
|
3
|
+
* types, subagents, and workflows. The subset accepts any JSON root, an
|
|
4
|
+
* annotation-only schema for unconstrained JSON, one scalar `type`, object
|
|
5
|
+
* `properties`/`required`/boolean `additionalProperties`, array `items`,
|
|
6
|
+
* type-correct scalar `enum`/`const`, and exact-one `oneOf`.
|
|
7
|
+
*
|
|
8
|
+
* Unsupported or misplaced keywords reject rather than being accepted without
|
|
9
|
+
* enforcement. Consumers that require an object root apply
|
|
10
|
+
* {@link assertObjectJsonSchema} before accepting input.
|
|
11
|
+
* @module qilin-tools/json-schema
|
|
12
|
+
*/
|
|
13
|
+
import { HarnessError } from '@qilin-agent/llm';
|
|
14
|
+
import { assertNever, isJsonValue } from '@qilin-agent/util-values';
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a raw schema falls outside the enforced subset. `violations`
|
|
17
|
+
* lists every offending path instead of stopping at the first author error.
|
|
18
|
+
*/
|
|
19
|
+
export class JsonSchemaError extends HarnessError {
|
|
20
|
+
/** Individual schema violations in walk order. */
|
|
21
|
+
violations;
|
|
22
|
+
constructor(violations) {
|
|
23
|
+
super(`unsupported JSON schema: ${violations.join('; ')}`, 'UNSUPPORTED_SCHEMA');
|
|
24
|
+
this.name = 'JsonSchemaError';
|
|
25
|
+
this.violations = violations;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const CONSTRAINT_KEYWORDS = new Set([
|
|
29
|
+
'type',
|
|
30
|
+
'oneOf',
|
|
31
|
+
'properties',
|
|
32
|
+
'required',
|
|
33
|
+
'additionalProperties',
|
|
34
|
+
'items',
|
|
35
|
+
'enum',
|
|
36
|
+
'const',
|
|
37
|
+
]);
|
|
38
|
+
const ANNOTATION_KEYWORDS = new Set(['description', 'title', 'default', 'examples']);
|
|
39
|
+
const SCHEMA_TYPES = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'];
|
|
40
|
+
/* jscpd:ignore-start -- this realm boundary mirrors the session-owned lossless-JSON intrinsic test */
|
|
41
|
+
/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
|
|
42
|
+
function hasIntrinsicConstructor(prototype, name) {
|
|
43
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor');
|
|
44
|
+
const constructor = descriptor?.value;
|
|
45
|
+
if (typeof constructor !== 'function')
|
|
46
|
+
return false;
|
|
47
|
+
try {
|
|
48
|
+
return constructor.name === name
|
|
49
|
+
&& constructor.prototype === prototype
|
|
50
|
+
&& Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Whether a candidate is one realm's intrinsic `Object.prototype`. */
|
|
57
|
+
function isIntrinsicObjectPrototype(value) {
|
|
58
|
+
return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Test for a realm-agnostic plain JSON record without accepting arrays or
|
|
62
|
+
* exotic objects.
|
|
63
|
+
* @param value - candidate record from any JavaScript realm.
|
|
64
|
+
* @returns Whether the value has a plain-object prototype chain.
|
|
65
|
+
*/
|
|
66
|
+
export function isPlainJsonRecord(value) {
|
|
67
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
const prototype = Object.getPrototypeOf(value);
|
|
71
|
+
return prototype === null
|
|
72
|
+
|| typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Whether an array uses one realm's intrinsic `Array.prototype`. */
|
|
79
|
+
function hasPlainArrayPrototype(value) {
|
|
80
|
+
const prototype = Object.getPrototypeOf(value);
|
|
81
|
+
if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array'))
|
|
82
|
+
return false;
|
|
83
|
+
const objectPrototype = Object.getPrototypeOf(prototype);
|
|
84
|
+
return typeof objectPrototype === 'object'
|
|
85
|
+
&& objectPrototype !== null
|
|
86
|
+
&& isIntrinsicObjectPrototype(objectPrototype);
|
|
87
|
+
}
|
|
88
|
+
/* jscpd:ignore-end */
|
|
89
|
+
/** Return whether a record contains only own enumerable string keys. */
|
|
90
|
+
function hasOnlyEnumerableStringKeys(value) {
|
|
91
|
+
try {
|
|
92
|
+
return Reflect.ownKeys(value)
|
|
93
|
+
.every(key => typeof key === 'string' && Object.prototype.propertyIsEnumerable.call(value, key));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Test for an ordinary schema record whose keys survive JSON projection.
|
|
101
|
+
* @param value - candidate record from any JavaScript realm.
|
|
102
|
+
* @returns Whether the record has an intrinsic prototype and only own enumerable string keys.
|
|
103
|
+
*/
|
|
104
|
+
export function isJsonSchemaRecord(value) {
|
|
105
|
+
return isPlainJsonRecord(value) && hasOnlyEnumerableStringKeys(value);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Test for a dense ordinary array with no JSON-invisible decorations.
|
|
109
|
+
* @param value - candidate array from any JavaScript realm.
|
|
110
|
+
* @returns Whether the array is intrinsic, dense, and undecorated.
|
|
111
|
+
*/
|
|
112
|
+
export function isPlainJsonArray(value) {
|
|
113
|
+
if (!Array.isArray(value))
|
|
114
|
+
return false;
|
|
115
|
+
try {
|
|
116
|
+
if (!hasPlainArrayPrototype(value) || Reflect.ownKeys(value).length !== value.length + 1)
|
|
117
|
+
return false;
|
|
118
|
+
for (let index = 0; index < value.length; index++) {
|
|
119
|
+
if (!Object.hasOwn(value, index))
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Lossless finite JSON number, excluding negative zero. */
|
|
129
|
+
function isJsonNumber(value) {
|
|
130
|
+
return typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0);
|
|
131
|
+
}
|
|
132
|
+
/** Whether a scalar is valid for one declared schema type. */
|
|
133
|
+
function scalarMatches(type, value) {
|
|
134
|
+
switch (type) {
|
|
135
|
+
case 'string': return typeof value === 'string';
|
|
136
|
+
case 'number': return isJsonNumber(value);
|
|
137
|
+
case 'integer': return isJsonNumber(value) && Number.isInteger(value);
|
|
138
|
+
case 'boolean': return typeof value === 'boolean';
|
|
139
|
+
case 'null': return value === null;
|
|
140
|
+
/* v8 ignore next -- JsonSchemaScalarType is closed; this retains compile-time exhaustiveness. */
|
|
141
|
+
default: return assertNever(type, 'JsonSchemaType');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Keywords that are invalid beside `oneOf`. */
|
|
145
|
+
const ONE_OF_SIBLING_KEYWORDS = ['properties', 'required', 'additionalProperties', 'items', 'enum', 'const'];
|
|
146
|
+
/** Validate object-only fields after its property schemas have been visited. */
|
|
147
|
+
function checkObjectSchemaTail(node, path, properties, violations) {
|
|
148
|
+
const hasRequired = Object.hasOwn(node, 'required');
|
|
149
|
+
const required = hasRequired ? node.required : undefined;
|
|
150
|
+
if (hasRequired) {
|
|
151
|
+
if (!isPlainJsonArray(required) || required.some(entry => typeof entry !== 'string')) {
|
|
152
|
+
violations.push(`${path}.required must be an array of strings`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const declared = isJsonSchemaRecord(properties) ? properties : {};
|
|
156
|
+
for (const key of required) {
|
|
157
|
+
if (!Object.hasOwn(declared, key))
|
|
158
|
+
violations.push(`${path}.required names "${key}" which is not in properties`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (Object.hasOwn(node, 'additionalProperties') && typeof node.additionalProperties !== 'boolean') {
|
|
163
|
+
violations.push(`${path}.additionalProperties must be a boolean`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Collect every violation for one raw schema tree without using the JavaScript call stack. */
|
|
167
|
+
function checkSchemaNode(root, rootPath, violations, seen) {
|
|
168
|
+
const tasks = [{ kind: 'enter', node: root, path: rootPath }];
|
|
169
|
+
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
|
|
170
|
+
if (task.kind === 'leave') {
|
|
171
|
+
seen.delete(task.node);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (task.kind === 'one-of-tail') {
|
|
175
|
+
for (const key of ONE_OF_SIBLING_KEYWORDS) {
|
|
176
|
+
if (Object.hasOwn(task.node, key))
|
|
177
|
+
violations.push(`${task.path}.${key} is not supported beside oneOf`);
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (task.kind === 'object-tail') {
|
|
182
|
+
checkObjectSchemaTail(task.node, task.path, task.properties, violations);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const { node, path } = task;
|
|
186
|
+
if (!isJsonSchemaRecord(node)) {
|
|
187
|
+
violations.push(`${path} must be a schema object`);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (seen.has(node)) {
|
|
191
|
+
violations.push(`${path} is circular`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
seen.add(node);
|
|
195
|
+
tasks.push({ kind: 'leave', node });
|
|
196
|
+
for (const key of Object.keys(node)) {
|
|
197
|
+
if (CONSTRAINT_KEYWORDS.has(key))
|
|
198
|
+
continue;
|
|
199
|
+
if (ANNOTATION_KEYWORDS.has(key)) {
|
|
200
|
+
try {
|
|
201
|
+
if (!isJsonValue(node[key]))
|
|
202
|
+
violations.push(`${path}.${key} annotation must be lossless JSON data`);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
violations.push(`${path}.${key} annotation must be lossless JSON data`);
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
violations.push(`${path}.${key} is not a supported keyword (subset: type/oneOf/properties/required/additionalProperties/items/enum/const + annotations)`);
|
|
210
|
+
}
|
|
211
|
+
if (Object.hasOwn(node, 'description') && typeof node.description !== 'string') {
|
|
212
|
+
violations.push(`${path}.description must be a string`);
|
|
213
|
+
}
|
|
214
|
+
if (Object.hasOwn(node, 'title') && typeof node.title !== 'string') {
|
|
215
|
+
violations.push(`${path}.title must be a string`);
|
|
216
|
+
}
|
|
217
|
+
const hasType = Object.hasOwn(node, 'type');
|
|
218
|
+
const hasOneOf = Object.hasOwn(node, 'oneOf');
|
|
219
|
+
if (hasType && hasOneOf) {
|
|
220
|
+
violations.push(`${path} cannot declare both type and oneOf`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (!hasType && !hasOneOf) {
|
|
224
|
+
for (const key of ONE_OF_SIBLING_KEYWORDS) {
|
|
225
|
+
if (Object.hasOwn(node, key))
|
|
226
|
+
violations.push(`${path}.${key} requires type or oneOf`);
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (hasOneOf) {
|
|
231
|
+
const oneOf = node.oneOf;
|
|
232
|
+
tasks.push({ kind: 'one-of-tail', node, path });
|
|
233
|
+
if (!isPlainJsonArray(oneOf) || oneOf.length < 2) {
|
|
234
|
+
violations.push(`${path}.oneOf must be an array of at least two schemas`);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
for (let index = oneOf.length - 1; index >= 0; index--) {
|
|
238
|
+
tasks.push({ kind: 'enter', node: oneOf[index], path: `${path}.oneOf[${index}]` });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const type = node.type;
|
|
244
|
+
if (typeof type !== 'string' || !SCHEMA_TYPES.includes(type)) {
|
|
245
|
+
violations.push(Array.isArray(type)
|
|
246
|
+
? `${path}.type must be a single type string (type arrays are not supported)`
|
|
247
|
+
: `${path}.type must be one of ${SCHEMA_TYPES.join('/')}`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const schemaType = type;
|
|
251
|
+
const allowedFor = {
|
|
252
|
+
properties: ['object'],
|
|
253
|
+
required: ['object'],
|
|
254
|
+
additionalProperties: ['object'],
|
|
255
|
+
items: ['array'],
|
|
256
|
+
enum: ['string', 'number', 'integer', 'boolean', 'null'],
|
|
257
|
+
const: ['string', 'number', 'integer', 'boolean', 'null'],
|
|
258
|
+
};
|
|
259
|
+
for (const [key, types] of Object.entries(allowedFor)) {
|
|
260
|
+
if (Object.hasOwn(node, key) && !types.includes(schemaType)) {
|
|
261
|
+
violations.push(`${path}.${key} is not supported on type "${schemaType}"`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
switch (schemaType) {
|
|
265
|
+
case 'object': {
|
|
266
|
+
const properties = Object.hasOwn(node, 'properties') ? node.properties : undefined;
|
|
267
|
+
tasks.push({ kind: 'object-tail', node, path, properties });
|
|
268
|
+
if (Object.hasOwn(node, 'properties')) {
|
|
269
|
+
if (!isJsonSchemaRecord(properties)) {
|
|
270
|
+
violations.push(`${path}.properties must be an object of schemas`);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
const entries = Object.entries(properties);
|
|
274
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
275
|
+
const entry = entries[index];
|
|
276
|
+
/* v8 ignore next -- the loop is bounded by the captured entry count. */
|
|
277
|
+
if (entry === undefined)
|
|
278
|
+
continue;
|
|
279
|
+
tasks.push({ kind: 'enter', node: entry[1], path: `${path}.properties.${entry[0]}` });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case 'array': {
|
|
286
|
+
if (Object.hasOwn(node, 'items'))
|
|
287
|
+
tasks.push({ kind: 'enter', node: node.items, path: `${path}.items` });
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case 'string':
|
|
291
|
+
case 'number':
|
|
292
|
+
case 'integer':
|
|
293
|
+
case 'boolean':
|
|
294
|
+
case 'null': {
|
|
295
|
+
const hasEnum = Object.hasOwn(node, 'enum');
|
|
296
|
+
const allowed = hasEnum ? node.enum : undefined;
|
|
297
|
+
const enumValid = isPlainJsonArray(allowed)
|
|
298
|
+
&& allowed.length > 0
|
|
299
|
+
&& allowed.every(entry => scalarMatches(schemaType, entry));
|
|
300
|
+
if (hasEnum && !enumValid) {
|
|
301
|
+
violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`);
|
|
302
|
+
}
|
|
303
|
+
const hasConst = Object.hasOwn(node, 'const');
|
|
304
|
+
const declaredConst = hasConst ? node.const : undefined;
|
|
305
|
+
const constValid = scalarMatches(schemaType, declaredConst);
|
|
306
|
+
if (hasConst) {
|
|
307
|
+
if (!constValid) {
|
|
308
|
+
violations.push(`${path}.const must be a ${schemaType} value`);
|
|
309
|
+
}
|
|
310
|
+
else if (enumValid && !allowed.includes(declaredConst)) {
|
|
311
|
+
violations.push(`${path}.const must be one of ${path}.enum when both are declared`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
/* v8 ignore next -- schemaType was narrowed from the closed SCHEMA_TYPES table above. */
|
|
317
|
+
default: assertNever(schemaType, 'JsonSchemaType');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Assert that an arbitrary raw schema uses only the enforced subset.
|
|
323
|
+
* Annotation-only schemas are accepted as the standard unconstrained-JSON
|
|
324
|
+
* form; callers that require an object root use {@link assertObjectJsonSchema}.
|
|
325
|
+
* @param schema - untrusted raw JSON Schema.
|
|
326
|
+
* @returns Assertion that the schema belongs to the supported subset.
|
|
327
|
+
*/
|
|
328
|
+
export function assertSupportedJsonSchema(schema) {
|
|
329
|
+
const violations = [];
|
|
330
|
+
checkSchemaNode(schema, 'schema', violations, new Set());
|
|
331
|
+
if (violations.length > 0)
|
|
332
|
+
throw new JsonSchemaError(violations);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Assert the enforced subset plus the object-root constraint retained by
|
|
336
|
+
* subagent and workflow structured outputs.
|
|
337
|
+
* @param schema - untrusted caller-supplied schema.
|
|
338
|
+
* @returns Assertion that the schema belongs to the supported subset and has an object root.
|
|
339
|
+
*/
|
|
340
|
+
export function assertObjectJsonSchema(schema) {
|
|
341
|
+
const violations = [];
|
|
342
|
+
checkSchemaNode(schema, 'schema', violations, new Set());
|
|
343
|
+
if (violations.length === 0
|
|
344
|
+
&& (!isJsonSchemaRecord(schema) || !Object.hasOwn(schema, 'type') || schema.type !== 'object')) {
|
|
345
|
+
violations.push('schema.type must be "object" (structured output is object-rooted)');
|
|
346
|
+
}
|
|
347
|
+
if (violations.length > 0)
|
|
348
|
+
throw new JsonSchemaError(violations);
|
|
349
|
+
}
|
|
350
|
+
/** Safely test the lossless JSON boundary when a getter may throw. */
|
|
351
|
+
function safelyIsJsonValue(value) {
|
|
352
|
+
try {
|
|
353
|
+
return isJsonValue(value);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Root-aware diagnostic path for the parameter validator's empty sentinel. */
|
|
360
|
+
function diagnosticPath(path) {
|
|
361
|
+
return path === '' ? 'arguments' : path;
|
|
362
|
+
}
|
|
363
|
+
/** Append one object property without a leading dot at an implicit root. */
|
|
364
|
+
function propertyPath(path, key) {
|
|
365
|
+
return path === '' ? key : `${path}.${key}`;
|
|
366
|
+
}
|
|
367
|
+
/** The generic exception-containment diagnostic owned by one valid schema node. */
|
|
368
|
+
function losslessValueViolation(path) {
|
|
369
|
+
return [`"${diagnosticPath(path)}" must be a lossless JSON value`];
|
|
370
|
+
}
|
|
371
|
+
/** Append diagnostics without spreading a potentially wide child result as call arguments. */
|
|
372
|
+
function appendViolations(target, source) {
|
|
373
|
+
for (const violation of source)
|
|
374
|
+
target.push(violation);
|
|
375
|
+
}
|
|
376
|
+
/** Initialize one validation frame with empty aggregation state. */
|
|
377
|
+
function valueFrame(node, value, path) {
|
|
378
|
+
return {
|
|
379
|
+
node,
|
|
380
|
+
value,
|
|
381
|
+
path,
|
|
382
|
+
catches: false,
|
|
383
|
+
phase: 'start',
|
|
384
|
+
children: [],
|
|
385
|
+
childIndex: 0,
|
|
386
|
+
violations: [],
|
|
387
|
+
tailViolations: [],
|
|
388
|
+
matches: 0,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/** Validate one scalar node after its primitive type check. */
|
|
392
|
+
function checkScalarValue(node, value, path) {
|
|
393
|
+
const allowed = Object.hasOwn(node, 'enum') ? node.enum : undefined;
|
|
394
|
+
if (allowed !== undefined && !allowed.includes(value)) {
|
|
395
|
+
return [`"${diagnosticPath(path)}" must be one of ${JSON.stringify(allowed)}`];
|
|
396
|
+
}
|
|
397
|
+
if (Object.hasOwn(node, 'const') && value !== node.const) {
|
|
398
|
+
return [`"${diagnosticPath(path)}" must be ${JSON.stringify(node.const)}`];
|
|
399
|
+
}
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
/** Validate one trusted schema/value pair with explicit frames rather than recursive calls. */
|
|
403
|
+
function checkValue(schema, value, path) {
|
|
404
|
+
const frames = [valueFrame(schema, value, path)];
|
|
405
|
+
let rootResult;
|
|
406
|
+
const receive = (result) => {
|
|
407
|
+
const parent = frames.at(-1);
|
|
408
|
+
if (parent === undefined) {
|
|
409
|
+
rootResult = result;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (parent.kind === 'oneOf') {
|
|
413
|
+
if (result.length === 0)
|
|
414
|
+
parent.matches++;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
appendViolations(parent.violations, result);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
const finish = (result) => {
|
|
421
|
+
frames.pop();
|
|
422
|
+
receive(result);
|
|
423
|
+
};
|
|
424
|
+
while (frames.length > 0) {
|
|
425
|
+
const frame = frames.at(-1);
|
|
426
|
+
/* v8 ignore next -- the loop condition guarantees a current frame. */
|
|
427
|
+
if (frame === undefined)
|
|
428
|
+
break;
|
|
429
|
+
try {
|
|
430
|
+
if (frame.phase === 'children') {
|
|
431
|
+
if (frame.childIndex < frame.children.length) {
|
|
432
|
+
const child = frame.children[frame.childIndex];
|
|
433
|
+
/* v8 ignore next -- childIndex is bounded by children.length. */
|
|
434
|
+
if (child === undefined)
|
|
435
|
+
throw new Error('missing schema-value child frame');
|
|
436
|
+
frame.childIndex++;
|
|
437
|
+
frames.push(valueFrame(child.node, child.value, child.path));
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (frame.kind === 'oneOf') {
|
|
441
|
+
finish(frame.matches === 1 ? [] : [`"${diagnosticPath(frame.path)}" must match exactly one oneOf branch (matched ${frame.matches})`]);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
appendViolations(frame.violations, frame.tailViolations);
|
|
445
|
+
if (frame.violations.length > 0) {
|
|
446
|
+
finish(frame.violations);
|
|
447
|
+
}
|
|
448
|
+
else if (frame.kind === 'object') {
|
|
449
|
+
finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a lossless JSON object`]);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
finish(safelyIsJsonValue(frame.value) ? [] : [`"${diagnosticPath(frame.path)}" must be a dense lossless JSON array`]);
|
|
453
|
+
}
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const nodeType = Object.hasOwn(frame.node, 'type') ? frame.node.type : undefined;
|
|
457
|
+
frame.catches = !(nodeType !== undefined && !SCHEMA_TYPES.includes(nodeType));
|
|
458
|
+
const oneOf = Object.hasOwn(frame.node, 'oneOf') ? frame.node.oneOf : undefined;
|
|
459
|
+
if (oneOf !== undefined) {
|
|
460
|
+
frame.kind = 'oneOf';
|
|
461
|
+
frame.children = Array.from(oneOf, branch => ({ node: branch, value: frame.value, path: frame.path }));
|
|
462
|
+
frame.childIndex = 0;
|
|
463
|
+
frame.matches = 0;
|
|
464
|
+
frame.phase = 'children';
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (nodeType === undefined) {
|
|
468
|
+
finish(safelyIsJsonValue(frame.value) ? [] : losslessValueViolation(frame.path));
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
switch (nodeType) {
|
|
472
|
+
case 'object': {
|
|
473
|
+
if (!isPlainJsonRecord(frame.value)) {
|
|
474
|
+
finish([`"${diagnosticPath(frame.path)}" must be an object`]);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
const properties = Object.hasOwn(frame.node, 'properties') ? frame.node.properties ?? {} : {};
|
|
478
|
+
const violations = [];
|
|
479
|
+
const required = Object.hasOwn(frame.node, 'required') ? frame.node.required ?? [] : [];
|
|
480
|
+
for (const key of required) {
|
|
481
|
+
if (!Object.hasOwn(frame.value, key) || frame.value[key] === undefined) {
|
|
482
|
+
violations.push(`missing required property "${propertyPath(frame.path, key)}"`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const children = [];
|
|
486
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
487
|
+
if (!Object.hasOwn(frame.value, key) || frame.value[key] === undefined)
|
|
488
|
+
continue;
|
|
489
|
+
children.push({ node: child, value: frame.value[key], path: propertyPath(frame.path, key) });
|
|
490
|
+
}
|
|
491
|
+
const tailViolations = [];
|
|
492
|
+
if (Object.hasOwn(frame.node, 'additionalProperties') && frame.node.additionalProperties === false) {
|
|
493
|
+
for (const key of Object.keys(frame.value)) {
|
|
494
|
+
if (!Object.hasOwn(properties, key)) {
|
|
495
|
+
tailViolations.push(`"${propertyPath(frame.path, key)}" is not a declared property (additionalProperties: false)`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
frame.kind = 'object';
|
|
500
|
+
frame.children = children;
|
|
501
|
+
frame.childIndex = 0;
|
|
502
|
+
frame.violations = violations;
|
|
503
|
+
frame.tailViolations = tailViolations;
|
|
504
|
+
frame.phase = 'children';
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case 'array': {
|
|
508
|
+
if (!Array.isArray(frame.value)) {
|
|
509
|
+
finish([`"${diagnosticPath(frame.path)}" must be an array`]);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
const items = Object.hasOwn(frame.node, 'items') ? frame.node.items : undefined;
|
|
513
|
+
const children = items === undefined
|
|
514
|
+
? []
|
|
515
|
+
: frame.value.flatMap((entry, index) => [{ node: items, value: entry, path: `${frame.path}[${index}]` }]);
|
|
516
|
+
frame.kind = 'array';
|
|
517
|
+
frame.children = children;
|
|
518
|
+
frame.childIndex = 0;
|
|
519
|
+
frame.violations = [];
|
|
520
|
+
frame.phase = 'children';
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case 'string':
|
|
524
|
+
finish(typeof frame.value === 'string'
|
|
525
|
+
? checkScalarValue(frame.node, frame.value, frame.path)
|
|
526
|
+
: [`"${diagnosticPath(frame.path)}" must be a string`]);
|
|
527
|
+
break;
|
|
528
|
+
case 'number':
|
|
529
|
+
finish(typeof frame.value !== 'number'
|
|
530
|
+
? [`"${diagnosticPath(frame.path)}" must be a number`]
|
|
531
|
+
: !isJsonNumber(frame.value)
|
|
532
|
+
? [`"${diagnosticPath(frame.path)}" must be a finite JSON number`]
|
|
533
|
+
: checkScalarValue(frame.node, frame.value, frame.path));
|
|
534
|
+
break;
|
|
535
|
+
case 'integer':
|
|
536
|
+
finish(!isJsonNumber(frame.value) || !Number.isInteger(frame.value)
|
|
537
|
+
? [`"${diagnosticPath(frame.path)}" must be an integer`]
|
|
538
|
+
: checkScalarValue(frame.node, frame.value, frame.path));
|
|
539
|
+
break;
|
|
540
|
+
case 'boolean':
|
|
541
|
+
finish(typeof frame.value === 'boolean'
|
|
542
|
+
? checkScalarValue(frame.node, frame.value, frame.path)
|
|
543
|
+
: [`"${diagnosticPath(frame.path)}" must be a boolean`]);
|
|
544
|
+
break;
|
|
545
|
+
case 'null':
|
|
546
|
+
finish(frame.value === null
|
|
547
|
+
? checkScalarValue(frame.node, frame.value, frame.path)
|
|
548
|
+
: [`"${diagnosticPath(frame.path)}" must be null`]);
|
|
549
|
+
break;
|
|
550
|
+
default:
|
|
551
|
+
finish(assertNever(nodeType, 'JsonSchemaType'));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
let failed = frames.pop();
|
|
556
|
+
while (failed !== undefined && !failed.catches)
|
|
557
|
+
failed = frames.pop();
|
|
558
|
+
if (failed === undefined)
|
|
559
|
+
throw error;
|
|
560
|
+
receive(losslessValueViolation(failed.path));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/* v8 ignore next -- every root frame finishes or throws. */
|
|
564
|
+
return rootResult ?? losslessValueViolation(path);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Validate a candidate value against an asserted raw schema. The function is
|
|
568
|
+
* total for arbitrary values and returns path-qualified violations.
|
|
569
|
+
* @param schema - a schema accepted by {@link assertSupportedJsonSchema}.
|
|
570
|
+
* @param value - the candidate JSON value.
|
|
571
|
+
* @param path - root label used in diagnostics.
|
|
572
|
+
* @returns All violations in walk order; empty means valid.
|
|
573
|
+
*/
|
|
574
|
+
export function validateJsonSchemaValue(schema, value, path = 'value') {
|
|
575
|
+
return checkValue(schema, value, path);
|
|
576
|
+
}
|
|
577
|
+
//# sourceMappingURL=json-schema.js.map
|