schema-components 1.20.0 → 1.22.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 (77) hide show
  1. package/README.md +1 -1
  2. package/dist/core/adapter.d.mts +28 -4
  3. package/dist/core/adapter.mjs +408 -71
  4. package/dist/core/constraints.d.mts +2 -2
  5. package/dist/core/constraints.mjs +0 -2
  6. package/dist/core/diagnostics.d.mts +1 -1
  7. package/dist/core/errors.d.mts +1 -1
  8. package/dist/core/errors.mjs +9 -15
  9. package/dist/core/fieldOrder.d.mts +1 -1
  10. package/dist/core/formats.d.mts +22 -1
  11. package/dist/core/formats.mjs +21 -0
  12. package/dist/core/limits.d.mts +2 -0
  13. package/dist/core/limits.mjs +23 -0
  14. package/dist/core/merge.d.mts +11 -2
  15. package/dist/core/merge.mjs +11 -0
  16. package/dist/core/normalise.d.mts +36 -4
  17. package/dist/core/normalise.mjs +2 -2
  18. package/dist/core/openapi30.d.mts +24 -1
  19. package/dist/core/openapi30.mjs +2 -2
  20. package/dist/core/ref.d.mts +1 -1
  21. package/dist/core/ref.mjs +35 -9
  22. package/dist/core/renderer.d.mts +1 -1
  23. package/dist/core/renderer.mjs +0 -2
  24. package/dist/core/swagger2.d.mts +1 -1
  25. package/dist/core/swagger2.mjs +1 -1
  26. package/dist/core/typeInference.d.mts +2 -2
  27. package/dist/core/types.d.mts +2 -2
  28. package/dist/core/types.mjs +1 -4
  29. package/dist/core/version.d.mts +1 -1
  30. package/dist/core/walkBuilders.d.mts +13 -5
  31. package/dist/core/walkBuilders.mjs +11 -3
  32. package/dist/core/walker.d.mts +1 -1
  33. package/dist/core/walker.mjs +110 -26
  34. package/dist/{diagnostics-CbBPsxSt.d.mts → diagnostics-D0QCYGv0.d.mts} +1 -1
  35. package/dist/{errors-C2iABcn9.d.mts → errors-DpFwqs5C.d.mts} +7 -11
  36. package/dist/html/a11y.d.mts +2 -2
  37. package/dist/html/a11y.mjs +10 -3
  38. package/dist/html/renderToHtml.d.mts +10 -3
  39. package/dist/html/renderToHtml.mjs +13 -3
  40. package/dist/html/renderToHtmlStream.d.mts +2 -2
  41. package/dist/html/renderers.d.mts +2 -2
  42. package/dist/html/renderers.mjs +1 -6
  43. package/dist/html/streamRenderers.d.mts +5 -4
  44. package/dist/html/streamRenderers.mjs +91 -30
  45. package/dist/limits-Cw5QZND8.d.mts +29 -0
  46. package/dist/{normalise-CMMEl4cd.mjs → normalise-DVEJQmF7.mjs} +791 -141
  47. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  48. package/dist/openapi/ApiLinks.d.mts +1 -1
  49. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  50. package/dist/openapi/ApiSecurity.d.mts +1 -1
  51. package/dist/openapi/ApiSecurity.mjs +127 -7
  52. package/dist/openapi/components.d.mts +175 -21
  53. package/dist/openapi/components.mjs +145 -21
  54. package/dist/openapi/parser.d.mts +1 -1
  55. package/dist/openapi/parser.mjs +74 -7
  56. package/dist/openapi/resolve.d.mts +70 -12
  57. package/dist/openapi/resolve.mjs +265 -42
  58. package/dist/react/SchemaComponent.d.mts +100 -35
  59. package/dist/react/SchemaComponent.mjs +66 -24
  60. package/dist/react/SchemaView.d.mts +3 -3
  61. package/dist/react/SchemaView.mjs +2 -2
  62. package/dist/react/fieldPath.d.mts +1 -1
  63. package/dist/react/headless.d.mts +1 -1
  64. package/dist/react/headless.mjs +1 -2
  65. package/dist/react/headlessRenderers.d.mts +3 -4
  66. package/dist/react/headlessRenderers.mjs +11 -31
  67. package/dist/{ref-C8JbwfiS.d.mts → ref-D-_JBZkF.d.mts} +7 -2
  68. package/dist/{renderer-SOIbJBtk.d.mts → renderer-BaRlQIuN.d.mts} +2 -2
  69. package/dist/themes/mantine.d.mts +1 -1
  70. package/dist/themes/mui.d.mts +1 -1
  71. package/dist/themes/radix.d.mts +1 -1
  72. package/dist/themes/shadcn.d.mts +1 -1
  73. package/dist/typeInference-DkcUHfaM.d.mts +982 -0
  74. package/dist/{types-C9zw9wbX.d.mts → types-BrRMV0en.d.mts} +15 -12
  75. package/package.json +1 -3
  76. package/dist/typeInference-CDoD_LZ_.d.mts +0 -533
  77. /package/dist/{version-D-u7aMfy.d.mts → version-D2jfdX6E.d.mts} +0 -0
@@ -29,6 +29,8 @@ function normaliseOpenApi30Node(node) {
29
29
  return node;
30
30
  }
31
31
  const nullOption = { type: "null" };
32
+ if (typeof node.$ref === "string") return { anyOf: [{ $ref: node.$ref }, nullOption] };
33
+ if (Array.isArray(node.enum) && !node.enum.includes(null)) node.enum = [...node.enum, null];
32
34
  if (Array.isArray(node.anyOf)) {
33
35
  node.anyOf = [...node.anyOf, nullOption];
34
36
  delete node.nullable;
@@ -113,10 +115,270 @@ function normaliseOpenApi30Discriminator(node) {
113
115
  }
114
116
  if ("oneOf" in node) node.oneOf = normalisedComposite;
115
117
  else if ("anyOf" in node) node.anyOf = normalisedComposite;
116
- delete node.discriminator;
118
+ const extensions = {};
119
+ for (const [key, value] of Object.entries(discriminator)) if (key.startsWith("x-")) extensions[key] = value;
120
+ if (Object.keys(extensions).length > 0) node.discriminator = {
121
+ propertyName,
122
+ ...extensions
123
+ };
124
+ else delete node.discriminator;
117
125
  return node;
118
126
  }
119
127
  /**
128
+ * Returns the schema name a `$ref` points at when it targets
129
+ * `#/components/schemas/<Name>`, or `undefined` otherwise.
130
+ *
131
+ * The walker only resolves intra-document refs and other allOf-base
132
+ * patterns; refs into `definitions` (Swagger 2.0) are already rewritten
133
+ * before this stage.
134
+ */
135
+ function componentSchemaName(ref) {
136
+ if (typeof ref !== "string") return void 0;
137
+ if (!ref.startsWith("#/components/schemas/")) return void 0;
138
+ const name = ref.slice(21);
139
+ return name.length > 0 ? name : void 0;
140
+ }
141
+ /**
142
+ * Find every immediate `$ref` that an `allOf` array contains pointing
143
+ * back at a `components/schemas/<Name>` entry. Used to discover
144
+ * "Cat extends Pet"-style inheritance — the subtype's `allOf` lists
145
+ * the base by `$ref` alongside its own local fields.
146
+ */
147
+ function listAllOfBaseRefs(schema) {
148
+ const allOf = schema.allOf;
149
+ if (!Array.isArray(allOf)) return [];
150
+ const result = [];
151
+ for (const entry of allOf) {
152
+ if (!isObject(entry)) continue;
153
+ const name = componentSchemaName(entry.$ref);
154
+ if (name !== void 0) result.push(name);
155
+ }
156
+ return result;
157
+ }
158
+ /**
159
+ * Collect discriminator subtypes for a base schema. Entries come from:
160
+ *
161
+ * 1. The base's `discriminator.mapping` (explicit author intent — the
162
+ * mapping key supplies the `const` value, the ref names the subtype).
163
+ * 2. Component schemas whose `allOf` lists this base by `$ref` and
164
+ * were not already named in the mapping. The `const` value defaults
165
+ * to the subtype's component name.
166
+ *
167
+ * Returned in deterministic order: mapping entries first (preserving
168
+ * authored order), then implicit subtypes alphabetically.
169
+ */
170
+ function collectDiscriminatorSubtypes(baseName, discriminator, componentSchemas) {
171
+ const result = [];
172
+ const seen = /* @__PURE__ */ new Set();
173
+ const mapping = isObject(discriminator.mapping) ? discriminator.mapping : void 0;
174
+ if (mapping !== void 0) for (const [constValue, ref] of Object.entries(mapping)) {
175
+ const name = componentSchemaName(ref);
176
+ if (name === void 0) continue;
177
+ if (!isObject(componentSchemas[name])) continue;
178
+ if (seen.has(name)) continue;
179
+ seen.add(name);
180
+ result.push({
181
+ name,
182
+ constValue
183
+ });
184
+ }
185
+ const implicitNames = [];
186
+ for (const [name, schema] of Object.entries(componentSchemas)) {
187
+ if (!isObject(schema)) continue;
188
+ if (seen.has(name)) continue;
189
+ if (!listAllOfBaseRefs(schema).includes(baseName)) continue;
190
+ implicitNames.push(name);
191
+ }
192
+ implicitNames.sort();
193
+ for (const name of implicitNames) {
194
+ result.push({
195
+ name,
196
+ constValue: name
197
+ });
198
+ seen.add(name);
199
+ }
200
+ return result;
201
+ }
202
+ /**
203
+ * Inject the discriminator `const` on a subtype schema in-place.
204
+ *
205
+ * When the subtype already declares a matching const we leave it
206
+ * alone. Otherwise the const is added in whichever location the walker
207
+ * will actually observe:
208
+ *
209
+ * - Subtype declares `allOf`: append a new `allOf` entry carrying just
210
+ * `{ properties: { [propertyName]: { const } } }`. The walker's
211
+ * `mergeAllOf` merges every entry's `properties` into the resolved
212
+ * schema, so the const propagates through to the merged result. A
213
+ * top-level `properties` sibling of `allOf` would be ignored by the
214
+ * merge.
215
+ * - Subtype does not declare `allOf`: extend the top-level `properties`
216
+ * block — the walker reads this directly.
217
+ */
218
+ function injectSubtypeConst(subtype, propertyName, constValue) {
219
+ if (subtypeAlreadyDeclaresConst(subtype, propertyName)) return;
220
+ const constEntry = { properties: { [propertyName]: { const: constValue } } };
221
+ if (Array.isArray(subtype.allOf)) {
222
+ subtype.allOf = [...subtype.allOf, constEntry];
223
+ return;
224
+ }
225
+ const existingProps = isObject(subtype.properties) ? { ...subtype.properties } : {};
226
+ const existingDisc = existingProps[propertyName];
227
+ existingProps[propertyName] = {
228
+ ...isObject(existingDisc) ? existingDisc : {},
229
+ const: constValue
230
+ };
231
+ subtype.properties = existingProps;
232
+ }
233
+ /**
234
+ * Check whether a subtype (or any of its `allOf` entries) already
235
+ * carries a `const` for the discriminator property. Used to avoid
236
+ * overwriting an author-supplied const.
237
+ */
238
+ function subtypeAlreadyDeclaresConst(subtype, propertyName) {
239
+ if (hasConstProp(subtype.properties, propertyName)) return true;
240
+ if (Array.isArray(subtype.allOf)) for (const entry of subtype.allOf) {
241
+ if (!isObject(entry)) continue;
242
+ if (hasConstProp(entry.properties, propertyName)) return true;
243
+ }
244
+ return false;
245
+ }
246
+ function hasConstProp(properties, propertyName) {
247
+ if (!isObject(properties)) return false;
248
+ const prop = properties[propertyName];
249
+ return isObject(prop) && "const" in prop;
250
+ }
251
+ /**
252
+ * Strip every `$ref` entry in a subtype's `allOf` that targets the
253
+ * discriminator base. The base's own schema content (properties,
254
+ * required, type) is replicated into a synthesised `allOf` entry so
255
+ * the subtype remains structurally complete — without this the base's
256
+ * synthesised `oneOf` would cycle through the subtype's `$ref`s on
257
+ * every walk (Dog → Pet.oneOf → Dog → ...).
258
+ */
259
+ function rewriteSubtypeAllOf(subtype, baseName, baseInherited) {
260
+ const allOf = subtype.allOf;
261
+ if (!Array.isArray(allOf)) return;
262
+ const baseRefPrefix = `#/components/schemas/${baseName}`;
263
+ const rewritten = [];
264
+ let removedBaseRef = false;
265
+ for (const entry of allOf) {
266
+ if (isObject(entry) && typeof entry.$ref === "string" && entry.$ref === baseRefPrefix) {
267
+ removedBaseRef = true;
268
+ continue;
269
+ }
270
+ rewritten.push(entry);
271
+ }
272
+ if (!removedBaseRef) return;
273
+ subtype.allOf = [baseInherited, ...rewritten];
274
+ }
275
+ /**
276
+ * Capture the "inheritable" portion of a base schema before rewriting:
277
+ * `properties`, `required`, `type`, and any other constraint that
278
+ * subtypes used to inherit through `$ref`. The discriminator keyword
279
+ * and the synthesised `oneOf` are intentionally excluded — subtypes
280
+ * never inherited those and including them would re-introduce the
281
+ * Pet → Dog → Pet cycle.
282
+ */
283
+ function captureBaseInherited(base) {
284
+ const inherited = {};
285
+ for (const [key, value] of Object.entries(base)) {
286
+ if (key === "discriminator") continue;
287
+ if (key === "oneOf") continue;
288
+ if (key === "anyOf") continue;
289
+ inherited[key] = value;
290
+ }
291
+ return inherited;
292
+ }
293
+ /**
294
+ * Build an inline `oneOf` option that targets a subtype via `$ref` and
295
+ * carries the discriminator property's `const` at the option's top
296
+ * level. The const sibling is what makes `detectDiscriminated` classify
297
+ * the parent `oneOf` as a discriminated union — it inspects each
298
+ * option's literal `properties`, not the resolved schema.
299
+ */
300
+ function buildDiscriminatorOption(subtype, propertyName) {
301
+ return {
302
+ $ref: `#/components/schemas/${subtype.name}`,
303
+ properties: { [propertyName]: { const: subtype.constValue } }
304
+ };
305
+ }
306
+ /**
307
+ * Document-level pre-pass for OpenAPI discriminators that are declared
308
+ * on a base schema and inherited by subtypes via `allOf`.
309
+ *
310
+ * The per-node {@link normaliseOpenApi30Discriminator} only handles
311
+ * discriminators that already sit alongside `oneOf`/`anyOf`. For the
312
+ * canonical "Cat extends Pet" pattern — where `Pet` carries the
313
+ * discriminator and `Cat`/`Dog` reference `Pet` via `allOf` — the
314
+ * discriminator is silently lost. This pre-pass:
315
+ *
316
+ * 1. Injects the discriminator `const` on each subtype's local
317
+ * `properties` (so a direct render of the subtype validates the
318
+ * discriminator value correctly).
319
+ * 2. Synthesises a `oneOf` on the base whenever it lacks one, listing
320
+ * each subtype as `{ $ref, properties: { propertyName: { const } } }`.
321
+ * The per-node discriminator transform then sees `oneOf` and clears
322
+ * the `discriminator` keyword, and the walker's
323
+ * `detectDiscriminated` finds the per-option `const`s.
324
+ *
325
+ * Mutates a shallow clone of `components/schemas` — the input document
326
+ * is never modified.
327
+ */
328
+ function applyDiscriminatorAllOfPrepass(doc) {
329
+ const components = doc.components;
330
+ if (!isObject(components)) return doc;
331
+ const schemas = components.schemas;
332
+ if (!isObject(schemas)) return doc;
333
+ const plans = [];
334
+ for (const [baseName, base] of Object.entries(schemas)) {
335
+ if (!isObject(base)) continue;
336
+ const discriminator = base.discriminator;
337
+ if (!isObject(discriminator)) continue;
338
+ const propertyName = discriminator.propertyName;
339
+ if (typeof propertyName !== "string") continue;
340
+ const subtypes = collectDiscriminatorSubtypes(baseName, discriminator, schemas);
341
+ if (subtypes.length === 0) continue;
342
+ plans.push({
343
+ baseName,
344
+ propertyName,
345
+ subtypes,
346
+ baseHasOneOfOrAnyOf: Array.isArray(base.oneOf) || Array.isArray(base.anyOf),
347
+ baseInherited: captureBaseInherited(base)
348
+ });
349
+ }
350
+ if (plans.length === 0) return doc;
351
+ const newSchemas = { ...schemas };
352
+ const cloneSchema = (name) => {
353
+ const existing = newSchemas[name];
354
+ if (!isObject(existing)) throw new Error(`applyDiscriminatorAllOfPrepass: schema "${name}" disappeared between planning and rewrite`);
355
+ const clone = { ...existing };
356
+ newSchemas[name] = clone;
357
+ return clone;
358
+ };
359
+ for (const plan of plans) {
360
+ for (const subtype of plan.subtypes) {
361
+ const clone = cloneSchema(subtype.name);
362
+ rewriteSubtypeAllOf(clone, plan.baseName, plan.baseInherited);
363
+ injectSubtypeConst(clone, plan.propertyName, subtype.constValue);
364
+ }
365
+ if (!plan.baseHasOneOfOrAnyOf) {
366
+ const baseClone = cloneSchema(plan.baseName);
367
+ baseClone.oneOf = plan.subtypes.map((subtype) => buildDiscriminatorOption(subtype, plan.propertyName));
368
+ delete baseClone.properties;
369
+ delete baseClone.required;
370
+ delete baseClone.type;
371
+ }
372
+ }
373
+ return {
374
+ ...doc,
375
+ components: {
376
+ ...components,
377
+ schemas: newSchemas
378
+ }
379
+ };
380
+ }
381
+ /**
120
382
  * Combined OpenAPI 3.0.x node transform: Draft 04 + nullable + discriminator.
121
383
  * Applied to every schema node in an OpenAPI 3.0 document.
122
384
  *
@@ -226,7 +488,7 @@ function normaliseParameter(param, normaliseSchema) {
226
488
  const content = param.content;
227
489
  if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
228
490
  if ("example" in result && !("examples" in result)) {
229
- result.examples = [result.example];
491
+ result.examples = { default: { value: result.example } };
230
492
  delete result.example;
231
493
  } else if ("example" in result) delete result.example;
232
494
  return result;
@@ -252,7 +514,7 @@ function normaliseHeader(header, normaliseSchema) {
252
514
  const content = header.content;
253
515
  if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
254
516
  if ("example" in result && !("examples" in result)) {
255
- result.examples = [result.example];
517
+ result.examples = { default: { value: result.example } };
256
518
  delete result.example;
257
519
  } else if ("example" in result) delete result.example;
258
520
  return result;
@@ -325,9 +587,19 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
325
587
  version: "0.0.0"
326
588
  }
327
589
  };
328
- if (typeof doc.host === "string" || typeof doc.basePath === "string" || Array.isArray(doc.schemes)) {
329
- const host = typeof doc.host === "string" ? doc.host : "localhost";
330
- const basePath = typeof doc.basePath === "string" ? doc.basePath : "/";
590
+ if (typeof doc.host !== "string") {
591
+ if (Array.isArray(doc.schemes) || typeof doc.basePath === "string") emitDiagnostic(diagnostics, {
592
+ code: "swagger-missing-host",
593
+ message: "Swagger 2.0 document declares schemes or basePath without host; skipping server URL synthesis",
594
+ pointer: "",
595
+ detail: {
596
+ hasSchemes: Array.isArray(doc.schemes),
597
+ hasBasePath: typeof doc.basePath === "string"
598
+ }
599
+ });
600
+ } else {
601
+ const host = doc.host;
602
+ const basePath = typeof doc.basePath === "string" ? doc.basePath : "";
331
603
  const schemes = Array.isArray(doc.schemes) ? doc.schemes : ["https"];
332
604
  result.servers = [{ url: `${typeof schemes[0] === "string" ? schemes[0] : "https"}://${host}${basePath}` }];
333
605
  }
@@ -343,31 +615,62 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
343
615
  const parameters = doc.parameters;
344
616
  const requestBodies = {};
345
617
  if (isObject(parameters)) {
346
- const globalConsumes = Array.isArray(doc.consumes) ? doc.consumes : ["application/json"];
618
+ const consumesResolution = resolveSwaggerContentTypes(void 0, doc.consumes);
619
+ const globalConsumes = consumesResolution.types;
347
620
  const convertedParameters = {};
348
621
  for (const [name, param] of Object.entries(parameters)) {
349
622
  if (!isObject(param)) {
350
623
  convertedParameters[name] = param;
351
624
  continue;
352
625
  }
353
- const resolved = resolveSwaggerParameter(param, doc);
626
+ const resolution = resolveSwaggerParameter(param, doc);
627
+ if (resolution.kind === "cycle") {
628
+ emitDiagnostic(diagnostics, {
629
+ code: "swagger-cyclic-parameter-ref",
630
+ message: `Cyclic Swagger 2.0 parameter $ref "${resolution.ref}"; skipping entry`,
631
+ pointer: appendPointer(appendPointer("", "parameters"), name),
632
+ detail: {
633
+ ref: resolution.ref,
634
+ name
635
+ }
636
+ });
637
+ continue;
638
+ }
639
+ const resolved = resolution.param;
354
640
  const location = resolved.in;
355
- if (location === "body") requestBodies[name] = buildRequestBody(resolved, globalConsumes);
356
- else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
357
- else convertedParameters[name] = normaliseSwaggerParameter(resolved, doc);
641
+ if (location === "body") {
642
+ if (consumesResolution.source === "synthesised") emitDiagnostic(diagnostics, {
643
+ code: "swagger-missing-consumes",
644
+ message: "Global body parameter declared but document-level `consumes` is absent; defaulting to application/json",
645
+ pointer: appendPointer(appendPointer("", "parameters"), name),
646
+ detail: {
647
+ level: "document",
648
+ name
649
+ }
650
+ });
651
+ requestBodies[name] = buildRequestBody(resolved, globalConsumes);
652
+ } else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
653
+ else {
654
+ const normalised = normaliseSwaggerParameter(resolved, doc, diagnostics, appendPointer(appendPointer("", "parameters"), name));
655
+ if (normalised !== void 0) convertedParameters[name] = normalised;
656
+ }
358
657
  }
359
658
  if (Object.keys(convertedParameters).length > 0) components.parameters = convertedParameters;
360
659
  }
361
660
  const responses = doc.responses;
362
661
  if (isObject(responses)) {
363
- const globalProduces = Array.isArray(doc.produces) ? doc.produces : ["application/json"];
662
+ const producesResolution = resolveSwaggerContentTypes(void 0, doc.produces);
364
663
  const convertedResponses = {};
365
- for (const [name, response] of Object.entries(responses)) convertedResponses[name] = isObject(response) ? normaliseSwaggerSingleResponse(response, doc, globalProduces) : response;
664
+ for (const [name, response] of Object.entries(responses)) convertedResponses[name] = isObject(response) ? normaliseSwaggerSingleResponse(response, doc, producesResolution.types, producesResolution.source, diagnostics, void 0, void 0, name) : response;
366
665
  components.responses = convertedResponses;
367
666
  }
368
667
  if (Object.keys(requestBodies).length > 0) components.requestBodies = requestBodies;
369
668
  const securityDefinitions = doc.securityDefinitions;
370
- if (isObject(securityDefinitions)) components.securitySchemes = { ...securityDefinitions };
669
+ if (isObject(securityDefinitions)) {
670
+ const translated = {};
671
+ for (const [name, scheme] of Object.entries(securityDefinitions)) translated[name] = isObject(scheme) ? translateSwaggerSecurityScheme(scheme) : scheme;
672
+ components.securitySchemes = translated;
673
+ }
371
674
  if (Object.keys(components).length > 0) result.components = components;
372
675
  if (Array.isArray(doc.tags)) result.tags = doc.tags;
373
676
  if (isObject(doc.externalDocs)) result.externalDocs = doc.externalDocs;
@@ -404,17 +707,29 @@ function normaliseSwaggerPaths(paths, doc, diagnostics) {
404
707
  normalisedPath[method] = normaliseSwaggerOperation(operation, doc, path, method, diagnostics);
405
708
  }
406
709
  const pathParams = pathItem.parameters;
407
- if (Array.isArray(pathParams)) normalisedPath.parameters = pathParams.map((p) => isObject(p) ? normaliseSwaggerParameter(p, doc) : p);
710
+ if (Array.isArray(pathParams)) {
711
+ const paramsPointer = appendPointer(appendPointer(appendPointer("", "paths"), path), "parameters");
712
+ const out = [];
713
+ for (const [index, p] of pathParams.entries()) {
714
+ if (!isObject(p)) {
715
+ out.push(p);
716
+ continue;
717
+ }
718
+ const normalised = normaliseSwaggerParameter(p, doc, diagnostics, appendPointer(paramsPointer, String(index)));
719
+ if (normalised !== void 0) out.push(normalised);
720
+ }
721
+ normalisedPath.parameters = out;
722
+ }
408
723
  result[path] = normalisedPath;
409
724
  }
410
725
  return result;
411
726
  }
412
727
  function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
413
728
  const result = {};
414
- const globalProduces = Array.isArray(doc.produces) ? doc.produces : ["application/json"];
415
- const globalConsumes = Array.isArray(doc.consumes) ? doc.consumes : ["application/json"];
416
- const produces = Array.isArray(operation.produces) ? operation.produces : globalProduces;
417
- const consumes = Array.isArray(operation.consumes) ? operation.consumes : globalConsumes;
729
+ const consumesResolution = resolveSwaggerContentTypes(operation.consumes, doc.consumes);
730
+ const producesResolution = resolveSwaggerContentTypes(operation.produces, doc.produces);
731
+ const produces = producesResolution.types;
732
+ const consumes = consumesResolution.types;
418
733
  for (const [key, value] of Object.entries(operation)) if (key !== "parameters" && key !== "responses" && key !== "produces" && key !== "consumes") result[key] = value;
419
734
  const params = operation.parameters;
420
735
  if (Array.isArray(params)) {
@@ -427,7 +742,17 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
427
742
  nonBodyParams.push(param);
428
743
  continue;
429
744
  }
430
- const resolvedParam = resolveSwaggerParameter(param, doc);
745
+ const paramResolution = resolveSwaggerParameter(param, doc);
746
+ if (paramResolution.kind === "cycle") {
747
+ emitDiagnostic(diagnostics, {
748
+ code: "swagger-cyclic-parameter-ref",
749
+ message: `Cyclic Swagger 2.0 parameter $ref "${paramResolution.ref}"; skipping entry`,
750
+ pointer: appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"), String(index)),
751
+ detail: { ref: paramResolution.ref }
752
+ });
753
+ continue;
754
+ }
755
+ const resolvedParam = paramResolution.param;
431
756
  const location = resolvedParam.in;
432
757
  if (location === "body") {
433
758
  if (bodyParam !== void 0) {
@@ -451,16 +776,51 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
451
776
  bodyParam = buildFormDataBody(resolvedParam, params);
452
777
  usesFormData = true;
453
778
  }
454
- } else nonBodyParams.push(normaliseSwaggerParameter(resolvedParam, doc));
779
+ } else {
780
+ const normalised = normaliseSwaggerParameter(resolvedParam, doc, diagnostics, appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"), String(index)));
781
+ if (normalised !== void 0) nonBodyParams.push(normalised);
782
+ }
455
783
  }
456
784
  if (nonBodyParams.length > 0) result.parameters = nonBodyParams;
457
- if (bodyParam !== void 0) result.requestBody = buildRequestBody(bodyParam, usesFormData ? formDataContentTypes(consumes) : consumes);
785
+ if (bodyParam !== void 0) {
786
+ const bodyContentTypes = usesFormData ? formDataContentTypes(consumes) : consumes;
787
+ if (!usesFormData && consumesResolution.source === "synthesised") emitDiagnostic(diagnostics, {
788
+ code: "swagger-missing-consumes",
789
+ message: "Operation declares a body parameter but neither operation-level nor document-level `consumes` is set; defaulting to application/json",
790
+ pointer: appendPointer(appendPointer(appendPointer("", "paths"), path), method),
791
+ detail: {
792
+ level: "operation",
793
+ method
794
+ }
795
+ });
796
+ result.requestBody = buildRequestBody(bodyParam, bodyContentTypes);
797
+ }
458
798
  }
459
799
  const responses = operation.responses;
460
- if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces);
800
+ if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces, producesResolution.source, diagnostics, path, method);
461
801
  return result;
462
802
  }
463
803
  /**
804
+ * Resolve a Swagger 2.0 `consumes` or `produces` array, recording
805
+ * where the value came from so callers can decide whether to emit a
806
+ * "missing content type" diagnostic. Per the Swagger 2.0 spec, absence
807
+ * at BOTH levels means no body — not an implicit `application/json`.
808
+ */
809
+ function resolveSwaggerContentTypes(operationLevel, documentLevel) {
810
+ if (Array.isArray(operationLevel)) return {
811
+ types: operationLevel,
812
+ source: "operation"
813
+ };
814
+ if (Array.isArray(documentLevel)) return {
815
+ types: documentLevel,
816
+ source: "document"
817
+ };
818
+ return {
819
+ types: ["application/json"],
820
+ source: "synthesised"
821
+ };
822
+ }
823
+ /**
464
824
  * Determine the request body media type for a Swagger 2.0 formData operation.
465
825
  *
466
826
  * Per the OAS 3 conversion rules, `application/x-www-form-urlencoded` is
@@ -475,12 +835,77 @@ function formDataContentTypes(consumes) {
475
835
  return ["multipart/form-data"];
476
836
  }
477
837
  /**
478
- * Resolve a Swagger parameter that may be a `$ref`.
838
+ * Every JSON-Schema-compatible constraint keyword Swagger 2.0 allows on
839
+ * a Parameter Object or Header Object alongside `type`/`format`. These
840
+ * lift into the synthesised `schema` so consumers see the original
841
+ * validation semantics under OAS 3.x's parameter shape.
842
+ *
843
+ * `allowEmptyValue` is included even though it is a Swagger 2.0
844
+ * parameter-level keyword in the source (not a schema keyword) — OAS
845
+ * 3.x defines it at the Parameter Object root, so the calling function
846
+ * keeps it at the parameter root rather than copying it into `schema`.
847
+ */
848
+ const SWAGGER_PARAM_SCHEMA_KEYWORDS = [
849
+ "enum",
850
+ "default",
851
+ "minimum",
852
+ "maximum",
853
+ "exclusiveMinimum",
854
+ "exclusiveMaximum",
855
+ "multipleOf",
856
+ "minLength",
857
+ "maxLength",
858
+ "pattern",
859
+ "minItems",
860
+ "maxItems",
861
+ "uniqueItems"
862
+ ];
863
+ /**
864
+ * Set of every Swagger 2.0 parameter-root keyword that must be lifted
865
+ * into the synthesised `schema` rather than copied onto the OAS 3.x
866
+ * parameter root. Includes `type`, `format`, `items` (Swagger 2.0
867
+ * parameter-shaped array element descriptor), `collectionFormat`
868
+ * (handled separately by the caller as `style`/`explode`), and every
869
+ * entry from {@link SWAGGER_PARAM_SCHEMA_KEYWORDS}.
870
+ */
871
+ const PARAM_KEYWORDS_LIFTED_INTO_SCHEMA = new Set([
872
+ "type",
873
+ "format",
874
+ "items",
875
+ "collectionFormat",
876
+ ...SWAGGER_PARAM_SCHEMA_KEYWORDS
877
+ ]);
878
+ /**
879
+ * Synthesise an OpenAPI 3.x `schema` object from a Swagger 2.0
880
+ * parameter-shaped node (parameter or header). Copies `type`,
881
+ * `format`, and every JSON-Schema-compatible constraint that Swagger
882
+ * 2.0 places at the parameter root. Nested `items` is recursively
883
+ * synthesised the same way so array element constraints survive.
884
+ */
885
+ function buildSchemaFromSwaggerParameterShape(node) {
886
+ const schema = { type: node.type };
887
+ if (typeof node.format === "string") schema.format = node.format;
888
+ for (const keyword of SWAGGER_PARAM_SCHEMA_KEYWORDS) if (node[keyword] !== void 0) schema[keyword] = node[keyword];
889
+ if (isObject(node.items)) schema.items = buildSchemaFromSwaggerParameterShape(node.items);
890
+ return schema;
891
+ }
892
+ /**
893
+ * Resolve a Swagger parameter that may be a `$ref`. Returns the
894
+ * resolved parameter object, or a cycle marker so the caller can
895
+ * decide how to surface the failure. Non-ref parameters resolve to
896
+ * themselves; ref targets that don't exist also resolve to the input
897
+ * (the caller treats unknown refs the same as bare parameters).
479
898
  */
480
899
  function resolveSwaggerParameter(param, doc, visited = /* @__PURE__ */ new Set()) {
481
900
  const ref = param.$ref;
482
- if (typeof ref !== "string" || !ref.startsWith("#/parameters/")) return param;
483
- if (visited.has(ref)) return param;
901
+ if (typeof ref !== "string" || !ref.startsWith("#/parameters/")) return {
902
+ kind: "ok",
903
+ param
904
+ };
905
+ if (visited.has(ref)) return {
906
+ kind: "cycle",
907
+ ref
908
+ };
484
909
  const nextVisited = new Set(visited);
485
910
  nextVisited.add(ref);
486
911
  const name = ref.slice(13);
@@ -489,33 +914,55 @@ function resolveSwaggerParameter(param, doc, visited = /* @__PURE__ */ new Set()
489
914
  const resolved = globalParams[name];
490
915
  if (isObject(resolved)) {
491
916
  if (typeof resolved.$ref === "string") return resolveSwaggerParameter(resolved, doc, nextVisited);
492
- return resolved;
917
+ return {
918
+ kind: "ok",
919
+ param: resolved
920
+ };
493
921
  }
494
922
  }
495
- return param;
923
+ return {
924
+ kind: "ok",
925
+ param
926
+ };
496
927
  }
497
928
  /**
498
929
  * Normalise a single Swagger parameter to OpenAPI 3.x form.
499
930
  */
500
- function normaliseSwaggerParameter(param, doc) {
931
+ function normaliseSwaggerParameter(param, doc, diagnostics, pointer = "") {
501
932
  if (typeof param.$ref === "string") {
502
- const resolved = resolveSwaggerParameter(param, doc);
503
- if (resolved !== param) return normaliseSwaggerParameter(resolved, doc);
933
+ const resolution = resolveSwaggerParameter(param, doc);
934
+ if (resolution.kind === "cycle") {
935
+ emitDiagnostic(diagnostics, {
936
+ code: "swagger-cyclic-parameter-ref",
937
+ message: `Cyclic Swagger 2.0 parameter $ref "${resolution.ref}"; skipping entry`,
938
+ pointer,
939
+ detail: { ref: resolution.ref }
940
+ });
941
+ return;
942
+ }
943
+ const resolved = resolution.param;
944
+ if (resolved !== param) return normaliseSwaggerParameter(resolved, doc, diagnostics, pointer);
504
945
  }
505
946
  const result = {};
506
947
  for (const [key, value] of Object.entries(param)) {
507
- if (key === "type" || key === "format" || key === "collectionFormat") continue;
948
+ if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
508
949
  result[key] = value;
509
950
  }
510
- if (typeof param.type === "string") {
511
- const schema = { type: param.type };
512
- if (typeof param.format === "string") schema.format = param.format;
513
- if (param.enum !== void 0) schema.enum = param.enum;
514
- if (param.default !== void 0) schema.default = param.default;
515
- if (param.minimum !== void 0) schema.minimum = param.minimum;
516
- if (param.maximum !== void 0) schema.maximum = param.maximum;
517
- result.schema = schema;
518
- }
951
+ if (typeof param.type === "string") if (param.type === "file" && param.in !== "formData") {
952
+ emitDiagnostic(diagnostics, {
953
+ code: "swagger-invalid-file-parameter",
954
+ message: `Swagger 2.0 type: "file" is only valid under in: formData; converting to { type: "string", format: "binary" }`,
955
+ pointer,
956
+ detail: {
957
+ name: param.name,
958
+ in: param.in
959
+ }
960
+ });
961
+ result.schema = {
962
+ type: "string",
963
+ format: "binary"
964
+ };
965
+ } else result.schema = buildSchemaFromSwaggerParameterShape(param);
519
966
  const cf = param.collectionFormat;
520
967
  if (typeof cf === "string") switch (cf) {
521
968
  case "csv":
@@ -606,14 +1053,14 @@ function resolveSwaggerResponse(response, doc, visited = /* @__PURE__ */ new Set
606
1053
  }
607
1054
  return response;
608
1055
  }
609
- function normaliseSwaggerResponses(responses, doc, produces) {
1056
+ function normaliseSwaggerResponses(responses, doc, produces, producesSource, diagnostics, path, method) {
610
1057
  const result = {};
611
1058
  for (const [code, response] of Object.entries(responses)) {
612
1059
  if (!isObject(response)) {
613
1060
  result[code] = response;
614
1061
  continue;
615
1062
  }
616
- result[code] = normaliseSwaggerSingleResponse(response, doc, produces);
1063
+ result[code] = normaliseSwaggerSingleResponse(response, doc, produces, producesSource, diagnostics, path, method, code);
617
1064
  }
618
1065
  return result;
619
1066
  }
@@ -626,7 +1073,7 @@ function normaliseSwaggerResponses(responses, doc, produces) {
626
1073
  * operation’s `responses` map or under document-level `responses`
627
1074
  * (now `components.responses`).
628
1075
  */
629
- function normaliseSwaggerSingleResponse(response, doc, produces) {
1076
+ function normaliseSwaggerSingleResponse(response, doc, produces, producesSource = "synthesised", diagnostics, path, method, statusCode) {
630
1077
  const resolved = resolveSwaggerResponse(response, doc);
631
1078
  const normalised = {};
632
1079
  for (const [key, value] of Object.entries(resolved)) if (key !== "schema" && key !== "headers") normalised[key] = value;
@@ -636,6 +1083,15 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
636
1083
  const contentTypes = produces.length > 0 ? produces : ["application/json"];
637
1084
  for (const ct of contentTypes) if (typeof ct === "string") content[ct] = { schema };
638
1085
  normalised.content = content;
1086
+ if (producesSource === "synthesised") emitDiagnostic(diagnostics, {
1087
+ code: "swagger-missing-consumes",
1088
+ message: "Response declares a schema but neither operation-level nor document-level `produces` is set; defaulting to application/json",
1089
+ pointer: path !== void 0 && method !== void 0 && statusCode !== void 0 ? appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "responses"), statusCode) : "",
1090
+ detail: {
1091
+ level: "response",
1092
+ statusCode
1093
+ }
1094
+ });
639
1095
  }
640
1096
  const headers = resolved.headers;
641
1097
  if (isObject(headers)) {
@@ -661,18 +1117,10 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
661
1117
  function normaliseSwaggerHeader(header) {
662
1118
  const result = {};
663
1119
  for (const [key, value] of Object.entries(header)) {
664
- if (key === "type" || key === "format" || key === "collectionFormat") continue;
1120
+ if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
665
1121
  result[key] = value;
666
1122
  }
667
- if (typeof header.type === "string") {
668
- const schema = { type: header.type };
669
- if (typeof header.format === "string") schema.format = header.format;
670
- if (header.enum !== void 0) schema.enum = header.enum;
671
- if (header.default !== void 0) schema.default = header.default;
672
- if (header.minimum !== void 0) schema.minimum = header.minimum;
673
- if (header.maximum !== void 0) schema.maximum = header.maximum;
674
- result.schema = schema;
675
- }
1123
+ if (typeof header.type === "string") result.schema = buildSchemaFromSwaggerParameterShape(header);
676
1124
  const cf = header.collectionFormat;
677
1125
  if (typeof cf === "string") switch (cf) {
678
1126
  case "csv":
@@ -722,6 +1170,64 @@ function rewriteSwaggerRefs(node) {
722
1170
  else if (Array.isArray(value)) for (const item of value) rewriteSwaggerRefs(item);
723
1171
  }
724
1172
  /**
1173
+ * Map from Swagger 2.0 `oauth2.flow` (singular) to the OAS 3.x flow key
1174
+ * under `flows.<key>`. `application` and `accessCode` were renamed in
1175
+ * OAS 3.x to align with RFC 6749 grant-type names.
1176
+ */
1177
+ const SWAGGER_OAUTH_FLOW_RENAME = {
1178
+ implicit: "implicit",
1179
+ password: "password",
1180
+ application: "clientCredentials",
1181
+ accessCode: "authorizationCode"
1182
+ };
1183
+ /**
1184
+ * Translate a Swagger 2.0 Security Scheme Object into an OpenAPI 3.x
1185
+ * Security Scheme Object. The Swagger 2.0 spec defines three types:
1186
+ *
1187
+ * - `basic` — has no other fields; OAS 3.x represents this as
1188
+ * `{ type: "http", scheme: "basic" }`.
1189
+ * - `apiKey` — carries `name`/`in`; OAS 3.x uses the same shape.
1190
+ * - `oauth2` — carries `flow`/`authorizationUrl`/`tokenUrl`/`scopes` at
1191
+ * the root. OAS 3.x nests these under `flows.<name>` where the flow
1192
+ * name maps via {@link SWAGGER_OAUTH_FLOW_RENAME}.
1193
+ *
1194
+ * Unknown `type` values pass through verbatim — downstream validation
1195
+ * (`unknown-security-scheme-type` diagnostic in the parser) handles
1196
+ * those cases.
1197
+ */
1198
+ function translateSwaggerSecurityScheme(scheme) {
1199
+ const type = scheme.type;
1200
+ if (type === "basic") {
1201
+ const result = {
1202
+ type: "http",
1203
+ scheme: "basic"
1204
+ };
1205
+ if (typeof scheme.description === "string") result.description = scheme.description;
1206
+ return result;
1207
+ }
1208
+ if (type === "oauth2") {
1209
+ const flowName = scheme.flow;
1210
+ if (typeof flowName !== "string") return {
1211
+ ...scheme,
1212
+ type: "oauth2"
1213
+ };
1214
+ const renamedFlow = SWAGGER_OAUTH_FLOW_RENAME[flowName] ?? flowName;
1215
+ const flowBody = {};
1216
+ if (typeof scheme.authorizationUrl === "string") flowBody.authorizationUrl = scheme.authorizationUrl;
1217
+ if (typeof scheme.tokenUrl === "string") flowBody.tokenUrl = scheme.tokenUrl;
1218
+ if (typeof scheme.refreshUrl === "string") flowBody.refreshUrl = scheme.refreshUrl;
1219
+ const scopes = scheme.scopes;
1220
+ flowBody.scopes = isObject(scopes) ? { ...scopes } : {};
1221
+ const result = {
1222
+ type: "oauth2",
1223
+ flows: { [renamedFlow]: flowBody }
1224
+ };
1225
+ if (typeof scheme.description === "string") result.description = scheme.description;
1226
+ return result;
1227
+ }
1228
+ return { ...scheme };
1229
+ }
1230
+ /**
725
1231
  * Recursively check whether any node in the supplied subtree carries an
726
1232
  * `xml` annotation. Walks both objects and arrays so the check works for
727
1233
  * schemas (definitions, parameter schemas, response schemas, request body
@@ -779,58 +1285,76 @@ const SINGLE_SUBSCHEMA_KEYS = new Set([
779
1285
  * Normalise each element of an unknown array by applying deepNormalise
780
1286
  * to object elements and passing others through unchanged.
781
1287
  */
782
- function normaliseArray(items, transform) {
1288
+ function normaliseArray(items, transform, visited) {
783
1289
  const result = [];
784
- for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform) : item);
1290
+ for (const item of items) result.push(isObject(item) ? deepNormalise(item, transform, visited) : item);
785
1291
  return result;
786
1292
  }
787
1293
  /**
788
1294
  * Normalise each value of a sub-schema map (e.g. properties, $defs).
789
1295
  */
790
- function normaliseSubSchemaMap(map, transform) {
1296
+ function normaliseSubSchemaMap(map, transform, visited) {
791
1297
  const result = {};
792
- for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform) : v;
1298
+ for (const [k, v] of Object.entries(map)) result[k] = isObject(v) ? deepNormalise(v, transform, visited) : v;
793
1299
  return result;
794
1300
  }
795
1301
  /**
796
1302
  * Deep-normalise a JSON Schema object by applying a per-node transform
797
1303
  * and recursing into every sub-schema location.
1304
+ *
1305
+ * The optional `visited` set guards against shared object references and
1306
+ * cycles introduced upstream (e.g. by the OpenAPI bundler's
1307
+ * `structuredClone`-based inlining of external refs). The walk skips
1308
+ * already-seen nodes by returning the original reference rather than
1309
+ * recursing forever.
798
1310
  */
799
- function deepNormalise(schema, transform) {
1311
+ function deepNormalise(schema, transform, visited = /* @__PURE__ */ new WeakSet()) {
1312
+ if (visited.has(schema)) return schema;
1313
+ visited.add(schema);
800
1314
  const node = transform({ ...schema });
801
1315
  const result = {};
802
- for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform);
803
- else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform);
804
- else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform);
805
- else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform);
806
- else if (isObject(value)) result[key] = deepNormalise(value, transform);
1316
+ for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMap(value, transform, visited);
1317
+ else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArray(value, transform, visited);
1318
+ else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormalise(value, transform, visited);
1319
+ else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArray(value, transform, visited);
1320
+ else if (isObject(value)) result[key] = deepNormalise(value, transform, visited);
807
1321
  else result[key] = value;
808
1322
  else if (key === "dependencies" && isObject(value)) {
809
1323
  const normalised = {};
810
- for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormalise(dv, transform);
1324
+ for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormalise(dv, transform, visited);
811
1325
  else normalised[dk] = dv;
812
1326
  result[key] = normalised;
813
1327
  } else result[key] = value;
814
1328
  return result;
815
1329
  }
1330
+ /**
1331
+ * Construct a child {@link NodeContext} that descends to `segment`,
1332
+ * preserving document-level flags (`documentHasDynamicAnchor`,
1333
+ * `documentHasRecursiveAnchor`, `declaredDraft`). Centralising the copy
1334
+ * keeps the recursion in `deepNormaliseWithContext` from drifting from
1335
+ * the {@link NodeContext} shape.
1336
+ */
1337
+ function childContext(ctx, segment) {
1338
+ return {
1339
+ diagnostics: ctx.diagnostics,
1340
+ pointer: appendPointer(ctx.pointer, segment),
1341
+ documentHasDynamicAnchor: ctx.documentHasDynamicAnchor,
1342
+ documentHasRecursiveAnchor: ctx.documentHasRecursiveAnchor,
1343
+ declaredDraft: ctx.declaredDraft
1344
+ };
1345
+ }
816
1346
  function normaliseArrayWithContext(items, transform, ctx) {
817
1347
  const result = [];
818
1348
  for (let i = 0; i < items.length; i++) {
819
1349
  const item = items[i];
820
- if (isObject(item)) result.push(deepNormaliseWithContext(item, transform, {
821
- diagnostics: ctx.diagnostics,
822
- pointer: appendPointer(ctx.pointer, String(i))
823
- }));
1350
+ if (isObject(item)) result.push(deepNormaliseWithContext(item, transform, childContext(ctx, String(i))));
824
1351
  else result.push(item);
825
1352
  }
826
1353
  return result;
827
1354
  }
828
1355
  function normaliseSubSchemaMapWithContext(map, transform, ctx) {
829
1356
  const result = {};
830
- for (const [k, v] of Object.entries(map)) if (isObject(v)) result[k] = deepNormaliseWithContext(v, transform, {
831
- diagnostics: ctx.diagnostics,
832
- pointer: appendPointer(ctx.pointer, k)
833
- });
1357
+ for (const [k, v] of Object.entries(map)) if (isObject(v)) result[k] = deepNormaliseWithContext(v, transform, childContext(ctx, k));
834
1358
  else result[k] = v;
835
1359
  return result;
836
1360
  }
@@ -846,34 +1370,16 @@ function normaliseSubSchemaMapWithContext(map, transform, ctx) {
846
1370
  function deepNormaliseWithContext(schema, transform, ctx) {
847
1371
  const node = transform({ ...schema }, ctx);
848
1372
  const result = {};
849
- for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMapWithContext(value, transform, {
850
- diagnostics: ctx.diagnostics,
851
- pointer: appendPointer(ctx.pointer, key)
852
- });
853
- else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArrayWithContext(value, transform, {
854
- diagnostics: ctx.diagnostics,
855
- pointer: appendPointer(ctx.pointer, key)
856
- });
857
- else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormaliseWithContext(value, transform, {
858
- diagnostics: ctx.diagnostics,
859
- pointer: appendPointer(ctx.pointer, key)
860
- });
861
- else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArrayWithContext(value, transform, {
862
- diagnostics: ctx.diagnostics,
863
- pointer: appendPointer(ctx.pointer, key)
864
- });
865
- else if (isObject(value)) result[key] = deepNormaliseWithContext(value, transform, {
866
- diagnostics: ctx.diagnostics,
867
- pointer: appendPointer(ctx.pointer, key)
868
- });
1373
+ for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMapWithContext(value, transform, childContext(ctx, key));
1374
+ else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArrayWithContext(value, transform, childContext(ctx, key));
1375
+ else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormaliseWithContext(value, transform, childContext(ctx, key));
1376
+ else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArrayWithContext(value, transform, childContext(ctx, key));
1377
+ else if (isObject(value)) result[key] = deepNormaliseWithContext(value, transform, childContext(ctx, key));
869
1378
  else result[key] = value;
870
1379
  else if (key === "dependencies" && isObject(value)) {
871
1380
  const normalised = {};
872
- const depsPointer = appendPointer(ctx.pointer, key);
873
- for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormaliseWithContext(dv, transform, {
874
- diagnostics: ctx.diagnostics,
875
- pointer: appendPointer(depsPointer, dk)
876
- });
1381
+ const depsContext = childContext(ctx, key);
1382
+ for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormaliseWithContext(dv, transform, childContext(depsContext, dk));
877
1383
  else normalised[dk] = dv;
878
1384
  result[key] = normalised;
879
1385
  } else result[key] = value;
@@ -976,6 +1482,26 @@ function validateDependentRequired(node, ctx) {
976
1482
  }
977
1483
  }
978
1484
  /**
1485
+ * Translate the legacy tuple-form `items: [Schema, Schema]` keyword to
1486
+ * Draft 2020-12's `prefixItems`. `additionalItems` becomes the rest-element
1487
+ * schema and is rewritten to `items`.
1488
+ *
1489
+ * Applies to Drafts 04, 06, and 07 — all of which used the tuple form
1490
+ * before Draft 2020-12 split positional schemas into `prefixItems`.
1491
+ *
1492
+ * No-op when `items` is already a single sub-schema, or when `prefixItems`
1493
+ * is already present (do not overwrite an explicit author choice).
1494
+ */
1495
+ function translateTupleItems(node) {
1496
+ if (!Array.isArray(node.items) || "prefixItems" in node) return;
1497
+ node.prefixItems = node.items;
1498
+ delete node.items;
1499
+ if ("additionalItems" in node) {
1500
+ node.items = node.additionalItems;
1501
+ delete node.additionalItems;
1502
+ }
1503
+ }
1504
+ /**
979
1505
  * Apply the version-agnostic Draft 04 keyword translations to a single
980
1506
  * node: boolean exclusive-min/max → number form, bare `id` → `$id`, and
981
1507
  * tuple-form `items` → `prefixItems`.
@@ -1036,14 +1562,7 @@ function applyDraft04Translations(node, ctx) {
1036
1562
  node.$id = node.id;
1037
1563
  delete node.id;
1038
1564
  }
1039
- if (Array.isArray(node.items) && !("prefixItems" in node)) {
1040
- node.prefixItems = node.items;
1041
- delete node.items;
1042
- if ("additionalItems" in node) {
1043
- node.items = node.additionalItems;
1044
- delete node.additionalItems;
1045
- }
1046
- }
1565
+ translateTupleItems(node);
1047
1566
  splitDependencies(node, ctx, false);
1048
1567
  validateDependentRequired(node, ctx);
1049
1568
  }
@@ -1089,8 +1608,14 @@ function normaliseDraft04NodeWithContext(node, ctx) {
1089
1608
  * (already in final form) and `const`/`examples`, but still use the
1090
1609
  * legacy `dependencies` keyword. Split it into `dependentRequired` /
1091
1610
  * `dependentSchemas` so the walker can process them uniformly.
1611
+ *
1612
+ * Drafts 06 and 07 also use the tuple form
1613
+ * `items: [Schema, Schema], additionalItems: Schema` — translate that to
1614
+ * Draft 2020-12's `prefixItems` + rest-`items` so the walker produces a
1615
+ * `TupleField` rather than silently dropping the positional schemas.
1092
1616
  */
1093
1617
  function normaliseDraft06Or07NodeWithContext(node, ctx) {
1618
+ translateTupleItems(node);
1094
1619
  splitDependencies(node, ctx, false);
1095
1620
  validateDependentRequired(node, ctx);
1096
1621
  return node;
@@ -1110,8 +1635,18 @@ function normaliseDraft06Or07NodeWithContext(node, ctx) {
1110
1635
  * `$recursiveAnchor` names are likewise preserved as `$anchor`.
1111
1636
  */
1112
1637
  function normaliseDraft201909NodeWithContext(node, ctx) {
1113
- if (typeof node.$recursiveRef === "string") {
1114
- node.$ref = node.$recursiveRef;
1638
+ const recursiveRef = node.$recursiveRef;
1639
+ if (typeof recursiveRef === "string") {
1640
+ if (!recursiveRef.startsWith("#")) emitDiagnostic(ctx.diagnostics, {
1641
+ code: "dynamic-ref-degraded",
1642
+ message: `Cross-document \`$recursiveRef\` "${recursiveRef}" rewritten to a static \`$ref\`; dynamic-scope resolution is not preserved`,
1643
+ pointer: appendPointer(ctx.pointer, "$recursiveRef"),
1644
+ detail: {
1645
+ keyword: "$recursiveRef",
1646
+ ref: recursiveRef
1647
+ }
1648
+ });
1649
+ node.$ref = recursiveRef;
1115
1650
  delete node.$recursiveRef;
1116
1651
  }
1117
1652
  if (node.$recursiveAnchor === true) {
@@ -1121,12 +1656,24 @@ function normaliseDraft201909NodeWithContext(node, ctx) {
1121
1656
  if (typeof node.$anchor !== "string") node.$anchor = node.$recursiveAnchor;
1122
1657
  delete node.$recursiveAnchor;
1123
1658
  }
1659
+ splitDependencies(node, ctx, false);
1124
1660
  validateDependentRequired(node, ctx);
1125
1661
  return node;
1126
1662
  }
1127
1663
  function normaliseDynamicRefNodeWithContext(node, ctx) {
1128
- if (typeof node.$dynamicRef === "string") {
1129
- node.$ref = node.$dynamicRef;
1664
+ const dynamicRef = node.$dynamicRef;
1665
+ if (typeof dynamicRef === "string") {
1666
+ const crossDocument = !dynamicRef.startsWith("#");
1667
+ if (crossDocument || ctx.documentHasDynamicAnchor) emitDiagnostic(ctx.diagnostics, {
1668
+ code: "dynamic-ref-degraded",
1669
+ message: crossDocument ? `Cross-document \`$dynamicRef\` "${dynamicRef}" rewritten to a static \`$ref\`; dynamic-scope resolution is not preserved` : `\`$dynamicRef\` "${dynamicRef}" rewritten to a static \`$ref\` in a document declaring \`$dynamicAnchor\`; dynamic-scope resolution is not preserved`,
1670
+ pointer: appendPointer(ctx.pointer, "$dynamicRef"),
1671
+ detail: {
1672
+ keyword: "$dynamicRef",
1673
+ ref: dynamicRef
1674
+ }
1675
+ });
1676
+ node.$ref = dynamicRef;
1130
1677
  delete node.$dynamicRef;
1131
1678
  }
1132
1679
  if (typeof node.$dynamicAnchor === "string") {
@@ -1138,6 +1685,63 @@ function normaliseDynamicRefNodeWithContext(node, ctx) {
1138
1685
  return node;
1139
1686
  }
1140
1687
  /**
1688
+ * Pick the per-node transform that normalises a single Schema Object to
1689
+ * canonical Draft 2020-12 form for the supplied draft. Exposed so the
1690
+ * OpenAPI 3.1 path can honour a non-default `jsonSchemaDialect`
1691
+ * declaration by routing each Schema Object through the matching
1692
+ * transform without re-implementing the dispatch.
1693
+ */
1694
+ function selectDraftTransform(draft) {
1695
+ switch (draft) {
1696
+ case "draft-04": return normaliseDraft04NodeWithContext;
1697
+ case "draft-06":
1698
+ case "draft-07": return normaliseDraft06Or07NodeWithContext;
1699
+ case "draft-2019-09": return normaliseDraft201909NodeWithContext;
1700
+ case "draft-2020-12": return normaliseDynamicRefNodeWithContext;
1701
+ }
1702
+ }
1703
+ /**
1704
+ * Scan a JSON document body for the presence of a named keyword
1705
+ * anywhere in the structure. Walks both arrays and objects without
1706
+ * regard to schema-vs-data position — the caller is responsible for
1707
+ * passing a keyword whose presence is meaningful at any depth.
1708
+ *
1709
+ * Cycle-safe: cyclic references introduced by the OpenAPI bundler's
1710
+ * `structuredClone` of external refs are short-circuited via the
1711
+ * `visited` set so the scan terminates.
1712
+ */
1713
+ function documentContainsKeyword(value, keyword, visited = /* @__PURE__ */ new WeakSet()) {
1714
+ if (Array.isArray(value)) {
1715
+ if (visited.has(value)) return false;
1716
+ visited.add(value);
1717
+ for (const item of value) if (documentContainsKeyword(item, keyword, visited)) return true;
1718
+ return false;
1719
+ }
1720
+ if (isObject(value)) {
1721
+ if (visited.has(value)) return false;
1722
+ visited.add(value);
1723
+ if (keyword in value) return true;
1724
+ for (const v of Object.values(value)) if (documentContainsKeyword(v, keyword, visited)) return true;
1725
+ return false;
1726
+ }
1727
+ return false;
1728
+ }
1729
+ /**
1730
+ * Build a root {@link NodeContext} for a document being normalised.
1731
+ * Pre-scans for `$dynamicAnchor` and `$recursiveAnchor` so per-node
1732
+ * transforms can decide whether a `$dynamicRef`/`$recursiveRef`
1733
+ * rewrite needs a `dynamic-ref-degraded` diagnostic.
1734
+ */
1735
+ function buildRootContext(schema, diagnostics, declaredDraft) {
1736
+ return {
1737
+ diagnostics,
1738
+ pointer: "",
1739
+ documentHasDynamicAnchor: documentContainsKeyword(schema, "$dynamicAnchor"),
1740
+ documentHasRecursiveAnchor: documentContainsKeyword(schema, "$recursiveAnchor"),
1741
+ declaredDraft
1742
+ };
1743
+ }
1744
+ /**
1141
1745
  * Normalise a JSON Schema to canonical Draft 2020-12 form.
1142
1746
  * Deep-clones the input — the original is never mutated.
1143
1747
  *
@@ -1147,27 +1751,8 @@ function normaliseDynamicRefNodeWithContext(node, ctx) {
1147
1751
  * `dependencies` reaching the 2020-12 path).
1148
1752
  */
1149
1753
  function normaliseJsonSchema(schema, draft, diagnostics) {
1150
- const ctx = {
1151
- diagnostics,
1152
- pointer: ""
1153
- };
1154
- let normalised;
1155
- switch (draft) {
1156
- case "draft-04":
1157
- normalised = deepNormaliseWithContext(schema, normaliseDraft04NodeWithContext, ctx);
1158
- break;
1159
- case "draft-2019-09":
1160
- normalised = deepNormaliseWithContext(schema, normaliseDraft201909NodeWithContext, ctx);
1161
- break;
1162
- case "draft-2020-12":
1163
- normalised = deepNormaliseWithContext(schema, normaliseDynamicRefNodeWithContext, ctx);
1164
- break;
1165
- case "draft-06":
1166
- case "draft-07":
1167
- normalised = deepNormaliseWithContext(schema, normaliseDraft06Or07NodeWithContext, ctx);
1168
- break;
1169
- }
1170
- return resolveRelativeRefs(normalised, diagnostics);
1754
+ const ctx = buildRootContext(schema, diagnostics, draft);
1755
+ return resolveRelativeRefs(deepNormaliseWithContext(schema, selectDraftTransform(draft), ctx), diagnostics);
1171
1756
  }
1172
1757
  /**
1173
1758
  * Parse a string as an absolute URI, returning `undefined` when it has
@@ -1205,6 +1790,29 @@ function stripFragment(url) {
1205
1790
  return clone.toString();
1206
1791
  }
1207
1792
  /**
1793
+ * Emit an `invalid-id-fragment` diagnostic when an `$id` value carries
1794
+ * a fragment that will be stripped during base-URI resolution. Per
1795
+ * JSON Schema 2020-12 §8.2.1, the URI in `$id` MUST NOT contain a
1796
+ * non-empty fragment (an empty `#` fragment is permitted for historical
1797
+ * reasons but conveys nothing). Stripping it silently loses authoring
1798
+ * intent — the caller almost certainly meant to declare an `$anchor`
1799
+ * or sibling identifier instead.
1800
+ */
1801
+ function reportFragmentInId(value, url, pointer, diagnostics) {
1802
+ if (diagnostics === void 0) return;
1803
+ if (typeof value !== "string") return;
1804
+ if (url.hash.length === 0) return;
1805
+ emitDiagnostic(diagnostics, {
1806
+ code: "invalid-id-fragment",
1807
+ message: `\`$id\` URI "${value}" includes the fragment "${url.hash}", which is not permitted by JSON Schema §8.2.1; the fragment is stripped before use`,
1808
+ pointer: appendPointer(pointer, "$id"),
1809
+ detail: {
1810
+ id: value,
1811
+ fragment: url.hash
1812
+ }
1813
+ });
1814
+ }
1815
+ /**
1208
1816
  * Recursively rewrite relative `$ref`s in a schema so they resolve
1209
1817
  * correctly under the JSON Schema base-URI rules (RFC 3986 + JSON
1210
1818
  * Schema §8.2). Refs that resolve to the document's own `$id` are
@@ -1219,14 +1827,29 @@ function resolveRelativeRefs(schema, diagnostics) {
1219
1827
  const docBaseUrl = parseAbsoluteUri(schema.$id);
1220
1828
  if (docBaseUrl === void 0) return schema;
1221
1829
  const docBase = stripFragment(docBaseUrl);
1222
- return rewriteRelativeRefsNode(schema, docBase, docBase, "", diagnostics);
1830
+ return rewriteRelativeRefsNode(schema, docBase, docBase, "", diagnostics, /* @__PURE__ */ new WeakSet());
1223
1831
  }
1224
- function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostics) {
1832
+ /**
1833
+ * Rewrite relative `$ref`s in a single node, recursing into sub-schemas.
1834
+ *
1835
+ * The `visited` set guards against shared object references and cycles
1836
+ * introduced upstream (e.g. by the OpenAPI bundler's `structuredClone`
1837
+ * inlining of external refs). When a node is re-encountered the rewrite
1838
+ * is short-circuited — returning the original reference unchanged is
1839
+ * sound because every relative `$ref` in the subtree was already
1840
+ * rewritten on the first visit.
1841
+ */
1842
+ function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostics, visited) {
1843
+ if (visited.has(node)) return node;
1844
+ visited.add(node);
1225
1845
  let nextBase = currentBase;
1226
1846
  const nodeId = node.$id;
1227
1847
  if (typeof nodeId === "string" && nodeId.length > 0) {
1228
1848
  const resolved = resolveAgainst(nodeId, currentBase);
1229
- if (resolved !== void 0) nextBase = stripFragment(resolved);
1849
+ if (resolved !== void 0) {
1850
+ reportFragmentInId(nodeId, resolved, pointer, diagnostics);
1851
+ nextBase = stripFragment(resolved);
1852
+ }
1230
1853
  }
1231
1854
  const result = {};
1232
1855
  for (const [key, value] of Object.entries(node)) {
@@ -1234,13 +1857,17 @@ function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostic
1234
1857
  result[key] = rewriteRef(value, nextBase, docBase, appendPointer(pointer, key), diagnostics);
1235
1858
  continue;
1236
1859
  }
1237
- result[key] = rewriteRelativeRefsValue(value, key, nextBase, docBase, appendPointer(pointer, key), diagnostics);
1860
+ result[key] = rewriteRelativeRefsValue(value, nextBase, docBase, appendPointer(pointer, key), diagnostics, visited);
1238
1861
  }
1239
1862
  return result;
1240
1863
  }
1241
- function rewriteRelativeRefsValue(value, parentKey, currentBase, docBase, pointer, diagnostics) {
1242
- if (Array.isArray(value)) return value.map((item, i) => rewriteRelativeRefsValue(item, parentKey, currentBase, docBase, appendPointer(pointer, String(i)), diagnostics));
1243
- if (isObject(value)) return rewriteRelativeRefsNode(value, currentBase, docBase, pointer, diagnostics);
1864
+ function rewriteRelativeRefsValue(value, currentBase, docBase, pointer, diagnostics, visited) {
1865
+ if (Array.isArray(value)) {
1866
+ if (visited.has(value)) return value;
1867
+ visited.add(value);
1868
+ return value.map((item, i) => rewriteRelativeRefsValue(item, currentBase, docBase, appendPointer(pointer, String(i)), diagnostics, visited));
1869
+ }
1870
+ if (isObject(value)) return rewriteRelativeRefsNode(value, currentBase, docBase, pointer, diagnostics, visited);
1244
1871
  return value;
1245
1872
  }
1246
1873
  /**
@@ -1290,7 +1917,25 @@ function rewriteRef(ref, currentBase, docBase, pointer, diagnostics) {
1290
1917
  */
1291
1918
  function normaliseOpenApiSchemas(doc, version, diagnostics) {
1292
1919
  if (isSwagger2(version)) return normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, diagnostics);
1293
- if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(doc, deepNormalise);
1920
+ if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(applyDiscriminatorAllOfPrepass(doc), deepNormalise);
1921
+ if (!isOpenApi31(version)) {
1922
+ const rawOpenApi = typeof doc.openapi === "string" ? doc.openapi : void 0;
1923
+ const rawSwagger = typeof doc.swagger === "string" ? doc.swagger : void 0;
1924
+ const versionLabel = rawOpenApi ?? rawSwagger ?? `${String(version.major)}.${String(version.minor)}.${String(version.patch)}`;
1925
+ const pointer = rawOpenApi !== void 0 ? "/openapi" : "/swagger";
1926
+ emitDiagnostic(diagnostics, {
1927
+ code: "unknown-openapi-version",
1928
+ message: `Unsupported OpenAPI/Swagger version "${versionLabel}"; falling back to the OpenAPI 3.1 pipeline`,
1929
+ pointer,
1930
+ detail: {
1931
+ version: versionLabel,
1932
+ major: version.major,
1933
+ minor: version.minor,
1934
+ patch: version.patch
1935
+ }
1936
+ });
1937
+ }
1938
+ let dialectDraft;
1294
1939
  if (isOpenApi31(version)) {
1295
1940
  const dialect = readJsonSchemaDialect(doc);
1296
1941
  if (dialect.kind === "unknown") emitDiagnostic(diagnostics, {
@@ -1299,8 +1944,13 @@ function normaliseOpenApiSchemas(doc, version, diagnostics) {
1299
1944
  pointer: "/jsonSchemaDialect",
1300
1945
  detail: { uri: dialect.uri }
1301
1946
  });
1947
+ else if (dialect.kind === "known") dialectDraft = dialect.draft;
1302
1948
  }
1303
- return deepNormaliseOpenApiDoc(doc, (schema) => deepNormalise(schema, normaliseOpenApi30Discriminator));
1949
+ return deepNormaliseOpenApiDoc(applyDiscriminatorAllOfPrepass(doc), (schema) => {
1950
+ let intermediate = schema;
1951
+ if (dialectDraft !== void 0 && dialectDraft !== "draft-2020-12") intermediate = deepNormaliseWithContext(intermediate, selectDraftTransform(dialectDraft), buildRootContext(intermediate, diagnostics, dialectDraft));
1952
+ return resolveRelativeRefs(deepNormalise(intermediate, normaliseOpenApi30Discriminator), diagnostics);
1953
+ });
1304
1954
  }
1305
1955
  //#endregion
1306
- export { normaliseOpenApiSchemas as a, deepNormaliseOpenApiDoc as c, normaliseOpenApi30Node as d, normaliseJsonSchema as i, normaliseOpenApi30Combined as l, deepNormaliseWithContext as n, normaliseSwagger2Document as o, normaliseDraft04Node as r, deepNormaliseOpenApi30Doc as s, deepNormalise as t, normaliseOpenApi30Discriminator as u };
1956
+ export { normaliseOpenApiSchemas as a, applyDiscriminatorAllOfPrepass as c, normaliseOpenApi30Combined as d, normaliseOpenApi30Discriminator as f, normaliseJsonSchema as i, deepNormaliseOpenApi30Doc as l, deepNormaliseWithContext as n, selectDraftTransform as o, normaliseOpenApi30Node as p, normaliseDraft04Node as r, normaliseSwagger2Document as s, deepNormalise as t, deepNormaliseOpenApiDoc as u };