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.
Files changed (83) hide show
  1. package/README.md +542 -32
  2. package/dist/authoring/index.cjs +13 -350
  3. package/dist/authoring/index.d.cts +2 -90
  4. package/dist/authoring/index.d.ts +2 -90
  5. package/dist/authoring/index.js +2 -304
  6. package/dist/authoring-Dnrg5TXm.cjs +389 -0
  7. package/dist/authoring-DyJNYs5o.js +362 -0
  8. package/dist/chunk-bFfwrPtJ.js +18 -0
  9. package/dist/compatibility/index.cjs +43 -0
  10. package/dist/compatibility/index.d.cts +2 -0
  11. package/dist/compatibility/index.d.ts +2 -0
  12. package/dist/compatibility/index.js +42 -0
  13. package/dist/definition/index.cjs +10 -2
  14. package/dist/definition/index.d.cts +3 -21
  15. package/dist/definition/index.d.ts +3 -21
  16. package/dist/definition/index.js +2 -2
  17. package/dist/definition-BH_LzXGc.js +1115 -0
  18. package/dist/definition-BLLBpLKv.cjs +1187 -0
  19. package/dist/index-1sDuqu05.d.ts +373 -0
  20. package/dist/index-BFSMT_2d.d.ts +53 -0
  21. package/dist/index-BGz7L_Fq.d.cts +73 -0
  22. package/dist/index-BTtdqi_M.d.ts +418 -0
  23. package/dist/index-BfbClf6R.d.cts +20 -0
  24. package/dist/index-CK7Is7vy.d.cts +420 -0
  25. package/dist/index-CUGhtuwA.d.cts +53 -0
  26. package/dist/index-CV8EZQy8.d.ts +92 -0
  27. package/dist/index-CzNxjqVg.d.cts +47 -0
  28. package/dist/index-DGHMUZ5m.d.cts +92 -0
  29. package/dist/{index-eK2Xgrwp.d.ts → index-DxvfPmw6.d.ts} +13 -9
  30. package/dist/index-T6nFhvGQ.d.ts +20 -0
  31. package/dist/{index-TACcK_mi.d.cts → index-ba_52Yu9.d.cts} +13 -9
  32. package/dist/index-hrcM9oJe.d.ts +47 -0
  33. package/dist/index-i7Au-FQ6.d.cts +373 -0
  34. package/dist/index-mu1znI1F.d.ts +73 -0
  35. package/dist/index.cjs +70 -58
  36. package/dist/index.d.cts +9 -7
  37. package/dist/index.d.ts +9 -7
  38. package/dist/index.js +10 -6
  39. package/dist/integrations/json-schema/index.cjs +328 -0
  40. package/dist/integrations/json-schema/index.d.cts +53 -0
  41. package/dist/integrations/json-schema/index.d.ts +53 -0
  42. package/dist/integrations/json-schema/index.js +324 -0
  43. package/dist/integrations/standard-schema/index.cjs +32 -0
  44. package/dist/integrations/standard-schema/index.d.cts +22 -0
  45. package/dist/integrations/standard-schema/index.d.ts +22 -0
  46. package/dist/integrations/standard-schema/index.js +32 -0
  47. package/dist/payload/index.cjs +45 -82
  48. package/dist/payload/index.d.cts +2 -56
  49. package/dist/payload/index.d.ts +2 -56
  50. package/dist/payload/index.js +45 -81
  51. package/dist/relations/index.cjs +3 -4
  52. package/dist/relations/index.d.cts +2 -2
  53. package/dist/relations/index.d.ts +2 -2
  54. package/dist/relations/index.js +2 -2
  55. package/dist/{relations-CMz_0nf4.cjs → relations-NUVJkAG8.cjs} +143 -211
  56. package/dist/{relations-By0bK3UK.js → relations-QtnCRW9x.js} +140 -202
  57. package/dist/resolve/index.cjs +308 -0
  58. package/dist/resolve/index.d.cts +2 -0
  59. package/dist/resolve/index.d.ts +2 -0
  60. package/dist/resolve/index.js +305 -0
  61. package/dist/schema-ref-key-BHVFVfj1.cjs +26 -0
  62. package/dist/schema-ref-key-DR__tPr-.js +14 -0
  63. package/dist/spec/index.cjs +9 -0
  64. package/dist/spec/index.d.cts +2 -2
  65. package/dist/spec/index.d.ts +2 -2
  66. package/dist/spec/index.js +3 -1
  67. package/dist/standard/index.cjs +810 -0
  68. package/dist/standard/index.d.cts +3 -0
  69. package/dist/standard/index.d.ts +3 -0
  70. package/dist/standard/index.js +777 -0
  71. package/dist/standard-BGMgbbEQ.js +471 -0
  72. package/dist/standard-etPFZDJI.cjs +513 -0
  73. package/dist/type-interpreter/index.cjs +3 -7
  74. package/dist/type-interpreter/index.d.cts +2 -99
  75. package/dist/type-interpreter/index.d.ts +2 -99
  76. package/dist/type-interpreter/index.js +2 -2
  77. package/dist/{type-interpreter-Ck0VDhzn.js → type-interpreter-BA8gcLUU.js} +58 -560
  78. package/dist/{type-interpreter-DAIBUqKf.cjs → type-interpreter-D8xETgq1.cjs} +63 -589
  79. package/package.json +60 -5
  80. package/dist/definition-BXj3T94L.js +0 -467
  81. package/dist/definition-DSZZTz4e.cjs +0 -473
  82. package/dist/index-CEzHfht2.d.ts +0 -269
  83. package/dist/index-rzkFjgrt.d.cts +0 -269
package/README.md CHANGED
@@ -1,36 +1,76 @@
1
1
  # SchemaIR for TypeScript
2
2
 
3
- The `schemair` package is the TypeScript implementation of SchemaIR. It exports
4
- model types, definition and payload validators, assignability checks, a type
5
- expression interpreter, and authoring builders. For the language-independent
6
- model and evaluation rules, start with the
7
- [project README](https://github.com/schemair/schemair#readme) and
8
- [protocol](https://github.com/schemair/schemair/tree/main/spec).
3
+ [![npm version](https://img.shields.io/npm/v/schemair)](https://www.npmjs.com/package/schemair)
4
+ [![CI](https://github.com/schemair/schemair/actions/workflows/ci.yml/badge.svg)](https://github.com/schemair/schemair/actions/workflows/ci.yml)
5
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](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
- Once the package is published:
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
- Until then, run `pnpm install` in this directory to work with the repository
19
- copy. The package is configured to publish ESM `.js` files for `import` and
20
- CJS `.cjs` files for `require`, with matching TypeScript declarations.
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
- ## Define and validate a schema
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 { record, requiredField, string } from 'schemair/authoring';
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 { string } from 'schemair/authoring';
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 { arrayOf, string } from 'schemair/authoring';
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
- ## Sync and async APIs
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
- Use synchronous calls with in-memory resolvers and providers. For database or
91
- network-backed capabilities, use the asynchronous companions:
427
+ ## Compare expression terms
92
428
 
93
- | Synchronous | Asynchronous |
94
- | --- | --- |
95
- | `validatePayload` | `validatePayloadAsync` |
96
- | `checkSchemaRelation` | `checkSchemaRelationAsync` |
97
- | `evaluateExpression` | `evaluateExpressionAsync` |
98
- | `checkTypeTermRelation` | `checkTypeTermRelationAsync` |
99
- | `solveTypeVariables` | `solveTypeVariablesAsync` |
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
- `validateSchemaNodeDefinition` is always synchronous. The async companions
102
- await caller-provided capabilities and use the same result categories as their
103
- sync counterparts.
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/authoring` | Data-construction helpers |
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