@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
@@ -0,0 +1,676 @@
1
+ // 2019-09 dialect (DESIGN.md M4-A): assembled from its own vocabulary URIs,
2
+ // reusing 2020-12 behavior objects wherever spec semantics are identical
3
+ // (validation is byte-for-byte the same set; most applicators too) and
4
+ // registering new behaviors only where 2019-09 diverges — no `$dynamicRef`/
5
+ // `$dynamicAnchor` (2019-09 has `$recursiveRef`/`$recursiveAnchor` instead),
6
+ // tuple-form `items` folds what 2020-12 splits into `prefixItems`+`items`,
7
+ // and `additionalItems` (retired in 2020-12) fills the gap that leaves.
8
+
9
+ import { isObject } from "../json.js";
10
+ import {
11
+ DialectRegistry,
12
+ KeywordBehavior,
13
+ StaticFacts,
14
+ SubschemaApplication,
15
+ identifiers2019,
16
+ } from "../dialect.js";
17
+ import { childCursor } from "../cursor.js";
18
+ import { LowerExpr, LowerStmt, lowerIR } from "../lowering.js";
19
+ import {
20
+ coreVocabulary,
21
+ VOCAB_CORE_2019,
22
+ $recursiveRef,
23
+ $recursiveAnchor,
24
+ annotationOnly,
25
+ SELF,
26
+ } from "./core.js";
27
+ import {
28
+ allOf,
29
+ anyOf,
30
+ oneOf,
31
+ not,
32
+ ifKeyword,
33
+ conditionalBranch,
34
+ dependentSchemas,
35
+ properties,
36
+ patternProperties,
37
+ propertyNames,
38
+ additionalProperties,
39
+ contains,
40
+ } from "./applicator.js";
41
+ import { validationVocabulary } from "./validation.js";
42
+
43
+ /** 2019-09 core vocabulary URI. */
44
+ export const VOCAB_CORE_2019_09 = VOCAB_CORE_2019;
45
+ /** 2019-09 applicator vocabulary URI. */
46
+ export const VOCAB_APPLICATOR_2019 =
47
+ "https://json-schema.org/draft/2019-09/vocab/applicator";
48
+ /** 2019-09 validation vocabulary URI. */
49
+ export const VOCAB_VALIDATION_2019 =
50
+ "https://json-schema.org/draft/2019-09/vocab/validation";
51
+ /** 2019-09 meta-data vocabulary URI. */
52
+ export const VOCAB_META_DATA_2019 =
53
+ "https://json-schema.org/draft/2019-09/vocab/meta-data";
54
+ /** 2019-09 format vocabulary URI. */
55
+ export const VOCAB_FORMAT_2019 =
56
+ "https://json-schema.org/draft/2019-09/vocab/format";
57
+ /** 2019-09 content vocabulary URI. */
58
+ export const VOCAB_CONTENT_2019 =
59
+ "https://json-schema.org/draft/2019-09/vocab/content";
60
+
61
+ /** 2019-09 dialect URI. */
62
+ export const DIALECT_2019_09 = "https://json-schema.org/draft/2019-09/schema";
63
+
64
+ const id = (name: string): string => `${VOCAB_APPLICATOR_2019}#${name}`;
65
+
66
+ // 2019-09 core: same keyword set as 2020-12's core minus $dynamicRef/
67
+ // $dynamicAnchor, plus $recursiveRef/$recursiveAnchor. $id/$schema/$anchor/
68
+ // $vocabulary/$comment/$defs behaviors are draft-agnostic (structural or
69
+ // value-is-the-position), so they're shared as-is — the milestone contract
70
+ // allows a shared behavior's id to stay a 2020-12 URI.
71
+ const core2019Vocabulary: Record<string, KeywordBehavior> = {
72
+ $ref: coreVocabulary.$ref!,
73
+ $defs: coreVocabulary.$defs!,
74
+ $id: coreVocabulary.$id!,
75
+ $schema: coreVocabulary.$schema!,
76
+ $anchor: coreVocabulary.$anchor!,
77
+ $vocabulary: coreVocabulary.$vocabulary!,
78
+ $comment: coreVocabulary.$comment!,
79
+ $recursiveRef,
80
+ $recursiveAnchor,
81
+ };
82
+
83
+ /**
84
+ * `items` (2019-09 form): a schema value applies to every element (produces
85
+ * `true`, matching 2020-12 `items`' post-`prefixItems` annotation shape); an
86
+ * array value (tuple) applies element-wise to the first N and produces the
87
+ * largest index reached, or `true` when it covers the whole array — this is
88
+ * the pre-2020-12 keyword that `prefixItems`/`items` later split in two, so
89
+ * its annotation shape mirrors 2020-12 `prefixItems`'.
90
+ */
91
+ export const items2019: KeywordBehavior = {
92
+ id: id("items"),
93
+ // evaluatesIndexes mirrors prefixItems (array form: a fixed-count prefix)
94
+ // and items (schema form: every index from 0 — 2019-09 folds prefixItems
95
+ // into this same keyword, so there's no separate sibling to start after)
96
+ // — the coverage facts unevaluatedItems2019 needs for static licensing.
97
+ analyze: (value): StaticFacts =>
98
+ Array.isArray(value)
99
+ ? {
100
+ subschemas: value.map((_, i) => [i]),
101
+ applications: value.map((_, i): SubschemaApplication => ({
102
+ path: [i],
103
+ mode: "childByIndex",
104
+ conditional: false,
105
+ asserts: true,
106
+ })),
107
+ produces: [id("items")],
108
+ evaluatesIndexes: { kind: "prefix", count: value.length },
109
+ }
110
+ : {
111
+ ...SELF,
112
+ applications: [
113
+ { path: [], mode: "childSweep", conditional: false, asserts: true },
114
+ ],
115
+ produces: [id("items")],
116
+ evaluatesIndexes: { kind: "allFrom", start: 0 },
117
+ },
118
+ // Two forms, same as evaluate(): tuple (per-index, guarded by the array's
119
+ // length like prefixItems' compiled form) or schema (every element from
120
+ // index 0, like `items`' compiled form but with no sibling prefixItems to
121
+ // start after — 2019-09 has no such keyword). Each form's annotation
122
+ // mirrors evaluate(): tuple produces the largest applied index (or true
123
+ // when it covered the array), schema produces true iff it applied to any
124
+ // element.
125
+ lower: (value, lctx) => {
126
+ if (Array.isArray(value)) {
127
+ value.forEach((_, i) => {
128
+ lctx.emit(
129
+ lowerIR.when(
130
+ lowerIR.and(
131
+ lowerIR.typeIs(lctx.instance, "array"),
132
+ lowerIR.cmp(
133
+ ">",
134
+ lowerIR.helper("lengthOf", lctx.instance),
135
+ lowerIR.constant(i),
136
+ ),
137
+ ),
138
+ [
139
+ {
140
+ kind: "apply",
141
+ apply: {
142
+ path: [i],
143
+ cursor: {
144
+ kind: "child",
145
+ of: { kind: "here" },
146
+ segment: i,
147
+ },
148
+ fold: "allMustPass",
149
+ },
150
+ },
151
+ ],
152
+ ),
153
+ );
154
+ });
155
+ lctx.emit({
156
+ kind: "produce",
157
+ value: { kind: "collectedIndexes", render: "largestOrTrue" },
158
+ });
159
+ return;
160
+ }
161
+ const b = lctx.binding();
162
+ lctx.emit(
163
+ lowerIR.when(lowerIR.typeIs(lctx.instance, "array"), [
164
+ {
165
+ kind: "forEachIndex",
166
+ target: lctx.instance,
167
+ binding: b,
168
+ start: 0,
169
+ body: [
170
+ {
171
+ kind: "apply",
172
+ apply: {
173
+ path: [],
174
+ cursor: {
175
+ kind: "child",
176
+ of: { kind: "here" },
177
+ segment: { kind: "binding", id: b },
178
+ },
179
+ fold: "allMustPass",
180
+ },
181
+ },
182
+ ],
183
+ },
184
+ ]),
185
+ );
186
+ lctx.emit({
187
+ kind: "produce",
188
+ value: { kind: "collectedIndexes", render: "appliedTrue" },
189
+ });
190
+ },
191
+ evaluate: (value, cursor, ctx) => {
192
+ if (!Array.isArray(cursor.value)) return true;
193
+ if (Array.isArray(value)) {
194
+ const n = Math.min(value.length, cursor.value.length);
195
+ let ok = true;
196
+ for (let i = 0; i < n; i++) {
197
+ if (!ctx.apply(["items", i], childCursor(cursor, i, cursor.value[i]!)))
198
+ ok = false;
199
+ }
200
+ // Dependency data only from an accepting keyword (Appendix D; see
201
+ // applicator.ts properties) — the same rule for every producer here.
202
+ if (n > 0 && ok) ctx.produce(n === cursor.value.length ? true : n - 1);
203
+ return ok;
204
+ }
205
+ let ok = true;
206
+ let applied = false;
207
+ for (let i = 0; i < cursor.value.length; i++) {
208
+ applied = true;
209
+ if (!ctx.apply(["items"], childCursor(cursor, i, cursor.value[i]!)))
210
+ ok = false;
211
+ }
212
+ if (applied && ok) ctx.produce(true);
213
+ return ok;
214
+ },
215
+ };
216
+
217
+ /**
218
+ * `additionalItems`: applies only when the sibling `items` is present *and*
219
+ * an array (tuple form) — a schema-form `items` already covers every
220
+ * element, so `additionalItems` "does nothing" against it (suite: "when
221
+ * items is schema, additionalItems does nothing"). Sibling-read, same
222
+ * pattern as `if`/`then`/`else` and `contains`' `minContains`/`maxContains`
223
+ * siblings.
224
+ */
225
+ export const additionalItems: KeywordBehavior = {
226
+ id: id("additionalItems"),
227
+ // Sibling read at plan time (AnalyzeContext.schema), same as `if`
228
+ // declaring applications for `then`/`else`: no edge at all when `items`
229
+ // isn't an array, so the planner never visits a target this keyword can
230
+ // never reach.
231
+ analyze: (_value, context): StaticFacts => {
232
+ const siblingItems = context?.schema.items;
233
+ if (!Array.isArray(siblingItems)) {
234
+ return { ...SELF, produces: [id("additionalItems")] };
235
+ }
236
+ return {
237
+ ...SELF,
238
+ applications: [
239
+ { path: [], mode: "childSweep", conditional: false, asserts: true },
240
+ ],
241
+ produces: [id("additionalItems")],
242
+ evaluatesIndexes: { kind: "allFrom", start: siblingItems.length },
243
+ };
244
+ },
245
+ // Sibling `items` is plan-time data (lctx.schema.items): when it isn't an
246
+ // array, this keyword contributes nothing at all, so the lowering emits
247
+ // no statements — same static-read pattern as `if`/`then`/`else`.
248
+ lower: (_value, lctx) => {
249
+ const siblingItems = lctx.schema.items;
250
+ if (!Array.isArray(siblingItems)) return;
251
+ const start = siblingItems.length;
252
+ const b = lctx.binding();
253
+ lctx.emit(
254
+ lowerIR.when(lowerIR.typeIs(lctx.instance, "array"), [
255
+ {
256
+ kind: "forEachIndex",
257
+ target: lctx.instance,
258
+ binding: b,
259
+ start,
260
+ body: [
261
+ {
262
+ kind: "apply",
263
+ apply: {
264
+ path: [],
265
+ cursor: {
266
+ kind: "child",
267
+ of: { kind: "here" },
268
+ segment: { kind: "binding", id: b },
269
+ },
270
+ fold: "allMustPass",
271
+ },
272
+ },
273
+ ],
274
+ },
275
+ ]),
276
+ );
277
+ // Annotation: true iff it applied to any element past the tuple prefix.
278
+ lctx.emit({
279
+ kind: "produce",
280
+ value: { kind: "collectedIndexes", render: "appliedTrue" },
281
+ });
282
+ },
283
+ evaluate: (_value, cursor, ctx) => {
284
+ if (!Array.isArray(cursor.value)) return true;
285
+ const siblingItems = ctx.schema.items;
286
+ if (!Array.isArray(siblingItems)) return true;
287
+ let ok = true;
288
+ let applied = false;
289
+ for (let i = siblingItems.length; i < cursor.value.length; i++) {
290
+ applied = true;
291
+ if (
292
+ !ctx.apply(
293
+ ["additionalItems"],
294
+ childCursor(cursor, i, cursor.value[i]!),
295
+ )
296
+ )
297
+ ok = false;
298
+ }
299
+ // Boolean data: additionalItems has no index-range shape of its own,
300
+ // only whether it applied to any element.
301
+ if (applied && ok) ctx.produce(true);
302
+ return ok;
303
+ },
304
+ };
305
+
306
+ const applicator2019Vocabulary: Record<string, KeywordBehavior> = {
307
+ allOf,
308
+ anyOf,
309
+ oneOf,
310
+ not,
311
+ if: ifKeyword,
312
+ then: conditionalBranch(id("then"), "then", true),
313
+ else: conditionalBranch(id("else"), "else", false),
314
+ dependentSchemas,
315
+ properties,
316
+ patternProperties,
317
+ propertyNames,
318
+ additionalProperties,
319
+ items: items2019,
320
+ additionalItems,
321
+ contains,
322
+ };
323
+
324
+ // unevaluatedItems/unevaluatedProperties (2019-09): same consumer pattern as
325
+ // unevaluated.ts, wired to this dialect's producer ids — items2019 folds
326
+ // prefixItems+items into one id, and additionalItems is a genuinely new
327
+ // producer 2020-12 doesn't have. Spec-wise these two live in the applicator
328
+ // vocabulary (2019-09 has no separate unevaluated vocabulary yet).
329
+ const unevaluatedItems2019: KeywordBehavior = {
330
+ id: `${VOCAB_APPLICATOR_2019}#unevaluatedItems`,
331
+ phase: 1,
332
+ analyze: (): StaticFacts => ({
333
+ subschemas: SELF.subschemas,
334
+ consumes: [
335
+ items2019.id,
336
+ additionalItems.id,
337
+ contains.id,
338
+ `${VOCAB_APPLICATOR_2019}#unevaluatedItems`,
339
+ ],
340
+ produces: [`${VOCAB_APPLICATOR_2019}#unevaluatedItems`],
341
+ // Self-covering (D9a): once this keyword runs, everything is evaluated —
342
+ // needed both for the planner's own edge (childSweep to its own
343
+ // position) and so an outer scope's coverage computation can fold this
344
+ // node's post-success contribution.
345
+ evaluatesIndexes: { kind: "all" },
346
+ applications: [
347
+ { path: [], mode: "childSweep", conditional: false, asserts: true },
348
+ ],
349
+ }),
350
+ // Static-coverage path only (D9a), same discipline as 2020-12
351
+ // unevaluatedItems (unevaluated.ts): a sibling `contains` always declares
352
+ // evaluatesIndexes: dynamic (applicator.ts), so the planner classifies
353
+ // this node interpreted whenever `contains` is present — lower() is never
354
+ // called with a null/incomplete coverage, and never needs the
355
+ // per-index coveredIdx set evaluate() tracks (that set is populated only
356
+ // from a dynamic `contains` match list, which forces interpretation).
357
+ lower: (_value, lctx) => {
358
+ // Runtime-coverage path (phase B activates it): same shape as 2020-12
359
+ // unevaluatedItems — fold the channel into a coveredPrefix/coveredIdx
360
+ // summary over this array's length and sweep the uncovered indexes.
361
+ if (lctx.runtimeCoverage()) {
362
+ const f = lctx.binding();
363
+ const b = lctx.binding();
364
+ lctx.emit(
365
+ lowerIR.when(lowerIR.typeIs(lctx.instance, "array"), [
366
+ { kind: "coverageFold", half: "indexes", binding: f },
367
+ {
368
+ kind: "forEachIndex",
369
+ target: lctx.instance,
370
+ binding: b,
371
+ start: 0,
372
+ body: [
373
+ lowerIR.when(
374
+ lowerIR.not({
375
+ kind: "coverageCovers",
376
+ fold: f,
377
+ target: { kind: "binding", id: b },
378
+ }),
379
+ [
380
+ {
381
+ kind: "apply",
382
+ apply: {
383
+ path: [],
384
+ cursor: {
385
+ kind: "child",
386
+ of: { kind: "here" },
387
+ segment: { kind: "binding", id: b },
388
+ },
389
+ fold: "allMustPass",
390
+ },
391
+ },
392
+ ],
393
+ ),
394
+ ],
395
+ },
396
+ {
397
+ kind: "produce",
398
+ value: { kind: "collectedIndexes", render: "appliedTrue" },
399
+ },
400
+ ]),
401
+ );
402
+ return;
403
+ }
404
+ const coverage = lctx.staticCoverage();
405
+ if (coverage === null) {
406
+ throw new Error(
407
+ "unevaluatedItems2019 lowering requires static coverage (planner bug)",
408
+ );
409
+ }
410
+ if (coverage.coversAllIndexes) return; // statically vacuous
411
+ const b = lctx.binding();
412
+ lctx.emit(
413
+ lowerIR.when(lowerIR.typeIs(lctx.instance, "array"), [
414
+ {
415
+ kind: "forEachIndex",
416
+ target: lctx.instance,
417
+ binding: b,
418
+ start: coverage.prefixCount,
419
+ body: [
420
+ {
421
+ kind: "apply",
422
+ apply: {
423
+ path: [],
424
+ cursor: {
425
+ kind: "child",
426
+ of: { kind: "here" },
427
+ segment: { kind: "binding", id: b },
428
+ },
429
+ fold: "allMustPass",
430
+ },
431
+ },
432
+ ],
433
+ },
434
+ ]),
435
+ );
436
+ // Annotation: true iff it applied to any unevaluated index.
437
+ lctx.emit({
438
+ kind: "produce",
439
+ value: { kind: "collectedIndexes", render: "appliedTrue" },
440
+ });
441
+ },
442
+ evaluate: (_value, cursor, ctx) => {
443
+ if (!Array.isArray(cursor.value)) return true;
444
+ const length = cursor.value.length;
445
+ let coveredPrefix = 0;
446
+ const coveredIdx = new Set<number>();
447
+ for (const p of ctx.visible([
448
+ items2019.id,
449
+ additionalItems.id,
450
+ contains.id,
451
+ unevaluatedItems2019.id,
452
+ ])) {
453
+ if (p.behaviorId === contains.id) {
454
+ if (p.data === true) coveredPrefix = length;
455
+ else for (const i of p.data as number[]) coveredIdx.add(i);
456
+ } else if (p.data === true) {
457
+ coveredPrefix = length;
458
+ } else if (p.behaviorId === items2019.id) {
459
+ coveredPrefix = Math.max(coveredPrefix, (p.data as number) + 1);
460
+ }
461
+ }
462
+ let ok = true;
463
+ let applied = false;
464
+ for (let i = coveredPrefix; i < length; i++) {
465
+ if (coveredIdx.has(i)) continue;
466
+ applied = true;
467
+ if (
468
+ !ctx.apply(
469
+ ["unevaluatedItems"],
470
+ childCursor(cursor, i, cursor.value[i]!),
471
+ )
472
+ )
473
+ ok = false;
474
+ }
475
+ if (applied && ok) ctx.produce(true);
476
+ return ok;
477
+ },
478
+ };
479
+
480
+ const unevaluatedProperties2019: KeywordBehavior = {
481
+ id: `${VOCAB_APPLICATOR_2019}#unevaluatedProperties`,
482
+ phase: 1,
483
+ analyze: (): StaticFacts => ({
484
+ subschemas: SELF.subschemas,
485
+ consumes: [
486
+ properties.id,
487
+ patternProperties.id,
488
+ additionalProperties.id,
489
+ `${VOCAB_APPLICATOR_2019}#unevaluatedProperties`,
490
+ ],
491
+ produces: [`${VOCAB_APPLICATOR_2019}#unevaluatedProperties`],
492
+ evaluatesNames: { kind: "all" },
493
+ applications: [
494
+ { path: [], mode: "childSweep", conditional: false, asserts: true },
495
+ ],
496
+ }),
497
+ // Static-coverage path only (D9a) — same discipline as 2020-12
498
+ // unevaluatedProperties (unevaluated.ts). `properties`/`patternProperties`/
499
+ // `additionalProperties` here are the shared 2020-12 behaviors
500
+ // (applicator.ts), so their evaluatesNames facts are identical.
501
+ lower: (_value, lctx) => {
502
+ // Runtime-coverage path (phase B activates it): same shape as 2020-12
503
+ // unevaluatedProperties — fold the channel into an evaluated-name set and
504
+ // sweep the names it does not cover.
505
+ if (lctx.runtimeCoverage()) {
506
+ const f = lctx.binding();
507
+ const b = lctx.binding();
508
+ lctx.emit(
509
+ lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), [
510
+ { kind: "coverageFold", half: "names", binding: f },
511
+ {
512
+ kind: "forEachKey",
513
+ target: lctx.instance,
514
+ binding: b,
515
+ body: [
516
+ lowerIR.when(
517
+ lowerIR.not({
518
+ kind: "coverageCovers",
519
+ fold: f,
520
+ target: { kind: "binding", id: b },
521
+ }),
522
+ [
523
+ {
524
+ kind: "apply",
525
+ apply: {
526
+ path: [],
527
+ cursor: {
528
+ kind: "child",
529
+ of: { kind: "here" },
530
+ segment: { kind: "binding", id: b },
531
+ },
532
+ fold: "allMustPass",
533
+ },
534
+ },
535
+ ],
536
+ ),
537
+ ],
538
+ },
539
+ { kind: "produce", value: { kind: "collectedNames" } },
540
+ ]),
541
+ );
542
+ return;
543
+ }
544
+ const coverage = lctx.staticCoverage();
545
+ if (coverage === null) {
546
+ throw new Error(
547
+ "unevaluatedProperties2019 lowering requires static coverage (planner bug)",
548
+ );
549
+ }
550
+ // The sweep is statically vacuous when sibling coverage is total, but the
551
+ // produce is not: evaluate() emits an (empty) names annotation for every
552
+ // object regardless. So the object-type guard always wraps a produce; the
553
+ // sweep is added only when some name can still be unevaluated.
554
+ const body: LowerStmt[] = [];
555
+ if (!coverage.coversAllNames) {
556
+ const b = lctx.binding();
557
+ const covered: LowerExpr[] = [
558
+ ...coverage.names.map((n): LowerExpr =>
559
+ lowerIR.cmp("===", { kind: "binding", id: b }, lowerIR.constant(n)),
560
+ ),
561
+ ...coverage.patterns.map((p): LowerExpr =>
562
+ lowerIR.regexTest(p, { kind: "binding", id: b }),
563
+ ),
564
+ ];
565
+ body.push({
566
+ kind: "forEachKey",
567
+ target: lctx.instance,
568
+ binding: b,
569
+ body: [
570
+ lowerIR.when(
571
+ covered.length === 0
572
+ ? lowerIR.constant(true)
573
+ : lowerIR.not(lowerIR.or(...covered)),
574
+ [
575
+ {
576
+ kind: "apply",
577
+ apply: {
578
+ path: [],
579
+ cursor: {
580
+ kind: "child",
581
+ of: { kind: "here" },
582
+ segment: { kind: "binding", id: b },
583
+ },
584
+ fold: "allMustPass",
585
+ },
586
+ },
587
+ ],
588
+ ),
589
+ ],
590
+ });
591
+ }
592
+ body.push({ kind: "produce", value: { kind: "collectedNames" } });
593
+ // Produce iff the instance is an object (nothing otherwise), matching
594
+ // evaluate()'s object-type guard before ctx.produce.
595
+ lctx.emit(lowerIR.when(lowerIR.typeIs(lctx.instance, "object"), body));
596
+ },
597
+ evaluate: (_value, cursor, ctx) => {
598
+ if (!isObject(cursor.value)) return true;
599
+ const seen = new Set<string>();
600
+ for (const p of ctx.visible([
601
+ properties.id,
602
+ patternProperties.id,
603
+ additionalProperties.id,
604
+ unevaluatedProperties2019.id,
605
+ ])) {
606
+ for (const name of p.data as string[]) seen.add(name);
607
+ }
608
+ let ok = true;
609
+ const matched: string[] = [];
610
+ for (const name of Object.keys(cursor.value)) {
611
+ if (seen.has(name)) continue;
612
+ matched.push(name);
613
+ if (
614
+ !ctx.apply(
615
+ ["unevaluatedProperties"],
616
+ childCursor(cursor, name, cursor.value[name]!),
617
+ )
618
+ )
619
+ ok = false;
620
+ }
621
+ if (ok) ctx.produce(matched);
622
+ return ok;
623
+ },
624
+ };
625
+
626
+ Object.assign(applicator2019Vocabulary, {
627
+ unevaluatedItems: unevaluatedItems2019,
628
+ unevaluatedProperties: unevaluatedProperties2019,
629
+ });
630
+
631
+ const metaDataVocabulary2019 = Object.fromEntries(
632
+ [
633
+ "title",
634
+ "description",
635
+ "default",
636
+ "deprecated",
637
+ "readOnly",
638
+ "writeOnly",
639
+ "examples",
640
+ ].map((name) => [name, annotationOnly(`${VOCAB_META_DATA_2019}#${name}`)]),
641
+ );
642
+
643
+ const contentVocabulary2019 = Object.fromEntries(
644
+ ["contentMediaType", "contentEncoding", "contentSchema"].map((name) => [
645
+ name,
646
+ annotationOnly(`${VOCAB_CONTENT_2019}#${name}`),
647
+ ]),
648
+ );
649
+
650
+ /** Registers the 2019-09 vocabularies and dialect. */
651
+ export function registerDialect2019(
652
+ registry: DialectRegistry,
653
+ format: (id: string) => KeywordBehavior = annotationOnly,
654
+ ): void {
655
+ registry.registerVocabulary(VOCAB_CORE_2019_09, core2019Vocabulary);
656
+ registry.registerVocabulary(VOCAB_APPLICATOR_2019, applicator2019Vocabulary);
657
+ registry.registerVocabulary(VOCAB_VALIDATION_2019, validationVocabulary);
658
+ registry.registerVocabulary(VOCAB_META_DATA_2019, metaDataVocabulary2019);
659
+ registry.registerVocabulary(VOCAB_FORMAT_2019, {
660
+ format: format(`${VOCAB_FORMAT_2019}#format`),
661
+ });
662
+ registry.registerVocabulary(VOCAB_CONTENT_2019, contentVocabulary2019);
663
+
664
+ registry.registerDialect(
665
+ DIALECT_2019_09,
666
+ [
667
+ VOCAB_CORE_2019_09,
668
+ VOCAB_APPLICATOR_2019,
669
+ VOCAB_VALIDATION_2019,
670
+ VOCAB_META_DATA_2019,
671
+ VOCAB_FORMAT_2019,
672
+ VOCAB_CONTENT_2019,
673
+ ],
674
+ { identifiers: identifiers2019 },
675
+ );
676
+ }