@json-schema-engine/core 0.0.1

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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/coverage.d.ts +30 -0
  4. package/dist/coverage.d.ts.map +1 -0
  5. package/dist/coverage.js +81 -0
  6. package/dist/coverage.js.map +1 -0
  7. package/dist/cursor.d.ts +16 -0
  8. package/dist/cursor.d.ts.map +1 -0
  9. package/dist/cursor.js +28 -0
  10. package/dist/cursor.js.map +1 -0
  11. package/dist/dialect.d.ts +316 -0
  12. package/dist/dialect.d.ts.map +1 -0
  13. package/dist/dialect.js +135 -0
  14. package/dist/dialect.js.map +1 -0
  15. package/dist/engine.d.ts +203 -0
  16. package/dist/engine.d.ts.map +1 -0
  17. package/dist/engine.js +493 -0
  18. package/dist/engine.js.map +1 -0
  19. package/dist/index.d.ts +182 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +370 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/json.d.ts +49 -0
  24. package/dist/json.d.ts.map +1 -0
  25. package/dist/json.js +128 -0
  26. package/dist/json.js.map +1 -0
  27. package/dist/keywords/applicator.d.ts +57 -0
  28. package/dist/keywords/applicator.d.ts.map +1 -0
  29. package/dist/keywords/applicator.js +789 -0
  30. package/dist/keywords/applicator.js.map +1 -0
  31. package/dist/keywords/core.d.ts +42 -0
  32. package/dist/keywords/core.d.ts.map +1 -0
  33. package/dist/keywords/core.js +139 -0
  34. package/dist/keywords/core.js.map +1 -0
  35. package/dist/keywords/format.d.ts +43 -0
  36. package/dist/keywords/format.d.ts.map +1 -0
  37. package/dist/keywords/format.js +101 -0
  38. package/dist/keywords/format.js.map +1 -0
  39. package/dist/keywords/metaschemas2019.d.ts +3 -0
  40. package/dist/keywords/metaschemas2019.d.ts.map +1 -0
  41. package/dist/keywords/metaschemas2019.js +406 -0
  42. package/dist/keywords/metaschemas2019.js.map +1 -0
  43. package/dist/keywords/metaschemas2020.d.ts +3 -0
  44. package/dist/keywords/metaschemas2020.d.ts.map +1 -0
  45. package/dist/keywords/metaschemas2020.js +441 -0
  46. package/dist/keywords/metaschemas2020.js.map +1 -0
  47. package/dist/keywords/metaschemas6.d.ts +3 -0
  48. package/dist/keywords/metaschemas6.d.ts.map +1 -0
  49. package/dist/keywords/metaschemas6.js +221 -0
  50. package/dist/keywords/metaschemas6.js.map +1 -0
  51. package/dist/keywords/metaschemas7.d.ts +3 -0
  52. package/dist/keywords/metaschemas7.d.ts.map +1 -0
  53. package/dist/keywords/metaschemas7.js +248 -0
  54. package/dist/keywords/metaschemas7.js.map +1 -0
  55. package/dist/keywords/unevaluated.d.ts +14 -0
  56. package/dist/keywords/unevaluated.d.ts.map +1 -0
  57. package/dist/keywords/unevaluated.js +296 -0
  58. package/dist/keywords/unevaluated.js.map +1 -0
  59. package/dist/keywords/validation.d.ts +21 -0
  60. package/dist/keywords/validation.d.ts.map +1 -0
  61. package/dist/keywords/validation.js +332 -0
  62. package/dist/keywords/validation.js.map +1 -0
  63. package/dist/keywords/vocab2019.d.ts +36 -0
  64. package/dist/keywords/vocab2019.d.ts.map +1 -0
  65. package/dist/keywords/vocab2019.js +585 -0
  66. package/dist/keywords/vocab2019.js.map +1 -0
  67. package/dist/keywords/vocab2020.d.ts +20 -0
  68. package/dist/keywords/vocab2020.d.ts.map +1 -0
  69. package/dist/keywords/vocab2020.js +55 -0
  70. package/dist/keywords/vocab2020.js.map +1 -0
  71. package/dist/keywords/vocab7.d.ts +46 -0
  72. package/dist/keywords/vocab7.d.ts.map +1 -0
  73. package/dist/keywords/vocab7.js +305 -0
  74. package/dist/keywords/vocab7.js.map +1 -0
  75. package/dist/loader.d.ts +41 -0
  76. package/dist/loader.d.ts.map +1 -0
  77. package/dist/loader.js +13 -0
  78. package/dist/loader.js.map +1 -0
  79. package/dist/lowering.d.ts +371 -0
  80. package/dist/lowering.d.ts.map +1 -0
  81. package/dist/lowering.js +59 -0
  82. package/dist/lowering.js.map +1 -0
  83. package/dist/output.d.ts +251 -0
  84. package/dist/output.d.ts.map +1 -0
  85. package/dist/output.js +357 -0
  86. package/dist/output.js.map +1 -0
  87. package/dist/records.d.ts +71 -0
  88. package/dist/records.d.ts.map +1 -0
  89. package/dist/records.js +150 -0
  90. package/dist/records.js.map +1 -0
  91. package/dist/ref.d.ts +10 -0
  92. package/dist/ref.d.ts.map +1 -0
  93. package/dist/ref.js +5 -0
  94. package/dist/ref.js.map +1 -0
  95. package/dist/regex.d.ts +45 -0
  96. package/dist/regex.d.ts.map +1 -0
  97. package/dist/regex.js +163 -0
  98. package/dist/regex.js.map +1 -0
  99. package/dist/registry.d.ts +141 -0
  100. package/dist/registry.d.ts.map +1 -0
  101. package/dist/registry.js +407 -0
  102. package/dist/registry.js.map +1 -0
  103. package/dist/result.d.ts +109 -0
  104. package/dist/result.d.ts.map +1 -0
  105. package/dist/result.js +112 -0
  106. package/dist/result.js.map +1 -0
  107. package/dist/uri.d.ts +18 -0
  108. package/dist/uri.d.ts.map +1 -0
  109. package/dist/uri.js +27 -0
  110. package/dist/uri.js.map +1 -0
  111. package/dist/walk.d.ts +25 -0
  112. package/dist/walk.d.ts.map +1 -0
  113. package/dist/walk.js +65 -0
  114. package/dist/walk.js.map +1 -0
  115. package/package.json +32 -0
  116. package/src/coverage.ts +88 -0
  117. package/src/cursor.ts +42 -0
  118. package/src/dialect.ts +428 -0
  119. package/src/engine.ts +741 -0
  120. package/src/index.ts +699 -0
  121. package/src/json.ts +137 -0
  122. package/src/keywords/applicator.ts +895 -0
  123. package/src/keywords/core.ts +168 -0
  124. package/src/keywords/format.ts +143 -0
  125. package/src/keywords/metaschemas2019.ts +410 -0
  126. package/src/keywords/metaschemas2020.ts +443 -0
  127. package/src/keywords/metaschemas6.ts +226 -0
  128. package/src/keywords/metaschemas7.ts +253 -0
  129. package/src/keywords/unevaluated.ts +329 -0
  130. package/src/keywords/validation.ts +627 -0
  131. package/src/keywords/vocab2019.ts +676 -0
  132. package/src/keywords/vocab2020.ts +82 -0
  133. package/src/keywords/vocab7.ts +393 -0
  134. package/src/loader.ts +60 -0
  135. package/src/lowering.ts +414 -0
  136. package/src/output.ts +651 -0
  137. package/src/records.ts +226 -0
  138. package/src/ref.ts +14 -0
  139. package/src/regex.ts +171 -0
  140. package/src/registry.ts +481 -0
  141. package/src/result.ts +252 -0
  142. package/src/uri.ts +35 -0
  143. package/src/walk.ts +88 -0
package/src/index.ts ADDED
@@ -0,0 +1,699 @@
1
+ // @json-schema-engine/core public API: sync evaluation over registered schemas (D7); the
2
+ // 2020-12 dialect preloaded; custom vocabularies/dialects via the same
3
+ // registry the built-ins use. Resource I/O is the one async boundary:
4
+ // load/loadSchema pull in referenced resources through caller-supplied
5
+ // loaders and assemble dialects from metaschema $vocabulary declarations.
6
+
7
+ import { JsonValue, isObject } from "./json.js";
8
+ import { resolveUri, splitFragment, UnresolvableRefError } from "./uri.js";
9
+ import {
10
+ DialectRegistry,
11
+ DialectOptions,
12
+ KeywordBehavior,
13
+ UnknownDialectError,
14
+ UnknownVocabularyError,
15
+ } from "./dialect.js";
16
+ import { DEFAULT_MAX_DEPTH, SchemaRegistry } from "./registry.js";
17
+ import { runEvaluation } from "./engine.js";
18
+ import {
19
+ RegexCache,
20
+ RegexEngine,
21
+ UnsafeRegexError,
22
+ detectUnsafeRegex,
23
+ } from "./regex.js";
24
+ import {
25
+ LoadedDocument,
26
+ SchemaLoader,
27
+ SourceLocation,
28
+ SourceRange,
29
+ } from "./loader.js";
30
+ import { AnnotationUnit, ErrorUnit, makeRecordPredicate } from "./output.js";
31
+ import {
32
+ type EvaluateOptions,
33
+ type Result,
34
+ type ResultFor,
35
+ assembleResult,
36
+ resolveOutputDemand,
37
+ } from "./result.js";
38
+ import {
39
+ type RecordSets,
40
+ renderError,
41
+ renderSelected,
42
+ schemaLocationOf,
43
+ toRenderNode,
44
+ } from "./records.js";
45
+ import {
46
+ DIALECT_2020_12,
47
+ registerStandardDialects,
48
+ } from "./keywords/vocab2020.js";
49
+ import {
50
+ VOCAB_FORMAT_ASSERTION,
51
+ assertingFormat,
52
+ type FormatTable,
53
+ } from "./keywords/format.js";
54
+ import { METASCHEMAS_2020_12 } from "./keywords/metaschemas2020.js";
55
+ import { METASCHEMAS_2019_09 } from "./keywords/metaschemas2019.js";
56
+ import { METASCHEMAS_DRAFT_07 } from "./keywords/metaschemas7.js";
57
+ import { METASCHEMAS_DRAFT_06 } from "./keywords/metaschemas6.js";
58
+ import { VOCAB_CORE_2019 } from "./keywords/core.js";
59
+ import { identifiers2019, identifiers2020 } from "./dialect.js";
60
+
61
+ export type { JsonValue, JsonType } from "./json.js";
62
+ export type { Cursor } from "./cursor.js";
63
+ export { rootCursor, childCursor, instancePointer } from "./cursor.js";
64
+ export type { SchemaRef } from "./ref.js";
65
+ export type {
66
+ Dialect,
67
+ DialectKeyword,
68
+ KeywordBehavior,
69
+ KeywordContext,
70
+ StaticFacts,
71
+ DependencyView,
72
+ DialectOptions,
73
+ IdentifierFacts,
74
+ IdentifierExtractor,
75
+ } from "./dialect.js";
76
+ export {
77
+ DialectRegistry,
78
+ ReadOnlyRegistryError,
79
+ UnknownDialectError,
80
+ UnknownVocabularyError,
81
+ identifiers2020,
82
+ identifiers2019,
83
+ identifiersLegacy,
84
+ } from "./dialect.js";
85
+ export {
86
+ DEFAULT_MAX_DEPTH,
87
+ InvalidSchemaError,
88
+ MaxDepthExceededError,
89
+ SchemaRegistry,
90
+ } from "./registry.js";
91
+ export type { DocumentLocation } from "./registry.js";
92
+ export { UnsafeRegexError, detectUnsafeRegex } from "./regex.js";
93
+ export type { RegexEngine, CompiledRegex } from "./regex.js";
94
+ export { UnresolvableRefError } from "./uri.js";
95
+ export {
96
+ InfiniteLoopError,
97
+ KeywordContractError,
98
+ UndeclaredConsumptionError,
99
+ UndeclaredProductionError,
100
+ UnknownKeywordError,
101
+ evaluateFragment,
102
+ materializePath,
103
+ runEvaluation,
104
+ } from "./engine.js";
105
+ export type {
106
+ AnnotationRecord,
107
+ DependencyRecord,
108
+ ErrorRecord,
109
+ EvalState,
110
+ Frame,
111
+ FragmentOptions,
112
+ KeywordTrace,
113
+ PathNode,
114
+ RecordPredicate,
115
+ TraceNode,
116
+ } from "./engine.js";
117
+ export {
118
+ canonicalKey,
119
+ codePointLength,
120
+ escapeSegment,
121
+ firstDuplicatePair,
122
+ hasDuplicateItems,
123
+ jsonEqual,
124
+ unescapeSegment,
125
+ } from "./json.js";
126
+ export { isMultipleOf } from "./keywords/validation.js";
127
+ export { walkSchema } from "./walk.js";
128
+ export type { SchemaWalkVisit } from "./walk.js";
129
+ export type {
130
+ AnalyzeContext,
131
+ ErrorParams,
132
+ IndexCoverage,
133
+ NameCoverage,
134
+ SubschemaApplication,
135
+ } from "./dialect.js";
136
+ export { lowerIR } from "./lowering.js";
137
+ export {
138
+ makeRecordPredicate,
139
+ renderBasic,
140
+ renderDetailed,
141
+ renderHierarchical,
142
+ renderList,
143
+ renderTrace,
144
+ renderVerbose,
145
+ } from "./output.js";
146
+ export {
147
+ renderAnnotation,
148
+ renderError,
149
+ traceToRenderNodes,
150
+ } from "./records.js";
151
+ export type { MutableRenderNode } from "./records.js";
152
+ export {
153
+ OutputOptionsError,
154
+ assembleResult,
155
+ resolveOutputDemand,
156
+ } from "./result.js";
157
+ export type {
158
+ EvaluateOptions,
159
+ OutputDemand,
160
+ OutputDocuments,
161
+ OutputFormat,
162
+ Result,
163
+ ResultFor,
164
+ ResultUnits,
165
+ } from "./result.js";
166
+ export type {
167
+ LowerApply,
168
+ LowerCursor,
169
+ LowerExpr,
170
+ LowerHelper,
171
+ LowerMessage,
172
+ LowerParams,
173
+ LowerProduceValue,
174
+ LowerStmt,
175
+ LoweringContext,
176
+ } from "./lowering.js";
177
+ export {
178
+ foldNameCoverage,
179
+ foldIndexCoverage,
180
+ harvestCoverage,
181
+ } from "./coverage.js";
182
+ export { RegexCache, defaultRegexEngine } from "./regex.js";
183
+ export type {
184
+ AnnotationSelection,
185
+ AnnotationUnit,
186
+ BasicAnnotationUnit,
187
+ BasicErrorUnit,
188
+ BasicOutputDocument,
189
+ DetailedOutputUnit,
190
+ ErrorUnit,
191
+ IrrelevantRendering,
192
+ ListOutputDocument,
193
+ OutputUnit,
194
+ RenderInput,
195
+ RenderNode,
196
+ TraceUnit,
197
+ } from "./output.js";
198
+ export type {
199
+ LoadedDocument,
200
+ SchemaLoader,
201
+ SourceLocation,
202
+ SourcePosition,
203
+ SourceRange,
204
+ SourceSpan,
205
+ } from "./loader.js";
206
+ export { DIALECT_2020_12 } from "./keywords/vocab2020.js";
207
+ export {
208
+ UnknownFormatError,
209
+ VOCAB_FORMAT_ASSERTION,
210
+ assertingFormat,
211
+ } from "./keywords/format.js";
212
+ export type { FormatDefinition, FormatTable } from "./keywords/format.js";
213
+ export { DIALECT_2019_09 } from "./keywords/vocab2019.js";
214
+ export { DIALECT_DRAFT_07, DIALECT_DRAFT_06 } from "./keywords/vocab7.js";
215
+
216
+ /** Thrown by {@link EngineOptions.validateSchemas} when a registered schema fails its metaschema. */
217
+ export class SchemaValidationError extends Error {
218
+ constructor(
219
+ message: string,
220
+ readonly errors: readonly ErrorUnit[],
221
+ ) {
222
+ super(message);
223
+ }
224
+ }
225
+
226
+ /** Options for {@link Engine}'s constructor. */
227
+ export interface EngineOptions {
228
+ /** dialect for documents without $schema; default 2020-12 */
229
+ defaultDialect?: string;
230
+ /** resource loaders, tried in order (D7) */
231
+ loaders?: readonly SchemaLoader[];
232
+ /**
233
+ * Validate each load/register target against its metaschema when that
234
+ * metaschema is registered as a schema resource. The standard metaschemas
235
+ * for the built-in dialects are bundled; for custom dialects, an
236
+ * unavailable metaschema means "cannot check", not failure — supply a
237
+ * loader for it to get the check.
238
+ */
239
+ validateSchemas?: boolean;
240
+ /**
241
+ * Compile `pattern`/`patternProperties` through this engine instead of the
242
+ * native `RegExp`. Supply a linear-time engine (e.g. RE2) to evaluate
243
+ * untrusted schemas without exposure to catastrophic backtracking (ReDoS).
244
+ * See docs/guide/security.md.
245
+ */
246
+ regexEngine?: RegexEngine;
247
+ /**
248
+ * Reject a schema at registration when a `pattern`/`patternProperties`
249
+ * regex looks exponential-time ({@link detectUnsafeRegex}), throwing
250
+ * {@link UnsafeRegexError}. A conservative static screen, off by default;
251
+ * a linear-time `regexEngine` remains the only hard guarantee.
252
+ */
253
+ rejectUnsafeRegex?: boolean;
254
+ /**
255
+ * Maximum schema-nesting (registration) and schema-application
256
+ * (evaluation) depth before {@link MaxDepthExceededError}. Bounds otherwise
257
+ * unbounded recursion on adversarial input; default
258
+ * {@link DEFAULT_MAX_DEPTH}.
259
+ */
260
+ maxDepth?: number;
261
+ /**
262
+ * Format implementations (M7; \@json-schema-engine/formats supplies standard tables).
263
+ * Enables the format-assertion vocabulary: a `$vocabulary` dialect
264
+ * declaring it gets an asserting `format` that REFUSES unsupported
265
+ * formats at registration ({@link UnknownFormatError}).
266
+ */
267
+ formats?: FormatTable;
268
+ /**
269
+ * Assert recognized formats in the standard dialects (all drafts) —
270
+ * the spec's opt-in assertion configuration. Best effort: formats the
271
+ * table lacks fall back to annotation-only. Requires `formats`.
272
+ */
273
+ assertFormats?: boolean;
274
+ }
275
+
276
+ /**
277
+ * \@json-schema-engine/core public API: synchronous evaluation over registered schemas,
278
+ * with the 2020-12 dialect preloaded and async resource loading for
279
+ * `$ref` closures and `$vocabulary`-assembled dialects.
280
+ */
281
+ export class Engine {
282
+ readonly dialects = new DialectRegistry();
283
+ private schemas: SchemaRegistry;
284
+ private defaultDialect: string;
285
+ private loaders: SchemaLoader[];
286
+ private validateSchemas: boolean;
287
+ private regexCache: RegexCache;
288
+ private maxDepth: number;
289
+ private formatTable: FormatTable | undefined;
290
+ // Dialect URIs whose assembly is in progress, to fail metaschema cycles.
291
+ private assembling = new Set<string>();
292
+
293
+ constructor(options: EngineOptions = {}) {
294
+ if (options.assertFormats && !options.formats) {
295
+ throw new TypeError("assertFormats requires a formats table");
296
+ }
297
+ registerStandardDialects(
298
+ this.dialects,
299
+ options.assertFormats
300
+ ? {
301
+ format: (id) => assertingFormat(id, options.formats!, false),
302
+ }
303
+ : {},
304
+ );
305
+ // The format-assertion vocabulary exists whenever a table is supplied;
306
+ // without one, a dialect requiring it fails with UnknownVocabularyError,
307
+ // which is the correct "cannot honor the assertion promise" answer.
308
+ this.formatTable = options.formats;
309
+ if (options.formats) {
310
+ const table = options.formats;
311
+ this.dialects.registerVocabulary(VOCAB_FORMAT_ASSERTION, {
312
+ format: assertingFormat(
313
+ `${VOCAB_FORMAT_ASSERTION}#format`,
314
+ table,
315
+ true,
316
+ ),
317
+ });
318
+ }
319
+ this.defaultDialect = splitFragment(
320
+ options.defaultDialect ?? DIALECT_2020_12,
321
+ ).resource;
322
+ this.regexCache = new RegexCache(options.regexEngine);
323
+ this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
324
+ this.schemas = new SchemaRegistry(
325
+ this.dialects,
326
+ this.defaultDialect,
327
+ this.maxDepth,
328
+ );
329
+ this.loaders = [...(options.loaders ?? [])];
330
+ this.validateSchemas = options.validateSchemas ?? false;
331
+ // Standard metaschemas are registered as ordinary schema resources so
332
+ // that $refs to them resolve without loaders and validateSchemas can
333
+ // check standard-dialect documents. Registered directly (not through
334
+ // registerSchema) so the policy never self-validates them here.
335
+ for (const [uri, doc] of METASCHEMAS_2020_12) {
336
+ this.schemas.register(doc, uri);
337
+ }
338
+ for (const [uri, doc] of METASCHEMAS_2019_09) {
339
+ this.schemas.register(doc, uri);
340
+ }
341
+ for (const [uri, doc] of METASCHEMAS_DRAFT_07) {
342
+ this.schemas.register(doc, uri);
343
+ }
344
+ for (const [uri, doc] of METASCHEMAS_DRAFT_06) {
345
+ this.schemas.register(doc, uri);
346
+ }
347
+ // Installed after the trusted metaschemas register, so the screen applies
348
+ // only to caller schemas (D20).
349
+ if (options.rejectUnsafeRegex) {
350
+ this.schemas.onRegex = (pattern, location) => {
351
+ const verdict = detectUnsafeRegex(pattern);
352
+ if (!verdict.safe) {
353
+ throw new UnsafeRegexError(
354
+ `unsafe regex at '${location}': ${verdict.reason}`,
355
+ );
356
+ }
357
+ };
358
+ }
359
+ }
360
+
361
+ /** Registers an additional resource loader, tried after existing ones (D7). */
362
+ addLoader(loader: SchemaLoader): void {
363
+ this.loaders.push(loader);
364
+ }
365
+
366
+ /**
367
+ * The engine's schema registry — the compiler tier's read surface (M6):
368
+ * resolved refs, per-resource dialects, identifier indexes. Mutations go
369
+ * through the Engine methods, not the registry; `snapshot()` yields the
370
+ * read-only view a compiled artifact binds to.
371
+ */
372
+ get registry(): SchemaRegistry {
373
+ return this.schemas;
374
+ }
375
+
376
+ /** The engine's compiled-pattern cache, shared with compiled artifacts (M6). */
377
+ get patternCache(): RegexCache {
378
+ return this.regexCache;
379
+ }
380
+
381
+ /**
382
+ * The format table backing this engine's asserting `format`, or undefined
383
+ * when none was configured — the compiler tier's read surface for
384
+ * format-assertion lowering (M7). The asserting `format` behaviors close
385
+ * over this same table, so a compiled `formatTest` and the interpreter agree.
386
+ */
387
+ get formats(): FormatTable | undefined {
388
+ return this.formatTable;
389
+ }
390
+
391
+ /**
392
+ * Register a local schema document synchronously; returns its canonical
393
+ * base URI. References to unregistered resources are not loaded — use
394
+ * loadSchema for that.
395
+ */
396
+ registerSchema(
397
+ schema: JsonValue,
398
+ retrievalUri: string,
399
+ dialectUri?: string,
400
+ getRange?: (pointer: string) => SourceRange | undefined,
401
+ ): string {
402
+ const baseUri = this.schemas.register(
403
+ schema,
404
+ retrievalUri,
405
+ dialectUri,
406
+ getRange,
407
+ );
408
+ this.maybeValidate(baseUri);
409
+ return baseUri;
410
+ }
411
+
412
+ /**
413
+ * Register a schema document and load everything it transitively
414
+ * references through the configured loaders. Loader misses for referenced
415
+ * resources are not errors here; evaluation reports them if the reference
416
+ * is actually followed.
417
+ */
418
+ async loadSchema(
419
+ schema: JsonValue,
420
+ retrievalUri: string,
421
+ dialectUri?: string,
422
+ getRange?: (pointer: string) => SourceRange | undefined,
423
+ ): Promise<string> {
424
+ await this.ensureDialectFor(schema, retrievalUri, dialectUri);
425
+ const baseUri = this.schemas.register(
426
+ schema,
427
+ retrievalUri,
428
+ dialectUri,
429
+ getRange,
430
+ );
431
+ await this.loadPending();
432
+ this.maybeValidate(baseUri);
433
+ return baseUri;
434
+ }
435
+
436
+ /** Fetch a resource by URI through the loaders and register its closure. */
437
+ async load(uri: string): Promise<string> {
438
+ const resource = splitFragment(uri).resource;
439
+ const baseUri = await this.loadResource(resource);
440
+ if (baseUri === undefined) {
441
+ throw new UnresolvableRefError(`no loader provided '${resource}'`);
442
+ }
443
+ await this.loadPending();
444
+ this.maybeValidate(baseUri);
445
+ return baseUri;
446
+ }
447
+
448
+ /** Registers a vocabulary's keyword behaviors under its URI. */
449
+ registerVocabulary(
450
+ uri: string,
451
+ keywords: Readonly<Record<string, KeywordBehavior>>,
452
+ ): void {
453
+ this.dialects.registerVocabulary(uri, keywords);
454
+ }
455
+
456
+ /**
457
+ * Assembles a dialect from already-registered vocabularies.
458
+ * @throws UnknownDialectError if a listed vocabulary is not registered.
459
+ */
460
+ registerDialect(
461
+ uri: string,
462
+ vocabularyUris: readonly string[],
463
+ options?: DialectOptions,
464
+ ): void {
465
+ this.dialects.registerDialect(uri, vocabularyUris, options);
466
+ }
467
+
468
+ /**
469
+ * Where a schema resource lives within its registered document: the
470
+ * containing document plus the resource root's document-rooted pointer
471
+ * (D17 bridge; see loader.ts), for source-position lookup.
472
+ */
473
+ documentLocation(resourceUri: string) {
474
+ return this.schemas.documentLocation(resourceUri);
475
+ }
476
+
477
+ /**
478
+ * Translates a canonical schema location (`resourceUri#/pointer`) to its
479
+ * document, document-rooted pointer, and — when the document's loader
480
+ * reported positions — source range (D17 bridge; see loader.ts).
481
+ */
482
+ locate(schemaLocation: string): SourceLocation | undefined {
483
+ const { resource, fragment } = splitFragment(schemaLocation);
484
+ const loc = this.schemas.documentLocation(resource);
485
+ if (loc === undefined) return undefined;
486
+ const pointer = loc.pointer + (fragment ?? "");
487
+ const range = this.schemas.range(loc.documentUri, pointer);
488
+ return range === undefined
489
+ ? { documentUri: loc.documentUri, pointer }
490
+ : { documentUri: loc.documentUri, pointer, range };
491
+ }
492
+
493
+ /**
494
+ * Evaluates an instance against a registered schema. Literal options
495
+ * narrow the result ({@link ResultFor}); an unsupported combination of
496
+ * options throws {@link OutputOptionsError} before evaluating.
497
+ */
498
+ evaluate<O extends EvaluateOptions>(
499
+ schemaUri: string,
500
+ instance: JsonValue,
501
+ options?: O,
502
+ ): ResultFor<O>;
503
+ evaluate(
504
+ schemaUri: string,
505
+ instance: JsonValue,
506
+ options: EvaluateOptions = {},
507
+ ): Result {
508
+ const demand = resolveOutputDemand(options);
509
+ const { valid, state } = runEvaluation(
510
+ this.schemas,
511
+ schemaUri,
512
+ instance,
513
+ demand.tracing,
514
+ makeRecordPredicate(demand.annotations),
515
+ this.regexCache,
516
+ this.maxDepth,
517
+ );
518
+ if (demand.format === "flag") return { valid };
519
+
520
+ // The flat surface first; its record arrays stay paired with the unit
521
+ // arrays so the located tree can index the units.
522
+ const params = options.errorParams ?? false;
523
+ const records: RecordSets = {
524
+ errors: state.errors,
525
+ droppedErrors: [],
526
+ annotations: [],
527
+ droppedAnnotations: [],
528
+ };
529
+ const errors = state.errors.map((e) => renderError(e, params));
530
+ let annotations: AnnotationUnit[] = [];
531
+ if (valid && demand.annotations !== false) {
532
+ const selected = renderSelected(
533
+ state.rootAnnotations,
534
+ demand.annotations,
535
+ );
536
+ records.annotations = selected.records;
537
+ annotations = selected.units;
538
+ }
539
+ let droppedErrors: ErrorUnit[] = [];
540
+ let droppedAnnotations: AnnotationUnit[] = [];
541
+ if (demand.verbose) {
542
+ records.droppedErrors = state.droppedErrors ?? [];
543
+ droppedErrors = records.droppedErrors.map((e) => renderError(e, params));
544
+ if (demand.annotations !== false) {
545
+ // The relevant annotations are a valid run's root survivors; an
546
+ // invalid run has none (draft-03 §12.2).
547
+ const relevant = new Set(valid ? state.rootAnnotations : []);
548
+ const selected = renderSelected(
549
+ (state.allAnnotations ?? []).filter((a) => !relevant.has(a)),
550
+ demand.annotations,
551
+ );
552
+ records.droppedAnnotations = selected.records;
553
+ droppedAnnotations = selected.units;
554
+ }
555
+ }
556
+ const root = demand.tracing
557
+ ? toRenderNode(state.traceRoot!, records)
558
+ : null;
559
+ const result = assembleResult(
560
+ demand,
561
+ valid,
562
+ { errors, droppedErrors, annotations, droppedAnnotations },
563
+ root,
564
+ schemaLocationOf(this.schemas.rootRef(schemaUri), null),
565
+ options.trace === true,
566
+ );
567
+ if (options.positions) {
568
+ this.decorate(result.errors);
569
+ this.decorate(result.annotations);
570
+ this.decorate(result.droppedErrors);
571
+ this.decorate(result.droppedAnnotations);
572
+ }
573
+ return result;
574
+ }
575
+
576
+ private decorate(
577
+ units: readonly ErrorUnit[] | readonly AnnotationUnit[] | undefined,
578
+ ): void {
579
+ for (const unit of units ?? []) {
580
+ const source = this.locate(unit.schemaLocation);
581
+ if (source !== undefined) unit.source = source;
582
+ }
583
+ }
584
+
585
+ private async fetch(resource: string): Promise<LoadedDocument | undefined> {
586
+ for (const loader of this.loaders) {
587
+ const doc = await loader(resource);
588
+ if (doc !== undefined) return doc;
589
+ }
590
+ return undefined;
591
+ }
592
+
593
+ /** Fetch + register one resource; undefined when no loader handles it. */
594
+ private async loadResource(resource: string): Promise<string | undefined> {
595
+ if (this.schemas.has(resource)) return resource;
596
+ const doc = await this.fetch(resource);
597
+ if (doc === undefined) return undefined;
598
+ await this.ensureDialectFor(doc.value, resource);
599
+ return this.schemas.register(doc.value, resource, undefined, doc.getRange);
600
+ }
601
+
602
+ // Drain reference targets collected by registration walks until closure.
603
+ // Each round may register documents whose own walks add new targets.
604
+ private async loadPending(): Promise<void> {
605
+ for (;;) {
606
+ const missing = this.schemas.takeUnresolved();
607
+ if (missing.length === 0) return;
608
+ for (const resource of missing) {
609
+ await this.loadResource(resource);
610
+ }
611
+ }
612
+ }
613
+
614
+ // Make the document's dialect exist before registration: known dialects
615
+ // pass through; otherwise the $schema target is loaded as a metaschema and
616
+ // a dialect is assembled from its $vocabulary (spec §8.1).
617
+ private async ensureDialectFor(
618
+ schema: JsonValue,
619
+ retrievalUri: string,
620
+ dialectUri?: string,
621
+ ): Promise<void> {
622
+ let effective = splitFragment(dialectUri ?? this.defaultDialect).resource;
623
+ if (isObject(schema) && typeof schema.$schema === "string") {
624
+ effective = splitFragment(
625
+ resolveUri(schema.$schema, retrievalUri),
626
+ ).resource;
627
+ }
628
+ if (this.dialects.hasDialect(effective)) return;
629
+ if (this.assembling.has(effective)) {
630
+ throw new UnknownDialectError(`metaschema cycle at '${effective}'`);
631
+ }
632
+ this.assembling.add(effective);
633
+ try {
634
+ const metaBase = await this.loadResource(effective);
635
+ const meta =
636
+ metaBase === undefined ? undefined : this.schemas.document(metaBase);
637
+ if (meta === undefined) {
638
+ throw new UnknownDialectError(
639
+ `dialect '${effective}' is not registered and no loader provides its metaschema`,
640
+ );
641
+ }
642
+ this.assembleDialect(effective, meta);
643
+ } finally {
644
+ this.assembling.delete(effective);
645
+ }
646
+ }
647
+
648
+ private assembleDialect(uri: string, meta: JsonValue): void {
649
+ const declared = isObject(meta) ? meta.$vocabulary : undefined;
650
+ if (!isObject(declared)) {
651
+ // The spec leaves $vocabulary-less metaschemas open; the least-surprise
652
+ // reading is the default dialect's vocabulary set and syntax options.
653
+ const base = this.dialects.getDialect(this.defaultDialect);
654
+ this.dialects.registerDialect(uri, base.vocabularyUris, {
655
+ allowUnknownKeywords: base.allowUnknownKeywords,
656
+ identifiers: base.identifiers,
657
+ refIgnoresSiblings: base.refIgnoresSiblings,
658
+ });
659
+ return;
660
+ }
661
+ const uris: string[] = [];
662
+ for (const [vocabUri, required] of Object.entries(declared)) {
663
+ if (this.dialects.hasVocabulary(vocabUri)) {
664
+ uris.push(vocabUri);
665
+ } else if (required === true) {
666
+ throw new UnknownVocabularyError(
667
+ `dialect '${uri}' requires unknown vocabulary '${vocabUri}'`,
668
+ );
669
+ }
670
+ // Unknown optional vocabularies are skipped; their keywords fall to
671
+ // unknown-keyword annotation handling (spec MUST for false).
672
+ }
673
+ // Identifier syntax travels with the core vocabulary (D18): a dialect
674
+ // assembled around the 2019-09 core gets 2019-09 identifier handling.
675
+ this.dialects.registerDialect(uri, uris, {
676
+ identifiers: uris.includes(VOCAB_CORE_2019)
677
+ ? identifiers2019
678
+ : identifiers2020,
679
+ });
680
+ }
681
+
682
+ private maybeValidate(baseUri: string): void {
683
+ if (!this.validateSchemas) return;
684
+ const dialectUri = this.schemas.dialectUriFor(baseUri);
685
+ if (!this.schemas.has(dialectUri)) return; // metaschema document unavailable
686
+ const doc = this.schemas.document(baseUri)!;
687
+ const result = this.evaluate(dialectUri, doc, { output: "list" });
688
+ if (!result.valid) {
689
+ throw new SchemaValidationError(
690
+ `schema '${baseUri}' fails its metaschema '${dialectUri}'`,
691
+ result.errors ?? [],
692
+ );
693
+ }
694
+ }
695
+ }
696
+
697
+ /** Creates a new {@link Engine}. */
698
+ export const createEngine = (options?: EngineOptions): Engine =>
699
+ new Engine(options);