schemair 0.1.0 → 0.3.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/README.md +542 -32
- package/dist/authoring/index.cjs +13 -350
- package/dist/authoring/index.d.cts +2 -90
- package/dist/authoring/index.d.ts +2 -90
- package/dist/authoring/index.js +2 -304
- package/dist/authoring-Dnrg5TXm.cjs +389 -0
- package/dist/authoring-DyJNYs5o.js +362 -0
- package/dist/chunk-bFfwrPtJ.js +18 -0
- package/dist/compatibility/index.cjs +43 -0
- package/dist/compatibility/index.d.cts +2 -0
- package/dist/compatibility/index.d.ts +2 -0
- package/dist/compatibility/index.js +42 -0
- package/dist/definition/index.cjs +10 -2
- package/dist/definition/index.d.cts +3 -21
- package/dist/definition/index.d.ts +3 -21
- package/dist/definition/index.js +2 -2
- package/dist/definition-BH_LzXGc.js +1115 -0
- package/dist/definition-BLLBpLKv.cjs +1187 -0
- package/dist/index-1sDuqu05.d.ts +373 -0
- package/dist/index-BFSMT_2d.d.ts +53 -0
- package/dist/index-BGz7L_Fq.d.cts +73 -0
- package/dist/index-BTtdqi_M.d.ts +418 -0
- package/dist/index-BfbClf6R.d.cts +20 -0
- package/dist/index-CK7Is7vy.d.cts +420 -0
- package/dist/index-CUGhtuwA.d.cts +53 -0
- package/dist/index-CV8EZQy8.d.ts +92 -0
- package/dist/index-CzNxjqVg.d.cts +47 -0
- package/dist/index-DGHMUZ5m.d.cts +92 -0
- package/dist/{index-eK2Xgrwp.d.ts → index-DxvfPmw6.d.ts} +13 -9
- package/dist/index-T6nFhvGQ.d.ts +20 -0
- package/dist/{index-TACcK_mi.d.cts → index-ba_52Yu9.d.cts} +13 -9
- package/dist/index-hrcM9oJe.d.ts +47 -0
- package/dist/index-i7Au-FQ6.d.cts +373 -0
- package/dist/index-mu1znI1F.d.ts +73 -0
- package/dist/index.cjs +70 -58
- package/dist/index.d.cts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/index.js +10 -6
- package/dist/integrations/json-schema/index.cjs +328 -0
- package/dist/integrations/json-schema/index.d.cts +53 -0
- package/dist/integrations/json-schema/index.d.ts +53 -0
- package/dist/integrations/json-schema/index.js +324 -0
- package/dist/integrations/standard-schema/index.cjs +32 -0
- package/dist/integrations/standard-schema/index.d.cts +22 -0
- package/dist/integrations/standard-schema/index.d.ts +22 -0
- package/dist/integrations/standard-schema/index.js +32 -0
- package/dist/payload/index.cjs +45 -82
- package/dist/payload/index.d.cts +2 -56
- package/dist/payload/index.d.ts +2 -56
- package/dist/payload/index.js +45 -81
- package/dist/relations/index.cjs +3 -4
- package/dist/relations/index.d.cts +2 -2
- package/dist/relations/index.d.ts +2 -2
- package/dist/relations/index.js +2 -2
- package/dist/{relations-CMz_0nf4.cjs → relations-NUVJkAG8.cjs} +143 -211
- package/dist/{relations-By0bK3UK.js → relations-QtnCRW9x.js} +140 -202
- package/dist/resolve/index.cjs +308 -0
- package/dist/resolve/index.d.cts +2 -0
- package/dist/resolve/index.d.ts +2 -0
- package/dist/resolve/index.js +305 -0
- package/dist/schema-ref-key-BHVFVfj1.cjs +26 -0
- package/dist/schema-ref-key-DR__tPr-.js +14 -0
- package/dist/spec/index.cjs +9 -0
- package/dist/spec/index.d.cts +2 -2
- package/dist/spec/index.d.ts +2 -2
- package/dist/spec/index.js +3 -1
- package/dist/standard/index.cjs +810 -0
- package/dist/standard/index.d.cts +3 -0
- package/dist/standard/index.d.ts +3 -0
- package/dist/standard/index.js +777 -0
- package/dist/standard-BGMgbbEQ.js +471 -0
- package/dist/standard-etPFZDJI.cjs +513 -0
- package/dist/type-interpreter/index.cjs +3 -7
- package/dist/type-interpreter/index.d.cts +2 -99
- package/dist/type-interpreter/index.d.ts +2 -99
- package/dist/type-interpreter/index.js +2 -2
- package/dist/{type-interpreter-Ck0VDhzn.js → type-interpreter-BA8gcLUU.js} +58 -560
- package/dist/{type-interpreter-DAIBUqKf.cjs → type-interpreter-D8xETgq1.cjs} +63 -589
- package/package.json +60 -5
- package/dist/definition-BXj3T94L.js +0 -467
- package/dist/definition-DSZZTz4e.cjs +0 -473
- package/dist/index-CEzHfht2.d.ts +0 -269
- package/dist/index-rzkFjgrt.d.cts +0 -269
package/README.md
CHANGED
|
@@ -1,36 +1,76 @@
|
|
|
1
1
|
# SchemaIR for TypeScript
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/schemair)
|
|
4
|
+
[](https://github.com/schemair/schemair/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/schemair/schemair/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
**Types as data.**
|
|
8
|
+
|
|
9
|
+
SchemaIR is a language-independent protocol and intermediate representation
|
|
10
|
+
for portable data contracts, operation definitions, and type calculations.
|
|
11
|
+
|
|
12
|
+
The [`schemair` package](https://www.npmjs.com/package/schemair) is the
|
|
13
|
+
TypeScript implementation. It represents contracts, operations, and type
|
|
14
|
+
expressions as serializable data.
|
|
15
|
+
|
|
16
|
+
For the protocol's theory, IR model, design philosophy, standard vocabulary,
|
|
17
|
+
cross-language boundaries, and conformance rules, see the [SchemaIR project
|
|
18
|
+
README](https://github.com/schemair/schemair#readme) and the [protocol
|
|
19
|
+
specification](https://github.com/schemair/schemair/tree/main/spec). This page
|
|
20
|
+
focuses on using SchemaIR from TypeScript and JavaScript.
|
|
21
|
+
|
|
22
|
+
## About this implementation
|
|
23
|
+
|
|
24
|
+
The package exports model types, definition and payload validators,
|
|
25
|
+
assignability checks, a type expression interpreter, and authoring builders.
|
|
26
|
+
|
|
27
|
+
The `schemair/authoring` entry point exposes two namespaces: `schema` contains
|
|
28
|
+
builders for serializable `SchemaNode` contracts, while `expression` contains
|
|
29
|
+
builders for serializable `SchemaTypeExpr` calculations. A schema node and an
|
|
30
|
+
expression are both plain data and can be stored, exchanged, or evaluated by
|
|
31
|
+
the interpreter.
|
|
32
|
+
|
|
33
|
+
For statically authored contracts, `schemair/authoring` also exports the
|
|
34
|
+
TypeScript-only `Infer<Schema>` utility. It projects a source-known schema into
|
|
35
|
+
a payload carrier type; it does not alter the portable declaration or replace
|
|
36
|
+
runtime payload validation.
|
|
37
|
+
|
|
38
|
+
The optional `schemair/standard` entry point provides the protocol's `std`
|
|
39
|
+
semantic and constraint vocabulary, including reusable paths, definitions,
|
|
40
|
+
standard payload predicates, constraint reasoning, and an opt-in relation
|
|
41
|
+
context. Core validation and relation APIs remain provider-driven; import
|
|
42
|
+
`standardPayloadContext` or `standardRelationContext` when the standard
|
|
43
|
+
vocabulary policy is wanted.
|
|
44
|
+
|
|
45
|
+
This binding implements the shared protocol and conformance rules. See the
|
|
46
|
+
[implementation matrix](../IMPLEMENTATIONS.md) for coverage and status.
|
|
9
47
|
|
|
10
48
|
## Install
|
|
11
49
|
|
|
12
|
-
|
|
50
|
+
Install the published package from npm:
|
|
13
51
|
|
|
14
52
|
```sh
|
|
53
|
+
npm install schemair
|
|
54
|
+
# or
|
|
15
55
|
pnpm add schemair
|
|
16
56
|
```
|
|
17
57
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
58
|
+
To work on the implementation in this repository, run `pnpm install` in this
|
|
59
|
+
directory. The published package provides ESM `.js` files for `import` and CJS
|
|
60
|
+
`.cjs` files for `require`, with matching TypeScript declarations.
|
|
21
61
|
|
|
22
|
-
##
|
|
62
|
+
## Quick start
|
|
23
63
|
|
|
24
64
|
Builders return ordinary serializable objects. You can also write the same
|
|
25
65
|
`SchemaNode` data directly or read it from JSON. Validate untrusted definitions
|
|
26
66
|
before using them.
|
|
27
67
|
|
|
28
68
|
```ts
|
|
29
|
-
import {
|
|
69
|
+
import { schema as s } from 'schemair/authoring';
|
|
30
70
|
import { validateSchemaNodeDefinition } from 'schemair/definition';
|
|
31
71
|
import { validatePayload } from 'schemair/payload';
|
|
32
72
|
|
|
33
|
-
const user = record([requiredField('name', string())]);
|
|
73
|
+
const user = s.record([s.requiredField('name', s.string())]);
|
|
34
74
|
const definition = validateSchemaNodeDefinition(user);
|
|
35
75
|
if (!definition.ok) throw new Error(definition.issues[0]?.message);
|
|
36
76
|
|
|
@@ -44,16 +84,283 @@ if (result.status !== 'accepted') console.error(result.issues);
|
|
|
44
84
|
needed `resolveRef`, `semanticProvider`, or `constraintProvider` in the payload
|
|
45
85
|
context. Missing capabilities can produce `unknown`.
|
|
46
86
|
|
|
87
|
+
For a catalog-backed schema, resolve refs once and reuse the snapshot for
|
|
88
|
+
synchronous validation:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { resolveSchemaReferencesAsync } from 'schemair/resolve';
|
|
92
|
+
import { validatePayload } from 'schemair/payload';
|
|
93
|
+
|
|
94
|
+
const closure = await resolveSchemaReferencesAsync(schema, { resolveRef });
|
|
95
|
+
if (closure.status !== 'resolved') throw new Error(closure.issues[0]?.message);
|
|
96
|
+
const result = validatePayload(value, schema, { refs: closure.graph.refs });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The `refs` context is a list of `{ refPath, schema }` entries. The path uses
|
|
100
|
+
the same string-array shape as a `SchemaNode` ref; callers do not need to
|
|
101
|
+
encode it into an object key.
|
|
102
|
+
|
|
103
|
+
The validator does not perform catalog I/O when `refs` is supplied. Semantic
|
|
104
|
+
and constraint providers are synchronous; prepare any external policy data
|
|
105
|
+
before checking the payload.
|
|
106
|
+
|
|
107
|
+
## Infer a TypeScript payload type
|
|
108
|
+
|
|
109
|
+
`Infer<Schema, Registry>` is available when the schema is authored in TypeScript and its
|
|
110
|
+
literal structure remains available to the compiler. It supports primitive,
|
|
111
|
+
literal, literal-union, nullable, array, tuple, record, and tagged-union
|
|
112
|
+
nodes.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { schema as s, type Infer } from 'schemair/authoring';
|
|
116
|
+
|
|
117
|
+
const user = s.record([
|
|
118
|
+
s.requiredField('id', s.string()),
|
|
119
|
+
s.requiredField('age', s.number()),
|
|
120
|
+
s.optionalField('nickname', s.string()),
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
type User = Infer<typeof user>;
|
|
124
|
+
// { id: string; age: number; nickname?: string }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`Infer` is intentionally conservative. Named `ref` declarations can be resolved
|
|
128
|
+
by passing a statically authored registry as the second parameter, including
|
|
129
|
+
references nested inside another registered schema:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const registry = s.defineSchemaRegistry([
|
|
133
|
+
{ refPath: ['users', 'User'] as const, name: 'User', schema: user },
|
|
134
|
+
]);
|
|
135
|
+
const order = s.record([s.requiredField('owner', s.ref(['users', 'User']))]);
|
|
136
|
+
type Order = Infer<typeof order, typeof registry>;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Unresolved, dynamically shaped, ambiguous, or cyclic references project to
|
|
140
|
+
`unknown`. `Infer` is intentionally conservative. Typed
|
|
141
|
+
additional record fields, broad `SchemaNode` values, and unsupported or
|
|
142
|
+
dynamically loaded declarations project to `unknown`. A structural `selfRef`
|
|
143
|
+
can refer back to the root schema, so common tree-shaped records can be
|
|
144
|
+
inferred recursively. Semantic paths and constraints remain their carrier
|
|
145
|
+
types: an email is `string`, and an integer is `number`. Always use
|
|
146
|
+
payload validation for data received at runtime.
|
|
147
|
+
|
|
148
|
+
For statically known references, pair `InferRef` with a plain declaration
|
|
149
|
+
registry:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { schema as s, type InferRef } from 'schemair/authoring';
|
|
153
|
+
|
|
154
|
+
const registry = s.defineSchemaRegistry([
|
|
155
|
+
{ refPath: ['users', 'User'], name: 'User', schema: user },
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
type UserFromRef = InferRef<typeof registry, ['users', 'User']>;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Missing or dynamically shaped paths project to `unknown`. The registry helper
|
|
162
|
+
does not resolve references or replace runtime definition validation.
|
|
163
|
+
|
|
164
|
+
`InferResult<Schema, Registry>` is the diagnostic form of the same projection.
|
|
165
|
+
It keeps failures out of ordinary payload types and reports a small, stable
|
|
166
|
+
reason such as `unresolved-ref`, `dynamic-ref-path`, or `unsupported-schema`:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import type { InferResult } from 'schemair/authoring';
|
|
170
|
+
|
|
171
|
+
type CheckedUser = InferResult<typeof user>;
|
|
172
|
+
// { ok: true; type: { id: string; age: number; nickname?: string } }
|
|
173
|
+
type Missing = InferResult<typeof s.ref(['users', 'Missing'])>;
|
|
174
|
+
// { ok: false; reason: 'unresolved-ref' }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This is a compile-time utility, not runtime validation. Use the definition
|
|
178
|
+
validators when the declaration comes from JSON, a database, or an AI tool.
|
|
179
|
+
|
|
180
|
+
## Validate expressions before evaluation
|
|
181
|
+
|
|
182
|
+
Use `validateSchemaTypeExprDefinition(unknown)` to check an expression from
|
|
183
|
+
JSON, a database, or an AI proposal. Use `validateSchemaTypeTermDefinition`
|
|
184
|
+
when the declaration may be either a `SchemaNode` or a `SchemaTypeExpr`.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { validateSchemaTypeExprDefinition } from 'schemair/definition';
|
|
188
|
+
import { expression as e } from 'schemair/authoring';
|
|
189
|
+
|
|
190
|
+
const result = validateSchemaTypeExprDefinition(e.fieldOf(e.typeVar('T'), 'name'));
|
|
191
|
+
if (result.ok) console.log(result.expression);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Validation preserves the original declaration and reports diagnostics with
|
|
195
|
+
paths. It checks nested terms, selectors, constraints, and JSON compatibility;
|
|
196
|
+
it does not evaluate expressions, bind variables, resolve references, or call
|
|
197
|
+
providers. Unbound variables and unresolved references are valid declarations.
|
|
198
|
+
A structurally valid expression can still be rejected during evaluation (for
|
|
199
|
+
example, selecting a field from a string). Validation has a depth limit of 64
|
|
200
|
+
and a work limit of 10,000 visited JSON values; exceeding either rejects the
|
|
201
|
+
check with an `expression.budget` diagnostic.
|
|
202
|
+
|
|
203
|
+
## Use the standard vocabulary
|
|
204
|
+
|
|
205
|
+
The optional `schemair/standard` entry provides 20 semantic definitions and
|
|
206
|
+
13 constraint definitions, their path constants, registry metadata, and
|
|
207
|
+
payload/relation providers. Install the providers explicitly when you want
|
|
208
|
+
standard vocabulary decisions:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { schema as s } from 'schemair/authoring';
|
|
212
|
+
import { validatePayload } from 'schemair/payload';
|
|
213
|
+
import {
|
|
214
|
+
STANDARD_SEMANTIC_PATHS, STANDARD_CONSTRAINT_PATHS,
|
|
215
|
+
standardPayloadContext,
|
|
216
|
+
} from 'schemair/standard';
|
|
217
|
+
|
|
218
|
+
const email = s.string({
|
|
219
|
+
semanticPath: [...STANDARD_SEMANTIC_PATHS.string.email],
|
|
220
|
+
constraints: [{ constraintPath: [...STANDARD_CONSTRAINT_PATHS.string.maxLength], args: 254 }],
|
|
221
|
+
});
|
|
222
|
+
const result = validatePayload('ada@example.com', email, standardPayloadContext);
|
|
223
|
+
console.log(result.status); // accepted
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Use `STANDARD_SEMANTIC_DEFINITIONS` and `STANDARD_CONSTRAINT_DEFINITIONS` to
|
|
227
|
+
build vocabulary pickers, show validation versus annotation roles, or inspect
|
|
228
|
+
constraint targets and argument domains. `standardRelationContext` supplies
|
|
229
|
+
accepted-set reasoning to `checkSchemaRelation`; `createStandardRelationContext`
|
|
230
|
+
lets the host select an additional semantic identity policy.
|
|
231
|
+
|
|
232
|
+
The portable vocabulary specifies declarations and meaning. The TypeScript
|
|
233
|
+
providers implement those meanings within a JavaScript host: numbers use
|
|
234
|
+
finite binary64 values; string lengths count Unicode code points; patterns
|
|
235
|
+
use the ECMAScript regex engine. Unsupported pattern syntax and unknown or
|
|
236
|
+
extension paths produce `unknown` and need an appropriate host provider.
|
|
237
|
+
An annotation such as `std/string/secret/password` labels a string; it imposes no
|
|
238
|
+
password-strength validation. `standardSchemaSatisfiability` reports
|
|
239
|
+
`inhabited`, `emptySet`, or `unknown`; an undecidable combination is never
|
|
240
|
+
silently treated as empty. See the [standard vocabulary specification](https://github.com/schemair/schemair/blob/main/spec/protocol/standard-vocabulary.md)
|
|
241
|
+
for portable semantics and implementation boundaries.
|
|
242
|
+
|
|
243
|
+
## Define an operation
|
|
244
|
+
|
|
245
|
+
An operation describes its input and output contracts as data. The host owns
|
|
246
|
+
invocation and execution.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { schema as s } from 'schemair/authoring';
|
|
250
|
+
import type { OperationDefinition } from 'schemair/spec';
|
|
251
|
+
|
|
252
|
+
const lookupUser: OperationDefinition = {
|
|
253
|
+
input: { kind: 'structured', schema: s.record([s.requiredField('id', s.string())]) },
|
|
254
|
+
output: { kind: 'structured', schema: s.record([s.requiredField('name', s.string())]) },
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
For TypeScript consumers, operation inference projects structured payloads and
|
|
259
|
+
lets the host choose the carrier for binary payloads. The carrier is a host
|
|
260
|
+
policy and is not part of the portable operation declaration:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import type {
|
|
264
|
+
InferOperationInput, InferOperationOutput, InferOperationErrors,
|
|
265
|
+
InferOperationEmits,
|
|
266
|
+
} from 'schemair/authoring';
|
|
267
|
+
|
|
268
|
+
type Binary = { bytes: Uint8Array; mime?: string };
|
|
269
|
+
type Input = InferOperationInput<typeof lookupUser>;
|
|
270
|
+
type Output = InferOperationOutput<typeof lookupUser, never, Binary>;
|
|
271
|
+
type Errors = InferOperationErrors<typeof lookupUser>;
|
|
272
|
+
type Events = InferOperationEmits<typeof lookupUser, never, Binary>;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Absent input/output or payload data is represented as `undefined`; named error
|
|
276
|
+
and event maps retain their keys. This is a TypeScript convenience layer and
|
|
277
|
+
does not add functions, generic templates, or host carriers to the protocol.
|
|
278
|
+
|
|
279
|
+
An operation definition is a concrete, transport-neutral contract. Its
|
|
280
|
+
structured payloads contain `SchemaNode` data; functions, unresolved
|
|
281
|
+
`SchemaTypeExpr` terms, and type-variable templates belong to the authoring
|
|
282
|
+
layer and must be resolved before publishing the operation. This makes the
|
|
283
|
+
same declaration usable by visual tools, workflow systems, and AI-assisted
|
|
284
|
+
contract tools without requiring them to understand HTTP details.
|
|
285
|
+
|
|
286
|
+
## Validate portable operation declarations
|
|
287
|
+
|
|
288
|
+
Use `validateOperationDefinition` and `validateOperationContainerDefinition`
|
|
289
|
+
from `schemair/definition` when loading declarations from JSON, a database, or
|
|
290
|
+
an AI-assisted tool. Successful results return the original `operation` or
|
|
291
|
+
`container`; failures return `issues` with `code`, `path`, and `message`.
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
import { validateOperationDefinition } from 'schemair/definition';
|
|
295
|
+
|
|
296
|
+
const declaration: unknown = JSON.parse('{"emits":{"progress":{}}}');
|
|
297
|
+
const result = validateOperationDefinition(declaration);
|
|
298
|
+
if (result.ok) console.log(result.operation);
|
|
299
|
+
else console.error(result.issues);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Operation payloads and error details contain concrete `SchemaNode` contracts.
|
|
303
|
+
Type expressions, generic templates, functions, and execution bindings are
|
|
304
|
+
outside this model. Each embedded schema is validated as its own root;
|
|
305
|
+
reference resolution and vocabulary policy are supplied separately.
|
|
306
|
+
|
|
307
|
+
## Compare operation contracts
|
|
308
|
+
|
|
309
|
+
`checkOperationRelation(source, target)` explains whether `source` can replace
|
|
310
|
+
`target` for a caller that follows the target contract. Its diagnostics retain
|
|
311
|
+
the operation path and include `details.component` (`input`, `output`, `error`,
|
|
312
|
+
or `emission`) plus the comparison direction when the issue comes from a
|
|
313
|
+
nested schema relation. An input comparison is reported in the reversed
|
|
314
|
+
direction because callers supply the target's input shape. An unproved
|
|
315
|
+
semantic, constraint, reference, or binary metadata implication remains
|
|
316
|
+
`unknown`.
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { checkOperationRelation } from 'schemair/relations';
|
|
320
|
+
|
|
321
|
+
const relation = checkOperationRelation(sourceOperation, targetOperation);
|
|
322
|
+
for (const issue of relation.issues) {
|
|
323
|
+
console.log(issue.path, issue.details?.component, issue.message);
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The `schemair/compatibility` entry point reports both substitution directions
|
|
328
|
+
for a previous and next schema or operation. `previousToNext` asks whether
|
|
329
|
+
values accepted by the previous declaration are accepted by the next one;
|
|
330
|
+
`nextToPrevious` checks the reverse. Each side preserves the original relation
|
|
331
|
+
result and diagnostics, while the report summarizes the pair as
|
|
332
|
+
`compatible`, `breaking`, `unknown`, or `invalid`.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { reportSchemaCompatibility } from 'schemair/compatibility';
|
|
336
|
+
|
|
337
|
+
const report = reportSchemaCompatibility(previousSchema, nextSchema);
|
|
338
|
+
if (report.status === 'breaking') {
|
|
339
|
+
console.log(report.previousToNext.relation.issues);
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
This is a relation-based compatibility report. It does not choose a migration,
|
|
344
|
+
versioning, or publication policy for the host.
|
|
345
|
+
|
|
346
|
+
`validateStructuredOperationDefinition` and
|
|
347
|
+
`validateStructuredOperationContainerDefinition` apply the same checks and
|
|
348
|
+
reject binary payloads anywhere in the declaration. Their boolean companions,
|
|
349
|
+
`isStructuredOperationDefinition` and
|
|
350
|
+
`isStructuredOperationContainerDefinition`, narrow unknown input to the
|
|
351
|
+
exported `StructuredOperation*` types. A successful check preserves the
|
|
352
|
+
original declaration, including absent payloads and empty channels.
|
|
353
|
+
|
|
47
354
|
## Compare types
|
|
48
355
|
|
|
49
356
|
`checkSchemaRelation(source, target)` asks whether values admitted by the
|
|
50
357
|
source can be used where the target is expected.
|
|
51
358
|
|
|
52
359
|
```ts
|
|
53
|
-
import {
|
|
360
|
+
import { schema as s } from 'schemair/authoring';
|
|
54
361
|
import { checkSchemaRelation } from 'schemair/relations';
|
|
55
362
|
|
|
56
|
-
const relation = checkSchemaRelation(string(), string());
|
|
363
|
+
const relation = checkSchemaRelation(s.string(), s.string());
|
|
57
364
|
if (relation.status === 'assignable') {
|
|
58
365
|
// The source can be connected to the target.
|
|
59
366
|
}
|
|
@@ -61,7 +368,9 @@ if (relation.status === 'assignable') {
|
|
|
61
368
|
|
|
62
369
|
The result is `assignable`, `incompatible`, `unknown`, or `rejected`. Supply
|
|
63
370
|
resolver and provider callbacks in the relation context when comparison needs
|
|
64
|
-
external information.
|
|
371
|
+
external information. If references have already been loaded by a host, pass
|
|
372
|
+
the immutable snapshot as `refs`; relation checking then remains synchronous and
|
|
373
|
+
does not perform catalog I/O.
|
|
65
374
|
|
|
66
375
|
## Evaluate expressions
|
|
67
376
|
|
|
@@ -69,11 +378,36 @@ The type interpreter calculates authoring-time expressions. An evaluated
|
|
|
69
378
|
result always has a `term`; it also has `schema` when that term can be
|
|
70
379
|
materialized as a `SchemaNode`.
|
|
71
380
|
|
|
381
|
+
For a synchronous, fully materialized run, load the reachable reference
|
|
382
|
+
closure first and pass its immutable graph to the interpreter:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
import { evaluateExpression } from 'schemair/type-interpreter';
|
|
386
|
+
import { resolveSchemaReferencesAsync } from 'schemair/resolve';
|
|
387
|
+
|
|
388
|
+
const closure = await resolveSchemaReferencesAsync(expression, { resolveRef });
|
|
389
|
+
if (closure.status !== 'resolved') throw new Error(closure.issues[0]?.message);
|
|
390
|
+
|
|
391
|
+
const result = evaluateExpression(expression, { refs: closure.graph.refs });
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`resolveSchemaReferencesAsync` recursively loads every external reference reachable
|
|
395
|
+
from the entry, including references inside resolved schemas. It preserves ref
|
|
396
|
+
paths rather than inlining schemas. The `schemair/resolve` loader performs
|
|
397
|
+
host-owned asynchronous I/O. `evaluateExpression` then uses the explicit `refs`
|
|
398
|
+
snapshot, so the same evaluation request can run in a worker, server process,
|
|
399
|
+
or WASM implementation. Term relations and solvers may additionally use
|
|
400
|
+
explicit synchronous host policy callbacks.
|
|
401
|
+
`selfRef` remains local to each schema root; recursive external refs stay as
|
|
402
|
+
graph edges during loading and are bounded by the closure limits. The returned
|
|
403
|
+
graph exposes those edges as `graph.cycles`. Resolve a closure once when refs
|
|
404
|
+
come from a database or network, then reuse its snapshot across these calls.
|
|
405
|
+
|
|
72
406
|
```ts
|
|
73
|
-
import {
|
|
407
|
+
import { schema as s, expression as e } from 'schemair/authoring';
|
|
74
408
|
import { evaluateExpression } from 'schemair/type-interpreter';
|
|
75
409
|
|
|
76
|
-
const result = evaluateExpression(arrayOf(string()));
|
|
410
|
+
const result = evaluateExpression(e.arrayOf(s.string()));
|
|
77
411
|
if (result.status === 'evaluated') {
|
|
78
412
|
console.log(result.term);
|
|
79
413
|
console.log(result.schema);
|
|
@@ -85,22 +419,193 @@ Unbound type variables remain in the evaluated term and appear in
|
|
|
85
419
|
with structured issues. `checkTypeTermRelation` compares expression terms;
|
|
86
420
|
`solveTypeVariables` infers invocation-local bindings for generic ports.
|
|
87
421
|
|
|
88
|
-
|
|
422
|
+
Evaluation validates portable declarations before computing their results,
|
|
423
|
+
including constraints on unresolved terms. It does not mutate input
|
|
424
|
+
declarations and accepts frozen declarations. Type-variable upper bounds use
|
|
425
|
+
the same preloaded reference environment as the binding being checked.
|
|
89
426
|
|
|
90
|
-
|
|
91
|
-
network-backed capabilities, use the asynchronous companions:
|
|
427
|
+
## Compare expression terms
|
|
92
428
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
429
|
+
Unlike `checkSchemaRelation`, this API also compares terms that cannot become
|
|
430
|
+
`SchemaNode`, such as opaque host types. Equal host paths match directly;
|
|
431
|
+
the host supplies any other native type relationship.
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import { schema as s, expression as e } from 'schemair/authoring';
|
|
435
|
+
import { checkTypeTermRelation } from 'schemair/type-interpreter';
|
|
436
|
+
|
|
437
|
+
const mouseEvent = e.opaqueHostType(['dom', 'MouseEvent']);
|
|
438
|
+
const event = e.opaqueHostType(['dom', 'Event']);
|
|
439
|
+
const relation = checkTypeTermRelation(mouseEvent, event, {
|
|
440
|
+
hostTypeRelation: (source, target) =>
|
|
441
|
+
source.join('/') === 'dom/MouseEvent' && target.join('/') === 'dom/Event',
|
|
442
|
+
});
|
|
443
|
+
console.log(relation.status); // 'assignable'
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Solve type variables
|
|
447
|
+
|
|
448
|
+
For a map-like operation, infer `T` from its input array and `U` from the
|
|
449
|
+
mapper's output. The source-to-target constraints represent port connections.
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { schema as s, expression as e } from 'schemair/authoring';
|
|
453
|
+
import { evaluateExpression, solveTypeVariables } from 'schemair/type-interpreter';
|
|
454
|
+
|
|
455
|
+
const solution = solveTypeVariables({
|
|
456
|
+
variables: ['T', 'U'],
|
|
457
|
+
constraints: [
|
|
458
|
+
{ source: s.array(s.number()), target: e.arrayOf(e.typeVar('T')) },
|
|
459
|
+
{ source: s.string(), target: e.typeVar('U') },
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
if (solution.status === 'solved') {
|
|
464
|
+
const output = evaluateExpression(e.arrayOf(e.typeVar('U')), {
|
|
465
|
+
typeVars: solution.bindings,
|
|
466
|
+
});
|
|
467
|
+
if (output.status === 'evaluated') console.log(output.schema);
|
|
468
|
+
// { kind: 'array', elementSchema: { kind: 'primitive', name: 'string' } }
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
When a variable has no evidence, the solver lists it in `unresolved` instead
|
|
473
|
+
of inventing a binding. Inspect `status` and `issues` before using a result.
|
|
474
|
+
|
|
475
|
+
## Asynchronous boundaries
|
|
476
|
+
|
|
477
|
+
Schema definition validation, expression evaluation, schema and operation
|
|
478
|
+
relations, compatibility reports, type-variable solving, and payload validation are synchronous
|
|
479
|
+
calculations. They accept in-memory data and synchronous callbacks. When refs
|
|
480
|
+
are stored remotely, use `resolveSchemaReferencesAsync` or `resolveOperationClosureAsync`
|
|
481
|
+
from `schemair/resolve` to load them first, then pass the resulting `refs`
|
|
482
|
+
snapshot to the calculation. For an in-memory catalog, use the synchronous
|
|
483
|
+
`resolveSchemaReferences` or `resolveOperationClosure` instead. Both modes
|
|
484
|
+
share traversal, caching, limits, and diagnostics. Sync loaders reject promise
|
|
485
|
+
results with an `incomplete` diagnostic; async loaders accept both immediate
|
|
486
|
+
and promise results.
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { resolveSchemaReferences } from 'schemair/resolve';
|
|
490
|
+
import { schema as s } from 'schemair/authoring';
|
|
491
|
+
|
|
492
|
+
const loaded = resolveSchemaReferences(s.ref(['UserName']), {
|
|
493
|
+
resolveRef: (path) => path[0] === 'UserName' ? s.string() : undefined,
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
For example, load a reference catalog from a database before checking a value:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
import { schema as s } from 'schemair/authoring';
|
|
501
|
+
import { validatePayload } from 'schemair/payload';
|
|
502
|
+
import { resolveSchemaReferencesAsync } from 'schemair/resolve';
|
|
503
|
+
|
|
504
|
+
const user = s.record([s.requiredField('name', s.string())]);
|
|
505
|
+
const ref = s.ref(['catalog', 'user']);
|
|
506
|
+
const loaded = await resolveSchemaReferencesAsync(ref, {
|
|
507
|
+
resolveRef: async (path) =>
|
|
508
|
+
path.join('/') === 'catalog/user' ? user : undefined,
|
|
509
|
+
});
|
|
510
|
+
if (loaded.status !== 'resolved') throw new Error(loaded.issues[0]?.message);
|
|
511
|
+
const result = validatePayload({ name: 'Ada' }, ref, { refs: loaded.graph.refs });
|
|
512
|
+
console.log(result.status); // 'accepted'
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Semantic and constraint providers are synchronous policies. Prepare external
|
|
516
|
+
policy data before validation. Database uniqueness, permissions, and other
|
|
517
|
+
business checks belong in the host workflow rather than the core validator.
|
|
518
|
+
|
|
519
|
+
## Standard Schema integration
|
|
520
|
+
|
|
521
|
+
The `schemair/standard-schema` subpath exposes a runtime-only Standard Schema
|
|
522
|
+
adapter for dynamic `SchemaNode` data:
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
import { schemaIRToStandardSchema } from 'schemair/standard-schema';
|
|
526
|
+
import { schema as s } from 'schemair/authoring';
|
|
527
|
+
|
|
528
|
+
const standard = schemaIRToStandardSchema(
|
|
529
|
+
s.record([s.requiredField('name', s.string())]),
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const result = standard['~standard'].validate({ name: 'Ada' });
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
The adapter uses synchronous payload validation with preloaded `refs` and
|
|
536
|
+
synchronous `resolveRef`, `semanticProvider`, and `constraintProvider`
|
|
537
|
+
callbacks. Load external references before constructing the adapter.
|
|
538
|
+
Successful validation returns the original
|
|
539
|
+
input reference. Failed, unknown, unsupported, and unresolved validation
|
|
540
|
+
outcomes are projected to Standard Schema `issues`; native SchemaIR payload
|
|
541
|
+
diagnostics remain available through `schemair/payload`.
|
|
542
|
+
|
|
543
|
+
Because the adapter receives runtime schema data, it intentionally does not
|
|
544
|
+
claim a static `~standard.types` input/output type. It is a runtime adapter,
|
|
545
|
+
not a typed authoring inference surface.
|
|
546
|
+
|
|
547
|
+
## JSON Schema and OpenAPI integration
|
|
548
|
+
|
|
549
|
+
The `schemair/json-schema` subpath projects a `SchemaNode` to JSON Schema
|
|
550
|
+
Draft 2020-12 by default. Draft-07 is also supported with its legacy tuple and
|
|
551
|
+
exclusive-bound keywords. OpenAPI 3.0 is available as a separate Schema Object
|
|
552
|
+
projection through `schemaNodeToOpenApiSchema` or the Standard JSON Schema V1
|
|
553
|
+
target. It is intentionally described as a projection rather than a full JSON
|
|
554
|
+
Schema dialect because OpenAPI 3.0 cannot represent every SchemaIR construct.
|
|
555
|
+
|
|
556
|
+
The exporter uses native keywords whenever the meaning is equivalent and
|
|
557
|
+
returns projection diagnostics for anything that cannot be represented exactly:
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
import { schemaNodeToJsonSchema } from 'schemair/json-schema';
|
|
561
|
+
|
|
562
|
+
const result = schemaNodeToJsonSchema({
|
|
563
|
+
kind: 'primitive',
|
|
564
|
+
name: 'string',
|
|
565
|
+
semanticPath: ['ext', 'money'],
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// result.schema contains { type: 'string', 'x-schemair': { semanticPath: [...] } }
|
|
569
|
+
// result.fidelity is 'exact', 'lossy', or 'unsupported'.
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Select a JSON Schema dialect explicitly when needed:
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
const draft07 = schemaNodeToJsonSchema(tupleNode, { target: 'draft-07' });
|
|
576
|
+
// Draft-07 tuples use items: [...] and additionalItems: false.
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
OpenAPI 3.0 uses `nullable: true` for nullable schemas and preserves tuple
|
|
580
|
+
positions, literals, semantic identity, and other non-native details under
|
|
581
|
+
`x-schemair`. Standalone `null` schemas and unresolved references are reported
|
|
582
|
+
as `unsupported`; lossy projections are reported in `diagnostics` and are not
|
|
583
|
+
silently treated as exact.
|
|
584
|
+
|
|
585
|
+
Unmapped semantic paths, constraints, and references are retained under the
|
|
586
|
+
`x-schemair` object. A caller may supply `semanticMapper`, `constraintMapper`,
|
|
587
|
+
and `refStrategy` options for host-owned vocabularies and reference catalogs.
|
|
588
|
+
The exporter never silently drops SchemaIR metadata.
|
|
589
|
+
|
|
590
|
+
The same subpath also implements [Standard JSON Schema V1](https://standardschema.dev/json-schema):
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
import { schemaIRToStandardJsonSchema } from 'schemair/json-schema';
|
|
594
|
+
|
|
595
|
+
const standard = schemaIRToStandardJsonSchema(user);
|
|
596
|
+
const jsonSchema = standard['~standard'].jsonSchema.input({
|
|
597
|
+
target: 'draft-2020-12',
|
|
598
|
+
});
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
`input()` and `output()` are identical because SchemaIR declarations do not
|
|
602
|
+
perform value transformations. Unsupported targets or projections throw; a
|
|
603
|
+
lossy projection is returned when its original metadata is preserved in
|
|
604
|
+
`x-schemair`.
|
|
100
605
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
606
|
+
See the [JSON Schema compatibility matrix](./docs/json-schema-compatibility.md)
|
|
607
|
+
for the target-by-target fidelity rules, extension fields, capability
|
|
608
|
+
callbacks, and required diagnostics.
|
|
104
609
|
|
|
105
610
|
## Package entry points
|
|
106
611
|
|
|
@@ -111,8 +616,13 @@ sync counterparts.
|
|
|
111
616
|
| `schemair/definition` | Schema definition validation |
|
|
112
617
|
| `schemair/payload` | Payload validation |
|
|
113
618
|
| `schemair/relations` | SchemaNode assignability |
|
|
619
|
+
| `schemair/compatibility` | Directional schema and operation compatibility reports |
|
|
114
620
|
| `schemair/type-interpreter` | Expression evaluation and type-term relations |
|
|
115
|
-
| `schemair/
|
|
621
|
+
| `schemair/resolve` | Host-owned synchronous and asynchronous schema and operation reference loading |
|
|
622
|
+
| `schemair/authoring` | `schema` builders, `expression` builders, and TypeScript-only `Infer<Schema>` |
|
|
623
|
+
| `schemair/standard` | Standard semantic/constraint paths, registries, predicates, and relation helpers |
|
|
624
|
+
| `schemair/standard-schema` | Standard Schema runtime adapter |
|
|
625
|
+
| `schemair/json-schema` | JSON Schema Draft-2020-12/Draft-07 and OpenAPI 3.0 projections |
|
|
116
626
|
|
|
117
627
|
`src/internal/` is not exported. The package provides no ref registry,
|
|
118
628
|
semantic vocabulary implementation, persistence layer, graph scheduler, or
|