@texaryn/schema-json 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +30 -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 +952 -156
  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,6 +1,6 @@
1
- import { mergeNode, isSchemaNode } from 'json-schema-library';
2
- import { childDeclaring, followRef, itemDeclaring, locationInfo, onlyPoints, positionOf, } from './identity.js';
3
- import { inferProjectionShape, shapeDiagnostic } from './projection-shape.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, projectionTypeFamilies, shapeDiagnostic, shapeOfFamilies, } from './projection-shape.js';
4
4
  import { POSITION } from './schema-graph.js';
5
5
  const VALID_TYPES = new Set([
6
6
  'string',
@@ -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
  }
@@ -46,7 +58,7 @@ function compositionHasRenderableAlternative(node) {
46
58
  return false;
47
59
  if (resolveExplicitType(schema))
48
60
  return true;
49
- return inferProjectionShape(schema).kind === 'resolved';
61
+ return inferAllOfProjectionShape(resolved, schema).kind === 'resolved';
50
62
  });
51
63
  }
52
64
  function extractConstraints(schema) {
@@ -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,58 +128,413 @@ 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 inferAllOfProjectionShape(node, schema = node.schema) {
277
+ const families = new Set();
278
+ const addSchema = (candidate) => {
279
+ if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate))
280
+ return;
281
+ for (const family of projectionTypeFamilies(candidate)) {
282
+ families.add(family);
283
+ }
284
+ };
285
+ addSchema(schema);
286
+ const visited = new Set();
287
+ const visit = (current) => {
288
+ const position = positionOf(current);
289
+ if (visited.has(position))
290
+ return;
291
+ visited.add(position);
292
+ for (const branch of current.allOf ?? []) {
293
+ const reference = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
294
+ if (reference?.unresolved || (reference && !isSchemaNode(reference.node)))
295
+ continue;
296
+ const target = reference?.node ?? branch;
297
+ if (!isSchemaNode(target))
298
+ continue;
299
+ addSchema(target.schema);
300
+ visit(target);
301
+ }
302
+ };
303
+ visit(node);
304
+ return shapeOfFamilies(families);
305
+ }
306
+ function visitReferenceDefaultSites(node, cache, visitor, dynamicReferenceProjection = false) {
307
+ if (cache.dialect === 'draft-07' || referenceOf(node) === undefined)
308
+ return;
309
+ const pending = [node];
310
+ const visited = new Set();
311
+ while (pending.length > 0) {
312
+ const current = pending.pop();
313
+ const position = positionOf(current);
314
+ if (visited.has(position))
315
+ continue;
316
+ visited.add(position);
317
+ const authored = authoredSchema(current, cache);
318
+ if (authored !== null &&
319
+ typeof authored === 'object' &&
320
+ 'default' in authored) {
321
+ visitor(current);
322
+ }
323
+ for (const keyword of referenceKeywords(current)) {
324
+ if (dynamicReferenceProjection && (keyword === '$dynamicRef' || keyword === '$recursiveRef'))
325
+ continue;
326
+ const target = followRef(current, keyword);
327
+ if (target)
328
+ pending.push(target);
329
+ }
330
+ }
331
+ }
332
+ function traversalKey(node) {
333
+ const parent = node.parent;
334
+ const isInPlaceBranch = parent !== undefined &&
335
+ ['if', 'then', 'else', 'contains'].some((keyword) => parent[keyword] === node || node.evaluationPath === `${parent.evaluationPath}/${keyword}`);
336
+ if (isInPlaceBranch) {
337
+ return positionOf(node);
338
+ }
339
+ const location = node.schemaLocation;
340
+ return typeof location === 'string' ? location : positionOf(node);
341
+ }
342
+ function allOfExplicitType(node, visited = new Set()) {
343
+ const position = positionOf(node);
344
+ if (visited.has(position))
345
+ return undefined;
346
+ visited.add(position);
347
+ for (const branch of node.allOf ?? []) {
348
+ const reference = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
349
+ if (reference?.unresolved || (reference && !isSchemaNode(reference.node)))
350
+ continue;
351
+ const target = reference?.node ?? branch;
352
+ const schema = target.schema;
353
+ if (schema && typeof schema === 'object') {
354
+ const type = resolveExplicitType(schema);
355
+ if (type)
356
+ return type;
357
+ }
358
+ const nested = allOfExplicitType(target, visited);
359
+ if (nested)
360
+ return nested;
361
+ }
362
+ return undefined;
363
+ }
364
+ function declaresObject(node, visited = new Set()) {
365
+ const resolved = dereference(node);
366
+ if (!isSchemaNode(resolved))
367
+ return false;
368
+ const position = positionOf(resolved);
369
+ if (visited.has(position))
370
+ return false;
371
+ visited.add(position);
372
+ const schema = resolved.schema;
373
+ if (!schema || typeof schema !== 'object')
374
+ return false;
375
+ const explicitType = resolveExplicitType(schema);
376
+ if (explicitType)
377
+ return explicitType === 'object';
378
+ const allOfType = allOfExplicitType(resolved);
379
+ if (allOfType)
380
+ return allOfType === 'object';
381
+ const shape = inferAllOfProjectionShape(resolved, schema);
382
+ if (shape.kind === 'resolved')
383
+ return shape.type === 'object';
384
+ return (resolved.allOf ?? []).some((branch) => declaresObject(branch, visited));
385
+ }
386
+ function declaresArray(node, visited = new Set()) {
387
+ const resolved = dereference(node);
388
+ if (!isSchemaNode(resolved))
389
+ return false;
390
+ const position = positionOf(resolved);
391
+ if (visited.has(position))
392
+ return false;
393
+ visited.add(position);
394
+ const schema = resolved.schema;
395
+ if (!schema || typeof schema !== 'object')
396
+ return false;
397
+ const explicitType = resolveExplicitType(schema);
398
+ if (explicitType)
399
+ return explicitType === 'array';
400
+ const allOfType = allOfExplicitType(resolved);
401
+ if (allOfType)
402
+ return allOfType === 'array';
403
+ const shape = inferAllOfProjectionShape(resolved, schema);
404
+ if (shape.kind === 'resolved')
405
+ return shape.type === 'array';
406
+ return (resolved.allOf ?? []).some((branch) => declaresArray(branch, visited));
407
+ }
408
+ function referenceCompositionType(parts) {
409
+ const explicit = new Set(parts
410
+ .map((part) => {
411
+ const schema = part.schema;
412
+ return (schema && typeof schema === 'object' ? resolveExplicitType(schema) : undefined) ?? allOfExplicitType(part);
413
+ })
414
+ .filter((type) => type !== undefined));
415
+ if (explicit.size === 1)
416
+ return [...explicit][0];
417
+ if (explicit.size > 1)
418
+ return undefined;
419
+ const object = parts.some((part) => declaresObject(part));
420
+ const array = parts.some((part) => declaresArray(part));
421
+ if (object === array)
422
+ return undefined;
423
+ return object ? 'object' : 'array';
424
+ }
425
+ function hasReferencedAllOf(node, visited = new Set()) {
426
+ const position = positionOf(node);
427
+ if (visited.has(position))
428
+ return false;
429
+ visited.add(position);
430
+ for (const branch of node.allOf ?? []) {
431
+ if (referenceOf(branch) !== undefined)
432
+ return true;
433
+ if (hasReferencedAllOf(branch, visited))
434
+ return true;
435
+ }
436
+ return false;
437
+ }
438
+ function hasUnresolvedAllOfReference(node, visited = new Set()) {
439
+ const position = positionOf(node);
440
+ if (visited.has(position))
441
+ return false;
442
+ visited.add(position);
443
+ for (const branch of node.allOf ?? []) {
444
+ const source = referenceOf(branch) !== undefined ? dereferenceChecked(branch) : undefined;
445
+ if (source?.unresolved || (source && !isSchemaNode(source.node)))
446
+ return true;
447
+ const target = source?.node ?? branch;
448
+ if (hasUnresolvedAllOfReference(target, visited))
449
+ return true;
450
+ }
451
+ return false;
452
+ }
453
+ function expandAllOfReferences(node, stack = new Set()) {
454
+ const position = positionOf(node);
455
+ if (stack.has(position))
456
+ return node;
457
+ const nextStack = new Set(stack).add(position);
458
+ const branches = [];
459
+ let changed = false;
460
+ for (const branch of node.allOf ?? []) {
461
+ if (referenceOf(branch) !== undefined) {
462
+ const { node: target, unresolved } = dereferenceChecked(branch);
463
+ if (unresolved || !isSchemaNode(target))
464
+ return undefined;
465
+ if (nextStack.has(positionOf(target))) {
466
+ branches.push(branch);
467
+ continue;
468
+ }
469
+ const expanded = expandAllOfReferences(target, nextStack);
470
+ if (!expanded)
471
+ return undefined;
472
+ branches.push(expanded);
473
+ changed = true;
474
+ continue;
475
+ }
476
+ const expanded = expandAllOfReferences(branch, nextStack);
477
+ if (!expanded)
478
+ return undefined;
479
+ branches.push(expanded);
480
+ changed ||= expanded !== branch;
481
+ }
482
+ return changed ? { ...node, allOf: branches } : node;
483
+ }
484
+ function reduceAllOfForProjection(node, data, scope = []) {
485
+ if (hasUnresolvedAllOfReference(node))
486
+ return undefined;
487
+ if (node.getDraftVersion() !== 'draft-07')
488
+ return node.reduceNode(data, { path: [...scope] }).node;
489
+ const expanded = expandAllOfReferences(node);
490
+ return expanded?.reduceNode(data, { path: [...scope] }).node;
491
+ }
134
492
  // A dialect whose reducer leaves $ref unresolved reduces a selected `{ $ref }` branch to `{}`.
135
- function resolveSelectedBranch(original, reduced, data) {
493
+ function resolveSelectedBranch(original, reduced, data, scope = [], branchApplicability) {
136
494
  const reducedSchema = reduced.schema;
137
495
  if (reducedSchema && typeof reducedSchema === 'object') {
138
- if (resolveExplicitType(reducedSchema) || inferProjectionShape(reducedSchema).kind === 'resolved') {
496
+ if (resolveExplicitType(reducedSchema) ||
497
+ inferAllOfProjectionShape(reduced, reducedSchema).kind === 'resolved') {
139
498
  return reduced;
140
499
  }
141
500
  }
142
501
  const selected = [];
143
502
  if (original.oneOf) {
144
- const matching = original.oneOf.filter((branch) => branchApplies(branch, data));
503
+ const matching = original.oneOf.filter((branch) => branchApplies(branch, data, scope, branchApplicability));
145
504
  if (matching.length === 1)
146
505
  selected.push(matching[0]);
147
506
  }
148
507
  for (const branch of original.anyOf ?? []) {
149
- if (branchApplies(branch, data))
508
+ if (branchApplies(branch, data, scope, branchApplicability))
150
509
  selected.push(branch);
151
510
  }
152
- if (!selected.some((branch) => typeof branch.$ref === 'string'))
511
+ if (!selected.some((branch) => referenceOf(branch) !== undefined))
153
512
  return reduced;
154
513
  let merged = reduced;
155
514
  for (const branch of selected) {
156
515
  const target = dereference(branch);
157
516
  if (!isSchemaNode(target))
158
517
  continue;
159
- const targetReduced = target.reduceNode(data).node ?? target;
518
+ const targetReduced = target.reduceNode(data, { path: [...scope] }).node ?? target;
160
519
  merged = mergeNode(merged, targetReduced) ?? merged;
161
520
  }
162
521
  return merged;
163
522
  }
523
+ /** Fills properties a selected Draft 7 conditional `$ref` loses during reduction. */
524
+ function resolveSelectedConditionalReference(original, reduced, data, scope = [], branchApplicability) {
525
+ if (original.getDraftVersion() !== 'draft-07' || !original.if)
526
+ return reduced;
527
+ const selected = branchApplies(original.if, data, scope, branchApplicability) ? original.then : original.else;
528
+ if (!selected || typeof selected.$ref !== 'string')
529
+ return reduced;
530
+ const target = dereference(selected);
531
+ if (!isSchemaNode(target))
532
+ return reduced;
533
+ const targetReduced = reduceAllOfForProjection(target, data, scope) ?? target;
534
+ const reducedProperties = reduced.properties ?? {};
535
+ const hasUnprojectedProperty = Object.keys(targetReduced.properties ?? {}).some((key) => !Object.hasOwn(reducedProperties, key));
536
+ return hasUnprojectedProperty ? mergeNode(reduced, targetReduced) ?? reduced : reduced;
537
+ }
164
538
  export const DEFAULT_LIMITS = { objects: 16, nodes: 512 };
165
539
  function walkOrder(a, b) {
166
540
  for (let i = 0; i < Math.min(a.length, b.length); i++)
@@ -168,10 +542,19 @@ function walkOrder(a, b) {
168
542
  return a[i] - b[i];
169
543
  return a.length - b.length;
170
544
  }
171
- function addBoundary(ctx, pointer, reason) {
545
+ function boundaryToken(pointer, schemaKey, generation) {
546
+ return JSON.stringify([pointer, schemaKey, generation]);
547
+ }
548
+ function addBoundary(ctx, pointer, reason, targetPointer, schemaKey) {
172
549
  const reasons = ctx.boundaries.get(pointer) ?? new Set();
173
550
  reasons.add(reason);
174
551
  ctx.boundaries.set(pointer, reasons);
552
+ const targets = ctx.boundaryTargets.get(pointer) ?? new Map();
553
+ if (![...targets.values()].some((target) => target.reason === reason)) {
554
+ const token = boundaryToken(targetPointer, schemaKey, ctx.boundaryGeneration);
555
+ targets.set(token, { pointer: toPointer(targetPointer), reason, token });
556
+ }
557
+ ctx.boundaryTargets.set(pointer, targets);
175
558
  }
176
559
  // A leaf needs no reduction, so it rides with its parent's budget unit.
177
560
  function isLeafSchema(schema) {
@@ -184,17 +567,14 @@ function isLeafSchema(schema) {
184
567
  return ('enum' in record || 'const' in record) && !('properties' in record);
185
568
  }
186
569
  /**
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.
570
+ * The authored schema position as a JSON Pointer with the root as the empty
571
+ * string. Reference sites and targets keep separate positions, and encoded
572
+ * reference segments use their document spelling. Both adapters report
573
+ * sources in this form so diagnostics mean the same thing whichever produced
574
+ * them.
194
575
  */
195
576
  function documentPointer(node) {
196
- const location = node.schemaLocation;
197
- return typeof location === 'string' ? location.replace(/^#/, '') : '';
577
+ return positionOf(node).replace(/^#/, '');
198
578
  }
199
579
  /**
200
580
  * Visits every schema position that applies to one location whatever the
@@ -217,16 +597,13 @@ function documentPointer(node) {
217
597
  * several places at once: `properties/x` on the node's own schema and on each
218
598
  * of its `allOf` branches are all declarations of the same location.
219
599
  */
220
- function eachUnconditional(roots, visitor) {
600
+ function eachUnconditional(roots, visitor, referenceVisitor, cache) {
221
601
  const visited = new Set();
222
602
  const visit = (current) => {
603
+ if (referenceVisitor && cache)
604
+ visitReferenceDefaultSites(current, cache, referenceVisitor);
223
605
  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;
606
+ const key = traversalKey(resolved);
230
607
  if (visited.has(key))
231
608
  return;
232
609
  visited.add(key);
@@ -249,9 +626,15 @@ function eachUnconditional(roots, visitor) {
249
626
  * own descent already survives the shapes that throw (#119, #121), and a
250
627
  * conflict is the wrong place to surface one.
251
628
  */
252
- function branchApplies(branch, data) {
629
+ function branchApplies(branch, data, scope = [], branchApplicability) {
630
+ if (data === undefined && branchApplicability?.has(branch)) {
631
+ return branchApplicability.get(branch);
632
+ }
253
633
  try {
254
- return branch.validate(data).valid === true;
634
+ const applies = branch.validate(data, '#', [...scope]).valid === true;
635
+ if (data === undefined)
636
+ branchApplicability?.set(branch, applies);
637
+ return applies;
255
638
  }
256
639
  catch {
257
640
  return false;
@@ -270,18 +653,23 @@ function branchApplies(branch, data) {
270
653
  * rule the projection uses to expose that branch, so the two cannot disagree
271
654
  * about which branch it is.
272
655
  */
273
- function eachApplicable(roots, data, visitor) {
656
+ function eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope = [], dynamicReferenceProjection = false) {
274
657
  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;
658
+ const visit = (current, parentScope) => {
659
+ if (referenceVisitor && cache)
660
+ visitReferenceDefaultSites(current, cache, referenceVisitor, dynamicReferenceProjection);
661
+ const entered = dynamicReferenceProjection
662
+ ? enterDynamicScope(current, parentScope)
663
+ : { node: dereference(current), path: parentScope, dynamicReference: false };
664
+ const resolved = entered.node;
665
+ const currentScope = entered.path;
666
+ const key = traversalKey(resolved);
279
667
  if (visited.has(key))
280
668
  return;
281
669
  visited.add(key);
282
670
  visitor(resolved);
283
671
  for (const branch of resolved.allOf ?? [])
284
- visit(branch);
672
+ visit(branch, currentScope);
285
673
  // `oneOf` contributes the one branch it selects, which is the evaluator's
286
674
  // rule and not "every branch the data matches": an empty object matches
287
675
  // every permissive branch, and treating all of them as applicable would
@@ -290,82 +678,123 @@ function eachApplicable(roots, data, visitor) {
290
678
  // branch stands in, which is what the projection exposes.
291
679
  const oneOf = resolved.oneOf ?? [];
292
680
  if (oneOf.length > 0) {
293
- const matching = oneOf.filter((branch) => branchApplies(branch, data));
681
+ const matching = oneOf.filter((branch) => branchApplies(branch, data, currentScope, branchApplicability));
294
682
  const selected = matching.length === 1
295
683
  ? matching[0]
296
684
  : selectProvisionalBranch(resolved, typeof data === 'object' && data !== null
297
685
  ? data
298
686
  : undefined);
299
687
  if (selected)
300
- visit(selected);
688
+ visit(selected, currentScope);
301
689
  }
302
690
  // `anyOf` is the opposite case and needs no such rule: several of its
303
691
  // branches applying at once is ordinary, and each that validates
304
692
  // contributes its annotations, so each competes.
305
693
  for (const branch of resolved.anyOf ?? []) {
306
- if (branchApplies(branch, data))
307
- visit(branch);
694
+ if (branchApplies(branch, data, currentScope, branchApplicability))
695
+ visit(branch, currentScope);
308
696
  }
309
697
  if (resolved.if) {
310
- const taken = branchApplies(resolved.if, data) ? resolved.then : resolved.else;
698
+ const taken = branchApplies(resolved.if, data, currentScope, branchApplicability) ? resolved.then : resolved.else;
311
699
  if (taken)
312
- visit(taken);
700
+ visit(taken, currentScope);
313
701
  }
314
702
  const dependent = resolved.dependentSchemas;
315
703
  if (dependent && typeof data === 'object' && data !== null && !Array.isArray(data)) {
316
704
  for (const [dependency, subschema] of Object.entries(dependent)) {
317
705
  if (Object.prototype.hasOwnProperty.call(data, dependency))
318
- visit(subschema);
706
+ visit(subschema, currentScope);
319
707
  }
320
708
  }
321
709
  };
322
710
  for (const root of roots)
323
- visit(root);
711
+ visit(root, scope);
324
712
  }
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) });
713
+ function enterDynamicScope(node, scope) {
714
+ const path = [...scope];
715
+ let current = node;
716
+ let dynamicReference = false;
717
+ const seen = new Set();
718
+ while (typeof current.$ref === 'string' ||
719
+ typeof current.schema.$dynamicRef === 'string' ||
720
+ typeof current.schema.$recursiveRef === 'string') {
721
+ const position = positionOf(current);
722
+ if (seen.has(position))
723
+ break;
724
+ seen.add(position);
725
+ const resolved = current.resolveRef({ pointer: '#', path });
726
+ if (!isSchemaNode(resolved))
727
+ break;
728
+ const schema = current.schema;
729
+ if (typeof schema.$dynamicRef === 'string' || typeof schema.$recursiveRef === 'string') {
730
+ dynamicReference = true;
331
731
  }
332
- });
732
+ if (resolved === current)
733
+ break;
734
+ if (typeof current.$ref === 'string' &&
735
+ current.getDraftVersion() !== 'draft-07' &&
736
+ !onlyPoints(current)) {
737
+ return { node: dereference(current), path, dynamicReference };
738
+ }
739
+ current = resolved;
740
+ }
741
+ if (path[path.length - 1]?.node !== current)
742
+ path.push({ pointer: '#', node: current });
743
+ return { node: current, path, dynamicReference };
744
+ }
745
+ function declarationsFrom(walkRoots, cache) {
746
+ const declarations = [];
747
+ const seen = new Set();
748
+ const add = (node) => {
749
+ const schema = authoredSchema(node, cache);
750
+ if (schema === null || typeof schema !== 'object' || !('default' in schema))
751
+ return;
752
+ const record = schema;
753
+ if (typeof record.$ref === 'string' && cache.dialect === 'draft-07')
754
+ return;
755
+ const source = documentPointer(node);
756
+ if (seen.has(source))
757
+ return;
758
+ seen.add(source);
759
+ declarations.push({ value: record.default, source });
760
+ };
761
+ walkRoots(add, add);
333
762
  return declarations;
334
763
  }
335
- function collectUnconditionalDefaults(roots) {
336
- return declarationsFrom((visitor) => eachUnconditional(roots, visitor));
764
+ function collectUnconditionalDefaults(roots, cache) {
765
+ return declarationsFrom((visitor, referenceVisitor) => eachUnconditional(roots, visitor, referenceVisitor, cache), cache);
337
766
  }
338
- function collectApplicableDefaults(roots, data) {
339
- return declarationsFrom((visitor) => eachApplicable(roots, data, visitor));
767
+ function collectApplicableDefaults(roots, data, cache, scope = [], dynamicReferenceProjection = false, branchApplicability) {
768
+ return declarationsFrom((visitor, referenceVisitor) => eachApplicable(roots, data, branchApplicability, visitor, referenceVisitor, cache, scope, dynamicReferenceProjection), cache);
340
769
  }
341
770
  /** The positions that declare one property of `roots`, by both reachability rules. */
342
- function childPositions(roots, key, data, declaring) {
771
+ function childPositions(roots, key, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
343
772
  const unconditional = [];
344
773
  eachUnconditional(roots.unconditional, (node) => {
345
- const child = node.properties?.[key];
774
+ const child = memberSchema(node, key);
346
775
  if (child)
347
776
  unconditional.push(child);
348
777
  });
349
778
  const applicable = [];
350
- eachApplicable(roots.applicable, data, (node) => {
351
- const child = node.properties?.[key];
779
+ eachApplicable(roots.applicable, data, branchApplicability, (node) => {
780
+ const child = memberSchema(node, key);
352
781
  if (child)
353
782
  applicable.push(child);
354
- });
783
+ }, undefined, undefined, scope, dynamicReferenceProjection);
355
784
  return { unconditional, applicable, declaring };
356
785
  }
357
786
  /** The positions that declare the elements of `roots`, by both reachability rules. */
358
- function itemPositions(roots, data, declaring) {
787
+ function itemPositions(roots, data, declaring, scope = [], dynamicReferenceProjection = false, branchApplicability) {
359
788
  const unconditional = [];
360
789
  eachUnconditional(roots.unconditional, (node) => {
361
790
  if (node.items)
362
791
  unconditional.push(node.items);
363
792
  });
364
793
  const applicable = [];
365
- eachApplicable(roots.applicable, data, (node) => {
794
+ eachApplicable(roots.applicable, data, branchApplicability, (node) => {
366
795
  if (node.items)
367
796
  applicable.push(node.items);
368
- });
797
+ }, undefined, undefined, scope, dynamicReferenceProjection);
369
798
  return { unconditional, applicable, declaring };
370
799
  }
371
800
  /**
@@ -380,16 +809,75 @@ function disagreeingDefaults(declarations) {
380
809
  const [first, ...rest] = declarations;
381
810
  return rest.every((other) => deepEqual(other.value, first.value)) ? undefined : declarations;
382
811
  }
812
+ function additionalPropertyKeys(node, data) {
813
+ if (!node?.additionalProperties)
814
+ return [];
815
+ const keys = new Set([...Object.keys(data ?? {}), ...(node.required ?? [])]);
816
+ return [...keys].filter((key) => !Object.hasOwn(node.properties ?? {}, key) &&
817
+ !node.patternProperties?.some(({ pattern }) => pattern.test(key)));
818
+ }
819
+ function additionalPropertySchema(node, key) {
820
+ if (!node || Object.hasOwn(node.properties ?? {}, key))
821
+ return undefined;
822
+ if (node.patternProperties?.some(({ pattern }) => pattern.test(key)))
823
+ return undefined;
824
+ return node.additionalProperties;
825
+ }
826
+ function memberSchema(node, key) {
827
+ if (!node)
828
+ return undefined;
829
+ if (Object.hasOwn(node.properties ?? {}, key))
830
+ return node.properties?.[key];
831
+ return additionalPropertySchema(node, key);
832
+ }
833
+ function applicableAdditionalPropertySchemas(roots, key, data, scope = [], dynamicReferenceProjection = false, branchApplicability) {
834
+ const schemas = [];
835
+ const seen = new Set();
836
+ eachApplicable(roots, data, branchApplicability, (node) => {
837
+ const schema = additionalPropertySchema(node, key);
838
+ if (!schema)
839
+ return;
840
+ const position = positionOf(schema);
841
+ if (seen.has(position))
842
+ return;
843
+ seen.add(position);
844
+ schemas.push(schema);
845
+ }, undefined, undefined, scope, dynamicReferenceProjection);
846
+ return schemas;
847
+ }
848
+ function applicableRequiredKeys(roots, data, scope = [], dynamicReferenceProjection = false, branchApplicability) {
849
+ const keys = new Set();
850
+ eachApplicable(roots, data, branchApplicability, (node) => {
851
+ for (const key of node.required ?? [])
852
+ keys.add(key);
853
+ }, undefined, undefined, scope, dynamicReferenceProjection);
854
+ return keys;
855
+ }
856
+ function unconditionalRequiredKeys(roots) {
857
+ const keys = new Set();
858
+ eachUnconditional(roots, (node) => {
859
+ for (const key of node.required ?? [])
860
+ keys.add(key);
861
+ });
862
+ return keys;
863
+ }
864
+ function reduceAdditionalPropertySchema(source, data, scope = []) {
865
+ if (source.getDraftVersion() !== 'draft-07')
866
+ return source.reduceNode(data, { path: [...scope] }).node ?? source;
867
+ const { node, unresolved } = dereferenceChecked(source);
868
+ if (unresolved || !isSchemaNode(node))
869
+ return source;
870
+ return reduceAllOfForProjection(node, data) ?? node;
871
+ }
872
+ const candidatePropertyCache = new WeakMap();
383
873
  /**
384
- * Every property key that could appear at this node's own instance location,
385
- * across every branch that might apply to it.
874
+ * Property keys that might need to appear at this instance location.
386
875
  *
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.
876
+ * Declared keys remain static candidates across all branches. Keys governed
877
+ * only by `additionalProperties` come from the current data, since the schema
878
+ * has no finite set of names to enumerate; required names are candidates too.
879
+ * `reduceNode` decides which candidates apply now, so an inactive branch keeps
880
+ * its declared pointers with `active: false`.
393
881
  *
394
882
  * **Recursion is through applicators only, and only those acting on this same
395
883
  * instance location**: `if`, `then`, `else`, `allOf`, `anyOf`, `oneOf`,
@@ -409,20 +897,20 @@ function disagreeingDefaults(declarations) {
409
897
  * - **`items` and `prefixItems`**, for the same reason: an array's items are
410
898
  * their own locations.
411
899
  *
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.
900
+ * Inline conditional branches use `positionOf` because the library can assign
901
+ * `if`, `then` and `else` the same `schemaLocation` after resolving a
902
+ * reference. Other nodes keep their location key, so references still close
903
+ * at the same point in the traversal as before.
422
904
  */
423
- function collectCandidateProperties(node) {
905
+ function collectCandidateProperties(node, data, declarations = []) {
906
+ if (declarations.length === 0 && candidatePropertyCache.has(node)) {
907
+ const cached = candidatePropertyCache.get(node);
908
+ if (cached)
909
+ return cached;
910
+ }
424
911
  const candidates = new Map();
425
912
  const visited = new Set();
913
+ const scopes = [];
426
914
  const record = (key, propNode, direct) => {
427
915
  const entry = candidates.get(key) ?? { alternatives: [] };
428
916
  if (direct)
@@ -433,26 +921,11 @@ function collectCandidateProperties(node) {
433
921
  };
434
922
  const visit = (current, own) => {
435
923
  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;
924
+ const key = traversalKey(resolved);
453
925
  if (visited.has(key))
454
926
  return;
455
927
  visited.add(key);
928
+ scopes.push({ node: resolved, own });
456
929
  for (const [key, propNode] of Object.entries(resolved.properties ?? {})) {
457
930
  record(key, propNode, own);
458
931
  }
@@ -460,7 +933,9 @@ function collectCandidateProperties(node) {
460
933
  if (branch)
461
934
  visit(branch, false);
462
935
  }
463
- for (const branches of [resolved.allOf, resolved.anyOf, resolved.oneOf]) {
936
+ for (const branch of resolved.allOf ?? [])
937
+ visit(branch, own);
938
+ for (const branches of [resolved.anyOf, resolved.oneOf]) {
464
939
  for (const branch of branches ?? [])
465
940
  visit(branch, false);
466
941
  }
@@ -471,8 +946,32 @@ function collectCandidateProperties(node) {
471
946
  }
472
947
  };
473
948
  visit(node, true);
949
+ for (const declaration of declarations)
950
+ visit(declaration, false);
951
+ const memberKeys = new Set(Object.keys(data ?? {}));
952
+ for (const scope of scopes)
953
+ for (const key of scope.node.required ?? [])
954
+ memberKeys.add(key);
955
+ for (const { node: scope, own } of scopes) {
956
+ for (const key of memberKeys) {
957
+ const propNode = additionalPropertySchema(scope, key);
958
+ if (propNode)
959
+ record(key, propNode, own);
960
+ }
961
+ }
962
+ const hasDataDependentAdditionalProperties = scopes.some(({ node: scope }) => Boolean(scope.additionalProperties));
963
+ if (declarations.length === 0) {
964
+ candidatePropertyCache.set(node, hasDataDependentAdditionalProperties ? null : candidates);
965
+ }
474
966
  return candidates;
475
967
  }
968
+ function hasRetainedDescendant(position, cache) {
969
+ for (const retained of cache.retainedPositions) {
970
+ if (retained === position || retained.startsWith(`${position}/`))
971
+ return true;
972
+ }
973
+ return false;
974
+ }
476
975
  /**
477
976
  * The schema to project for a candidate when no branch is active.
478
977
  *
@@ -646,6 +1145,96 @@ function computeRequiredSet(resolved, reducedSchema, dataRecord) {
646
1145
  }
647
1146
  return required;
648
1147
  }
1148
+ // reduceNode recompiles a subtree when any descendant has reduction work.
1149
+ const REDUCE_NODE_SCHEMA_FIELDS = [
1150
+ '$ref',
1151
+ '$dynamicRef',
1152
+ '$recursiveRef',
1153
+ '$defs',
1154
+ 'if',
1155
+ 'then',
1156
+ 'else',
1157
+ 'allOf',
1158
+ 'anyOf',
1159
+ 'oneOf',
1160
+ 'contains',
1161
+ 'dependentSchemas',
1162
+ 'dependentRequired',
1163
+ 'definitions',
1164
+ 'dependencies',
1165
+ 'patternProperties',
1166
+ 'propertyDependencies',
1167
+ ];
1168
+ const SCHEMA_MAP_KEYWORDS = [
1169
+ '$defs',
1170
+ 'definitions',
1171
+ 'dependentSchemas',
1172
+ 'patternProperties',
1173
+ 'properties',
1174
+ ];
1175
+ const SCHEMA_SINGLE_KEYWORDS = [
1176
+ 'additionalItems',
1177
+ 'additionalProperties',
1178
+ 'contains',
1179
+ 'if',
1180
+ 'items',
1181
+ 'not',
1182
+ 'propertyNames',
1183
+ 'then',
1184
+ 'else',
1185
+ 'unevaluatedItems',
1186
+ 'unevaluatedProperties',
1187
+ ];
1188
+ const SCHEMA_ARRAY_KEYWORDS = ['allOf', 'anyOf', 'oneOf', 'prefixItems'];
1189
+ const reductionRequirementCache = new WeakMap();
1190
+ function schemaRequiresReduction(schema, visiting = new WeakSet()) {
1191
+ if (schema === true)
1192
+ return true;
1193
+ if (typeof schema !== 'object' || schema === null)
1194
+ return false;
1195
+ const cached = reductionRequirementCache.get(schema);
1196
+ if (cached !== undefined)
1197
+ return cached;
1198
+ if (visiting.has(schema))
1199
+ return false;
1200
+ visiting.add(schema);
1201
+ const record = schema;
1202
+ const direct = REDUCE_NODE_SCHEMA_FIELDS.some((field) => Object.prototype.hasOwnProperty.call(record, field)) ||
1203
+ Object.prototype.hasOwnProperty.call(record, 'oneOfProperty') ||
1204
+ Array.isArray(record.type);
1205
+ const children = [];
1206
+ for (const keyword of SCHEMA_MAP_KEYWORDS) {
1207
+ const value = record[keyword];
1208
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1209
+ children.push(...Object.values(value));
1210
+ }
1211
+ }
1212
+ for (const keyword of SCHEMA_SINGLE_KEYWORDS) {
1213
+ const value = record[keyword];
1214
+ if (keyword === 'items' && Array.isArray(value))
1215
+ children.push(...value);
1216
+ else if (value !== undefined)
1217
+ children.push(value);
1218
+ }
1219
+ for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
1220
+ const value = record[keyword];
1221
+ if (Array.isArray(value))
1222
+ children.push(...value);
1223
+ }
1224
+ for (const value of Object.values(record.dependencies ?? {}))
1225
+ children.push(value);
1226
+ for (const values of Object.values(record.propertyDependencies ?? {})) {
1227
+ if (typeof values === 'object' && values !== null)
1228
+ children.push(...Object.values(values));
1229
+ }
1230
+ const required = direct || children.some((child) => schemaRequiresReduction(child, visiting));
1231
+ visiting.delete(schema);
1232
+ reductionRequirementCache.set(schema, required);
1233
+ return required;
1234
+ }
1235
+ function requiresReduction(node) {
1236
+ return node.reducers.length > 0 || schemaRequiresReduction(node.schema);
1237
+ }
649
1238
  // An unreadable identity is never a repeat, and is budgeted so it cannot expand without end.
650
1239
  const budgeted = (info) => info.cyclic || info.key === '';
651
1240
  /**
@@ -675,9 +1264,15 @@ declaredAt, ctx,
675
1264
  /** Entered as an object member, which is the only way to be past the data. */
676
1265
  member,
677
1266
  /** The past-the-data ancestors, nearest first, up to the nearest location holding data. */
678
- lineage, path) {
1267
+ lineage, path, scope, dynamicReferenceAtLocation = false) {
1268
+ if (ctx.cache.dynamicReferenceProjection) {
1269
+ const entered = enterDynamicScope(node, scope);
1270
+ node = entered.node;
1271
+ scope = entered.path;
1272
+ dynamicReferenceAtLocation ||= entered.dynamicReference;
1273
+ }
679
1274
  const info = locationInfo(declaredAt.declaring, ctx.cache);
680
- const { node: original, cycle: referenceCycle } = dereferenceChecked(node);
1275
+ const { node: original, cycle: referenceCycle, unresolved: unresolvedReference } = dereferenceChecked(node);
681
1276
  if (info.cycle || referenceCycle) {
682
1277
  diagnostics.push({
683
1278
  pointer: toPointer(pointer),
@@ -688,10 +1283,13 @@ lineage, path) {
688
1283
  return;
689
1284
  }
690
1285
  const pastData = member && (data === undefined || data === null);
691
- const recursive = pastData && ((lineage?.recursive ?? false) || budgeted(info));
1286
+ const token = boundaryToken(pointer, info.key, ctx.boundaryGeneration);
1287
+ const recursive = pastData && !unresolvedReference && ((lineage?.recursive ?? false) || budgeted(info));
692
1288
  for (let ancestor = pastData ? lineage : undefined; ancestor; ancestor = ancestor.parent) {
693
- if (info.key !== '' && ancestor.key === info.key) {
694
- addBoundary(ctx, ancestor.pointer, 'recursion');
1289
+ if (info.key !== '' &&
1290
+ ancestor.key === info.key &&
1291
+ !ctx.expandedBoundaryTokens.has(token)) {
1292
+ addBoundary(ctx, ancestor.pointer, 'recursion', pointer, info.key);
695
1293
  ctx.removed.add(pointer);
696
1294
  return;
697
1295
  }
@@ -702,21 +1300,29 @@ lineage, path) {
702
1300
  let admit;
703
1301
  if (isLeafSchema(originalSchema)) {
704
1302
  const covered = ctx.reserved.has(parent);
705
- admit = covered || ctx.nodesUsed < ctx.limits.nodes;
1303
+ admit = ctx.expandedBoundaryTokens.has(token) || covered || ctx.nodesUsed < ctx.limits.nodes;
706
1304
  if (admit && !covered)
707
1305
  ctx.nodesUsed += 1;
708
1306
  }
709
1307
  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;
1308
+ const explicitlyExpanded = ctx.expandedBoundaryTokens.has(token);
1309
+ const retainedDeclarations = declaredAt.declaring.filter((declaration) => hasRetainedDescendant(positionOf(declaration), ctx.cache));
1310
+ const candidateDeclarations = dynamicReferenceAtLocation
1311
+ ? []
1312
+ : [...declaredAt.applicable, ...retainedDeclarations];
1313
+ const leaves = [...collectCandidateProperties(original, undefined, candidateDeclarations).values()].filter((candidate) => isLeafSchema(dereference(candidatePrototype(candidate)).schema)).length;
1314
+ admit =
1315
+ explicitlyExpanded ||
1316
+ (ctx.objectsUsed < ctx.limits.objects && ctx.nodesUsed + 1 + leaves <= ctx.limits.nodes);
712
1317
  if (admit) {
713
1318
  ctx.objectsUsed += 1;
714
- ctx.nodesUsed += 1 + leaves;
715
- ctx.reserved.add(pointer);
1319
+ ctx.nodesUsed += explicitlyExpanded ? 1 : 1 + leaves;
1320
+ if (!explicitlyExpanded)
1321
+ ctx.reserved.add(pointer);
716
1322
  }
717
1323
  }
718
1324
  if (!admit) {
719
- addBoundary(ctx, parent, 'budget');
1325
+ addBoundary(ctx, parent, 'budget', pointer, info.key);
720
1326
  ctx.removed.add(pointer);
721
1327
  return;
722
1328
  }
@@ -727,6 +1333,36 @@ lineage, path) {
727
1333
  let resolved = original;
728
1334
  let schema = originalSchema;
729
1335
  let type = resolveExplicitType(schema);
1336
+ const localShape = inferAllOfProjectionShape(original, schema);
1337
+ const allOfType = original.allOf ? allOfExplicitType(original) : undefined;
1338
+ const hasReferencedAllOfBranch = original.allOf ? hasReferencedAllOf(original) : false;
1339
+ if (original.allOf &&
1340
+ !original.oneOf &&
1341
+ !original.anyOf &&
1342
+ !type &&
1343
+ (localShape.kind !== 'resolved' || allOfType !== undefined) &&
1344
+ (hasReferencedAllOfBranch || allOfType !== undefined)) {
1345
+ const reductionData = (allOfType === 'object' || declaresObject(original)) &&
1346
+ (data === undefined || data === null)
1347
+ ? {}
1348
+ : data;
1349
+ const reduced = reduceAllOfForProjection(original, reductionData, scope);
1350
+ const reducedSchema = reduced?.schema;
1351
+ if (reduced && reducedSchema && typeof reducedSchema === 'object') {
1352
+ resolved = reduced;
1353
+ schema = reducedSchema;
1354
+ if (!type)
1355
+ type = resolveExplicitType(schema);
1356
+ }
1357
+ }
1358
+ if (!type && ctx.cache.dynamicReferenceProjection && original.allOf?.length) {
1359
+ const reduced = original.reduceNode(data, { path: [...scope] }).node;
1360
+ if (reduced) {
1361
+ resolved = reduced;
1362
+ schema = reduced.schema;
1363
+ type = resolveExplicitType(schema);
1364
+ }
1365
+ }
730
1366
  // Whether this node's own active branch could be determined. Stays true for every
731
1367
  // node except a typeless oneOf/anyOf wrapper whose branch could not be resolved
732
1368
  // (see below), where the node's own presence in the projection is the thing in
@@ -750,9 +1386,9 @@ lineage, path) {
750
1386
  let wrapperIdentified = false;
751
1387
  if (!type && (original.oneOf || original.anyOf)) {
752
1388
  composedAgainstData = true;
753
- const { node: branchNode } = original.reduceNode(data);
1389
+ const { node: branchNode } = original.reduceNode(data, { path: [...scope] });
754
1390
  if (branchNode) {
755
- resolved = resolveSelectedBranch(original, branchNode, data);
1391
+ resolved = resolveSelectedBranch(original, branchNode, data, scope, ctx.branchApplicability);
756
1392
  schema = resolved.schema;
757
1393
  type = resolveExplicitType(schema);
758
1394
  }
@@ -812,7 +1448,7 @@ lineage, path) {
812
1448
  // chosen, and inferring first would take a schema carrying both `properties`
813
1449
  // and `oneOf` down the object path without ever resolving its branch.
814
1450
  if (!type) {
815
- const shape = inferProjectionShape(schema);
1451
+ const shape = inferAllOfProjectionShape(resolved, schema);
816
1452
  if (shape.kind === 'resolved') {
817
1453
  type = shape.type;
818
1454
  }
@@ -834,6 +1470,15 @@ lineage, path) {
834
1470
  diagnostics.push(shapeDiagnostic(toPointer(pointer), shape, schema.enum !== undefined));
835
1471
  }
836
1472
  }
1473
+ // Arrays do not pass through the object path that reduces the active schema.
1474
+ if (type === 'array' && resolved === original && original.allOf) {
1475
+ const reduced = reduceAllOfForProjection(original, data, scope);
1476
+ const reducedSchema = reduced?.schema;
1477
+ if (reduced && reducedSchema && typeof reducedSchema === 'object') {
1478
+ resolved = reduced;
1479
+ schema = reducedSchema;
1480
+ }
1481
+ }
837
1482
  if (!type)
838
1483
  return;
839
1484
  const selfLineage = pastData
@@ -843,7 +1488,7 @@ lineage, path) {
843
1488
  // pointer with no node has nothing to omit an annotation from, and saying
844
1489
  // that its `default` is undecidable on top of saying it cannot be drawn at
845
1490
  // all would be the same schema reported twice.
846
- const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional));
1491
+ const ambiguousDefault = disagreeingDefaults(collectUnconditionalDefaults(declaredAt.unconditional, ctx.cache));
847
1492
  if (ambiguousDefault) {
848
1493
  diagnostics.push({
849
1494
  pointer: toPointer(pointer),
@@ -858,13 +1503,18 @@ lineage, path) {
858
1503
  // selects. A superset of the diagnostic's: every unconditional disagreement
859
1504
  // is also an applicable one, so the node carries both kinds and a consumer
860
1505
  // reads one place.
861
- const applicableDeclarations = collectApplicableDefaults(declaredAt.applicable, data);
1506
+ const applicableData = type === 'object' && !(typeof data === 'object' && data !== null) ? {} : data;
1507
+ const applicableDeclarations = collectApplicableDefaults(declaredAt.applicable, applicableData, ctx.cache, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
862
1508
  const applicableConflict = disagreeingDefaults(applicableDeclarations);
863
1509
  const conflictedDefault = applicableConflict !== undefined || ambiguousDefault !== undefined;
864
1510
  const defaultConflict = (applicableConflict ?? ambiguousDefault)?.map((declaration) => declaration.source);
865
- const defaultSources = !conflictedDefault && 'default' in schema && info.cyclic
1511
+ const defaultSources = !conflictedDefault && applicableDeclarations.length > 0 && info.cyclic
866
1512
  ? [...new Set(applicableDeclarations.map((declaration) => declaration.source))].sort()
867
1513
  : undefined;
1514
+ const defaultOverride = {
1515
+ present: applicableDeclarations.length > 0,
1516
+ value: applicableDeclarations[0]?.value,
1517
+ };
868
1518
  const nodeActive = active && branchResolved;
869
1519
  // A node the schema applies is never also provisional; the two report
870
1520
  // different facts and only the second is a choice this projection made. The
@@ -889,19 +1539,73 @@ lineage, path) {
889
1539
  // wrapper case), it has already been reduced against the real `data` at this
890
1540
  // pointer; re-reducing it against `dataRecord ?? {}` here would be redundant (and,
891
1541
  // 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;
1542
+ let reducedNode = resolved === original
1543
+ ? requiresReduction(resolved)
1544
+ ? original.allOf && hasReferencedAllOf(original)
1545
+ ? reduceAllOfForProjection(resolved, dataRecord ?? {}, scope)
1546
+ : resolved.reduceNode(dataRecord ?? {}, { path: [...scope] }).node
1547
+ : resolved
1548
+ : resolved;
1549
+ if (reducedNode && resolved === original && (!member || data !== undefined)) {
1550
+ // Missing child data has no evaluated conditional branch to recover.
1551
+ reducedNode = resolveSelectedConditionalReference(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
1552
+ }
893
1553
  if (reducedNode &&
894
1554
  resolved === original &&
895
1555
  !resolveExplicitType(originalSchema) &&
896
1556
  (original.oneOf || original.anyOf)) {
897
- reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {});
1557
+ reducedNode = resolveSelectedBranch(original, reducedNode, dataRecord ?? {}, scope, ctx.branchApplicability);
898
1558
  }
899
1559
  const reducedSchema = reducedNode?.schema;
900
1560
  const reducedProperties = reducedSchema?.properties ??
901
1561
  schema.properties ??
902
1562
  {};
903
- const activeKeys = new Set(Object.keys(reducedProperties));
1563
+ // Dynamic applicability is rebuilt below; only retained declarations belong beside it here.
1564
+ const retainedDeclarations = declaredAt.declaring.filter((declaration) => hasRetainedDescendant(positionOf(declaration), ctx.cache));
1565
+ const candidateDeclarations = dynamicReferenceAtLocation
1566
+ ? []
1567
+ : [...declaredAt.applicable, ...retainedDeclarations];
1568
+ let candidateProps = collectCandidateProperties(original, dataRecord, candidateDeclarations);
1569
+ const activeKeys = new Set([
1570
+ ...Object.keys(reducedProperties),
1571
+ ...additionalPropertyKeys(reducedNode, dataRecord),
1572
+ ]);
1573
+ if (!reducedNode) {
1574
+ for (const [key, candidate] of candidateProps) {
1575
+ if (candidate.direct)
1576
+ activeKeys.add(key);
1577
+ }
1578
+ }
904
1579
  const requiredSet = computeRequiredSet(resolved, reducedSchema, dataRecord);
1580
+ const dynamicApplicableNodes = [];
1581
+ if (ctx.cache.dynamicReferenceProjection) {
1582
+ eachApplicable([original], data, ctx.branchApplicability, (applicableNode) => {
1583
+ dynamicApplicableNodes.push(applicableNode);
1584
+ for (const key of Object.keys(applicableNode.properties ?? {}))
1585
+ activeKeys.add(key);
1586
+ for (const key of applicableNode.required ?? [])
1587
+ requiredSet.add(key);
1588
+ }, undefined, undefined, scope, true);
1589
+ const expandedCandidates = new Map(candidateProps);
1590
+ const activeCandidateNode = reducedNode ?? resolved;
1591
+ if (activeCandidateNode !== original) {
1592
+ for (const [key, candidate] of collectCandidateProperties(activeCandidateNode, dataRecord)) {
1593
+ if (!expandedCandidates.has(key))
1594
+ expandedCandidates.set(key, candidate);
1595
+ }
1596
+ }
1597
+ for (const applicableNode of dynamicApplicableNodes) {
1598
+ for (const [key, propertyNode] of Object.entries(applicableNode.properties ?? {})) {
1599
+ if (!expandedCandidates.has(key))
1600
+ expandedCandidates.set(key, { direct: propertyNode, alternatives: [] });
1601
+ }
1602
+ }
1603
+ candidateProps = expandedCandidates;
1604
+ }
1605
+ if (!reducedNode) {
1606
+ for (const key of unconditionalRequiredKeys(declaredAt.unconditional))
1607
+ requiredSet.add(key);
1608
+ }
905
1609
  // Two conditions, and they are not the same kind of condition.
906
1610
  //
907
1611
  // Exposure is load-bearing. Selection is local, but whether this node is
@@ -921,11 +1625,32 @@ lineage, path) {
921
1625
  : undefined;
922
1626
  const provisionalSchema = provisionalBranch?.schema;
923
1627
  const provisionalKeys = new Set(Object.keys(provisionalSchema?.properties ?? {}));
1628
+ for (const key of additionalPropertyKeys(provisionalBranch, dataRecord))
1629
+ provisionalKeys.add(key);
924
1630
  const provisionalRequired = new Set(Array.isArray(provisionalSchema?.required) ? provisionalSchema.required : []);
925
1631
  // Candidates are collected from `original`, not `resolved`, so every oneOf/anyOf
926
1632
  // branch's properties are represented (the matching branch alone, via `resolved`,
927
1633
  // would only expose its own properties).
928
- const candidateProps = collectCandidateProperties(original);
1634
+ const applicableRequired = applicableRequiredKeys(declaredAt.applicable, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
1635
+ const provisionalRequiredKeys = provisionalBranch
1636
+ ? applicableRequiredKeys([provisionalBranch], applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability)
1637
+ : new Set();
1638
+ for (const key of candidateProps.keys()) {
1639
+ const present = dataRecord !== undefined && Object.hasOwn(dataRecord, key);
1640
+ if ((present || applicableRequired.has(key)) &&
1641
+ applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0) {
1642
+ activeKeys.add(key);
1643
+ }
1644
+ }
1645
+ if (provisionalBranch) {
1646
+ for (const key of candidateProps.keys()) {
1647
+ if (provisionalRequiredKeys.has(key) &&
1648
+ (applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0 ||
1649
+ applicableAdditionalPropertySchemas([provisionalBranch], key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability).length > 0)) {
1650
+ provisionalKeys.add(key);
1651
+ }
1652
+ }
1653
+ }
929
1654
  const propKeys = [...candidateProps.keys()];
930
1655
  const children = propKeys.length > 0
931
1656
  ? propKeys.map((key) => {
@@ -951,7 +1676,7 @@ lineage, path) {
951
1676
  active: nodeActive,
952
1677
  provisional: nodeProvisional ? true : undefined,
953
1678
  defaultConflict,
954
- annotations: extractAnnotations(schema, conflictedDefault),
1679
+ annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
955
1680
  ...(defaultSources !== undefined ? { defaultSources } : {}),
956
1681
  });
957
1682
  for (const [index, key] of propKeys.entries()) {
@@ -961,29 +1686,89 @@ lineage, path) {
961
1686
  // selected ancestor its own locally applicable properties are
962
1687
  // provisional too, while a locally inactive one stays inactive.
963
1688
  const childProvisional = !childActive && nodeExposed && (activeKeys.has(key) || provisionalKeys.has(key));
964
- const reducedChildNode = reducedNode?.properties?.[key];
1689
+ const reducedChildNode = memberSchema(reducedNode, key);
965
1690
  // Precedence matters as much as the selection does. Marking the right
966
1691
  // branch provisional while taking its shape from `candidatePrototype`,
967
1692
  // which is first-wins across branches, would render another branch's
968
1693
  // widget and annotations under the selected branch's name, and would hand
969
1694
  // ADR-003's pass another branch's `default`.
970
- const provisionalChildNode = provisionalBranch?.properties?.[key];
1695
+ const provisionalChildNode = memberSchema(provisionalBranch, key);
971
1696
  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 ??
1697
+ const childData = dataRecord !== undefined && Object.hasOwn(dataRecord, key) ? dataRecord[key] : undefined;
1698
+ const sourceChildNode = reducedChildNode ?? candidate.direct ?? candidatePrototype(candidate);
1699
+ const sourceSchema = sourceChildNode.schema;
1700
+ let scopedChildNode;
1701
+ let childScope = scope;
1702
+ if (ctx.cache.dynamicReferenceProjection && childActive) {
1703
+ const nextScope = [...scope];
1704
+ if (typeof sourceSchema.$dynamicAnchor === 'string' ||
1705
+ typeof sourceSchema.$id === 'string' ||
1706
+ sourceSchema.$recursiveAnchor === true) {
1707
+ nextScope.push({ pointer: `#${childPointer}`, node: sourceChildNode });
1708
+ }
1709
+ const result = resolved.getNodeChild(key, dataRecord ?? {}, {
1710
+ pointer: `#${childPointer}`,
1711
+ path: nextScope,
1712
+ });
1713
+ if (!result.error && isSchemaNode(result.node)) {
1714
+ scopedChildNode = result.node;
1715
+ if (nextScope[nextScope.length - 1]?.node !== scopedChildNode) {
1716
+ nextScope.push({ pointer: `#${childPointer}`, node: scopedChildNode });
1717
+ }
1718
+ childScope = nextScope;
1719
+ }
1720
+ }
1721
+ let applicableChildNode = reducedChildNode;
1722
+ let scopedApplicableChildNode = scopedChildNode;
1723
+ const hasScopedReference = typeof sourceSchema.$dynamicRef === 'string' || typeof sourceSchema.$recursiveRef === 'string';
1724
+ for (const source of applicableAdditionalPropertySchemas(declaredAt.applicable, key, applicableData, scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability)) {
1725
+ const alreadyApplicable = applicableChildNode !== undefined && positionOf(applicableChildNode) === positionOf(source);
1726
+ const alreadyScoped = scopedApplicableChildNode !== undefined && positionOf(scopedApplicableChildNode) === positionOf(source);
1727
+ if (alreadyApplicable && (!scopedApplicableChildNode || alreadyScoped))
1728
+ continue;
1729
+ const reduced = reduceAdditionalPropertySchema(source, childData, scope);
1730
+ if (!alreadyApplicable) {
1731
+ applicableChildNode =
1732
+ applicableChildNode === undefined
1733
+ ? reduced
1734
+ : mergeNode(applicableChildNode, reduced) ?? applicableChildNode;
1735
+ }
1736
+ if (scopedApplicableChildNode && !alreadyScoped) {
1737
+ scopedApplicableChildNode = mergeNode(scopedApplicableChildNode, reduced) ?? scopedApplicableChildNode;
1738
+ }
1739
+ }
1740
+ // Applicable declarations retain their authored scope here. The parent
1741
+ // reducer merges `properties` across `allOf`, which can make an
1742
+ // `additionalProperties` schema in a separate branch appear inapplicable.
1743
+ // Failing that, use the reducer's child, then the selected branch, then a
1744
+ // stable placeholder from the candidate set.
1745
+ const childNode = (hasScopedReference ? scopedApplicableChildNode : undefined) ??
1746
+ applicableChildNode ??
978
1747
  composeChild(candidate.direct, provisionalChildNode) ??
979
1748
  candidatePrototype(candidate);
980
- const positions = childPositions(declaredAt, key,
1749
+ let positions = childPositions(declaredAt, key,
981
1750
  // This node's own data, not the child's: the branches being entered
982
1751
  // are this location's, so they are decided by the instance here. The
983
1752
  // 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]);
1753
+ // the child's data. `applicableData` is the reducer's object stand-in
1754
+ // when this location has no object value yet.
1755
+ applicableData, childDeclaring(info, key), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
1756
+ const dynamicChildDeclarations = dynamicApplicableNodes.flatMap((applicableNode) => {
1757
+ const dynamicChild = applicableNode.properties?.[key];
1758
+ return dynamicChild ? [dynamicChild] : [];
1759
+ });
1760
+ positions = {
1761
+ ...positions,
1762
+ declaring: extendDeclaring(positions.declaring, dynamicChildDeclarations),
1763
+ };
1764
+ if (scopedChildNode && hasScopedReference && !positions.declaring.includes(scopedChildNode)) {
1765
+ positions = {
1766
+ ...positions,
1767
+ applicable: [...positions.applicable, scopedChildNode],
1768
+ declaring: [...positions.declaring, scopedChildNode],
1769
+ };
1770
+ }
1771
+ const enter = () => walk(childNode, childPointer, childData, childActive, childProvisional, nodes, diagnostics, positions, ctx, true, selfLineage, [...path, index], childScope, hasScopedReference);
987
1772
  // Deferring the children the budget counts, by depth below the anchor, admits it breadth first.
988
1773
  const counted = selfLineage && (recursive || budgeted(locationInfo(positions.declaring, ctx.cache)));
989
1774
  if (counted)
@@ -1003,7 +1788,7 @@ lineage, path) {
1003
1788
  active: nodeActive,
1004
1789
  provisional: nodeProvisional ? true : undefined,
1005
1790
  defaultConflict,
1006
- annotations: extractAnnotations(schema, conflictedDefault),
1791
+ annotations: extractAnnotations(schema, conflictedDefault, defaultOverride),
1007
1792
  ...(defaultSources !== undefined ? { defaultSources } : {}),
1008
1793
  // Deliberately not given the flag above. A conflict under `items` belongs
1009
1794
  // to the element locations, and each of those reports its own; this field
@@ -1016,9 +1801,9 @@ lineage, path) {
1016
1801
  : undefined,
1017
1802
  });
1018
1803
  if (type === 'array' && resolved.items && Array.isArray(data)) {
1019
- const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info));
1804
+ const elementPositions = itemPositions(declaredAt, data, itemDeclaring(info), scope, ctx.cache.dynamicReferenceProjection, ctx.branchApplicability);
1020
1805
  data.forEach((item, index) => {
1021
- walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index]);
1806
+ walk(resolved.items, `${pointer}/${index}`, item, nodeActive, nodeProvisional, nodes, diagnostics, elementPositions, ctx, false, undefined, [...path, index], scope);
1022
1807
  });
1023
1808
  }
1024
1809
  }
@@ -1032,13 +1817,17 @@ lineage, path) {
1032
1817
  * contract), while array items are walked from `data` (an array's length is a data-time
1033
1818
  * fact, not a schema-time one). $ref is resolved transparently via node.resolveRef().
1034
1819
  */
1035
- export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
1820
+ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS, options = {}) {
1036
1821
  const nodes = new Map();
1037
1822
  const diagnostics = [];
1038
1823
  const ctx = {
1039
1824
  cache,
1825
+ branchApplicability: cache.memoizeUndefinedBranchResults ? new WeakMap() : undefined,
1040
1826
  limits,
1041
1827
  boundaries: new Map(),
1828
+ boundaryTargets: new Map(),
1829
+ expandedBoundaryTokens: options.expandedBoundaryTokens ?? new Set(),
1830
+ boundaryGeneration: options.boundaryGeneration ?? 0,
1042
1831
  removed: new Set(),
1043
1832
  flagged: new Set(),
1044
1833
  reserved: new Set(),
@@ -1046,7 +1835,7 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
1046
1835
  objectsUsed: 0,
1047
1836
  nodesUsed: 0,
1048
1837
  };
1049
- walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, []);
1838
+ walk(root, '', data, true, false, nodes, diagnostics, { unconditional: [root], applicable: [root], declaring: [root] }, ctx, false, undefined, [], [{ pointer: '#', node: root }]);
1050
1839
  for (const level of ctx.queue)
1051
1840
  for (const { enter } of (level ?? []).sort((a, b) => walkOrder(a.path, b.path)))
1052
1841
  enter();
@@ -1066,6 +1855,13 @@ export function buildProjection(root, data, cache, limits = DEFAULT_LIMITS) {
1066
1855
  if (node) {
1067
1856
  ;
1068
1857
  node.boundaries = ['recursion', 'budget'].filter((reason) => reasons.has(reason));
1858
+ const targets = ctx.boundaryTargets.get(pointer);
1859
+ if (targets) {
1860
+ ;
1861
+ node.boundaryTargets = [
1862
+ ...targets.values(),
1863
+ ];
1864
+ }
1069
1865
  }
1070
1866
  }
1071
1867
  return { nodes, diagnostics };