@ultimat3/schema 1.0.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 +21 -0
- package/README.md +100 -0
- package/package.json +33 -0
- package/src/builder.d.ts +62 -0
- package/src/builder.d.ts.map +1 -0
- package/src/builder.js +93 -0
- package/src/builder.js.map +1 -0
- package/src/builder.ts +173 -0
- package/src/coerce.d.ts +15 -0
- package/src/coerce.d.ts.map +1 -0
- package/src/coerce.js +123 -0
- package/src/coerce.js.map +1 -0
- package/src/coerce.ts +125 -0
- package/src/errors.d.ts +62 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.js +85 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +140 -0
- package/src/index.d.ts +19 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +11 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +102 -0
- package/src/json-schema.d.ts +37 -0
- package/src/json-schema.d.ts.map +1 -0
- package/src/json-schema.js +108 -0
- package/src/json-schema.js.map +1 -0
- package/src/json-schema.ts +149 -0
- package/src/node.d.ts +32 -0
- package/src/node.d.ts.map +1 -0
- package/src/node.js +24 -0
- package/src/node.js.map +1 -0
- package/src/node.ts +75 -0
- package/src/provider.d.ts +32 -0
- package/src/provider.d.ts.map +1 -0
- package/src/provider.js +49 -0
- package/src/provider.js.map +1 -0
- package/src/provider.ts +67 -0
- package/src/standard.d.ts +44 -0
- package/src/standard.d.ts.map +1 -0
- package/src/standard.js +71 -0
- package/src/standard.js.map +1 -0
- package/src/standard.ts +160 -0
- package/src/t.d.ts +6 -0
- package/src/t.d.ts.map +1 -0
- package/src/t.js +79 -0
- package/src/t.js.map +1 -0
- package/src/t.ts +101 -0
- package/src/validators.d.ts +63 -0
- package/src/validators.d.ts.map +1 -0
- package/src/validators.js +271 -0
- package/src/validators.js.map +1 -0
- package/src/validators.ts +389 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Single responsibility: SchemaNode -> JSON Schema. Load-bearing: OpenAPI request/response
|
|
2
|
+
// bodies and MCP tool `inputSchema` are both this function's output, so an agent's view of an
|
|
3
|
+
// action and an HTTP client's view can never drift.
|
|
4
|
+
import { requiredKeys } from './node';
|
|
5
|
+
import { introspect } from './provider';
|
|
6
|
+
const DIALECTS = Object.freeze({
|
|
7
|
+
'2020-12': 'https://json-schema.org/draft/2020-12/schema',
|
|
8
|
+
'draft-07': 'http://json-schema.org/draft-07/schema#',
|
|
9
|
+
});
|
|
10
|
+
const FORMAT_MAP = Object.freeze({
|
|
11
|
+
uuid: 'uuid',
|
|
12
|
+
email: 'email',
|
|
13
|
+
uri: 'uri',
|
|
14
|
+
'date-time': 'date-time',
|
|
15
|
+
slug: 'slug',
|
|
16
|
+
timezone: 'iana-time-zone',
|
|
17
|
+
locale: 'bcp47-locale',
|
|
18
|
+
cursor: 'ultimate-cursor',
|
|
19
|
+
});
|
|
20
|
+
function stringNode(node) {
|
|
21
|
+
return {
|
|
22
|
+
type: 'string',
|
|
23
|
+
...(node.format === undefined ? {} : { format: FORMAT_MAP[node.format] ?? node.format }),
|
|
24
|
+
...(node.minLength === undefined ? {} : { minLength: node.minLength }),
|
|
25
|
+
...(node.maxLength === undefined ? {} : { maxLength: node.maxLength }),
|
|
26
|
+
...(node.pattern === undefined ? {} : { pattern: node.pattern }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function convert(node) {
|
|
30
|
+
const annotate = (schema) => ({
|
|
31
|
+
...schema,
|
|
32
|
+
...(node.description === undefined ? {} : { description: node.description }),
|
|
33
|
+
...(node.hasDefault === true ? { default: node.default } : {}),
|
|
34
|
+
});
|
|
35
|
+
switch (node.kind) {
|
|
36
|
+
case 'string':
|
|
37
|
+
return annotate(stringNode(node));
|
|
38
|
+
case 'number':
|
|
39
|
+
return annotate({
|
|
40
|
+
type: node.integer === true ? 'integer' : 'number',
|
|
41
|
+
...(node.minimum === undefined ? {} : { minimum: node.minimum }),
|
|
42
|
+
...(node.maximum === undefined ? {} : { maximum: node.maximum }),
|
|
43
|
+
});
|
|
44
|
+
case 'boolean':
|
|
45
|
+
return annotate({ type: 'boolean' });
|
|
46
|
+
case 'date':
|
|
47
|
+
return annotate({ type: 'string', format: 'date-time' });
|
|
48
|
+
case 'enum':
|
|
49
|
+
return annotate({ type: 'string', enum: node.values ?? [] });
|
|
50
|
+
case 'literal':
|
|
51
|
+
return annotate({ const: node.literal ?? null });
|
|
52
|
+
case 'array':
|
|
53
|
+
return annotate({
|
|
54
|
+
type: 'array',
|
|
55
|
+
items: node.items === undefined ? {} : convert(node.items),
|
|
56
|
+
});
|
|
57
|
+
case 'union':
|
|
58
|
+
return annotate({ anyOf: (node.anyOf ?? []).map(convert) });
|
|
59
|
+
case 'record':
|
|
60
|
+
return annotate({
|
|
61
|
+
type: 'object',
|
|
62
|
+
additionalProperties: node.valueNode === undefined ? true : convert(node.valueNode),
|
|
63
|
+
});
|
|
64
|
+
case 'money':
|
|
65
|
+
return annotate({
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
minor: { type: 'integer', description: 'amount in minor units, never a float' },
|
|
69
|
+
currency: { type: 'string', pattern: '^[A-Z]{3}$' },
|
|
70
|
+
},
|
|
71
|
+
required: ['minor', 'currency'],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
});
|
|
74
|
+
case 'object': {
|
|
75
|
+
const properties = {};
|
|
76
|
+
for (const [key, child] of Object.entries(node.properties ?? {})) {
|
|
77
|
+
properties[key] = convert(child);
|
|
78
|
+
}
|
|
79
|
+
return annotate({
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties,
|
|
82
|
+
required: requiredKeys(node),
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
return annotate({});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Convert any schema the active provider can introspect. */
|
|
91
|
+
export function toJsonSchema(schema, options) {
|
|
92
|
+
const converted = convert(introspect(schema));
|
|
93
|
+
const dialect = options?.dialect ?? '2020-12';
|
|
94
|
+
return {
|
|
95
|
+
...(options?.includeDialect === false ? {} : { $schema: DIALECTS[dialect] }),
|
|
96
|
+
...(options?.title === undefined ? {} : { title: options.title }),
|
|
97
|
+
...converted,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** The exact shape an MCP `tools/list` entry needs: draft-07, no `$schema`. */
|
|
101
|
+
export function toMcpInputSchema(schema) {
|
|
102
|
+
return toJsonSchema(schema, { dialect: 'draft-07', includeDialect: false });
|
|
103
|
+
}
|
|
104
|
+
/** Convert an already-introspected node — used by generators that walk the IR themselves. */
|
|
105
|
+
export function nodeToJsonSchema(node) {
|
|
106
|
+
return convert(node);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=json-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema.js","sourceRoot":"","sources":["json-schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Single responsibility: SchemaNode -> JSON Schema. Load-bearing: OpenAPI request/response
|
|
2
|
+
// bodies and MCP tool `inputSchema` are both this function's output, so an agent's view of an
|
|
3
|
+
// action and an HTTP client's view can never drift.
|
|
4
|
+
|
|
5
|
+
import { requiredKeys, type SchemaNode } from './node';
|
|
6
|
+
import { introspect } from './provider';
|
|
7
|
+
|
|
8
|
+
export type JsonSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array';
|
|
9
|
+
|
|
10
|
+
export interface JsonSchema {
|
|
11
|
+
readonly $schema?: string;
|
|
12
|
+
readonly type?: JsonSchemaType;
|
|
13
|
+
readonly format?: string;
|
|
14
|
+
readonly description?: string;
|
|
15
|
+
readonly default?: unknown;
|
|
16
|
+
readonly minLength?: number;
|
|
17
|
+
readonly maxLength?: number;
|
|
18
|
+
readonly pattern?: string;
|
|
19
|
+
readonly minimum?: number;
|
|
20
|
+
readonly maximum?: number;
|
|
21
|
+
readonly enum?: readonly (string | number)[];
|
|
22
|
+
readonly const?: string | number | boolean | null;
|
|
23
|
+
readonly properties?: Readonly<Record<string, JsonSchema>>;
|
|
24
|
+
readonly required?: readonly string[];
|
|
25
|
+
readonly additionalProperties?: boolean | JsonSchema;
|
|
26
|
+
readonly items?: JsonSchema;
|
|
27
|
+
readonly anyOf?: readonly JsonSchema[];
|
|
28
|
+
readonly title?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type JsonSchemaDialect = '2020-12' | 'draft-07';
|
|
32
|
+
|
|
33
|
+
const DIALECTS: Readonly<Record<JsonSchemaDialect, string>> = Object.freeze({
|
|
34
|
+
'2020-12': 'https://json-schema.org/draft/2020-12/schema',
|
|
35
|
+
'draft-07': 'http://json-schema.org/draft-07/schema#',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export interface ToJsonSchemaOptions {
|
|
39
|
+
/** MCP clients are happiest with `draft-07`; OpenAPI 3.1 wants `2020-12` (the default). */
|
|
40
|
+
readonly dialect?: JsonSchemaDialect | undefined;
|
|
41
|
+
/** Emit `$schema` at the root. Off for OpenAPI component schemas. */
|
|
42
|
+
readonly includeDialect?: boolean | undefined;
|
|
43
|
+
readonly title?: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const FORMAT_MAP: Readonly<Record<string, string>> = Object.freeze({
|
|
47
|
+
uuid: 'uuid',
|
|
48
|
+
email: 'email',
|
|
49
|
+
uri: 'uri',
|
|
50
|
+
'date-time': 'date-time',
|
|
51
|
+
slug: 'slug',
|
|
52
|
+
timezone: 'iana-time-zone',
|
|
53
|
+
locale: 'bcp47-locale',
|
|
54
|
+
cursor: 'ultimate-cursor',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function stringNode(node: SchemaNode): JsonSchema {
|
|
58
|
+
return {
|
|
59
|
+
type: 'string',
|
|
60
|
+
...(node.format === undefined ? {} : { format: FORMAT_MAP[node.format] ?? node.format }),
|
|
61
|
+
...(node.minLength === undefined ? {} : { minLength: node.minLength }),
|
|
62
|
+
...(node.maxLength === undefined ? {} : { maxLength: node.maxLength }),
|
|
63
|
+
...(node.pattern === undefined ? {} : { pattern: node.pattern }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function convert(node: SchemaNode): JsonSchema {
|
|
68
|
+
const annotate = (schema: JsonSchema): JsonSchema => ({
|
|
69
|
+
...schema,
|
|
70
|
+
...(node.description === undefined ? {} : { description: node.description }),
|
|
71
|
+
...(node.hasDefault === true ? { default: node.default } : {}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
switch (node.kind) {
|
|
75
|
+
case 'string':
|
|
76
|
+
return annotate(stringNode(node));
|
|
77
|
+
case 'number':
|
|
78
|
+
return annotate({
|
|
79
|
+
type: node.integer === true ? 'integer' : 'number',
|
|
80
|
+
...(node.minimum === undefined ? {} : { minimum: node.minimum }),
|
|
81
|
+
...(node.maximum === undefined ? {} : { maximum: node.maximum }),
|
|
82
|
+
});
|
|
83
|
+
case 'boolean':
|
|
84
|
+
return annotate({ type: 'boolean' });
|
|
85
|
+
case 'date':
|
|
86
|
+
return annotate({ type: 'string', format: 'date-time' });
|
|
87
|
+
case 'enum':
|
|
88
|
+
return annotate({ type: 'string', enum: node.values ?? [] });
|
|
89
|
+
case 'literal':
|
|
90
|
+
return annotate({ const: node.literal ?? null });
|
|
91
|
+
case 'array':
|
|
92
|
+
return annotate({
|
|
93
|
+
type: 'array',
|
|
94
|
+
items: node.items === undefined ? {} : convert(node.items),
|
|
95
|
+
});
|
|
96
|
+
case 'union':
|
|
97
|
+
return annotate({ anyOf: (node.anyOf ?? []).map(convert) });
|
|
98
|
+
case 'record':
|
|
99
|
+
return annotate({
|
|
100
|
+
type: 'object',
|
|
101
|
+
additionalProperties: node.valueNode === undefined ? true : convert(node.valueNode),
|
|
102
|
+
});
|
|
103
|
+
case 'money':
|
|
104
|
+
return annotate({
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
minor: { type: 'integer', description: 'amount in minor units, never a float' },
|
|
108
|
+
currency: { type: 'string', pattern: '^[A-Z]{3}$' },
|
|
109
|
+
},
|
|
110
|
+
required: ['minor', 'currency'],
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
});
|
|
113
|
+
case 'object': {
|
|
114
|
+
const properties: Record<string, JsonSchema> = {};
|
|
115
|
+
for (const [key, child] of Object.entries(node.properties ?? {})) {
|
|
116
|
+
properties[key] = convert(child);
|
|
117
|
+
}
|
|
118
|
+
return annotate({
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties,
|
|
121
|
+
required: requiredKeys(node),
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
return annotate({});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Convert any schema the active provider can introspect. */
|
|
131
|
+
export function toJsonSchema(schema: unknown, options?: ToJsonSchemaOptions): JsonSchema {
|
|
132
|
+
const converted = convert(introspect(schema));
|
|
133
|
+
const dialect = options?.dialect ?? '2020-12';
|
|
134
|
+
return {
|
|
135
|
+
...(options?.includeDialect === false ? {} : { $schema: DIALECTS[dialect] }),
|
|
136
|
+
...(options?.title === undefined ? {} : { title: options.title }),
|
|
137
|
+
...converted,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The exact shape an MCP `tools/list` entry needs: draft-07, no `$schema`. */
|
|
142
|
+
export function toMcpInputSchema(schema: unknown): JsonSchema {
|
|
143
|
+
return toJsonSchema(schema, { dialect: 'draft-07', includeDialect: false });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Convert an already-introspected node — used by generators that walk the IR themselves. */
|
|
147
|
+
export function nodeToJsonSchema(node: SchemaNode): JsonSchema {
|
|
148
|
+
return convert(node);
|
|
149
|
+
}
|
package/src/node.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type SchemaKind = 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array' | 'enum' | 'literal' | 'union' | 'record' | 'money' | 'unknown';
|
|
2
|
+
/** JSON Schema `format`, plus the framework's own semantic formats. */
|
|
3
|
+
export type SchemaFormat = 'uuid' | 'email' | 'uri' | 'date-time' | 'slug' | 'timezone' | 'locale' | 'cursor';
|
|
4
|
+
export interface SchemaNode {
|
|
5
|
+
readonly kind: SchemaKind;
|
|
6
|
+
readonly optional?: boolean | undefined;
|
|
7
|
+
/** Separate from `optional`: JSON Schema and the DB both distinguish null from absent. */
|
|
8
|
+
readonly nullable?: boolean | undefined;
|
|
9
|
+
readonly hasDefault?: boolean | undefined;
|
|
10
|
+
readonly default?: unknown;
|
|
11
|
+
readonly description?: string | undefined;
|
|
12
|
+
readonly format?: SchemaFormat | undefined;
|
|
13
|
+
readonly minLength?: number | undefined;
|
|
14
|
+
readonly maxLength?: number | undefined;
|
|
15
|
+
/** Source string of the RegExp, so the node stays JSON-serialisable. */
|
|
16
|
+
readonly pattern?: string | undefined;
|
|
17
|
+
readonly minimum?: number | undefined;
|
|
18
|
+
readonly maximum?: number | undefined;
|
|
19
|
+
readonly integer?: boolean | undefined;
|
|
20
|
+
readonly properties?: Readonly<Record<string, SchemaNode>> | undefined;
|
|
21
|
+
readonly items?: SchemaNode | undefined;
|
|
22
|
+
readonly values?: readonly (string | number)[] | undefined;
|
|
23
|
+
readonly literal?: string | number | boolean | null | undefined;
|
|
24
|
+
readonly anyOf?: readonly SchemaNode[] | undefined;
|
|
25
|
+
readonly valueNode?: SchemaNode | undefined;
|
|
26
|
+
}
|
|
27
|
+
export declare function isSchemaNode(value: unknown): value is SchemaNode;
|
|
28
|
+
/** Duck-typed: any object carrying a `node` is introspectable, whatever built it. */
|
|
29
|
+
export declare function nodeOf(value: unknown): SchemaNode | undefined;
|
|
30
|
+
/** Keys an object node requires — everything not optional and without a default. */
|
|
31
|
+
export declare function requiredKeys(node: SchemaNode): readonly string[];
|
|
32
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["node.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,UAAU,GAClB,QAAQ,GACR,QAAQ,GACR,SAAS,GACT,MAAM,GACN,QAAQ,GACR,OAAO,GACP,MAAM,GACN,SAAS,GACT,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,CAAC;AAEd,uEAAuE;AACvE,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,OAAO,GACP,KAAK,GACL,WAAW,GACX,MAAM,GACN,UAAU,GACV,QAAQ,GACR,QAAQ,CAAC;AAEb,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACxC,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,wEAAwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,GAAG,SAAS,CAAC;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,SAAS,CAAC;IAC3D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,UAAU,EAAE,GAAG,SAAS,CAAC;IACnD,QAAQ,CAAC,SAAS,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;CAC7C;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAMhE;AAED,qFAAqF;AACrF,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAI7D;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE,CAKhE"}
|
package/src/node.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Single responsibility: the introspectable schema IR. Flat on purpose — OpenAPI generation,
|
|
2
|
+
// MCP tool schemas, HTTP query coercion and the admin form generator all read this one shape,
|
|
3
|
+
// so it must stay trivially walkable.
|
|
4
|
+
export function isSchemaNode(value) {
|
|
5
|
+
return (typeof value === 'object' &&
|
|
6
|
+
value !== null &&
|
|
7
|
+
typeof value.kind === 'string');
|
|
8
|
+
}
|
|
9
|
+
/** Duck-typed: any object carrying a `node` is introspectable, whatever built it. */
|
|
10
|
+
export function nodeOf(value) {
|
|
11
|
+
if (typeof value !== 'object' || value === null || !('node' in value))
|
|
12
|
+
return undefined;
|
|
13
|
+
const node = value.node;
|
|
14
|
+
return isSchemaNode(node) ? node : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Keys an object node requires — everything not optional and without a default. */
|
|
17
|
+
export function requiredKeys(node) {
|
|
18
|
+
if (node.properties === undefined)
|
|
19
|
+
return [];
|
|
20
|
+
return Object.entries(node.properties)
|
|
21
|
+
.filter(([, child]) => child.optional !== true && child.hasDefault !== true)
|
|
22
|
+
.map(([key]) => key);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=node.js.map
|
package/src/node.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["node.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,8FAA8F;AAC9F,sCAAsC;AAmDtC,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAQ,KAA4B,CAAC,IAAI,KAAK,QAAQ,CACvD,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,MAAM,CAAC,KAAc;IACnC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACxF,MAAM,IAAI,GAAI,KAA2B,CAAC,IAAI,CAAC;IAC/C,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,YAAY,CAAC,IAAgB;IAC3C,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAC7C,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC;SACnC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;SAC3E,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC"}
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Single responsibility: the introspectable schema IR. Flat on purpose — OpenAPI generation,
|
|
2
|
+
// MCP tool schemas, HTTP query coercion and the admin form generator all read this one shape,
|
|
3
|
+
// so it must stay trivially walkable.
|
|
4
|
+
|
|
5
|
+
export type SchemaKind =
|
|
6
|
+
| 'string'
|
|
7
|
+
| 'number'
|
|
8
|
+
| 'boolean'
|
|
9
|
+
| 'date'
|
|
10
|
+
| 'object'
|
|
11
|
+
| 'array'
|
|
12
|
+
| 'enum'
|
|
13
|
+
| 'literal'
|
|
14
|
+
| 'union'
|
|
15
|
+
| 'record'
|
|
16
|
+
| 'money'
|
|
17
|
+
| 'unknown';
|
|
18
|
+
|
|
19
|
+
/** JSON Schema `format`, plus the framework's own semantic formats. */
|
|
20
|
+
export type SchemaFormat =
|
|
21
|
+
| 'uuid'
|
|
22
|
+
| 'email'
|
|
23
|
+
| 'uri'
|
|
24
|
+
| 'date-time'
|
|
25
|
+
| 'slug'
|
|
26
|
+
| 'timezone'
|
|
27
|
+
| 'locale'
|
|
28
|
+
| 'cursor';
|
|
29
|
+
|
|
30
|
+
export interface SchemaNode {
|
|
31
|
+
readonly kind: SchemaKind;
|
|
32
|
+
readonly optional?: boolean | undefined;
|
|
33
|
+
/** Separate from `optional`: JSON Schema and the DB both distinguish null from absent. */
|
|
34
|
+
readonly nullable?: boolean | undefined;
|
|
35
|
+
readonly hasDefault?: boolean | undefined;
|
|
36
|
+
readonly default?: unknown;
|
|
37
|
+
readonly description?: string | undefined;
|
|
38
|
+
readonly format?: SchemaFormat | undefined;
|
|
39
|
+
readonly minLength?: number | undefined;
|
|
40
|
+
readonly maxLength?: number | undefined;
|
|
41
|
+
/** Source string of the RegExp, so the node stays JSON-serialisable. */
|
|
42
|
+
readonly pattern?: string | undefined;
|
|
43
|
+
readonly minimum?: number | undefined;
|
|
44
|
+
readonly maximum?: number | undefined;
|
|
45
|
+
readonly integer?: boolean | undefined;
|
|
46
|
+
readonly properties?: Readonly<Record<string, SchemaNode>> | undefined;
|
|
47
|
+
readonly items?: SchemaNode | undefined;
|
|
48
|
+
readonly values?: readonly (string | number)[] | undefined;
|
|
49
|
+
readonly literal?: string | number | boolean | null | undefined;
|
|
50
|
+
readonly anyOf?: readonly SchemaNode[] | undefined;
|
|
51
|
+
readonly valueNode?: SchemaNode | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isSchemaNode(value: unknown): value is SchemaNode {
|
|
55
|
+
return (
|
|
56
|
+
typeof value === 'object' &&
|
|
57
|
+
value !== null &&
|
|
58
|
+
typeof (value as { kind?: unknown }).kind === 'string'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Duck-typed: any object carrying a `node` is introspectable, whatever built it. */
|
|
63
|
+
export function nodeOf(value: unknown): SchemaNode | undefined {
|
|
64
|
+
if (typeof value !== 'object' || value === null || !('node' in value)) return undefined;
|
|
65
|
+
const node = (value as { node: unknown }).node;
|
|
66
|
+
return isSchemaNode(node) ? node : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Keys an object node requires — everything not optional and without a default. */
|
|
70
|
+
export function requiredKeys(node: SchemaNode): readonly string[] {
|
|
71
|
+
if (node.properties === undefined) return [];
|
|
72
|
+
return Object.entries(node.properties)
|
|
73
|
+
.filter(([, child]) => child.optional !== true && child.hasDefault !== true)
|
|
74
|
+
.map(([key]) => key);
|
|
75
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type SchemaNode } from './node';
|
|
2
|
+
import { type TNamespace } from './validators';
|
|
3
|
+
export interface SchemaProvider {
|
|
4
|
+
/** Matches `~standard.vendor`, e.g. `ultimate`, `arktype`, `zod`. */
|
|
5
|
+
readonly vendor: string;
|
|
6
|
+
readonly t: TNamespace;
|
|
7
|
+
/**
|
|
8
|
+
* Return the IR for one of this provider's schemas. Required for OpenAPI and MCP tool
|
|
9
|
+
* schemas; omit it only if the provider also supplies `toJsonSchema`.
|
|
10
|
+
*/
|
|
11
|
+
introspect?(schema: unknown): SchemaNode | undefined;
|
|
12
|
+
}
|
|
13
|
+
export declare const builtinProvider: SchemaProvider;
|
|
14
|
+
/**
|
|
15
|
+
* Swap the library behind `t`.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { z } from 'zod';
|
|
19
|
+
* configureSchemaProvider({ vendor: 'zod', t: zodNamespace, introspect: zodToNode });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function configureSchemaProvider(provider: SchemaProvider): void;
|
|
23
|
+
export declare function schemaProvider(): SchemaProvider;
|
|
24
|
+
export declare function resetSchemaProvider(): void;
|
|
25
|
+
/**
|
|
26
|
+
* The IR for any schema, whoever built it. Throws `X_SCHEMA_UNSUPPORTED` rather than silently
|
|
27
|
+
* emitting an empty OpenAPI body.
|
|
28
|
+
*/
|
|
29
|
+
export declare function introspect(schema: unknown): SchemaNode;
|
|
30
|
+
/** Best-effort variant for callers that can degrade (HTTP query coercion). */
|
|
31
|
+
export declare function tryIntrospect(schema: unknown): SchemaNode | undefined;
|
|
32
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["provider.ts"],"names":[],"mappings":"AAKA,OAAO,EAAU,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC;IACvB;;;OAGG;IACH,UAAU,CAAC,CAAC,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAAC;CACtD;AAED,eAAO,MAAM,eAAe,EAAE,cAI5B,CAAC;AAIH;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAEtE;AAED,wBAAgB,cAAc,IAAI,cAAc,CAE/C;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,UAAU,CAUtD;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAErE"}
|
package/src/provider.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Single responsibility: the swap point. One provider is active per process; everything that
|
|
2
|
+
// consumes schemas (OpenAPI, MCP, coercion, the admin form generator) goes through `introspect`
|
|
3
|
+
// so a third-party library can be plugged in without touching the rest of the framework.
|
|
4
|
+
import { SchemaUnsupportedError } from './errors';
|
|
5
|
+
import { nodeOf } from './node';
|
|
6
|
+
import { builtinT } from './validators';
|
|
7
|
+
export const builtinProvider = Object.freeze({
|
|
8
|
+
vendor: 'ultimate',
|
|
9
|
+
t: builtinT,
|
|
10
|
+
introspect: nodeOf,
|
|
11
|
+
});
|
|
12
|
+
let active = builtinProvider;
|
|
13
|
+
/**
|
|
14
|
+
* Swap the library behind `t`.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { z } from 'zod';
|
|
18
|
+
* configureSchemaProvider({ vendor: 'zod', t: zodNamespace, introspect: zodToNode });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function configureSchemaProvider(provider) {
|
|
22
|
+
active = provider;
|
|
23
|
+
}
|
|
24
|
+
export function schemaProvider() {
|
|
25
|
+
return active;
|
|
26
|
+
}
|
|
27
|
+
export function resetSchemaProvider() {
|
|
28
|
+
active = builtinProvider;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The IR for any schema, whoever built it. Throws `X_SCHEMA_UNSUPPORTED` rather than silently
|
|
32
|
+
* emitting an empty OpenAPI body.
|
|
33
|
+
*/
|
|
34
|
+
export function introspect(schema) {
|
|
35
|
+
const node = active.introspect?.(schema) ?? nodeOf(schema);
|
|
36
|
+
if (node === undefined) {
|
|
37
|
+
throw new SchemaUnsupportedError({
|
|
38
|
+
cause: `provider "${active.vendor}" cannot describe this schema as a SchemaNode`,
|
|
39
|
+
fix: 'add introspect() to the object passed to configureSchemaProvider()',
|
|
40
|
+
meta: { vendor: active.vendor },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return node;
|
|
44
|
+
}
|
|
45
|
+
/** Best-effort variant for callers that can degrade (HTTP query coercion). */
|
|
46
|
+
export function tryIntrospect(schema) {
|
|
47
|
+
return active.introspect?.(schema) ?? nodeOf(schema);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["provider.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,gGAAgG;AAChG,yFAAyF;AAEzF,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,MAAM,EAAmB,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAmB,MAAM,cAAc,CAAC;AAazD,MAAM,CAAC,MAAM,eAAe,GAAmB,MAAM,CAAC,MAAM,CAAC;IAC3D,MAAM,EAAE,UAAU;IAClB,CAAC,EAAE,QAAQ;IACX,UAAU,EAAE,MAAM;CACnB,CAAC,CAAC;AAEH,IAAI,MAAM,GAAmB,eAAe,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAAwB;IAC9D,MAAM,GAAG,QAAQ,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,MAAM,GAAG,eAAe,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAe;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC;IAC3D,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,sBAAsB,CAAC;YAC/B,KAAK,EAAE,aAAa,MAAM,CAAC,MAAM,+CAA+C;YAChF,GAAG,EAAE,oEAAoE;YACzE,IAAI,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;SAChC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,MAAe;IAC3C,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC;AACvD,CAAC"}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Single responsibility: the swap point. One provider is active per process; everything that
|
|
2
|
+
// consumes schemas (OpenAPI, MCP, coercion, the admin form generator) goes through `introspect`
|
|
3
|
+
// so a third-party library can be plugged in without touching the rest of the framework.
|
|
4
|
+
|
|
5
|
+
import { SchemaUnsupportedError } from './errors';
|
|
6
|
+
import { nodeOf, type SchemaNode } from './node';
|
|
7
|
+
import { builtinT, type TNamespace } from './validators';
|
|
8
|
+
|
|
9
|
+
export interface SchemaProvider {
|
|
10
|
+
/** Matches `~standard.vendor`, e.g. `ultimate`, `arktype`, `zod`. */
|
|
11
|
+
readonly vendor: string;
|
|
12
|
+
readonly t: TNamespace;
|
|
13
|
+
/**
|
|
14
|
+
* Return the IR for one of this provider's schemas. Required for OpenAPI and MCP tool
|
|
15
|
+
* schemas; omit it only if the provider also supplies `toJsonSchema`.
|
|
16
|
+
*/
|
|
17
|
+
introspect?(schema: unknown): SchemaNode | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const builtinProvider: SchemaProvider = Object.freeze({
|
|
21
|
+
vendor: 'ultimate',
|
|
22
|
+
t: builtinT,
|
|
23
|
+
introspect: nodeOf,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let active: SchemaProvider = builtinProvider;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Swap the library behind `t`.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { z } from 'zod';
|
|
33
|
+
* configureSchemaProvider({ vendor: 'zod', t: zodNamespace, introspect: zodToNode });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function configureSchemaProvider(provider: SchemaProvider): void {
|
|
37
|
+
active = provider;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function schemaProvider(): SchemaProvider {
|
|
41
|
+
return active;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resetSchemaProvider(): void {
|
|
45
|
+
active = builtinProvider;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The IR for any schema, whoever built it. Throws `X_SCHEMA_UNSUPPORTED` rather than silently
|
|
50
|
+
* emitting an empty OpenAPI body.
|
|
51
|
+
*/
|
|
52
|
+
export function introspect(schema: unknown): SchemaNode {
|
|
53
|
+
const node = active.introspect?.(schema) ?? nodeOf(schema);
|
|
54
|
+
if (node === undefined) {
|
|
55
|
+
throw new SchemaUnsupportedError({
|
|
56
|
+
cause: `provider "${active.vendor}" cannot describe this schema as a SchemaNode`,
|
|
57
|
+
fix: 'add introspect() to the object passed to configureSchemaProvider()',
|
|
58
|
+
meta: { vendor: active.vendor },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return node;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Best-effort variant for callers that can degrade (HTTP query coercion). */
|
|
65
|
+
export function tryIntrospect(schema: unknown): SchemaNode | undefined {
|
|
66
|
+
return active.introspect?.(schema) ?? nodeOf(schema);
|
|
67
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ValidationIssue } from './errors';
|
|
2
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
3
|
+
readonly '~standard': StandardSchemaProps<Input, Output>;
|
|
4
|
+
}
|
|
5
|
+
export interface StandardSchemaProps<Input, Output> {
|
|
6
|
+
readonly version: 1;
|
|
7
|
+
/** Library name, e.g. `ultimate`, `arktype`, `zod`. */
|
|
8
|
+
readonly vendor: string;
|
|
9
|
+
readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
|
|
10
|
+
readonly types?: StandardTypes<Input, Output> | undefined;
|
|
11
|
+
}
|
|
12
|
+
export interface StandardTypes<Input, Output> {
|
|
13
|
+
readonly input: Input;
|
|
14
|
+
readonly output: Output;
|
|
15
|
+
}
|
|
16
|
+
export interface StandardSuccessResult<Output> {
|
|
17
|
+
readonly value: Output;
|
|
18
|
+
readonly issues?: undefined;
|
|
19
|
+
}
|
|
20
|
+
export interface StandardFailureResult {
|
|
21
|
+
readonly issues: readonly StandardIssue[];
|
|
22
|
+
}
|
|
23
|
+
export type StandardResult<Output> = StandardSuccessResult<Output> | StandardFailureResult;
|
|
24
|
+
export interface StandardPathSegment {
|
|
25
|
+
readonly key: PropertyKey;
|
|
26
|
+
}
|
|
27
|
+
export interface StandardIssue {
|
|
28
|
+
readonly message: string;
|
|
29
|
+
readonly path?: readonly (PropertyKey | StandardPathSegment)[] | undefined;
|
|
30
|
+
}
|
|
31
|
+
export type InferInput<S extends StandardSchemaV1> = NonNullable<S['~standard']['types']>['input'];
|
|
32
|
+
export type InferOutput<S extends StandardSchemaV1> = NonNullable<S['~standard']['types']>['output'];
|
|
33
|
+
/** `['items', 0, 'price']` -> `items[0].price` */
|
|
34
|
+
export declare function formatPath(path: readonly (PropertyKey | StandardPathSegment)[] | undefined): string;
|
|
35
|
+
/** Normalise any conforming library's issues into Ultimate's path-annotated shape. */
|
|
36
|
+
export declare function toValidationIssues(issues: readonly StandardIssue[]): readonly ValidationIssue[];
|
|
37
|
+
/** Validate against ANY conforming schema. Never throws on invalid input. */
|
|
38
|
+
export declare function validate<S extends StandardSchemaV1>(schema: S, value: unknown): StandardResult<InferOutput<S>>;
|
|
39
|
+
export declare function validateAsync<S extends StandardSchemaV1>(schema: S, value: unknown): Promise<StandardResult<InferOutput<S>>>;
|
|
40
|
+
/** Validate or throw `X_VALIDATION_FAILED` with a path-annotated issue list. */
|
|
41
|
+
export declare function parse<S extends StandardSchemaV1>(schema: S, value: unknown, root?: string): InferOutput<S>;
|
|
42
|
+
export declare function parseAsync<S extends StandardSchemaV1>(schema: S, value: unknown, root?: string): Promise<InferOutput<S>>;
|
|
43
|
+
export declare function isStandardSchema(value: unknown): value is StandardSchemaV1;
|
|
44
|
+
//# sourceMappingURL=standard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standard.d.ts","sourceRoot":"","sources":["standard.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiD,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAE/F,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;IAC/D,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,mBAAmB,CAAC,KAAK,EAAE,MAAM;IAChD,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,cAAc,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;IAChG,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;CAC3D;AAED,MAAM,WAAW,aAAa,CAAC,KAAK,EAAE,MAAM;IAC1C,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB,CAAC,MAAM;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;CAC3C;AAED,MAAM,MAAM,cAAc,CAAC,MAAM,IAAI,qBAAqB,CAAC,MAAM,CAAC,GAAG,qBAAqB,CAAC;AAE3F,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,mBAAmB,CAAC,EAAE,GAAG,SAAS,CAAC;CAC5E;AAED,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,gBAAgB,IAAI,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AAEnG,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,gBAAgB,IAAI,WAAW,CAC/D,CAAC,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CACxB,CAAC,QAAQ,CAAC,CAAC;AAEZ,kDAAkD;AAClD,wBAAgB,UAAU,CACxB,IAAI,EAAE,SAAS,CAAC,WAAW,GAAG,mBAAmB,CAAC,EAAE,GAAG,SAAS,GAC/D,MAAM,CAYR;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,eAAe,EAAE,CAO/F;AAgBD,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,CAAC,SAAS,gBAAgB,EACjD,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,OAAO,GACb,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAGhC;AAED,wBAAsB,aAAa,CAAC,CAAC,SAAS,gBAAgB,EAC5D,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAGzC;AAED,gFAAgF;AAChF,wBAAgB,KAAK,CAAC,CAAC,SAAS,gBAAgB,EAC9C,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,OAAO,EACd,IAAI,SAAU,GACb,WAAW,CAAC,CAAC,CAAC,CAIhB;AAED,wBAAsB,UAAU,CAAC,CAAC,SAAS,gBAAgB,EACzD,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,OAAO,EACd,IAAI,SAAU,GACb,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAIzB;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAS1E"}
|