@texaryn/schema-json 0.7.1 → 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 (50) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +28 -0
  3. package/THIRD-PARTY-NOTICES.md +7 -0
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +464 -13
  6. package/dist/adapter.js.map +1 -1
  7. package/dist/bare-maps.js +1 -1
  8. package/dist/identity.d.ts +11 -2
  9. package/dist/identity.d.ts.map +1 -1
  10. package/dist/identity.js +103 -22
  11. package/dist/identity.js.map +1 -1
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/local-pointer-aliases.d.ts +4 -0
  17. package/dist/local-pointer-aliases.d.ts.map +1 -0
  18. package/dist/local-pointer-aliases.js +345 -0
  19. package/dist/local-pointer-aliases.js.map +1 -0
  20. package/dist/normalize.d.ts +1 -1
  21. package/dist/normalize.d.ts.map +1 -1
  22. package/dist/normalize.js +3 -1
  23. package/dist/normalize.js.map +1 -1
  24. package/dist/projection-validation-coherence.d.ts +11 -0
  25. package/dist/projection-validation-coherence.d.ts.map +1 -0
  26. package/dist/projection-validation-coherence.js +213 -0
  27. package/dist/projection-validation-coherence.js.map +1 -0
  28. package/dist/projection.d.ts +2 -2
  29. package/dist/projection.d.ts.map +1 -1
  30. package/dist/projection.js +917 -152
  31. package/dist/projection.js.map +1 -1
  32. package/dist/resources.d.ts +19 -0
  33. package/dist/resources.d.ts.map +1 -0
  34. package/dist/resources.js +730 -0
  35. package/dist/resources.js.map +1 -0
  36. package/dist/root-reference.js +1 -1
  37. package/dist/schema-graph.d.ts +7 -0
  38. package/dist/schema-graph.d.ts.map +1 -1
  39. package/dist/schema-graph.js +79 -20
  40. package/dist/schema-graph.js.map +1 -1
  41. package/dist/submission-projection.d.ts +5 -0
  42. package/dist/submission-projection.d.ts.map +1 -0
  43. package/dist/submission-projection.js +233 -0
  44. package/dist/submission-projection.js.map +1 -0
  45. package/dist/types.d.ts +6 -1
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/vendor/json-schema-library/LICENSE.md +9 -0
  48. package/dist/vendor/json-schema-library/index.mjs +2 -0
  49. package/dist/vendor/json-schema-library/index.mjs.map +1 -0
  50. package/package.json +8 -3
@@ -1,5 +1,5 @@
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
3
  import { inferProjectionShape, shapeDiagnostic } from './projection-shape.js';
4
4
  import { POSITION } from './schema-graph.js';
5
5
  const VALID_TYPES = new Set([
@@ -11,6 +11,18 @@ const VALID_TYPES = new Set([
11
11
  'array',
12
12
  'null',
13
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
+ ]);
14
26
  function toPointer(value) {
15
27
  return value;
16
28
  }
@@ -84,8 +96,8 @@ function extractAnnotations(schema,
84
96
  * Set where two declarations apply to this location whatever the instance is
85
97
  * and disagree. The merged schema still carries one of them, and which one is
86
98
  * the merge's traversal order rather than an answer.
87
- */
88
- omitDefault = false) {
99
+ */
100
+ omitDefault = false, defaultOverride) {
89
101
  const annotations = {};
90
102
  if (typeof schema.title === 'string')
91
103
  annotations.title = schema.title;
@@ -99,8 +111,15 @@ omitDefault = false) {
99
111
  annotations.deprecated = schema.deprecated;
100
112
  if (Array.isArray(schema.examples))
101
113
  annotations.examples = schema.examples;
102
- if (!omitDefault && 'default' in schema)
103
- 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
+ }
104
123
  return annotations;
105
124
  }
106
125
  function extractEnumValues(schema) {
@@ -109,30 +128,339 @@ function extractEnumValues(schema) {
109
128
  return schema.enum.map((value) => ({ value }));
110
129
  }
111
130
  function dereferenceChecked(node) {
112
- let current = node;
113
- const seen = new Set();
114
- 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) => {
115
137
  const position = positionOf(current);
116
- if (seen.has(position))
117
- return { node: current, cycle: true };
118
- seen.add(position);
119
- const next = followRef(current);
120
- if (!next) {
121
- const raw = current.resolveRef();
122
- 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];
123
216
  }
124
- current = next;
125
- if (!onlyPoints(current))
126
- break;
127
217
  }
128
- 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 };
129
271
  }
130
272
  /** Follows a $ref to the node it points at; returns the node unchanged otherwise. */
131
273
  function dereference(node) {
132
274
  return dereferenceChecked(node).node;
133
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
+ }
134
462
  // A dialect whose reducer leaves $ref unresolved reduces a selected `{ $ref }` branch to `{}`.
135
- function resolveSelectedBranch(original, reduced, data) {
463
+ function resolveSelectedBranch(original, reduced, data, scope = [], branchApplicability) {
136
464
  const reducedSchema = reduced.schema;
137
465
  if (reducedSchema && typeof reducedSchema === 'object') {
138
466
  if (resolveExplicitType(reducedSchema) || inferProjectionShape(reducedSchema).kind === 'resolved') {
@@ -141,26 +469,41 @@ function resolveSelectedBranch(original, reduced, data) {
141
469
  }
142
470
  const selected = [];
143
471
  if (original.oneOf) {
144
- const matching = original.oneOf.filter((branch) => branchApplies(branch, data));
472
+ const matching = original.oneOf.filter((branch) => branchApplies(branch, data, scope, branchApplicability));
145
473
  if (matching.length === 1)
146
474
  selected.push(matching[0]);
147
475
  }
148
476
  for (const branch of original.anyOf ?? []) {
149
- if (branchApplies(branch, data))
477
+ if (branchApplies(branch, data, scope, branchApplicability))
150
478
  selected.push(branch);
151
479
  }
152
- if (!selected.some((branch) => typeof branch.$ref === 'string'))
480
+ if (!selected.some((branch) => referenceOf(branch) !== undefined))
153
481
  return reduced;
154
482
  let merged = reduced;
155
483
  for (const branch of selected) {
156
484
  const target = dereference(branch);
157
485
  if (!isSchemaNode(target))
158
486
  continue;
159
- const targetReduced = target.reduceNode(data).node ?? target;
487
+ const targetReduced = target.reduceNode(data, { path: [...scope] }).node ?? target;
160
488
  merged = mergeNode(merged, targetReduced) ?? merged;
161
489
  }
162
490
  return merged;
163
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
+ }
164
507
  export const DEFAULT_LIMITS = { objects: 16, nodes: 512 };
165
508
  function walkOrder(a, b) {
166
509
  for (let i = 0; i < Math.min(a.length, b.length); i++)
@@ -168,10 +511,19 @@ function walkOrder(a, b) {
168
511
  return a[i] - b[i];
169
512
  return a.length - b.length;
170
513
  }
171
- 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) {
172
518
  const reasons = ctx.boundaries.get(pointer) ?? new Set();
173
519
  reasons.add(reason);
174
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);
175
527
  }
176
528
  // A leaf needs no reduction, so it rides with its parent's budget unit.
177
529
  function isLeafSchema(schema) {
@@ -184,17 +536,14 @@ function isLeafSchema(schema) {
184
536
  return ('enum' in record || 'const' in record) && !('properties' in record);
185
537
  }
186
538
  /**
187
- * The position of a schema within the document it was authored in, as a JSON
188
- * Pointer with the root as the empty string.
189
- *
190
- * `schemaLocation` is already that, prefixed with the `#` of a URI fragment,
191
- * and it names what a `$ref` resolves to rather than the reference. Both
192
- * adapters report sources in this form so a diagnostic means the same thing
193
- * 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.
194
544
  */
195
545
  function documentPointer(node) {
196
- const location = node.schemaLocation;
197
- return typeof location === 'string' ? location.replace(/^#/, '') : '';
546
+ return positionOf(node).replace(/^#/, '');
198
547
  }
199
548
  /**
200
549
  * Visits every schema position that applies to one location whatever the
@@ -217,16 +566,13 @@ function documentPointer(node) {
217
566
  * several places at once: `properties/x` on the node's own schema and on each
218
567
  * of its `allOf` branches are all declarations of the same location.
219
568
  */
220
- function eachUnconditional(roots, visitor) {
569
+ function eachUnconditional(roots, visitor, referenceVisitor, cache) {
221
570
  const visited = new Set();
222
571
  const visit = (current) => {
572
+ if (referenceVisitor && cache)
573
+ visitReferenceDefaultSites(current, cache, referenceVisitor);
223
574
  const resolved = dereference(current);
224
- // The same guard, and for the same measured reason, as
225
- // `collectCandidateProperties`: `resolveRef()` returns a fresh node every
226
- // call, so identity alone never closes a cycle, and an inline branch has no
227
- // location of its own to key on.
228
- const location = resolved.schemaLocation;
229
- const key = typeof location === 'string' ? location : resolved;
575
+ const key = traversalKey(resolved);
230
576
  if (visited.has(key))
231
577
  return;
232
578
  visited.add(key);
@@ -249,9 +595,15 @@ function eachUnconditional(roots, visitor) {
249
595
  * own descent already survives the shapes that throw (#119, #121), and a
250
596
  * conflict is the wrong place to surface one.
251
597
  */
252
- function branchApplies(branch, data) {
598
+ function branchApplies(branch, data, scope = [], branchApplicability) {
599
+ if (data === undefined && branchApplicability?.has(branch)) {
600
+ return branchApplicability.get(branch);
601
+ }
253
602
  try {
254
- 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;
255
607
  }
256
608
  catch {
257
609
  return false;
@@ -270,18 +622,23 @@ function branchApplies(branch, data) {
270
622
  * rule the projection uses to expose that branch, so the two cannot disagree
271
623
  * about which branch it is.
272
624
  */
273
- function eachApplicable(roots, data, visitor) {
625
+ function eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope = [], dynamicReferenceProjection = false) {
274
626
  const visited = new Set();
275
- const visit = (current) => {
276
- const resolved = dereference(current);
277
- const location = resolved.schemaLocation;
278
- 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);
279
636
  if (visited.has(key))
280
637
  return;
281
638
  visited.add(key);
282
639
  visitor(resolved);
283
640
  for (const branch of resolved.allOf ?? [])
284
- visit(branch);
641
+ visit(branch, currentScope);
285
642
  // `oneOf` contributes the one branch it selects, which is the evaluator's
286
643
  // rule and not "every branch the data matches": an empty object matches
287
644
  // every permissive branch, and treating all of them as applicable would
@@ -290,82 +647,123 @@ function eachApplicable(roots, data, visitor) {
290
647
  // branch stands in, which is what the projection exposes.
291
648
  const oneOf = resolved.oneOf ?? [];
292
649
  if (oneOf.length > 0) {
293
- const matching = oneOf.filter((branch) => branchApplies(branch, data));
650
+ const matching = oneOf.filter((branch) => branchApplies(branch, data, currentScope, branchApplicability));
294
651
  const selected = matching.length === 1
295
652
  ? matching[0]
296
653
  : selectProvisionalBranch(resolved, typeof data === 'object' && data !== null
297
654
  ? data
298
655
  : undefined);
299
656
  if (selected)
300
- visit(selected);
657
+ visit(selected, currentScope);
301
658
  }
302
659
  // `anyOf` is the opposite case and needs no such rule: several of its
303
660
  // branches applying at once is ordinary, and each that validates
304
661
  // contributes its annotations, so each competes.
305
662
  for (const branch of resolved.anyOf ?? []) {
306
- if (branchApplies(branch, data))
307
- visit(branch);
663
+ if (branchApplies(branch, data, currentScope, branchApplicability))
664
+ visit(branch, currentScope);
308
665
  }
309
666
  if (resolved.if) {
310
- const taken = branchApplies(resolved.if, data) ? resolved.then : resolved.else;
667
+ const taken = branchApplies(resolved.if, data, currentScope, branchApplicability) ? resolved.then : resolved.else;
311
668
  if (taken)
312
- visit(taken);
669
+ visit(taken, currentScope);
313
670
  }
314
671
  const dependent = resolved.dependentSchemas;
315
672
  if (dependent && typeof data === 'object' && data !== null && !Array.isArray(data)) {
316
673
  for (const [dependency, subschema] of Object.entries(dependent)) {
317
674
  if (Object.prototype.hasOwnProperty.call(data, dependency))
318
- visit(subschema);
675
+ visit(subschema, currentScope);
319
676
  }
320
677
  }
321
678
  };
322
679
  for (const root of roots)
323
- visit(root);
680
+ visit(root, scope);
324
681
  }
325
- function declarationsFrom(walkRoots) {
326
- const declarations = [];
327
- walkRoots((node) => {
328
- const schema = node.schema;
329
- if (schema !== null && typeof schema === 'object' && 'default' in schema) {
330
- 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;
331
700
  }
332
- });
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);
333
731
  return declarations;
334
732
  }
335
- function collectUnconditionalDefaults(roots) {
336
- return declarationsFrom((visitor) => eachUnconditional(roots, visitor));
733
+ function collectUnconditionalDefaults(roots, cache) {
734
+ return declarationsFrom((visitor, referenceVisitor) => eachUnconditional(roots, visitor, referenceVisitor, cache), cache);
337
735
  }
338
- function collectApplicableDefaults(roots, data) {
339
- 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);
340
738
  }
341
739
  /** The positions that declare one property of `roots`, by both reachability rules. */
342
- function childPositions(roots, key, data, declaring) {
740
+ function childPositions(roots, key, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
343
741
  const unconditional = [];
344
742
  eachUnconditional(roots.unconditional, (node) => {
345
- const child = node.properties?.[key];
743
+ const child = memberSchema(node, key);
346
744
  if (child)
347
745
  unconditional.push(child);
348
746
  });
349
747
  const applicable = [];
350
- eachApplicable(roots.applicable, data, (node) => {
351
- const child = node.properties?.[key];
748
+ eachApplicable(roots.applicable, data, branchApplicability, (node) => {
749
+ const child = memberSchema(node, key);
352
750
  if (child)
353
751
  applicable.push(child);
354
- });
752
+ }, undefined, undefined, scope, dynamicReferenceProjection);
355
753
  return { unconditional, applicable, declaring };
356
754
  }
357
755
  /** The positions that declare the elements of `roots`, by both reachability rules. */
358
- function itemPositions(roots, data, declaring) {
756
+ function itemPositions(roots, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
359
757
  const unconditional = [];
360
758
  eachUnconditional(roots.unconditional, (node) => {
361
759
  if (node.items)
362
760
  unconditional.push(node.items);
363
761
  });
364
762
  const applicable = [];
365
- eachApplicable(roots.applicable, data, (node) => {
763
+ eachApplicable(roots.applicable, data, branchApplicability, (node) => {
366
764
  if (node.items)
367
765
  applicable.push(node.items);
368
- });
766
+ }, undefined, undefined, scope, dynamicReferenceProjection);
369
767
  return { unconditional, applicable, declaring };
370
768
  }
371
769
  /**
@@ -380,16 +778,75 @@ function disagreeingDefaults(declarations) {
380
778
  const [first, ...rest] = declarations;
381
779
  return rest.every((other) => deepEqual(other.value, first.value)) ? undefined : declarations;
382
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();
383
842
  /**
384
- * Every property key that could appear at this node's own instance location,
385
- * across every branch that might apply to it.
843
+ * Property keys that might need to appear at this instance location.
386
844
  *
387
- * This is the static half of the inactive-node contract: a branch the data does
388
- * not currently select still contributes its property pointers, which then
389
- * project with `active: false`. The dynamic half is `reduceNode`, which decides
390
- * which of them apply now, and the two must stay separate. Making the candidate
391
- * set data-driven would delete a pointer the moment its branch stopped
392
- * 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`.
393
850
  *
394
851
  * **Recursion is through applicators only, and only those acting on this same
395
852
  * instance location**: `if`, `then`, `else`, `allOf`, `anyOf`, `oneOf`,
@@ -409,20 +866,20 @@ function disagreeingDefaults(declarations) {
409
866
  * - **`items` and `prefixItems`**, for the same reason: an array's items are
410
867
  * their own locations.
411
868
  *
412
- * The cycle guard keys on `schemaLocation` rather than on node identity, which
413
- * was measured rather than assumed and is the opposite of what it looks like it
414
- * should be: `resolveRef()` returns a fresh `SchemaNode` on every call, and the
415
- * raw `schema` object it wraps is fresh too, so an identity `Set` never matches
416
- * and a recursive `$ref` would not terminate. `schemaLocation` is stable, is
417
- * distinct for each inline branch position, and repeats when a reference closes
418
- * a cycle, which is precisely the three properties needed. The set is per call,
419
- * because deduplicating a schema location is only sound while collecting names
420
- * for one instance location; the same schema legitimately recurs at a deeper
421
- * 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.
422
873
  */
423
- 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
+ }
424
880
  const candidates = new Map();
425
881
  const visited = new Set();
882
+ const scopes = [];
426
883
  const record = (key, propNode, direct) => {
427
884
  const entry = candidates.get(key) ?? { alternatives: [] };
428
885
  if (direct)
@@ -433,26 +890,11 @@ function collectCandidateProperties(node) {
433
890
  };
434
891
  const visit = (current, own) => {
435
892
  const resolved = dereference(current);
436
- // Keyed on `schemaLocation` where there is one, and on the node itself
437
- // where there is not, which together cover every way the traversal can
438
- // come back to where it started.
439
- //
440
- // Neither alone is sufficient and each covers what the other cannot. A
441
- // cycle requires a `$ref`, because an inline schema cannot nest into
442
- // itself, and `resolveRef()` returns a fresh `SchemaNode` on every call
443
- // whose raw `schema` object is fresh too, so identity never matches across
444
- // one; those nodes do carry a location. An inline branch is never resolved
445
- // through a ref, so its identity is stable within one traversal.
446
- //
447
- // This deliberately replaced a depth cap. A cap terminates, but it does so
448
- // by dropping candidates a valid schema declared, which is the silent
449
- // disappearance the whole projection-diagnostic design exists to prevent:
450
- // a field nested under 70 applicators is still a field.
451
- const location = resolved.schemaLocation;
452
- const key = typeof location === 'string' ? location : resolved;
893
+ const key = traversalKey(resolved);
453
894
  if (visited.has(key))
454
895
  return;
455
896
  visited.add(key);
897
+ scopes.push({ node: resolved, own });
456
898
  for (const [key, propNode] of Object.entries(resolved.properties ?? {})) {
457
899
  record(key, propNode, own);
458
900
  }
@@ -460,7 +902,9 @@ function collectCandidateProperties(node) {
460
902
  if (branch)
461
903
  visit(branch, false);
462
904
  }
463
- 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]) {
464
908
  for (const branch of branches ?? [])
465
909
  visit(branch, false);
466
910
  }
@@ -471,8 +915,32 @@ function collectCandidateProperties(node) {
471
915
  }
472
916
  };
473
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
+ }
474
935
  return candidates;
475
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
+ }
476
944
  /**
477
945
  * The schema to project for a candidate when no branch is active.
478
946
  *
@@ -646,6 +1114,96 @@ function computeRequiredSet(resolved, reducedSchema, dataRecord) {
646
1114
  }
647
1115
  return required;
648
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
+ }
649
1207
  // An unreadable identity is never a repeat, and is budgeted so it cannot expand without end.
650
1208
  const budgeted = (info) => info.cyclic || info.key === '';
651
1209
  /**
@@ -675,9 +1233,15 @@ declaredAt, ctx,
675
1233
  /** Entered as an object member, which is the only way to be past the data. */
676
1234
  member,
677
1235
  /** The past-the-data ancestors, nearest first, up to the nearest location holding data. */
678
- 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
+ }
679
1243
  const info = locationInfo(declaredAt.declaring, ctx.cache);
680
- const { node: original, cycle: referenceCycle } = dereferenceChecked(node);
1244
+ const { node: original, cycle: referenceCycle, unresolved: unresolvedReference } = dereferenceChecked(node);
681
1245
  if (info.cycle || referenceCycle) {
682
1246
  diagnostics.push({
683
1247
  pointer: toPointer(pointer),
@@ -688,10 +1252,13 @@ lineage, path) {
688
1252
  return;
689
1253
  }
690
1254
  const pastData = member && (data === undefined || data === null);
691
- 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));
692
1257
  for (let ancestor = pastData ? lineage : undefined; ancestor; ancestor = ancestor.parent) {
693
- if (info.key !== '' && ancestor.key === info.key) {
694
- 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);
695
1262
  ctx.removed.add(pointer);
696
1263
  return;
697
1264
  }
@@ -702,21 +1269,29 @@ lineage, path) {
702
1269
  let admit;
703
1270
  if (isLeafSchema(originalSchema)) {
704
1271
  const covered = ctx.reserved.has(parent);
705
- admit = covered || ctx.nodesUsed < ctx.limits.nodes;
1272
+ admit = ctx.expandedBoundaryTokens.has(token) || covered || ctx.nodesUsed < ctx.limits.nodes;
706
1273
  if (admit && !covered)
707
1274
  ctx.nodesUsed += 1;
708
1275
  }
709
1276
  else {
710
- const leaves = [...collectCandidateProperties(original).values()].filter((candidate) => isLeafSchema(dereference(candidatePrototype(candidate)).schema)).length;
711
- 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);
712
1286
  if (admit) {
713
1287
  ctx.objectsUsed += 1;
714
- ctx.nodesUsed += 1 + leaves;
715
- ctx.reserved.add(pointer);
1288
+ ctx.nodesUsed += explicitlyExpanded ? 1 : 1 + leaves;
1289
+ if (!explicitlyExpanded)
1290
+ ctx.reserved.add(pointer);
716
1291
  }
717
1292
  }
718
1293
  if (!admit) {
719
- addBoundary(ctx, parent, 'budget');
1294
+ addBoundary(ctx, parent, 'budget', pointer, info.key);
720
1295
  ctx.removed.add(pointer);
721
1296
  return;
722
1297
  }
@@ -727,6 +1302,36 @@ lineage, path) {
727
1302
  let resolved = original;
728
1303
  let schema = originalSchema;
729
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
+ }
730
1335
  // Whether this node's own active branch could be determined. Stays true for every
731
1336
  // node except a typeless oneOf/anyOf wrapper whose branch could not be resolved
732
1337
  // (see below), where the node's own presence in the projection is the thing in
@@ -750,9 +1355,9 @@ lineage, path) {
750
1355
  let wrapperIdentified = false;
751
1356
  if (!type && (original.oneOf || original.anyOf)) {
752
1357
  composedAgainstData = true;
753
- const { node: branchNode } = original.reduceNode(data);
1358
+ const { node: branchNode } = original.reduceNode(data, { path: [...scope] });
754
1359
  if (branchNode) {
755
- resolved = resolveSelectedBranch(original, branchNode, data);
1360
+ resolved = resolveSelectedBranch(original, branchNode, data, scope, ctx.branchApplicability);
756
1361
  schema = resolved.schema;
757
1362
  type = resolveExplicitType(schema);
758
1363
  }
@@ -834,6 +1439,15 @@ lineage, path) {
834
1439
  diagnostics.push(shapeDiagnostic(toPointer(pointer), shape, schema.enum !== undefined));
835
1440
  }
836
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;
1449
+ }
1450
+ }
837
1451
  if (!type)
838
1452
  return;
839
1453
  const selfLineage = pastData
@@ -843,7 +1457,7 @@ lineage, path) {
843
1457
  // pointer with no node has nothing to omit an annotation from, and saying
844
1458
  // that its `default` is undecidable on top of saying it cannot be drawn at
845
1459
  // all would be the same schema reported twice.
846
- const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional));
1460
+ const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional, ctx.cache));
847
1461
  if (ambiguousDefault) {
848
1462
  diagnostics.push({
849
1463
  pointer: toPointer(pointer),
@@ -858,13 +1472,18 @@ lineage, path) {
858
1472
  // selects. A superset of the diagnostic's: every unconditional disagreement
859
1473
  // is also an applicable one, so the node carries both kinds and a consumer
860
1474
  // reads one place.
861
- 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);
862
1477
  const applicableConflict = disagreeingDefaults(applicableDeclarations);
863
1478
  const conflictedDefault = applicableConflict !== undefined || ambiguousDefault !== undefined;
864
1479
  const defaultConflict = (applicableConflict ?? ambiguousDefault)?.map((declaration) => declaration.source);
865
- const defaultSources = !conflictedDefault && 'default' in schema && info.cyclic
1480
+ const defaultSources = !conflictedDefault && applicableDeclarations.length > 0 && info.cyclic
866
1481
  ? [...new Set(applicableDeclarations.map((declaration) => declaration.source))].sort()
867
1482
  : undefined;
1483
+ const defaultOverride = {
1484
+ present: applicableDeclarations.length > 0,
1485
+ value: applicableDeclarations[0]?.value,
1486
+ };
868
1487
  const nodeActive = active && branchResolved;
869
1488
  // A node the schema applies is never also provisional; the two report
870
1489
  // different facts and only the second is a choice this projection made. The
@@ -889,19 +1508,73 @@ lineage, path) {
889
1508
  // wrapper case), it has already been reduced against the real `data` at this
890
1509
  // pointer; re-reducing it against `dataRecord ?? {}` here would be redundant (and,
891
1510
  // for a branch with no dynamic keywords of its own, a no-op), so it is skipped.
892
- 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
+ }
893
1522
  if (reducedNode &&
894
1523
  resolved === original &&
895
1524
  !resolveExplicitType(originalSchema) &&
896
1525
  (original.oneOf || original.anyOf)) {
897
- reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {});
1526
+ reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
898
1527
  }
899
1528
  const reducedSchema = reducedNode?.schema;
900
1529
  const reducedProperties = reducedSchema?.properties ??
901
1530
  schema.properties ??
902
1531
  {};
903
- 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
+ }
904
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
+ }
905
1578
  // Two conditions, and they are not the same kind of condition.
906
1579
  //
907
1580
  // Exposure is load-bearing. Selection is local, but whether this node is
@@ -921,11 +1594,32 @@ lineage, path) {
921
1594
  : undefined;
922
1595
  const provisionalSchema = provisionalBranch?.schema;
923
1596
  const provisionalKeys = new Set(Object.keys(provisionalSchema?.properties ?? {}));
1597
+ for (const key of additionalPropertyKeys(provisionalBranch, dataRecord))
1598
+ provisionalKeys.add(key);
924
1599
  const provisionalRequired = new Set(Array.isArray(provisionalSchema?.required) ? provisionalSchema.required : []);
925
1600
  // Candidates are collected from `original`, not `resolved`, so every oneOf/anyOf
926
1601
  // branch's properties are represented (the matching branch alone, via `resolved`,
927
1602
  // would only expose its own properties).
928
- 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
+ }
929
1623
  const propKeys = [...candidateProps.keys()];
930
1624
  const children = propKeys.length > 0
931
1625
  ? propKeys.map((key) => {
@@ -951,7 +1645,7 @@ lineage, path) {
951
1645
  active: nodeActive,
952
1646
  provisional: nodeProvisional ? true : undefined,
953
1647
  defaultConflict,
954
- annotations: extractAnnotations(schema, conflictedDefault),
1648
+ annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
955
1649
  ...(defaultSources !== undefined ? { defaultSources } : {}),
956
1650
  });
957
1651
  for (const [index, key] of propKeys.entries()) {
@@ -961,29 +1655,89 @@ lineage, path) {
961
1655
  // selected ancestor its own locally applicable properties are
962
1656
  // provisional too, while a locally inactive one stays inactive.
963
1657
  const childProvisional = !childActive && nodeExposed && (activeKeys.has(key) || provisionalKeys.has(key));
964
- const reducedChildNode = reducedNode?.properties?.[key];
1658
+ const reducedChildNode = memberSchema(reducedNode, key);
965
1659
  // Precedence matters as much as the selection does. Marking the right
966
1660
  // branch provisional while taking its shape from `candidatePrototype`,
967
1661
  // which is first-wins across branches, would render another branch's
968
1662
  // widget and annotations under the selected branch's name, and would hand
969
1663
  // ADR-003's pass another branch's `default`.
970
- const provisionalChildNode = provisionalBranch?.properties?.[key];
1664
+ const provisionalChildNode = memberSchema(provisionalBranch, key);
971
1665
  const candidate = candidateProps.get(key);
972
- // Three states, in order of authority. What the evaluator reduced against
973
- // the data is the schema as it actually applies. Failing that, the node's
974
- // own declaration composed with the selected branch, which is what the
975
- // reduction would have produced had the branch been complete. Failing
976
- // both, a stated placeholder for a location no branch has claimed.
977
- 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 ??
978
1716
  composeChild(candidate.direct, provisionalChildNode) ??
979
1717
  candidatePrototype(candidate);
980
- const positions = childPositions(declaredAt, key,
1718
+ let positions = childPositions(declaredAt, key,
981
1719
  // This node's own data, not the child's: the branches being entered
982
1720
  // are this location's, so they are decided by the instance here. The
983
1721
  // child's own conditional edges are entered by the child's walk, with
984
- // the child's data.
985
- data, childDeclaring(info, key));
986
- 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);
987
1741
  // Deferring the children the budget counts, by depth below the anchor, admits it breadth first.
988
1742
  const counted = selfLineage && (recursive || budgeted(locationInfo(positions.declaring, ctx.cache)));
989
1743
  if (counted)
@@ -1003,7 +1757,7 @@ lineage, path) {
1003
1757
  active: nodeActive,
1004
1758
  provisional: nodeProvisional ? true : undefined,
1005
1759
  defaultConflict,
1006
- annotations: extractAnnotations(schema, conflictedDefault),
1760
+ annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
1007
1761
  ...(defaultSources !== undefined ? { defaultSources } : {}),
1008
1762
  // Deliberately not given the flag above. A conflict under `items` belongs
1009
1763
  // to the element locations, and each of those reports its own; this field
@@ -1016,9 +1770,9 @@ lineage, path) {
1016
1770
  : undefined,
1017
1771
  });
1018
1772
  if (type === 'array' && resolved.items && Array.isArray(data)) {
1019
- const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info));
1773
+ const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
1020
1774
  data.forEach((item, index) => {
1021
- 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);
1022
1776
  });
1023
1777
  }
1024
1778
  }
@@ -1032,13 +1786,17 @@ lineage, path) {
1032
1786
  * contract), while array items are walked from `data` (an array's length is a data-time
1033
1787
  * fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
1034
1788
  */
1035
- export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
1789
+ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS, options = {}) {
1036
1790
  const nodes = new Map();
1037
1791
  const diagnostics = [];
1038
1792
  const ctx = {
1039
1793
  cache,
1794
+ branchApplicability: cache.memoizeUndefinedBranchResults ? new WeakMap() : undefined,
1040
1795
  limits,
1041
1796
  boundaries: new Map(),
1797
+ boundaryTargets: new Map(),
1798
+ expandedBoundaryTokens: options.expandedBoundaryTokens ?? new Set(),
1799
+ boundaryGeneration: options.boundaryGeneration ?? 0,
1042
1800
  removed: new Set(),
1043
1801
  flagged: new Set(),
1044
1802
  reserved: new Set(),
@@ -1046,7 +1804,7 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
1046
1804
  objectsUsed: 0,
1047
1805
  nodesUsed: 0,
1048
1806
  };
1049
- 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 }]);
1050
1808
  for (const level of ctx.queue)
1051
1809
  for (const { enter } of (level ?? []).sort((a, b) => walkOrder(a.path, b.path)))
1052
1810
  enter();
@@ -1066,6 +1824,13 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
1066
1824
  if (node) {
1067
1825
  ;
1068
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
+ }
1069
1834
  }
1070
1835
  }
1071
1836
  return { nodes, diagnostics };