@texaryn/schema-json 0.7.0 → 0.8.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 (61) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +33 -1
  3. package/THIRD-PARTY-NOTICES.md +7 -0
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +479 -24
  6. package/dist/adapter.js.map +1 -1
  7. package/dist/bare-maps.d.ts +3 -0
  8. package/dist/bare-maps.d.ts.map +1 -0
  9. package/dist/bare-maps.js +44 -0
  10. package/dist/bare-maps.js.map +1 -0
  11. package/dist/identity.d.ts +11 -2
  12. package/dist/identity.d.ts.map +1 -1
  13. package/dist/identity.js +103 -22
  14. package/dist/identity.js.map +1 -1
  15. package/dist/index.d.ts +4 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/instance-pointer.d.ts +9 -0
  20. package/dist/instance-pointer.d.ts.map +1 -0
  21. package/dist/instance-pointer.js +115 -0
  22. package/dist/instance-pointer.js.map +1 -0
  23. package/dist/local-pointer-aliases.d.ts +4 -0
  24. package/dist/local-pointer-aliases.d.ts.map +1 -0
  25. package/dist/local-pointer-aliases.js +345 -0
  26. package/dist/local-pointer-aliases.js.map +1 -0
  27. package/dist/normalize.d.ts +1 -1
  28. package/dist/normalize.d.ts.map +1 -1
  29. package/dist/normalize.js +3 -1
  30. package/dist/normalize.js.map +1 -1
  31. package/dist/projection-shape.d.ts +25 -0
  32. package/dist/projection-shape.d.ts.map +1 -0
  33. package/dist/projection-shape.js +74 -0
  34. package/dist/projection-shape.js.map +1 -0
  35. package/dist/projection-validation-coherence.d.ts +11 -0
  36. package/dist/projection-validation-coherence.d.ts.map +1 -0
  37. package/dist/projection-validation-coherence.js +213 -0
  38. package/dist/projection-validation-coherence.js.map +1 -0
  39. package/dist/projection.d.ts +2 -2
  40. package/dist/projection.d.ts.map +1 -1
  41. package/dist/projection.js +922 -243
  42. package/dist/projection.js.map +1 -1
  43. package/dist/resources.d.ts +19 -0
  44. package/dist/resources.d.ts.map +1 -0
  45. package/dist/resources.js +730 -0
  46. package/dist/resources.js.map +1 -0
  47. package/dist/root-reference.js +1 -1
  48. package/dist/schema-graph.d.ts +7 -0
  49. package/dist/schema-graph.d.ts.map +1 -1
  50. package/dist/schema-graph.js +79 -20
  51. package/dist/schema-graph.js.map +1 -1
  52. package/dist/submission-projection.d.ts +5 -0
  53. package/dist/submission-projection.d.ts.map +1 -0
  54. package/dist/submission-projection.js +233 -0
  55. package/dist/submission-projection.js.map +1 -0
  56. package/dist/types.d.ts +6 -1
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/vendor/json-schema-library/LICENSE.md +9 -0
  59. package/dist/vendor/json-schema-library/index.mjs +2 -0
  60. package/dist/vendor/json-schema-library/index.mjs.map +1 -0
  61. package/package.json +9 -4
@@ -1,5 +1,6 @@
1
- import { mergeNode, isSchemaNode } from 'json-schema-library';
2
- import { childDeclaring, followRef, itemDeclaring, locationInfo, onlyPoints, positionOf, } from './identity.js';
1
+ import { mergeNode, isSchemaNode } from './vendor/json-schema-library/index.mjs';
2
+ import { authoredSchema, childDeclaring, extendDeclaring, followRef, itemDeclaring, locationInfo, onlyPoints, positionOf, referenceKeywords, referenceOf, } from './identity.js';
3
+ import { inferProjectionShape, shapeDiagnostic } from './projection-shape.js';
3
4
  import { POSITION } from './schema-graph.js';
4
5
  const VALID_TYPES = new Set([
5
6
  'string',
@@ -10,6 +11,18 @@ const VALID_TYPES = new Set([
10
11
  'array',
11
12
  'null',
12
13
  ]);
14
+ const NON_ASSERTION_REFERENCE_SIBLINGS = new Set([
15
+ '$id',
16
+ '$anchor',
17
+ '$dynamicAnchor',
18
+ '$recursiveAnchor',
19
+ '$schema',
20
+ '$vocabulary',
21
+ '$comment',
22
+ '$defs',
23
+ 'definitions',
24
+ POSITION,
25
+ ]);
13
26
  function toPointer(value) {
14
27
  return value;
15
28
  }
@@ -27,64 +40,6 @@ function resolveExplicitType(schema) {
27
40
  }
28
41
  return undefined;
29
42
  }
30
- /**
31
- * Keywords that apply to exactly one JSON type, grouped by that type.
32
- *
33
- * Two different jobs are done with these, and only one of them infers
34
- * anything. Every family takes part in detecting a conflict, because a schema
35
- * drawing keywords from two families implies no single shape. Only `object`
36
- * and `array` are shapes a form can be given, so those are the only two ever
37
- * inferred: `string` and `number` are here to be noticed, not chosen.
38
- *
39
- * Inferring a scalar would need a rule that is right rather than symmetrical,
40
- * and there is not one. `minimum` cannot tell `number` from `integer`, and a
41
- * wrong scalar guess selects the wrong widget, which is harder to notice than
42
- * a field that never appeared at all.
43
- *
44
- * `format` is deliberately absent: it annotates a string's contents rather
45
- * than describing structure, and schemas apply it to non-strings in practice.
46
- */
47
- const KEYWORD_FAMILIES = {
48
- object: [
49
- 'properties',
50
- 'patternProperties',
51
- 'additionalProperties',
52
- 'propertyNames',
53
- 'required',
54
- 'minProperties',
55
- 'maxProperties',
56
- 'dependentSchemas',
57
- 'dependentRequired',
58
- // json-schema-library normalises draft-07 `dependencies` into the two
59
- // above at parse time. Listed anyway, so the rule does not rely on that.
60
- 'dependencies',
61
- 'unevaluatedProperties',
62
- ],
63
- array: [
64
- 'items',
65
- 'prefixItems',
66
- 'additionalItems',
67
- 'contains',
68
- 'minItems',
69
- 'maxItems',
70
- 'uniqueItems',
71
- 'minContains',
72
- 'maxContains',
73
- 'unevaluatedItems',
74
- ],
75
- string: ['minLength', 'maxLength', 'pattern'],
76
- number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
77
- };
78
- /** Which families a schema draws keywords from, computed over all of them at once. */
79
- function projectionTypeFamilies(schema) {
80
- const families = new Set();
81
- for (const [family, keywords] of Object.entries(KEYWORD_FAMILIES)) {
82
- if (keywords.some((keyword) => schema[keyword] !== undefined)) {
83
- families.add(family);
84
- }
85
- }
86
- return families;
87
- }
88
43
  /**
89
44
  * Whether any branch of this node's `oneOf`/`anyOf` could be given a shape at
90
45
  * all, for some value.
@@ -106,17 +61,6 @@ function compositionHasRenderableAlternative(node) {
106
61
  return inferProjectionShape(schema).kind === 'resolved';
107
62
  });
108
63
  }
109
- function inferProjectionShape(schema) {
110
- const families = projectionTypeFamilies(schema);
111
- if (families.size > 1)
112
- return { kind: 'ambiguous', families: [...families].sort() };
113
- if (families.size === 1) {
114
- const [family] = families;
115
- if (family === 'object' || family === 'array')
116
- return { kind: 'resolved', type: family };
117
- }
118
- return { kind: 'none' };
119
- }
120
64
  function extractConstraints(schema) {
121
65
  const constraints = {};
122
66
  if (typeof schema.minLength === 'number')
@@ -152,8 +96,8 @@ function extractAnnotations(schema,
152
96
  * Set where two declarations apply to this location whatever the instance is
153
97
  * and disagree. The merged schema still carries one of them, and which one is
154
98
  * the merge's traversal order rather than an answer.
155
- */
156
- omitDefault = false) {
99
+ */
100
+ omitDefault = false, defaultOverride) {
157
101
  const annotations = {};
158
102
  if (typeof schema.title === 'string')
159
103
  annotations.title = schema.title;
@@ -167,8 +111,15 @@ omitDefault = false) {
167
111
  annotations.deprecated = schema.deprecated;
168
112
  if (Array.isArray(schema.examples))
169
113
  annotations.examples = schema.examples;
170
- if (!omitDefault && 'default' in schema)
171
- annotations.default = schema.default;
114
+ if (!omitDefault) {
115
+ if (defaultOverride) {
116
+ if (defaultOverride.present)
117
+ annotations.default = defaultOverride.value;
118
+ }
119
+ else if ('default' in schema) {
120
+ annotations.default = schema.default;
121
+ }
122
+ }
172
123
  return annotations;
173
124
  }
174
125
  function extractEnumValues(schema) {
@@ -177,30 +128,339 @@ function extractEnumValues(schema) {
177
128
  return schema.enum.map((value) => ({ value }));
178
129
  }
179
130
  function dereferenceChecked(node) {
180
- let current = node;
181
- const seen = new Set();
182
- while (typeof current.$ref === 'string') {
131
+ const completed = new Set();
132
+ const active = new Set();
133
+ const parts = [];
134
+ let cycle = false;
135
+ let unresolved = false;
136
+ const visit = (current, referringSite = false) => {
183
137
  const position = positionOf(current);
184
- if (seen.has(position))
185
- return { node: current, cycle: true };
186
- seen.add(position);
187
- const next = followRef(current);
188
- if (!next) {
189
- const raw = current.resolveRef();
190
- return { node: (raw ?? current), cycle: false };
138
+ if (active.has(position)) {
139
+ cycle = true;
140
+ return;
141
+ }
142
+ if (completed.has(position))
143
+ return;
144
+ const references = referenceKeywords(current);
145
+ if (references.length === 0) {
146
+ parts.push(current);
147
+ completed.add(position);
148
+ return;
149
+ }
150
+ if (!referringSite && current.getDraftVersion() !== 'draft-07' && !onlyPoints(current)) {
151
+ parts.push(current);
152
+ completed.add(position);
153
+ return;
154
+ }
155
+ active.add(position);
156
+ for (const keyword of references) {
157
+ const target = followRef(current, keyword);
158
+ if (target === undefined || !isSchemaNode(target)) {
159
+ unresolved = true;
160
+ }
161
+ else {
162
+ visit(target);
163
+ }
164
+ }
165
+ if (current.getDraftVersion() !== 'draft-07') {
166
+ const sibling = mergeNode(current, current, ...references);
167
+ const siblingSchema = sibling?.schema;
168
+ if (isSchemaNode(sibling) &&
169
+ Object.keys(siblingSchema ?? {}).some((keyword) => !NON_ASSERTION_REFERENCE_SIBLINGS.has(keyword))) {
170
+ parts.push(sibling);
171
+ }
172
+ }
173
+ active.delete(position);
174
+ completed.add(position);
175
+ };
176
+ visit(node, true);
177
+ if (cycle)
178
+ return { node, cycle: true, unresolved: false };
179
+ if (unresolved || parts.length === 0)
180
+ return { node, cycle: false, unresolved: true };
181
+ if (parts.length === 1)
182
+ return { node: parts[0], cycle: false, unresolved: false };
183
+ const position = JSON.stringify(parts.map(positionOf));
184
+ const type = referenceCompositionType(parts);
185
+ const oneOfSource = parts.find((part) => part.oneOf !== undefined);
186
+ const anyOfSource = parts.find((part) => part.anyOf !== undefined);
187
+ const projectionFields = {};
188
+ const projectionKeys = [
189
+ 'format',
190
+ 'minLength',
191
+ 'maxLength',
192
+ 'pattern',
193
+ 'minimum',
194
+ 'maximum',
195
+ 'exclusiveMinimum',
196
+ 'exclusiveMaximum',
197
+ 'multipleOf',
198
+ 'minItems',
199
+ 'maxItems',
200
+ 'uniqueItems',
201
+ 'enum',
202
+ 'title',
203
+ 'description',
204
+ 'readOnly',
205
+ 'writeOnly',
206
+ 'deprecated',
207
+ 'examples',
208
+ ];
209
+ for (const part of parts) {
210
+ const record = part.schema;
211
+ if (!record || typeof record !== 'object')
212
+ continue;
213
+ for (const keyword of projectionKeys) {
214
+ if (keyword in record)
215
+ projectionFields[keyword] = record[keyword];
191
216
  }
192
- current = next;
193
- if (!onlyPoints(current))
194
- break;
195
217
  }
196
- return { node: current, cycle: false };
218
+ const schema = {
219
+ ...projectionFields,
220
+ [POSITION]: `reference-composition:${position}`,
221
+ ...(type === undefined ? {} : { type }),
222
+ // Compile a harmless branch so json-schema-library installs the allOf
223
+ // reducer, then attach the already compiled components below. Recompiling
224
+ // their raw schemas here would resolve relative references in the first
225
+ // component's resource scope instead of each component's original scope.
226
+ allOf: [{}],
227
+ };
228
+ const first = parts[0];
229
+ const composed = first.compileSchema(schema, `${first.evaluationPath}/$ref`, `${first.schemaLocation}/reference-composition`);
230
+ composed.schema = {
231
+ ...composed.schema,
232
+ allOf: parts.map((part) => part.schema),
233
+ ...(oneOfSource?.oneOf ? { oneOf: oneOfSource.oneOf.map((branch) => branch.schema) } : {}),
234
+ ...(anyOfSource?.anyOf ? { anyOf: anyOfSource.anyOf.map((branch) => branch.schema) } : {}),
235
+ };
236
+ composed.allOf = [...parts];
237
+ if (oneOfSource?.oneOf)
238
+ composed.oneOf = [...oneOfSource.oneOf];
239
+ if (anyOfSource?.anyOf)
240
+ composed.anyOf = [...anyOfSource.anyOf];
241
+ composed.reduceNode = (data, options = {}) => {
242
+ let reduced;
243
+ let reductionFailed = false;
244
+ let reductionError;
245
+ for (const part of parts) {
246
+ const outcome = part.reduceNode(data, options);
247
+ if (!isSchemaNode(outcome.node)) {
248
+ reductionFailed = true;
249
+ reductionError ??= outcome.error;
250
+ continue;
251
+ }
252
+ const candidate = outcome.node;
253
+ reduced = reduced ? mergeNode(reduced, candidate) : candidate;
254
+ }
255
+ if (reductionFailed)
256
+ return { node: undefined, error: reductionError };
257
+ if (!reduced)
258
+ return { node: composed, error: undefined };
259
+ const reducedSchema = {
260
+ ...reduced.schema,
261
+ [POSITION]: `reference-composition:${position}`,
262
+ };
263
+ if (type !== undefined)
264
+ reducedSchema.type = type;
265
+ const reducedNode = { ...reduced, schema: reducedSchema, allOf: undefined };
266
+ if (type !== undefined)
267
+ reducedNode.type = type;
268
+ return { node: reducedNode, error: undefined };
269
+ };
270
+ return { node: composed, cycle: false, unresolved: false };
197
271
  }
198
272
  /** Follows a $ref to the node it points at; returns the node unchanged otherwise. */
199
273
  function dereference(node) {
200
274
  return dereferenceChecked(node).node;
201
275
  }
276
+ function visitReferenceDefaultSites(node, cache, visitor, dynamicReferenceProjection = false) {
277
+ if (cache.dialect === 'draft-07' || referenceOf(node) === undefined)
278
+ return;
279
+ const pending = [node];
280
+ const visited = new Set();
281
+ while (pending.length > 0) {
282
+ const current = pending.pop();
283
+ const position = positionOf(current);
284
+ if (visited.has(position))
285
+ continue;
286
+ visited.add(position);
287
+ const authored = authoredSchema(current, cache);
288
+ if (authored !== null &&
289
+ typeof authored === 'object' &&
290
+ 'default' in authored) {
291
+ visitor(current);
292
+ }
293
+ for (const keyword of referenceKeywords(current)) {
294
+ if (dynamicReferenceProjection && (keyword === '$dynamicRef' || keyword === '$recursiveRef'))
295
+ continue;
296
+ const target = followRef(current, keyword);
297
+ if (target)
298
+ pending.push(target);
299
+ }
300
+ }
301
+ }
302
+ function traversalKey(node) {
303
+ const parent = node.parent;
304
+ const isInPlaceBranch = parent !== undefined &&
305
+ ['if', 'then', 'else', 'contains'].some((keyword) => parent[keyword] === node || node.evaluationPath === `${parent.evaluationPath}/${keyword}`);
306
+ if (isInPlaceBranch) {
307
+ return positionOf(node);
308
+ }
309
+ const location = node.schemaLocation;
310
+ return typeof location === 'string' ? location : positionOf(node);
311
+ }
312
+ function allOfExplicitType(node, visited = new Set()) {
313
+ const position = positionOf(node);
314
+ if (visited.has(position))
315
+ return undefined;
316
+ visited.add(position);
317
+ for (const branch of node.allOf ?? []) {
318
+ const reference = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
319
+ if (reference?.unresolved || (reference && !isSchemaNode(reference.node)))
320
+ continue;
321
+ const target = reference?.node ?? branch;
322
+ const schema = target.schema;
323
+ if (schema && typeof schema === 'object') {
324
+ const type = resolveExplicitType(schema);
325
+ if (type)
326
+ return type;
327
+ }
328
+ const nested = allOfExplicitType(target, visited);
329
+ if (nested)
330
+ return nested;
331
+ }
332
+ return undefined;
333
+ }
334
+ function declaresObject(node, visited = new Set()) {
335
+ const resolved = dereference(node);
336
+ if (!isSchemaNode(resolved))
337
+ return false;
338
+ const position = positionOf(resolved);
339
+ if (visited.has(position))
340
+ return false;
341
+ visited.add(position);
342
+ const schema = resolved.schema;
343
+ if (!schema || typeof schema !== 'object')
344
+ return false;
345
+ const explicitType = resolveExplicitType(schema);
346
+ if (explicitType)
347
+ return explicitType === 'object';
348
+ const allOfType = allOfExplicitType(resolved);
349
+ if (allOfType)
350
+ return allOfType === 'object';
351
+ const shape = inferProjectionShape(schema);
352
+ if (shape.kind === 'resolved')
353
+ return shape.type === 'object';
354
+ return (resolved.allOf ?? []).some((branch) => declaresObject(branch, visited));
355
+ }
356
+ function declaresArray(node, visited = new Set()) {
357
+ const resolved = dereference(node);
358
+ if (!isSchemaNode(resolved))
359
+ return false;
360
+ const position = positionOf(resolved);
361
+ if (visited.has(position))
362
+ return false;
363
+ visited.add(position);
364
+ const schema = resolved.schema;
365
+ if (!schema || typeof schema !== 'object')
366
+ return false;
367
+ const explicitType = resolveExplicitType(schema);
368
+ if (explicitType)
369
+ return explicitType === 'array';
370
+ const allOfType = allOfExplicitType(resolved);
371
+ if (allOfType)
372
+ return allOfType === 'array';
373
+ const shape = inferProjectionShape(schema);
374
+ if (shape.kind === 'resolved')
375
+ return shape.type === 'array';
376
+ return (resolved.allOf ?? []).some((branch) => declaresArray(branch, visited));
377
+ }
378
+ function referenceCompositionType(parts) {
379
+ const explicit = new Set(parts
380
+ .map((part) => {
381
+ const schema = part.schema;
382
+ return (schema && typeof schema === 'object' ? resolveExplicitType(schema) : undefined) ?? allOfExplicitType(part);
383
+ })
384
+ .filter((type) => type !== undefined));
385
+ if (explicit.size === 1)
386
+ return [...explicit][0];
387
+ if (explicit.size > 1)
388
+ return undefined;
389
+ const object = parts.some((part) => declaresObject(part));
390
+ const array = parts.some((part) => declaresArray(part));
391
+ if (object === array)
392
+ return undefined;
393
+ return object ? 'object' : 'array';
394
+ }
395
+ function hasReferencedAllOf(node, visited = new Set()) {
396
+ const position = positionOf(node);
397
+ if (visited.has(position))
398
+ return false;
399
+ visited.add(position);
400
+ for (const branch of node.allOf ?? []) {
401
+ if (referenceOf(branch) !== undefined)
402
+ return true;
403
+ if (hasReferencedAllOf(branch, visited))
404
+ return true;
405
+ }
406
+ return false;
407
+ }
408
+ function hasUnresolvedAllOfReference(node, visited = new Set()) {
409
+ const position = positionOf(node);
410
+ if (visited.has(position))
411
+ return false;
412
+ visited.add(position);
413
+ for (const branch of node.allOf ?? []) {
414
+ const source = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
415
+ if (source?.unresolved || (source && !isSchemaNode(source.node)))
416
+ return true;
417
+ const target = source?.node ?? branch;
418
+ if (hasUnresolvedAllOfReference(target, visited))
419
+ return true;
420
+ }
421
+ return false;
422
+ }
423
+ function expandAllOfReferences(node, stack = new Set()) {
424
+ const position = positionOf(node);
425
+ if (stack.has(position))
426
+ return node;
427
+ const nextStack = new Set(stack).add(position);
428
+ const branches = [];
429
+ let changed = false;
430
+ for (const branch of node.allOf ?? []) {
431
+ if (referenceOf(branch) !== undefined) {
432
+ const { node: target, unresolved } = dereferenceChecked(branch);
433
+ if (unresolved || !isSchemaNode(target))
434
+ return undefined;
435
+ if (nextStack.has(positionOf(target))) {
436
+ branches.push(branch);
437
+ continue;
438
+ }
439
+ const expanded = expandAllOfReferences(target, nextStack);
440
+ if (!expanded)
441
+ return undefined;
442
+ branches.push(expanded);
443
+ changed = true;
444
+ continue;
445
+ }
446
+ const expanded = expandAllOfReferences(branch, nextStack);
447
+ if (!expanded)
448
+ return undefined;
449
+ branches.push(expanded);
450
+ changed ||= expanded !== branch;
451
+ }
452
+ return changed ? { ...node, allOf: branches } : node;
453
+ }
454
+ function reduceAllOfForProjection(node, data, scope = []) {
455
+ if (hasUnresolvedAllOfReference(node))
456
+ return undefined;
457
+ if (node.getDraftVersion() !== 'draft-07')
458
+ return node.reduceNode(data, { path: [...scope] }).node;
459
+ const expanded = expandAllOfReferences(node);
460
+ return expanded?.reduceNode(data, { path: [...scope] }).node;
461
+ }
202
462
  // A dialect whose reducer leaves $ref unresolved reduces a selected `{ $ref }` branch to `{}`.
203
- function resolveSelectedBranch(original, reduced, data) {
463
+ function resolveSelectedBranch(original, reduced, data, scope = [], branchApplicability) {
204
464
  const reducedSchema = reduced.schema;
205
465
  if (reducedSchema && typeof reducedSchema === 'object') {
206
466
  if (resolveExplicitType(reducedSchema) || inferProjectionShape(reducedSchema).kind === 'resolved') {
@@ -209,26 +469,41 @@ function resolveSelectedBranch(original, reduced, data) {
209
469
  }
210
470
  const selected = [];
211
471
  if (original.oneOf) {
212
- const matching = original.oneOf.filter((branch) => branchApplies(branch, data));
472
+ const matching = original.oneOf.filter((branch) => branchApplies(branch, data, scope, branchApplicability));
213
473
  if (matching.length === 1)
214
474
  selected.push(matching[0]);
215
475
  }
216
476
  for (const branch of original.anyOf ?? []) {
217
- if (branchApplies(branch, data))
477
+ if (branchApplies(branch, data, scope, branchApplicability))
218
478
  selected.push(branch);
219
479
  }
220
- if (!selected.some((branch) => typeof branch.$ref === 'string'))
480
+ if (!selected.some((branch) => referenceOf(branch) !== undefined))
221
481
  return reduced;
222
482
  let merged = reduced;
223
483
  for (const branch of selected) {
224
484
  const target = dereference(branch);
225
485
  if (!isSchemaNode(target))
226
486
  continue;
227
- const targetReduced = target.reduceNode(data).node ?? target;
487
+ const targetReduced = target.reduceNode(data, { path: [...scope] }).node ?? target;
228
488
  merged = mergeNode(merged, targetReduced) ?? merged;
229
489
  }
230
490
  return merged;
231
491
  }
492
+ /** Fills properties a selected Draft 7 conditional `$ref` loses during reduction. */
493
+ function resolveSelectedConditionalReference(original, reduced, data, scope = [], branchApplicability) {
494
+ if (original.getDraftVersion() !== 'draft-07' || !original.if)
495
+ return reduced;
496
+ const selected = branchApplies(original.if, data, scope, branchApplicability) ? original.then : original.else;
497
+ if (!selected || typeof selected.$ref !== 'string')
498
+ return reduced;
499
+ const target = dereference(selected);
500
+ if (!isSchemaNode(target))
501
+ return reduced;
502
+ const targetReduced = reduceAllOfForProjection(target, data, scope) ?? target;
503
+ const reducedProperties = reduced.properties ?? {};
504
+ const hasUnprojectedProperty = Object.keys(targetReduced.properties ?? {}).some((key) => !Object.hasOwn(reducedProperties, key));
505
+ return hasUnprojectedProperty ? mergeNode(reduced, targetReduced) ?? reduced : reduced;
506
+ }
232
507
  export const DEFAULT_LIMITS = { objects: 16, nodes: 512 };
233
508
  function walkOrder(a, b) {
234
509
  for (let i = 0; i < Math.min(a.length, b.length); i++)
@@ -236,10 +511,19 @@ function walkOrder(a, b) {
236
511
  return a[i] - b[i];
237
512
  return a.length - b.length;
238
513
  }
239
- function addBoundary(ctx, pointer, reason) {
514
+ function boundaryToken(pointer, schemaKey, generation) {
515
+ return JSON.stringify([pointer, schemaKey, generation]);
516
+ }
517
+ function addBoundary(ctx, pointer, reason, targetPointer, schemaKey) {
240
518
  const reasons = ctx.boundaries.get(pointer) ?? new Set();
241
519
  reasons.add(reason);
242
520
  ctx.boundaries.set(pointer, reasons);
521
+ const targets = ctx.boundaryTargets.get(pointer) ?? new Map();
522
+ if (![...targets.values()].some((target) => target.reason === reason)) {
523
+ const token = boundaryToken(targetPointer, schemaKey, ctx.boundaryGeneration);
524
+ targets.set(token, { pointer: toPointer(targetPointer), reason, token });
525
+ }
526
+ ctx.boundaryTargets.set(pointer, targets);
243
527
  }
244
528
  // A leaf needs no reduction, so it rides with its parent's budget unit.
245
529
  function isLeafSchema(schema) {
@@ -252,17 +536,14 @@ function isLeafSchema(schema) {
252
536
  return ('enum' in record || 'const' in record) && !('properties' in record);
253
537
  }
254
538
  /**
255
- * The position of a schema within the document it was authored in, as a JSON
256
- * Pointer with the root as the empty string.
257
- *
258
- * `schemaLocation` is already that, prefixed with the `#` of a URI fragment,
259
- * and it names what a `$ref` resolves to rather than the reference. Both
260
- * adapters report sources in this form so a diagnostic means the same thing
261
- * whichever one produced it.
539
+ * The authored schema position as a JSON Pointer with the root as the empty
540
+ * string. Reference sites and targets keep separate positions, and encoded
541
+ * reference segments use their document spelling. Both adapters report
542
+ * sources in this form so diagnostics mean the same thing whichever produced
543
+ * them.
262
544
  */
263
545
  function documentPointer(node) {
264
- const location = node.schemaLocation;
265
- return typeof location === 'string' ? location.replace(/^#/, '') : '';
546
+ return positionOf(node).replace(/^#/, '');
266
547
  }
267
548
  /**
268
549
  * Visits every schema position that applies to one location whatever the
@@ -285,16 +566,13 @@ function documentPointer(node) {
285
566
  * several places at once: `properties/x` on the node's own schema and on each
286
567
  * of its `allOf` branches are all declarations of the same location.
287
568
  */
288
- function eachUnconditional(roots, visitor) {
569
+ function eachUnconditional(roots, visitor, referenceVisitor, cache) {
289
570
  const visited = new Set();
290
571
  const visit = (current) => {
572
+ if (referenceVisitor && cache)
573
+ visitReferenceDefaultSites(current, cache, referenceVisitor);
291
574
  const resolved = dereference(current);
292
- // The same guard, and for the same measured reason, as
293
- // `collectCandidateProperties`: `resolveRef()` returns a fresh node every
294
- // call, so identity alone never closes a cycle, and an inline branch has no
295
- // location of its own to key on.
296
- const location = resolved.schemaLocation;
297
- const key = typeof location === 'string' ? location : resolved;
575
+ const key = traversalKey(resolved);
298
576
  if (visited.has(key))
299
577
  return;
300
578
  visited.add(key);
@@ -317,9 +595,15 @@ function eachUnconditional(roots, visitor) {
317
595
  * own descent already survives the shapes that throw (#119, #121), and a
318
596
  * conflict is the wrong place to surface one.
319
597
  */
320
- function branchApplies(branch, data) {
598
+ function branchApplies(branch, data, scope = [], branchApplicability) {
599
+ if (data === undefined && branchApplicability?.has(branch)) {
600
+ return branchApplicability.get(branch);
601
+ }
321
602
  try {
322
- return branch.validate(data).valid === true;
603
+ const applies = branch.validate(data, '#', [...scope]).valid === true;
604
+ if (data === undefined)
605
+ branchApplicability?.set(branch, applies);
606
+ return applies;
323
607
  }
324
608
  catch {
325
609
  return false;
@@ -338,18 +622,23 @@ function branchApplies(branch, data) {
338
622
  * rule the projection uses to expose that branch, so the two cannot disagree
339
623
  * about which branch it is.
340
624
  */
341
- function eachApplicable(roots, data, visitor) {
625
+ function eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope = [], dynamicReferenceProjection = false) {
342
626
  const visited = new Set();
343
- const visit = (current) => {
344
- const resolved = dereference(current);
345
- const location = resolved.schemaLocation;
346
- const key = typeof location === 'string' ? location : resolved;
627
+ const visit = (current, parentScope) => {
628
+ if (referenceVisitor && cache)
629
+ visitReferenceDefaultSites(current, cache, referenceVisitor, dynamicReferenceProjection);
630
+ const entered = dynamicReferenceProjection
631
+ ? enterDynamicScope(current, parentScope)
632
+ : { node: dereference(current), path: parentScope, dynamicReference: false };
633
+ const resolved = entered.node;
634
+ const currentScope = entered.path;
635
+ const key = traversalKey(resolved);
347
636
  if (visited.has(key))
348
637
  return;
349
638
  visited.add(key);
350
639
  visitor(resolved);
351
640
  for (const branch of resolved.allOf ?? [])
352
- visit(branch);
641
+ visit(branch, currentScope);
353
642
  // `oneOf` contributes the one branch it selects, which is the evaluator's
354
643
  // rule and not "every branch the data matches": an empty object matches
355
644
  // every permissive branch, and treating all of them as applicable would
@@ -358,82 +647,123 @@ function eachApplicable(roots, data, visitor) {
358
647
  // branch stands in, which is what the projection exposes.
359
648
  const oneOf = resolved.oneOf ?? [];
360
649
  if (oneOf.length > 0) {
361
- const matching = oneOf.filter((branch) => branchApplies(branch, data));
650
+ const matching = oneOf.filter((branch) => branchApplies(branch, data, currentScope, branchApplicability));
362
651
  const selected = matching.length === 1
363
652
  ? matching[0]
364
653
  : selectProvisionalBranch(resolved, typeof data === 'object' && data !== null
365
654
  ? data
366
655
  : undefined);
367
656
  if (selected)
368
- visit(selected);
657
+ visit(selected, currentScope);
369
658
  }
370
659
  // `anyOf` is the opposite case and needs no such rule: several of its
371
660
  // branches applying at once is ordinary, and each that validates
372
661
  // contributes its annotations, so each competes.
373
662
  for (const branch of resolved.anyOf ?? []) {
374
- if (branchApplies(branch, data))
375
- visit(branch);
663
+ if (branchApplies(branch, data, currentScope, branchApplicability))
664
+ visit(branch, currentScope);
376
665
  }
377
666
  if (resolved.if) {
378
- const taken = branchApplies(resolved.if, data) ? resolved.then : resolved.else;
667
+ const taken = branchApplies(resolved.if, data, currentScope, branchApplicability) ? resolved.then : resolved.else;
379
668
  if (taken)
380
- visit(taken);
669
+ visit(taken, currentScope);
381
670
  }
382
671
  const dependent = resolved.dependentSchemas;
383
672
  if (dependent && typeof data === 'object' && data !== null && !Array.isArray(data)) {
384
673
  for (const [dependency, subschema] of Object.entries(dependent)) {
385
674
  if (Object.prototype.hasOwnProperty.call(data, dependency))
386
- visit(subschema);
675
+ visit(subschema, currentScope);
387
676
  }
388
677
  }
389
678
  };
390
679
  for (const root of roots)
391
- visit(root);
680
+ visit(root, scope);
392
681
  }
393
- function declarationsFrom(walkRoots) {
394
- const declarations = [];
395
- walkRoots((node) => {
396
- const schema = node.schema;
397
- if (schema !== null && typeof schema === 'object' && 'default' in schema) {
398
- declarations.push({ value: schema.default, source: documentPointer(node) });
682
+ function enterDynamicScope(node, scope) {
683
+ const path = [...scope];
684
+ let current = node;
685
+ let dynamicReference = false;
686
+ const seen = new Set();
687
+ while (typeof current.$ref === 'string' ||
688
+ typeof current.schema.$dynamicRef === 'string' ||
689
+ typeof current.schema.$recursiveRef === 'string') {
690
+ const position = positionOf(current);
691
+ if (seen.has(position))
692
+ break;
693
+ seen.add(position);
694
+ const resolved = current.resolveRef({ pointer: '#', path });
695
+ if (!isSchemaNode(resolved))
696
+ break;
697
+ const schema = current.schema;
698
+ if (typeof schema.$dynamicRef === 'string' || typeof schema.$recursiveRef === 'string') {
699
+ dynamicReference = true;
399
700
  }
400
- });
701
+ if (resolved === current)
702
+ break;
703
+ if (typeof current.$ref === 'string' &&
704
+ current.getDraftVersion() !== 'draft-07' &&
705
+ !onlyPoints(current)) {
706
+ return { node: dereference(current), path, dynamicReference };
707
+ }
708
+ current = resolved;
709
+ }
710
+ if (path[path.length - 1]?.node !== current)
711
+ path.push({ pointer: '#', node: current });
712
+ return { node: current, path, dynamicReference };
713
+ }
714
+ function declarationsFrom(walkRoots, cache) {
715
+ const declarations = [];
716
+ const seen = new Set();
717
+ const add = (node) => {
718
+ const schema = authoredSchema(node, cache);
719
+ if (schema === null || typeof schema !== 'object' || !('default' in schema))
720
+ return;
721
+ const record = schema;
722
+ if (typeof record.$ref === 'string' && cache.dialect === 'draft-07')
723
+ return;
724
+ const source = documentPointer(node);
725
+ if (seen.has(source))
726
+ return;
727
+ seen.add(source);
728
+ declarations.push({ value: record.default, source });
729
+ };
730
+ walkRoots(add, add);
401
731
  return declarations;
402
732
  }
403
- function collectUnconditionalDefaults(roots) {
404
- return declarationsFrom((visitor) => eachUnconditional(roots, visitor));
733
+ function collectUnconditionalDefaults(roots, cache) {
734
+ return declarationsFrom((visitor, referenceVisitor) => eachUnconditional(roots, visitor, referenceVisitor, cache), cache);
405
735
  }
406
- function collectApplicableDefaults(roots, data) {
407
- return declarationsFrom((visitor) => eachApplicable(roots, data, visitor));
736
+ function collectApplicableDefaults(roots, data, cache, scope = [], dynamicReferenceProjection = false, branchApplicability) {
737
+ return declarationsFrom((visitor, referenceVisitor) => eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope, dynamicReferenceProjection), cache);
408
738
  }
409
739
  /** The positions that declare one property of `roots`, by both reachability rules. */
410
- function childPositions(roots, key, data, declaring) {
740
+ function childPositions(roots, key, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
411
741
  const unconditional = [];
412
742
  eachUnconditional(roots.unconditional, (node) => {
413
- const child = node.properties?.[key];
743
+ const child = memberSchema(node, key);
414
744
  if (child)
415
745
  unconditional.push(child);
416
746
  });
417
747
  const applicable = [];
418
- eachApplicable(roots.applicable, data, (node) => {
419
- const child = node.properties?.[key];
748
+ eachApplicable(roots.applicable, data, branchApplicability, (node) => {
749
+ const child = memberSchema(node, key);
420
750
  if (child)
421
751
  applicable.push(child);
422
- });
752
+ }, undefined, undefined, scope, dynamicReferenceProjection);
423
753
  return { unconditional, applicable, declaring };
424
754
  }
425
755
  /** The positions that declare the elements of `roots`, by both reachability rules. */
426
- function itemPositions(roots, data, declaring) {
756
+ function itemPositions(roots, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
427
757
  const unconditional = [];
428
758
  eachUnconditional(roots.unconditional, (node) => {
429
759
  if (node.items)
430
760
  unconditional.push(node.items);
431
761
  });
432
762
  const applicable = [];
433
- eachApplicable(roots.applicable, data, (node) => {
763
+ eachApplicable(roots.applicable, data, branchApplicability, (node) => {
434
764
  if (node.items)
435
765
  applicable.push(node.items);
436
- });
766
+ }, undefined, undefined, scope, dynamicReferenceProjection);
437
767
  return { unconditional, applicable, declaring };
438
768
  }
439
769
  /**
@@ -448,16 +778,75 @@ function disagreeingDefaults(declarations) {
448
778
  const [first, ...rest] = declarations;
449
779
  return rest.every((other) => deepEqual(other.value, first.value)) ? undefined : declarations;
450
780
  }
781
+ function additionalPropertyKeys(node, data) {
782
+ if (!node?.additionalProperties)
783
+ return [];
784
+ const keys = new Set([...Object.keys(data ?? {}), ...(node.required ?? [])]);
785
+ return [...keys].filter((key) => !Object.hasOwn(node.properties ?? {}, key) &&
786
+ !node.patternProperties?.some(({ pattern }) => pattern.test(key)));
787
+ }
788
+ function additionalPropertySchema(node, key) {
789
+ if (!node || Object.hasOwn(node.properties ?? {}, key))
790
+ return undefined;
791
+ if (node.patternProperties?.some(({ pattern }) => pattern.test(key)))
792
+ return undefined;
793
+ return node.additionalProperties;
794
+ }
795
+ function memberSchema(node, key) {
796
+ if (!node)
797
+ return undefined;
798
+ if (Object.hasOwn(node.properties ?? {}, key))
799
+ return node.properties?.[key];
800
+ return additionalPropertySchema(node, key);
801
+ }
802
+ function applicableAdditionalPropertySchemas(roots, key, data, scope = [], dynamicReferenceProjection = false, branchApplicability) {
803
+ const schemas = [];
804
+ const seen = new Set();
805
+ eachApplicable(roots, data, branchApplicability, (node) => {
806
+ const schema = additionalPropertySchema(node, key);
807
+ if (!schema)
808
+ return;
809
+ const position = positionOf(schema);
810
+ if (seen.has(position))
811
+ return;
812
+ seen.add(position);
813
+ schemas.push(schema);
814
+ }, undefined, undefined, scope, dynamicReferenceProjection);
815
+ return schemas;
816
+ }
817
+ function applicableRequiredKeys(roots, data, scope = [], dynamicReferenceProjection = false, branchApplicability) {
818
+ const keys = new Set();
819
+ eachApplicable(roots, data, branchApplicability, (node) => {
820
+ for (const key of node.required ?? [])
821
+ keys.add(key);
822
+ }, undefined, undefined, scope, dynamicReferenceProjection);
823
+ return keys;
824
+ }
825
+ function unconditionalRequiredKeys(roots) {
826
+ const keys = new Set();
827
+ eachUnconditional(roots, (node) => {
828
+ for (const key of node.required ?? [])
829
+ keys.add(key);
830
+ });
831
+ return keys;
832
+ }
833
+ function reduceAdditionalPropertySchema(source, data, scope = []) {
834
+ if (source.getDraftVersion() !== 'draft-07')
835
+ return source.reduceNode(data, { path: [...scope] }).node ?? source;
836
+ const { node, unresolved } = dereferenceChecked(source);
837
+ if (unresolved || !isSchemaNode(node))
838
+ return source;
839
+ return reduceAllOfForProjection(node, data) ?? node;
840
+ }
841
+ const candidatePropertyCache = new WeakMap();
451
842
  /**
452
- * Every property key that could appear at this node's own instance location,
453
- * across every branch that might apply to it.
843
+ * Property keys that might need to appear at this instance location.
454
844
  *
455
- * This is the static half of the inactive-node contract: a branch the data does
456
- * not currently select still contributes its property pointers, which then
457
- * project with `active: false`. The dynamic half is `reduceNode`, which decides
458
- * which of them apply now, and the two must stay separate. Making the candidate
459
- * set data-driven would delete a pointer the moment its branch stopped
460
- * matching, which is exactly the flicker the contract exists to prevent.
845
+ * Declared keys remain static candidates across all branches. Keys governed
846
+ * only by `additionalProperties` come from the current data, since the schema
847
+ * has no finite set of names to enumerate; required names are candidates too.
848
+ * `reduceNode` decides which candidates apply now, so an inactive branch keeps
849
+ * its declared pointers with `active: false`.
461
850
  *
462
851
  * **Recursion is through applicators only, and only those acting on this same
463
852
  * instance location**: `if`, `then`, `else`, `allOf`, `anyOf`, `oneOf`,
@@ -477,20 +866,20 @@ function disagreeingDefaults(declarations) {
477
866
  * - **`items` and `prefixItems`**, for the same reason: an array's items are
478
867
  * their own locations.
479
868
  *
480
- * The cycle guard keys on `schemaLocation` rather than on node identity, which
481
- * was measured rather than assumed and is the opposite of what it looks like it
482
- * should be: `resolveRef()` returns a fresh `SchemaNode` on every call, and the
483
- * raw `schema` object it wraps is fresh too, so an identity `Set` never matches
484
- * and a recursive `$ref` would not terminate. `schemaLocation` is stable, is
485
- * distinct for each inline branch position, and repeats when a reference closes
486
- * a cycle, which is precisely the three properties needed. The set is per call,
487
- * because deduplicating a schema location is only sound while collecting names
488
- * for one instance location; the same schema legitimately recurs at a deeper
489
- * pointer, and `walk` visits it again there.
869
+ * Inline conditional branches use `positionOf` because the library can assign
870
+ * `if`, `then` and `else` the same `schemaLocation` after resolving a
871
+ * reference. Other nodes keep their location key, so references still close
872
+ * at the same point in the traversal as before.
490
873
  */
491
- function collectCandidateProperties(node) {
874
+ function collectCandidateProperties(node, data, declarations = []) {
875
+ if (declarations.length === 0 && candidatePropertyCache.has(node)) {
876
+ const cached = candidatePropertyCache.get(node);
877
+ if (cached)
878
+ return cached;
879
+ }
492
880
  const candidates = new Map();
493
881
  const visited = new Set();
882
+ const scopes = [];
494
883
  const record = (key, propNode, direct) => {
495
884
  const entry = candidates.get(key) ?? { alternatives: [] };
496
885
  if (direct)
@@ -501,26 +890,11 @@ function collectCandidateProperties(node) {
501
890
  };
502
891
  const visit = (current, own) => {
503
892
  const resolved = dereference(current);
504
- // Keyed on `schemaLocation` where there is one, and on the node itself
505
- // where there is not, which together cover every way the traversal can
506
- // come back to where it started.
507
- //
508
- // Neither alone is sufficient and each covers what the other cannot. A
509
- // cycle requires a `$ref`, because an inline schema cannot nest into
510
- // itself, and `resolveRef()` returns a fresh `SchemaNode` on every call
511
- // whose raw `schema` object is fresh too, so identity never matches across
512
- // one; those nodes do carry a location. An inline branch is never resolved
513
- // through a ref, so its identity is stable within one traversal.
514
- //
515
- // This deliberately replaced a depth cap. A cap terminates, but it does so
516
- // by dropping candidates a valid schema declared, which is the silent
517
- // disappearance the whole projection-diagnostic design exists to prevent:
518
- // a field nested under 70 applicators is still a field.
519
- const location = resolved.schemaLocation;
520
- const key = typeof location === 'string' ? location : resolved;
893
+ const key = traversalKey(resolved);
521
894
  if (visited.has(key))
522
895
  return;
523
896
  visited.add(key);
897
+ scopes.push({ node: resolved, own });
524
898
  for (const [key, propNode] of Object.entries(resolved.properties ?? {})) {
525
899
  record(key, propNode, own);
526
900
  }
@@ -528,7 +902,9 @@ function collectCandidateProperties(node) {
528
902
  if (branch)
529
903
  visit(branch, false);
530
904
  }
531
- for (const branches of [resolved.allOf, resolved.anyOf, resolved.oneOf]) {
905
+ for (const branch of resolved.allOf ?? [])
906
+ visit(branch, own);
907
+ for (const branches of [resolved.anyOf, resolved.oneOf]) {
532
908
  for (const branch of branches ?? [])
533
909
  visit(branch, false);
534
910
  }
@@ -539,8 +915,32 @@ function collectCandidateProperties(node) {
539
915
  }
540
916
  };
541
917
  visit(node, true);
918
+ for (const declaration of declarations)
919
+ visit(declaration, false);
920
+ const memberKeys = new Set(Object.keys(data ?? {}));
921
+ for (const scope of scopes)
922
+ for (const key of scope.node.required ?? [])
923
+ memberKeys.add(key);
924
+ for (const { node: scope, own } of scopes) {
925
+ for (const key of memberKeys) {
926
+ const propNode = additionalPropertySchema(scope, key);
927
+ if (propNode)
928
+ record(key, propNode, own);
929
+ }
930
+ }
931
+ const hasDataDependentAdditionalProperties = scopes.some(({ node: scope }) => Boolean(scope.additionalProperties));
932
+ if (declarations.length === 0) {
933
+ candidatePropertyCache.set(node, hasDataDependentAdditionalProperties ? null : candidates);
934
+ }
542
935
  return candidates;
543
936
  }
937
+ function hasRetainedDescendant(position, cache) {
938
+ for (const retained of cache.retainedPositions) {
939
+ if (retained === position || retained.startsWith(`${position}/`))
940
+ return true;
941
+ }
942
+ return false;
943
+ }
544
944
  /**
545
945
  * The schema to project for a candidate when no branch is active.
546
946
  *
@@ -714,6 +1114,96 @@ function computeRequiredSet(resolved, reducedSchema, dataRecord) {
714
1114
  }
715
1115
  return required;
716
1116
  }
1117
+ // reduceNode recompiles a subtree when any descendant has reduction work.
1118
+ const REDUCE_NODE_SCHEMA_FIELDS = [
1119
+ '$ref',
1120
+ '$dynamicRef',
1121
+ '$recursiveRef',
1122
+ '$defs',
1123
+ 'if',
1124
+ 'then',
1125
+ 'else',
1126
+ 'allOf',
1127
+ 'anyOf',
1128
+ 'oneOf',
1129
+ 'contains',
1130
+ 'dependentSchemas',
1131
+ 'dependentRequired',
1132
+ 'definitions',
1133
+ 'dependencies',
1134
+ 'patternProperties',
1135
+ 'propertyDependencies',
1136
+ ];
1137
+ const SCHEMA_MAP_KEYWORDS = [
1138
+ '$defs',
1139
+ 'definitions',
1140
+ 'dependentSchemas',
1141
+ 'patternProperties',
1142
+ 'properties',
1143
+ ];
1144
+ const SCHEMA_SINGLE_KEYWORDS = [
1145
+ 'additionalItems',
1146
+ 'additionalProperties',
1147
+ 'contains',
1148
+ 'if',
1149
+ 'items',
1150
+ 'not',
1151
+ 'propertyNames',
1152
+ 'then',
1153
+ 'else',
1154
+ 'unevaluatedItems',
1155
+ 'unevaluatedProperties',
1156
+ ];
1157
+ const SCHEMA_ARRAY_KEYWORDS = ['allOf', 'anyOf', 'oneOf', 'prefixItems'];
1158
+ const reductionRequirementCache = new WeakMap();
1159
+ function schemaRequiresReduction(schema, visiting = new WeakSet()) {
1160
+ if (schema === true)
1161
+ return true;
1162
+ if (typeof schema !== 'object' || schema === null)
1163
+ return false;
1164
+ const cached = reductionRequirementCache.get(schema);
1165
+ if (cached !== undefined)
1166
+ return cached;
1167
+ if (visiting.has(schema))
1168
+ return false;
1169
+ visiting.add(schema);
1170
+ const record = schema;
1171
+ const direct = REDUCE_NODE_SCHEMA_FIELDS.some((field) => Object.prototype.hasOwnProperty.call(record, field)) ||
1172
+ Object.prototype.hasOwnProperty.call(record, 'oneOfProperty') ||
1173
+ Array.isArray(record.type);
1174
+ const children = [];
1175
+ for (const keyword of SCHEMA_MAP_KEYWORDS) {
1176
+ const value = record[keyword];
1177
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1178
+ children.push(...Object.values(value));
1179
+ }
1180
+ }
1181
+ for (const keyword of SCHEMA_SINGLE_KEYWORDS) {
1182
+ const value = record[keyword];
1183
+ if (keyword === 'items' && Array.isArray(value))
1184
+ children.push(...value);
1185
+ else if (value !== undefined)
1186
+ children.push(value);
1187
+ }
1188
+ for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
1189
+ const value = record[keyword];
1190
+ if (Array.isArray(value))
1191
+ children.push(...value);
1192
+ }
1193
+ for (const value of Object.values(record.dependencies ?? {}))
1194
+ children.push(value);
1195
+ for (const values of Object.values(record.propertyDependencies ?? {})) {
1196
+ if (typeof values === 'object' && values !== null)
1197
+ children.push(...Object.values(values));
1198
+ }
1199
+ const required = direct || children.some((child) => schemaRequiresReduction(child, visiting));
1200
+ visiting.delete(schema);
1201
+ reductionRequirementCache.set(schema, required);
1202
+ return required;
1203
+ }
1204
+ function requiresReduction(node) {
1205
+ return node.reducers.length > 0 || schemaRequiresReduction(node.schema);
1206
+ }
717
1207
  // An unreadable identity is never a repeat, and is budgeted so it cannot expand without end.
718
1208
  const budgeted = (info) => info.cyclic || info.key === '';
719
1209
  /**
@@ -743,9 +1233,15 @@ declaredAt, ctx,
743
1233
  /** Entered as an object member, which is the only way to be past the data. */
744
1234
  member,
745
1235
  /** The past-the-data ancestors, nearest first, up to the nearest location holding data. */
746
- lineage, path) {
1236
+ lineage, path, scope, dynamicReferenceAtLocation = false) {
1237
+ if (ctx.cache.dynamicReferenceProjection) {
1238
+ const entered = enterDynamicScope(node, scope);
1239
+ node = entered.node;
1240
+ scope = entered.path;
1241
+ dynamicReferenceAtLocation ||= entered.dynamicReference;
1242
+ }
747
1243
  const info = locationInfo(declaredAt.declaring, ctx.cache);
748
- const { node: original, cycle: referenceCycle } = dereferenceChecked(node);
1244
+ const { node: original, cycle: referenceCycle, unresolved: unresolvedReference } = dereferenceChecked(node);
749
1245
  if (info.cycle || referenceCycle) {
750
1246
  diagnostics.push({
751
1247
  pointer: toPointer(pointer),
@@ -756,10 +1252,13 @@ lineage, path) {
756
1252
  return;
757
1253
  }
758
1254
  const pastData = member && (data === undefined || data === null);
759
- const recursive = pastData && ((lineage?.recursive ?? false) || budgeted(info));
1255
+ const token = boundaryToken(pointer, info.key, ctx.boundaryGeneration);
1256
+ const recursive = pastData && !unresolvedReference && ((lineage?.recursive ?? false) || budgeted(info));
760
1257
  for (let ancestor = pastData ? lineage : undefined; ancestor; ancestor = ancestor.parent) {
761
- if (info.key !== '' && ancestor.key === info.key) {
762
- addBoundary(ctx, ancestor.pointer, 'recursion');
1258
+ if (info.key !== '' &&
1259
+ ancestor.key === info.key &&
1260
+ !ctx.expandedBoundaryTokens.has(token)) {
1261
+ addBoundary(ctx, ancestor.pointer, 'recursion', pointer, info.key);
763
1262
  ctx.removed.add(pointer);
764
1263
  return;
765
1264
  }
@@ -770,21 +1269,29 @@ lineage, path) {
770
1269
  let admit;
771
1270
  if (isLeafSchema(originalSchema)) {
772
1271
  const covered = ctx.reserved.has(parent);
773
- admit = covered || ctx.nodesUsed < ctx.limits.nodes;
1272
+ admit = ctx.expandedBoundaryTokens.has(token) || covered || ctx.nodesUsed < ctx.limits.nodes;
774
1273
  if (admit && !covered)
775
1274
  ctx.nodesUsed += 1;
776
1275
  }
777
1276
  else {
778
- const leaves = [...collectCandidateProperties(original).values()].filter((candidate) => isLeafSchema(dereference(candidatePrototype(candidate)).schema)).length;
779
- admit = ctx.objectsUsed < ctx.limits.objects && ctx.nodesUsed + 1 + leaves <= ctx.limits.nodes;
1277
+ const explicitlyExpanded = ctx.expandedBoundaryTokens.has(token);
1278
+ const retainedDeclarations = declaredAt.declaring.filter((declaration) => hasRetainedDescendant(positionOf(declaration), ctx.cache));
1279
+ const candidateDeclarations = dynamicReferenceAtLocation
1280
+ ? []
1281
+ : [...declaredAt.applicable, ...retainedDeclarations];
1282
+ const leaves = [...collectCandidateProperties(original, undefined, candidateDeclarations).values()].filter((candidate) => isLeafSchema(dereference(candidatePrototype(candidate)).schema)).length;
1283
+ admit =
1284
+ explicitlyExpanded ||
1285
+ (ctx.objectsUsed < ctx.limits.objects && ctx.nodesUsed + 1 + leaves <= ctx.limits.nodes);
780
1286
  if (admit) {
781
1287
  ctx.objectsUsed += 1;
782
- ctx.nodesUsed += 1 + leaves;
783
- ctx.reserved.add(pointer);
1288
+ ctx.nodesUsed += explicitlyExpanded ? 1 : 1 + leaves;
1289
+ if (!explicitlyExpanded)
1290
+ ctx.reserved.add(pointer);
784
1291
  }
785
1292
  }
786
1293
  if (!admit) {
787
- addBoundary(ctx, parent, 'budget');
1294
+ addBoundary(ctx, parent, 'budget', pointer, info.key);
788
1295
  ctx.removed.add(pointer);
789
1296
  return;
790
1297
  }
@@ -795,6 +1302,36 @@ lineage, path) {
795
1302
  let resolved = original;
796
1303
  let schema = originalSchema;
797
1304
  let type = resolveExplicitType(schema);
1305
+ const localShape = inferProjectionShape(schema);
1306
+ const allOfType = original.allOf ? allOfExplicitType(original) : undefined;
1307
+ const hasReferencedAllOfBranch = original.allOf ? hasReferencedAllOf(original) : false;
1308
+ if (original.allOf &&
1309
+ !original.oneOf &&
1310
+ !original.anyOf &&
1311
+ !type &&
1312
+ (localShape.kind !== 'resolved' || allOfType !== undefined) &&
1313
+ (hasReferencedAllOfBranch || allOfType !== undefined)) {
1314
+ const reductionData = (allOfType === 'object' || declaresObject(original)) &&
1315
+ (data === undefined || data === null)
1316
+ ? {}
1317
+ : data;
1318
+ const reduced = reduceAllOfForProjection(original, reductionData, scope);
1319
+ const reducedSchema = reduced?.schema;
1320
+ if (reduced && reducedSchema && typeof reducedSchema === 'object') {
1321
+ resolved = reduced;
1322
+ schema = reducedSchema;
1323
+ if (!type)
1324
+ type = resolveExplicitType(schema);
1325
+ }
1326
+ }
1327
+ if (!type && ctx.cache.dynamicReferenceProjection && original.allOf?.length) {
1328
+ const reduced = original.reduceNode(data, { path: [...scope] }).node;
1329
+ if (reduced) {
1330
+ resolved = reduced;
1331
+ schema = reduced.schema;
1332
+ type = resolveExplicitType(schema);
1333
+ }
1334
+ }
798
1335
  // Whether this node's own active branch could be determined. Stays true for every
799
1336
  // node except a typeless oneOf/anyOf wrapper whose branch could not be resolved
800
1337
  // (see below), where the node's own presence in the projection is the thing in
@@ -818,9 +1355,9 @@ lineage, path) {
818
1355
  let wrapperIdentified = false;
819
1356
  if (!type && (original.oneOf || original.anyOf)) {
820
1357
  composedAgainstData = true;
821
- const { node: branchNode } = original.reduceNode(data);
1358
+ const { node: branchNode } = original.reduceNode(data, { path: [...scope] });
822
1359
  if (branchNode) {
823
- resolved = resolveSelectedBranch(original, branchNode, data);
1360
+ resolved = resolveSelectedBranch(original, branchNode, data, scope, ctx.branchApplicability);
824
1361
  schema = resolved.schema;
825
1362
  type = resolveExplicitType(schema);
826
1363
  }
@@ -899,26 +1436,16 @@ lineage, path) {
899
1436
  // without a shape, so the caller is told which pointer was skipped and
900
1437
  // why, instead of finding out from a form that never collected the
901
1438
  // value.
902
- diagnostics.push(shape.kind === 'ambiguous'
903
- ? {
904
- pointer: toPointer(pointer),
905
- code: 'ambiguous-projection-shape',
906
- message: `No explicit "type", and keywords from more than one type apply ` +
907
- `(${shape.families.join(', ')}), so the shape to render is undecidable. ` +
908
- `Declare "type" on this schema to resolve it.`,
909
- }
910
- : {
911
- pointer: toPointer(pointer),
912
- code: 'unresolved-projection-shape',
913
- message: `No explicit "type", and no keyword that implies one, so there is no ` +
914
- `shape to render. Declare "type" on this schema.` +
915
- // Said only where it applies, because inferring `string` from
916
- // an enum is the tempting wrong rule and the reason deserves
917
- // to travel with the case rather than every message.
918
- (schema.enum !== undefined
919
- ? ` An "enum" alone does not imply a type, because its members may be of different types.`
920
- : ''),
921
- });
1439
+ diagnostics.push(shapeDiagnostic(toPointer(pointer), shape, schema.enum !== undefined));
1440
+ }
1441
+ }
1442
+ // Arrays do not pass through the object path that reduces the active schema.
1443
+ if (type === 'array' && resolved === original && original.allOf) {
1444
+ const reduced = reduceAllOfForProjection(original, data, scope);
1445
+ const reducedSchema = reduced?.schema;
1446
+ if (reduced && reducedSchema && typeof reducedSchema === 'object') {
1447
+ resolved = reduced;
1448
+ schema = reducedSchema;
922
1449
  }
923
1450
  }
924
1451
  if (!type)
@@ -930,7 +1457,7 @@ lineage, path) {
930
1457
  // pointer with no node has nothing to omit an annotation from, and saying
931
1458
  // that its `default` is undecidable on top of saying it cannot be drawn at
932
1459
  // all would be the same schema reported twice.
933
- const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional));
1460
+ const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional, ctx.cache));
934
1461
  if (ambiguousDefault) {
935
1462
  diagnostics.push({
936
1463
  pointer: toPointer(pointer),
@@ -945,13 +1472,18 @@ lineage, path) {
945
1472
  // selects. A superset of the diagnostic's: every unconditional disagreement
946
1473
  // is also an applicable one, so the node carries both kinds and a consumer
947
1474
  // reads one place.
948
- const applicableDeclarations = collectApplicableDefaults(declaredAt.applicable, data);
1475
+ const applicableData = type === 'object' && !(typeof data === 'object' && data !== null) ? {} : data;
1476
+ const applicableDeclarations = collectApplicableDefaults(declaredAt.applicable, applicableData, ctx.cache, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
949
1477
  const applicableConflict = disagreeingDefaults(applicableDeclarations);
950
1478
  const conflictedDefault = applicableConflict !== undefined || ambiguousDefault !== undefined;
951
1479
  const defaultConflict = (applicableConflict ?? ambiguousDefault)?.map((declaration) => declaration.source);
952
- const defaultSources = !conflictedDefault && 'default' in schema && info.cyclic
1480
+ const defaultSources = !conflictedDefault && applicableDeclarations.length > 0 && info.cyclic
953
1481
  ? [...new Set(applicableDeclarations.map((declaration) => declaration.source))].sort()
954
1482
  : undefined;
1483
+ const defaultOverride = {
1484
+ present: applicableDeclarations.length > 0,
1485
+ value: applicableDeclarations[0]?.value,
1486
+ };
955
1487
  const nodeActive = active && branchResolved;
956
1488
  // A node the schema applies is never also provisional; the two report
957
1489
  // different facts and only the second is a choice this projection made. The
@@ -976,19 +1508,73 @@ lineage, path) {
976
1508
  // wrapper case), it has already been reduced against the real `data` at this
977
1509
  // pointer; re-reducing it against `dataRecord ?? {}` here would be redundant (and,
978
1510
  // for a branch with no dynamic keywords of its own, a no-op), so it is skipped.
979
- let reducedNode = resolved === original ? resolved.reduceNode(dataRecord ?? {}).node : resolved;
1511
+ let reducedNode = resolved === original
1512
+ ? requiresReduction(resolved)
1513
+ ? original.allOf && hasReferencedAllOf(original)
1514
+ ? reduceAllOfForProjection(resolved, dataRecord ?? {}, scope)
1515
+ : resolved.reduceNode(dataRecord ?? {}, { path: [...scope] }).node
1516
+ : resolved
1517
+ : resolved;
1518
+ if (reducedNode && resolved === original && (!member || data !== undefined)) {
1519
+ // Missing child data has no evaluated conditional branch to recover.
1520
+ reducedNode = resolveSelectedConditionalReference(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
1521
+ }
980
1522
  if (reducedNode &&
981
1523
  resolved === original &&
982
1524
  !resolveExplicitType(originalSchema) &&
983
1525
  (original.oneOf || original.anyOf)) {
984
- reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {});
1526
+ reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
985
1527
  }
986
1528
  const reducedSchema = reducedNode?.schema;
987
1529
  const reducedProperties = reducedSchema?.properties ??
988
1530
  schema.properties ??
989
1531
  {};
990
- const activeKeys = new Set(Object.keys(reducedProperties));
1532
+ // Dynamic applicability is rebuilt below; only retained declarations belong beside it here.
1533
+ const retainedDeclarations = declaredAt.declaring.filter((declaration) => hasRetainedDescendant(positionOf(declaration), ctx.cache));
1534
+ const candidateDeclarations = dynamicReferenceAtLocation
1535
+ ? []
1536
+ : [...declaredAt.applicable, ...retainedDeclarations];
1537
+ let candidateProps = collectCandidateProperties(original, dataRecord, candidateDeclarations);
1538
+ const activeKeys = new Set([
1539
+ ...Object.keys(reducedProperties),
1540
+ ...additionalPropertyKeys(reducedNode, dataRecord),
1541
+ ]);
1542
+ if (!reducedNode) {
1543
+ for (const [key, candidate] of candidateProps) {
1544
+ if (candidate.direct)
1545
+ activeKeys.add(key);
1546
+ }
1547
+ }
991
1548
  const requiredSet = computeRequiredSet(resolved, reducedSchema, dataRecord);
1549
+ const dynamicApplicableNodes = [];
1550
+ if (ctx.cache.dynamicReferenceProjection) {
1551
+ eachApplicable([original], data, ctx.branchApplicability, (applicableNode) => {
1552
+ dynamicApplicableNodes.push(applicableNode);
1553
+ for (const key of Object.keys(applicableNode.properties ?? {}))
1554
+ activeKeys.add(key);
1555
+ for (const key of applicableNode.required ?? [])
1556
+ requiredSet.add(key);
1557
+ }, undefined, undefined, scope, true);
1558
+ const expandedCandidates = new Map(candidateProps);
1559
+ const activeCandidateNode = reducedNode ?? resolved;
1560
+ if (activeCandidateNode !== original) {
1561
+ for (const [key, candidate] of collectCandidateProperties(activeCandidateNode, dataRecord)) {
1562
+ if (!expandedCandidates.has(key))
1563
+ expandedCandidates.set(key, candidate);
1564
+ }
1565
+ }
1566
+ for (const applicableNode of dynamicApplicableNodes) {
1567
+ for (const [key, propertyNode] of Object.entries(applicableNode.properties ?? {})) {
1568
+ if (!expandedCandidates.has(key))
1569
+ expandedCandidates.set(key, { direct: propertyNode, alternatives: [] });
1570
+ }
1571
+ }
1572
+ candidateProps = expandedCandidates;
1573
+ }
1574
+ if (!reducedNode) {
1575
+ for (const key of unconditionalRequiredKeys(declaredAt.unconditional))
1576
+ requiredSet.add(key);
1577
+ }
992
1578
  // Two conditions, and they are not the same kind of condition.
993
1579
  //
994
1580
  // Exposure is load-bearing. Selection is local, but whether this node is
@@ -1008,11 +1594,32 @@ lineage, path) {
1008
1594
  : undefined;
1009
1595
  const provisionalSchema = provisionalBranch?.schema;
1010
1596
  const provisionalKeys = new Set(Object.keys(provisionalSchema?.properties ?? {}));
1597
+ for (const key of additionalPropertyKeys(provisionalBranch, dataRecord))
1598
+ provisionalKeys.add(key);
1011
1599
  const provisionalRequired = new Set(Array.isArray(provisionalSchema?.required) ? provisionalSchema.required : []);
1012
1600
  // Candidates are collected from `original`, not `resolved`, so every oneOf/anyOf
1013
1601
  // branch's properties are represented (the matching branch alone, via `resolved`,
1014
1602
  // would only expose its own properties).
1015
- const candidateProps = collectCandidateProperties(original);
1603
+ const applicableRequired = applicableRequiredKeys(declaredAt.applicable, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
1604
+ const provisionalRequiredKeys = provisionalBranch
1605
+ ? applicableRequiredKeys([provisionalBranch], applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability)
1606
+ : new Set();
1607
+ for (const key of candidateProps.keys()) {
1608
+ const present = dataRecord !== undefined && Object.hasOwn(dataRecord, key);
1609
+ if ((present || applicableRequired.has(key)) &&
1610
+ applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0) {
1611
+ activeKeys.add(key);
1612
+ }
1613
+ }
1614
+ if (provisionalBranch) {
1615
+ for (const key of candidateProps.keys()) {
1616
+ if (provisionalRequiredKeys.has(key) &&
1617
+ (applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0 ||
1618
+ applicableAdditionalPropertySchemas([provisionalBranch], key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0)) {
1619
+ provisionalKeys.add(key);
1620
+ }
1621
+ }
1622
+ }
1016
1623
  const propKeys = [...candidateProps.keys()];
1017
1624
  const children = propKeys.length > 0
1018
1625
  ? propKeys.map((key) => {
@@ -1038,7 +1645,7 @@ lineage, path) {
1038
1645
  active: nodeActive,
1039
1646
  provisional: nodeProvisional ? true : undefined,
1040
1647
  defaultConflict,
1041
- annotations: extractAnnotations(schema, conflictedDefault),
1648
+ annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
1042
1649
  ...(defaultSources !== undefined ? { defaultSources } : {}),
1043
1650
  });
1044
1651
  for (const [index, key] of propKeys.entries()) {
@@ -1048,29 +1655,89 @@ lineage, path) {
1048
1655
  // selected ancestor its own locally applicable properties are
1049
1656
  // provisional too, while a locally inactive one stays inactive.
1050
1657
  const childProvisional = !childActive && nodeExposed && (activeKeys.has(key) || provisionalKeys.has(key));
1051
- const reducedChildNode = reducedNode?.properties?.[key];
1658
+ const reducedChildNode = memberSchema(reducedNode, key);
1052
1659
  // Precedence matters as much as the selection does. Marking the right
1053
1660
  // branch provisional while taking its shape from `candidatePrototype`,
1054
1661
  // which is first-wins across branches, would render another branch's
1055
1662
  // widget and annotations under the selected branch's name, and would hand
1056
1663
  // ADR-003's pass another branch's `default`.
1057
- const provisionalChildNode = provisionalBranch?.properties?.[key];
1664
+ const provisionalChildNode = memberSchema(provisionalBranch, key);
1058
1665
  const candidate = candidateProps.get(key);
1059
- // Three states, in order of authority. What the evaluator reduced against
1060
- // the data is the schema as it actually applies. Failing that, the node's
1061
- // own declaration composed with the selected branch, which is what the
1062
- // reduction would have produced had the branch been complete. Failing
1063
- // both, a stated placeholder for a location no branch has claimed.
1064
- const childNode = reducedChildNode ??
1666
+ const childData = dataRecord !== undefined && Object.hasOwn(dataRecord, key) ? dataRecord[key] : undefined;
1667
+ const sourceChildNode = reducedChildNode ?? candidate.direct ?? candidatePrototype(candidate);
1668
+ const sourceSchema = sourceChildNode.schema;
1669
+ let scopedChildNode;
1670
+ let childScope = scope;
1671
+ if (ctx.cache.dynamicReferenceProjection && childActive) {
1672
+ const nextScope = [...scope];
1673
+ if (typeof sourceSchema.$dynamicAnchor === 'string' ||
1674
+ typeof sourceSchema.$id === 'string' ||
1675
+ sourceSchema.$recursiveAnchor === true) {
1676
+ nextScope.push({ pointer: `#${childPointer}`, node: sourceChildNode });
1677
+ }
1678
+ const result = resolved.getNodeChild(key, dataRecord ?? {}, {
1679
+ pointer: `#${childPointer}`,
1680
+ path: nextScope,
1681
+ });
1682
+ if (!result.error && isSchemaNode(result.node)) {
1683
+ scopedChildNode = result.node;
1684
+ if (nextScope[nextScope.length - 1]?.node !== scopedChildNode) {
1685
+ nextScope.push({ pointer: `#${childPointer}`, node: scopedChildNode });
1686
+ }
1687
+ childScope = nextScope;
1688
+ }
1689
+ }
1690
+ let applicableChildNode = reducedChildNode;
1691
+ let scopedApplicableChildNode = scopedChildNode;
1692
+ const hasScopedReference = typeof sourceSchema.$dynamicRef === 'string' || typeof sourceSchema.$recursiveRef === 'string';
1693
+ for (const source of applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability)) {
1694
+ const alreadyApplicable = applicableChildNode !== undefined && positionOf(applicableChildNode) === positionOf(source);
1695
+ const alreadyScoped = scopedApplicableChildNode !== undefined && positionOf(scopedApplicableChildNode) === positionOf(source);
1696
+ if (alreadyApplicable && (!scopedApplicableChildNode || alreadyScoped))
1697
+ continue;
1698
+ const reduced = reduceAdditionalPropertySchema(source, childData, scope);
1699
+ if (!alreadyApplicable) {
1700
+ applicableChildNode =
1701
+ applicableChildNode === undefined
1702
+ ? reduced
1703
+ : mergeNode(applicableChildNode, reduced) ?? applicableChildNode;
1704
+ }
1705
+ if (scopedApplicableChildNode && !alreadyScoped) {
1706
+ scopedApplicableChildNode = mergeNode(scopedApplicableChildNode, reduced) ?? scopedApplicableChildNode;
1707
+ }
1708
+ }
1709
+ // Applicable declarations retain their authored scope here. The parent
1710
+ // reducer merges `properties` across `allOf`, which can make an
1711
+ // `additionalProperties` schema in a separate branch appear inapplicable.
1712
+ // Failing that, use the reducer's child, then the selected branch, then a
1713
+ // stable placeholder from the candidate set.
1714
+ const childNode = (hasScopedReference ? scopedApplicableChildNode : undefined) ??
1715
+ applicableChildNode ??
1065
1716
  composeChild(candidate.direct, provisionalChildNode) ??
1066
1717
  candidatePrototype(candidate);
1067
- const positions = childPositions(declaredAt, key,
1718
+ let positions = childPositions(declaredAt, key,
1068
1719
  // This node's own data, not the child's: the branches being entered
1069
1720
  // are this location's, so they are decided by the instance here. The
1070
1721
  // child's own conditional edges are entered by the child's walk, with
1071
- // the child's data.
1072
- data, childDeclaring(info, key));
1073
- const enter = () => walk(childNode, childPointer, dataRecord !== undefined && Object.hasOwn(dataRecord, key) ? dataRecord[key] : undefined, childActive, childProvisional, nodes, diagnostics, positions, ctx, true, selfLineage, [...path, index]);
1722
+ // the child's data. `applicableData` is the reducer's object stand-in
1723
+ // when this location has no object value yet.
1724
+ applicableData, childDeclaring(info, key), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
1725
+ const dynamicChildDeclarations = dynamicApplicableNodes.flatMap((applicableNode) => {
1726
+ const dynamicChild = applicableNode.properties?.[key];
1727
+ return dynamicChild ? [dynamicChild] : [];
1728
+ });
1729
+ positions = {
1730
+ ...positions,
1731
+ declaring: extendDeclaring(positions.declaring, dynamicChildDeclarations),
1732
+ };
1733
+ if (scopedChildNode && hasScopedReference && !positions.declaring.includes(scopedChildNode)) {
1734
+ positions = {
1735
+ ...positions,
1736
+ applicable: [...positions.applicable, scopedChildNode],
1737
+ declaring: [...positions.declaring, scopedChildNode],
1738
+ };
1739
+ }
1740
+ const enter = () => walk(childNode, childPointer, childData, childActive, childProvisional, nodes, diagnostics, positions, ctx, true, selfLineage, [...path, index], childScope, hasScopedReference);
1074
1741
  // Deferring the children the budget counts, by depth below the anchor, admits it breadth first.
1075
1742
  const counted = selfLineage && (recursive || budgeted(locationInfo(positions.declaring, ctx.cache)));
1076
1743
  if (counted)
@@ -1080,6 +1747,7 @@ lineage, path) {
1080
1747
  }
1081
1748
  return;
1082
1749
  }
1750
+ const itemSchema = type === 'array' && resolved.items ? dereference(resolved.items).schema : undefined;
1083
1751
  nodes.set(toPointer(pointer), {
1084
1752
  type,
1085
1753
  format: typeof schema.format === 'string' ? schema.format : undefined,
@@ -1089,7 +1757,7 @@ lineage, path) {
1089
1757
  active: nodeActive,
1090
1758
  provisional: nodeProvisional ? true : undefined,
1091
1759
  defaultConflict,
1092
- annotations: extractAnnotations(schema, conflictedDefault),
1760
+ annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
1093
1761
  ...(defaultSources !== undefined ? { defaultSources } : {}),
1094
1762
  // Deliberately not given the flag above. A conflict under `items` belongs
1095
1763
  // to the element locations, and each of those reports its own; this field
@@ -1097,14 +1765,14 @@ lineage, path) {
1097
1765
  // omitting its `default` would be an omission no pointer could report. It
1098
1766
  // is also the only annotation set nothing reads a `default` from, and the
1099
1767
  // only one the other adapter does not produce at all.
1100
- itemAnnotations: type === 'array' && resolved.items
1101
- ? extractAnnotations(dereference(resolved.items).schema)
1768
+ itemAnnotations: typeof itemSchema === 'object' && itemSchema !== null
1769
+ ? extractAnnotations(itemSchema)
1102
1770
  : undefined,
1103
1771
  });
1104
1772
  if (type === 'array' && resolved.items && Array.isArray(data)) {
1105
- const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info));
1773
+ const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
1106
1774
  data.forEach((item, index) => {
1107
- walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index]);
1775
+ walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index], scope);
1108
1776
  });
1109
1777
  }
1110
1778
  }
@@ -1118,13 +1786,17 @@ lineage, path) {
1118
1786
  * contract), while array items are walked from `data` (an array's length is a data-time
1119
1787
  * fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
1120
1788
  */
1121
- export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
1789
+ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS, options = {}) {
1122
1790
  const nodes = new Map();
1123
1791
  const diagnostics = [];
1124
1792
  const ctx = {
1125
1793
  cache,
1794
+ branchApplicability: cache.memoizeUndefinedBranchResults ? new WeakMap() : undefined,
1126
1795
  limits,
1127
1796
  boundaries: new Map(),
1797
+ boundaryTargets: new Map(),
1798
+ expandedBoundaryTokens: options.expandedBoundaryTokens ?? new Set(),
1799
+ boundaryGeneration: options.boundaryGeneration ?? 0,
1128
1800
  removed: new Set(),
1129
1801
  flagged: new Set(),
1130
1802
  reserved: new Set(),
@@ -1132,7 +1804,7 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
1132
1804
  objectsUsed: 0,
1133
1805
  nodesUsed: 0,
1134
1806
  };
1135
- walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, []);
1807
+ walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, [], [{ pointer: '#', node: root }]);
1136
1808
  for (const level of ctx.queue)
1137
1809
  for (const { enter } of (level ?? []).sort((a, b) => walkOrder(a.path, b.path)))
1138
1810
  enter();
@@ -1152,6 +1824,13 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
1152
1824
  if (node) {
1153
1825
  ;
1154
1826
  node.boundaries = ['recursion', 'budget'].filter((reason) => reasons.has(reason));
1827
+ const targets = ctx.boundaryTargets.get(pointer);
1828
+ if (targets) {
1829
+ ;
1830
+ node.boundaryTargets = [
1831
+ ...targets.values(),
1832
+ ];
1833
+ }
1155
1834
  }
1156
1835
  }
1157
1836
  return { nodes, diagnostics };