schema-components 1.21.0 → 1.23.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 (91) hide show
  1. package/README.md +3 -1
  2. package/dist/core/adapter.d.mts +115 -4
  3. package/dist/core/adapter.mjs +405 -75
  4. package/dist/core/constraints.d.mts +2 -2
  5. package/dist/core/constraints.mjs +0 -7
  6. package/dist/core/cssClasses.d.mts +52 -0
  7. package/dist/core/cssClasses.mjs +51 -0
  8. package/dist/core/diagnostics.d.mts +1 -1
  9. package/dist/core/errors.d.mts +1 -1
  10. package/dist/core/errors.mjs +5 -13
  11. package/dist/core/fieldOrder.d.mts +1 -1
  12. package/dist/core/formats.d.mts +30 -2
  13. package/dist/core/formats.mjs +33 -1
  14. package/dist/core/idPath.d.mts +54 -0
  15. package/dist/core/idPath.mjs +66 -0
  16. package/dist/core/limits.d.mts +2 -0
  17. package/dist/core/limits.mjs +23 -0
  18. package/dist/core/merge.d.mts +10 -1
  19. package/dist/core/merge.mjs +49 -10
  20. package/dist/core/normalise.d.mts +40 -3
  21. package/dist/core/normalise.mjs +2 -2
  22. package/dist/core/openapi30.d.mts +15 -1
  23. package/dist/core/openapi30.mjs +2 -2
  24. package/dist/core/openapiConstants.d.mts +67 -0
  25. package/dist/core/openapiConstants.mjs +90 -0
  26. package/dist/core/ref.d.mts +2 -2
  27. package/dist/core/ref.mjs +85 -6
  28. package/dist/core/refChain.d.mts +70 -0
  29. package/dist/core/refChain.mjs +44 -0
  30. package/dist/core/renderer.d.mts +1 -1
  31. package/dist/core/renderer.mjs +0 -2
  32. package/dist/core/swagger2.d.mts +1 -1
  33. package/dist/core/swagger2.mjs +1 -1
  34. package/dist/core/typeInference.d.mts +982 -2
  35. package/dist/core/types.d.mts +2 -2
  36. package/dist/core/types.mjs +1 -4
  37. package/dist/core/unionMatch.d.mts +36 -0
  38. package/dist/core/unionMatch.mjs +53 -0
  39. package/dist/core/version.d.mts +1 -1
  40. package/dist/core/version.mjs +29 -17
  41. package/dist/core/walkBuilders.d.mts +23 -4
  42. package/dist/core/walkBuilders.mjs +27 -7
  43. package/dist/core/walker.d.mts +1 -1
  44. package/dist/core/walker.mjs +123 -47
  45. package/dist/{diagnostics-CbBPsxSt.d.mts → diagnostics-BS2kaUyE.d.mts} +1 -1
  46. package/dist/{errors-QEwOtQAA.d.mts → errors-g_MCTQel.d.mts} +10 -16
  47. package/dist/html/a11y.d.mts +9 -4
  48. package/dist/html/a11y.mjs +10 -12
  49. package/dist/html/renderToHtml.d.mts +10 -3
  50. package/dist/html/renderToHtml.mjs +13 -3
  51. package/dist/html/renderToHtmlStream.d.mts +2 -2
  52. package/dist/html/renderToHtmlStream.mjs +12 -1
  53. package/dist/html/renderers.d.mts +43 -8
  54. package/dist/html/renderers.mjs +136 -116
  55. package/dist/html/streamRenderers.d.mts +6 -6
  56. package/dist/html/streamRenderers.mjs +129 -89
  57. package/dist/limits-Cw5QZND8.d.mts +29 -0
  58. package/dist/{normalise-DaSrnr8g.mjs → normalise-DCYp06Sr.mjs} +770 -227
  59. package/dist/openapi/ApiCallbacks.d.mts +1 -1
  60. package/dist/openapi/ApiLinks.d.mts +1 -1
  61. package/dist/openapi/ApiResponseHeaders.d.mts +1 -1
  62. package/dist/openapi/ApiSecurity.d.mts +1 -1
  63. package/dist/openapi/ApiSecurity.mjs +16 -2
  64. package/dist/openapi/components.d.mts +234 -23
  65. package/dist/openapi/components.mjs +183 -52
  66. package/dist/openapi/parser.d.mts +9 -8
  67. package/dist/openapi/parser.mjs +252 -70
  68. package/dist/openapi/resolve.d.mts +31 -15
  69. package/dist/openapi/resolve.mjs +260 -40
  70. package/dist/react/SchemaComponent.d.mts +126 -36
  71. package/dist/react/SchemaComponent.mjs +95 -57
  72. package/dist/react/SchemaView.d.mts +30 -10
  73. package/dist/react/SchemaView.mjs +2 -2
  74. package/dist/react/a11y.d.mts +21 -0
  75. package/dist/react/a11y.mjs +24 -0
  76. package/dist/react/fieldPath.d.mts +1 -1
  77. package/dist/react/headless.d.mts +1 -1
  78. package/dist/react/headless.mjs +1 -2
  79. package/dist/react/headlessRenderers.d.mts +9 -11
  80. package/dist/react/headlessRenderers.mjs +51 -102
  81. package/dist/{ref-si8ViYun.d.mts → ref-DjLEKa_E.d.mts} +38 -3
  82. package/dist/{renderer-DI6ZYf7a.d.mts → renderer-CXJ8y0qw.d.mts} +2 -2
  83. package/dist/themes/mantine.d.mts +1 -1
  84. package/dist/themes/mui.d.mts +1 -1
  85. package/dist/themes/radix.d.mts +1 -1
  86. package/dist/themes/shadcn.d.mts +1 -1
  87. package/dist/themes/shadcn.mjs +2 -1
  88. package/dist/{types-BnxPEElk.d.mts → types-BTB73MB8.d.mts} +35 -14
  89. package/dist/{version-D-u7aMfy.d.mts → version-BFTVLsdb.d.mts} +7 -1
  90. package/package.json +1 -3
  91. package/dist/typeInference-Bxw3NOG1.d.mts +0 -647
@@ -1,6 +1,9 @@
1
1
  import { isObject } from "./core/guards.mjs";
2
2
  import { appendPointer, emitDiagnostic } from "./core/diagnostics.mjs";
3
- import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./core/version.mjs";
3
+ import { RECURSIVE_ANCHOR_SENTINEL } from "./core/ref.mjs";
4
+ import { isOpenApi30, isOpenApi31, isSwagger2, matchJsonSchemaDraftUri, readJsonSchemaDialect } from "./core/version.mjs";
5
+ import { SWAGGER_2_METHODS, rewriteSwaggerRefPrefix } from "./core/openapiConstants.mjs";
6
+ import { resolveRefChain } from "./core/refChain.mjs";
4
7
  //#region src/core/openapi30.ts
5
8
  /**
6
9
  * OpenAPI 3.0.x schema normalisation.
@@ -10,6 +13,29 @@ import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./c
10
13
  * responses, headers, callbacks, links, examples) to apply normalisation.
11
14
  */
12
15
  /**
16
+ * Lift OpenAPI 3.x singular `example` onto the plural `examples` key.
17
+ *
18
+ * Two output shapes are spec-correct depending on the parent object type:
19
+ * - `"array"` — Schema Object: `examples: [example]` (Draft 2020-12 plural).
20
+ * - `"map"` — Parameter / Header / Media Type Object: an Examples Map
21
+ * keyed by name. The single value is wrapped under the
22
+ * synthetic key `default` to produce a valid one-entry map
23
+ * of one Example Object.
24
+ *
25
+ * When both `example` and `examples` coexist the spec declares them mutually
26
+ * exclusive — `example` is dropped and `examples` wins.
27
+ */
28
+ function liftExampleToExamples(node, shape) {
29
+ if (!("example" in node)) return;
30
+ if ("examples" in node) {
31
+ delete node.example;
32
+ return;
33
+ }
34
+ if (shape === "array") node.examples = [node.example];
35
+ else node.examples = { default: { value: node.example } };
36
+ delete node.example;
37
+ }
38
+ /**
13
39
  * Normalise OpenAPI 3.0.x `nullable` keyword to `anyOf [T, null]`.
14
40
  *
15
41
  * OpenAPI 3.0 uses `nullable: true` instead of the JSON Schema standard
@@ -20,22 +46,32 @@ import { isOpenApi30, isOpenApi31, isSwagger2, readJsonSchemaDialect } from "./c
20
46
  * absent is the default and requires no transformation.
21
47
  */
22
48
  function normaliseOpenApi30Node(node) {
23
- if ("example" in node && !("examples" in node)) {
24
- node.examples = [node.example];
25
- delete node.example;
26
- } else if ("example" in node) delete node.example;
49
+ liftExampleToExamples(node, "array");
27
50
  if (node.nullable !== true) {
28
51
  if ("nullable" in node) delete node.nullable;
29
52
  return node;
30
53
  }
31
54
  const nullOption = { type: "null" };
55
+ if (typeof node.$ref === "string") {
56
+ const wrapper = { anyOf: [{ $ref: node.$ref }, nullOption] };
57
+ for (const key of REF_DOC_SIBLINGS) if (key in node) wrapper[key] = node[key];
58
+ return wrapper;
59
+ }
60
+ if (Array.isArray(node.enum)) {
61
+ if (node.enum.includes(null)) {
62
+ delete node.nullable;
63
+ return node;
64
+ }
65
+ }
32
66
  if (Array.isArray(node.anyOf)) {
33
- node.anyOf = [...node.anyOf, nullOption];
67
+ const existing = node.anyOf;
68
+ node.anyOf = compositeAlreadyAllowsNull(existing) ? existing : [...existing, nullOption];
34
69
  delete node.nullable;
35
70
  return node;
36
71
  }
37
72
  if (Array.isArray(node.oneOf)) {
38
- node.anyOf = [...node.oneOf, nullOption];
73
+ const existing = node.oneOf;
74
+ node.anyOf = compositeAlreadyAllowsNull(existing) ? existing : [...existing, nullOption];
39
75
  delete node.oneOf;
40
76
  delete node.nullable;
41
77
  return node;
@@ -51,6 +87,39 @@ function normaliseOpenApi30Node(node) {
51
87
  return { anyOf: [wrapper, nullOption] };
52
88
  }
53
89
  /**
90
+ * Documentary keys that may legitimately sit alongside a `$ref` in an
91
+ * OpenAPI 3.0 Schema Object. They carry author-facing metadata, not
92
+ * validation semantics, so lifting them onto the `anyOf` wrapper
93
+ * preserves authorial intent without violating the spec rule that a
94
+ * Reference Object itself only carry `$ref`.
95
+ */
96
+ const REF_DOC_SIBLINGS = [
97
+ "description",
98
+ "summary",
99
+ "title",
100
+ "deprecated",
101
+ "readOnly",
102
+ "writeOnly",
103
+ "example",
104
+ "examples",
105
+ "default"
106
+ ];
107
+ /**
108
+ * Returns `true` when at least one option in a composite (`anyOf` /
109
+ * `oneOf`) already permits `null` — either a literal `{ type: "null" }`
110
+ * branch or an `enum` containing `null`. Used to dedup the synthetic
111
+ * null option appended when normalising `nullable: true`.
112
+ */
113
+ function compositeAlreadyAllowsNull(options) {
114
+ for (const option of options) {
115
+ if (!isObject(option)) continue;
116
+ if (option.type === "null") return true;
117
+ if (Array.isArray(option.type) && option.type.includes("null")) return true;
118
+ if (Array.isArray(option.enum) && option.enum.includes(null)) return true;
119
+ }
120
+ return false;
121
+ }
122
+ /**
54
123
  * Normalise OpenAPI 3.0.x `discriminator` keyword by injecting `const`
55
124
  * values into each `oneOf`/`anyOf` option's discriminator property.
56
125
  *
@@ -78,7 +147,7 @@ function normaliseOpenApi30Discriminator(node) {
78
147
  normalisedComposite.push(option);
79
148
  continue;
80
149
  }
81
- const props = isObject(option.properties) ? { ...option.properties } : void 0;
150
+ const props = isObject(option.properties) ? option.properties : void 0;
82
151
  const discProp = props?.[propertyName];
83
152
  if (isObject(discProp) && "const" in discProp) {
84
153
  normalisedComposite.push(option);
@@ -100,7 +169,7 @@ function normaliseOpenApi30Discriminator(node) {
100
169
  if (entry !== void 0) constValue = entry[0];
101
170
  }
102
171
  if (constValue !== void 0) {
103
- const normalisedProps = props ?? {};
172
+ const normalisedProps = { ...props };
104
173
  normalisedProps[propertyName] = {
105
174
  ...isObject(discProp) ? discProp : {},
106
175
  const: constValue
@@ -113,7 +182,13 @@ function normaliseOpenApi30Discriminator(node) {
113
182
  }
114
183
  if ("oneOf" in node) node.oneOf = normalisedComposite;
115
184
  else if ("anyOf" in node) node.anyOf = normalisedComposite;
116
- delete node.discriminator;
185
+ const extensions = {};
186
+ for (const [key, value] of Object.entries(discriminator)) if (key.startsWith("x-")) extensions[key] = value;
187
+ if (Object.keys(extensions).length > 0) node.discriminator = {
188
+ propertyName,
189
+ ...extensions
190
+ };
191
+ else delete node.discriminator;
117
192
  return node;
118
193
  }
119
194
  /**
@@ -479,10 +554,7 @@ function normaliseParameter(param, normaliseSchema) {
479
554
  if (isObject(schema)) result.schema = normaliseSchema(schema);
480
555
  const content = param.content;
481
556
  if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
482
- if ("example" in result && !("examples" in result)) {
483
- result.examples = [result.example];
484
- delete result.example;
485
- } else if ("example" in result) delete result.example;
557
+ liftExampleToExamples(result, "map");
486
558
  return result;
487
559
  }
488
560
  function normaliseRequestBody(requestBody, normaliseSchema) {
@@ -505,10 +577,7 @@ function normaliseHeader(header, normaliseSchema) {
505
577
  if (isObject(schema)) result.schema = normaliseSchema(schema);
506
578
  const content = header.content;
507
579
  if (isObject(content)) result.content = normaliseContentMap(content, normaliseSchema);
508
- if ("example" in result && !("examples" in result)) {
509
- result.examples = [result.example];
510
- delete result.example;
511
- } else if ("example" in result) delete result.example;
580
+ liftExampleToExamples(result, "map");
512
581
  return result;
513
582
  }
514
583
  /**
@@ -531,10 +600,7 @@ function normaliseContentMap(content, normaliseSchema) {
531
600
  if (isObject(schema)) normalised.schema = normaliseSchema(schema);
532
601
  const encoding = mediaObj.encoding;
533
602
  if (isObject(encoding)) normalised.encoding = mapObjectValues(encoding, (enc) => isObject(enc) ? normaliseEncoding(enc, normaliseSchema) : enc);
534
- if ("example" in normalised && !("examples" in normalised)) {
535
- normalised.examples = { default: { value: normalised.example } };
536
- delete normalised.example;
537
- } else if ("example" in normalised) delete normalised.example;
603
+ liftExampleToExamples(normalised, "map");
538
604
  result[mediaType] = normalised;
539
605
  }
540
606
  return result;
@@ -579,9 +645,19 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
579
645
  version: "0.0.0"
580
646
  }
581
647
  };
582
- if (typeof doc.host === "string" || typeof doc.basePath === "string" || Array.isArray(doc.schemes)) {
583
- const host = typeof doc.host === "string" ? doc.host : "localhost";
584
- const basePath = typeof doc.basePath === "string" ? doc.basePath : "/";
648
+ if (typeof doc.host !== "string") {
649
+ if (Array.isArray(doc.schemes) || typeof doc.basePath === "string") emitDiagnostic(diagnostics, {
650
+ code: "swagger-missing-host",
651
+ message: "Swagger 2.0 document declares schemes or basePath without host; skipping server URL synthesis",
652
+ pointer: "",
653
+ detail: {
654
+ hasSchemes: Array.isArray(doc.schemes),
655
+ hasBasePath: typeof doc.basePath === "string"
656
+ }
657
+ });
658
+ } else {
659
+ const host = doc.host;
660
+ const basePath = typeof doc.basePath === "string" ? doc.basePath : "";
585
661
  const schemes = Array.isArray(doc.schemes) ? doc.schemes : ["https"];
586
662
  result.servers = [{ url: `${typeof schemes[0] === "string" ? schemes[0] : "https"}://${host}${basePath}` }];
587
663
  }
@@ -597,37 +673,90 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
597
673
  const parameters = doc.parameters;
598
674
  const requestBodies = {};
599
675
  if (isObject(parameters)) {
600
- const globalConsumes = Array.isArray(doc.consumes) ? doc.consumes : ["application/json"];
676
+ const consumesResolution = resolveSwaggerContentTypes(void 0, doc.consumes);
677
+ const globalConsumes = consumesResolution.types;
601
678
  const convertedParameters = {};
602
679
  for (const [name, param] of Object.entries(parameters)) {
603
680
  if (!isObject(param)) {
604
681
  convertedParameters[name] = param;
605
682
  continue;
606
683
  }
607
- const resolved = resolveSwaggerParameter(param, doc);
684
+ const resolution = resolveSwaggerParameter(param, doc);
685
+ if (resolution.kind === "cycle") {
686
+ emitDiagnostic(diagnostics, {
687
+ code: "swagger-cyclic-parameter-ref",
688
+ message: `Cyclic Swagger 2.0 parameter $ref "${resolution.ref}"; skipping entry`,
689
+ pointer: appendPointer(appendPointer("", "parameters"), name),
690
+ detail: {
691
+ ref: resolution.ref,
692
+ name
693
+ }
694
+ });
695
+ continue;
696
+ }
697
+ const resolved = resolution.param;
608
698
  const location = resolved.in;
609
- if (location === "body") requestBodies[name] = buildRequestBody(resolved, globalConsumes);
610
- else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
611
- else convertedParameters[name] = normaliseSwaggerParameter(resolved, doc);
699
+ if (location === "body") {
700
+ const paramPointer = appendPointer(appendPointer("", "parameters"), name);
701
+ let bodyContentTypes;
702
+ if (consumesResolution.source === "synthesised") {
703
+ bodyContentTypes = globalConsumes;
704
+ emitDiagnostic(diagnostics, {
705
+ code: "swagger-missing-consumes",
706
+ message: "Global body parameter declared but document-level `consumes` is absent; defaulting to application/json",
707
+ pointer: paramPointer,
708
+ detail: {
709
+ level: "document",
710
+ name
711
+ }
712
+ });
713
+ } else if (globalConsumes.length === 0) {
714
+ bodyContentTypes = [];
715
+ emitDiagnostic(diagnostics, {
716
+ code: "swagger-missing-consumes",
717
+ message: "Global body parameter declared but document-level `consumes` is an explicit empty array; preserving an empty content map",
718
+ pointer: paramPointer,
719
+ detail: {
720
+ level: "document",
721
+ name,
722
+ reason: "explicitly-cleared",
723
+ source: consumesResolution.source
724
+ }
725
+ });
726
+ } else bodyContentTypes = globalConsumes;
727
+ requestBodies[name] = buildRequestBody(resolved, bodyContentTypes);
728
+ } else if (location === "formData") requestBodies[name] = buildRequestBody(buildFormDataBody(resolved, [resolved]), formDataContentTypes(globalConsumes));
729
+ else {
730
+ const normalised = normaliseSwaggerParameter(resolved, doc, diagnostics, appendPointer(appendPointer("", "parameters"), name));
731
+ if (normalised !== void 0) convertedParameters[name] = normalised;
732
+ }
612
733
  }
613
734
  if (Object.keys(convertedParameters).length > 0) components.parameters = convertedParameters;
614
735
  }
615
736
  const responses = doc.responses;
616
737
  if (isObject(responses)) {
617
- const globalProduces = Array.isArray(doc.produces) ? doc.produces : ["application/json"];
738
+ const producesResolution = resolveSwaggerContentTypes(void 0, doc.produces);
618
739
  const convertedResponses = {};
619
- for (const [name, response] of Object.entries(responses)) convertedResponses[name] = isObject(response) ? normaliseSwaggerSingleResponse(response, doc, globalProduces) : response;
740
+ 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;
620
741
  components.responses = convertedResponses;
621
742
  }
622
743
  if (Object.keys(requestBodies).length > 0) components.requestBodies = requestBodies;
623
744
  const securityDefinitions = doc.securityDefinitions;
624
- if (isObject(securityDefinitions)) components.securitySchemes = { ...securityDefinitions };
745
+ if (isObject(securityDefinitions)) {
746
+ const translated = {};
747
+ const securityDefinitionsPointer = appendPointer("", "securityDefinitions");
748
+ for (const [name, scheme] of Object.entries(securityDefinitions)) {
749
+ const schemePointer = appendPointer(securityDefinitionsPointer, name);
750
+ translated[name] = isObject(scheme) ? translateSwaggerSecurityScheme(scheme, diagnostics, schemePointer, name) : scheme;
751
+ }
752
+ components.securitySchemes = translated;
753
+ }
625
754
  if (Object.keys(components).length > 0) result.components = components;
626
755
  if (Array.isArray(doc.tags)) result.tags = doc.tags;
627
756
  if (isObject(doc.externalDocs)) result.externalDocs = doc.externalDocs;
628
757
  if (Array.isArray(doc.security)) result.security = doc.security;
629
758
  rewriteSwaggerRefs(result);
630
- if (hasXmlAnywhere(doc.definitions) || hasXmlAnywhere(doc.paths) || hasXmlAnywhere(doc.parameters) || hasXmlAnywhere(doc.responses)) emitDiagnostic(diagnostics, {
759
+ if (documentContainsKeyword(doc.definitions, "xml") || documentContainsKeyword(doc.paths, "xml") || documentContainsKeyword(doc.parameters, "xml") || documentContainsKeyword(doc.responses, "xml")) emitDiagnostic(diagnostics, {
631
760
  code: "dropped-swagger-feature",
632
761
  message: "Swagger 2.0 xml markup is not supported and will be dropped",
633
762
  pointer: "",
@@ -637,38 +766,41 @@ function normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, dia
637
766
  }
638
767
  function normaliseSwaggerPaths(paths, doc, diagnostics) {
639
768
  const result = {};
640
- const METHODS = [
641
- "get",
642
- "post",
643
- "put",
644
- "patch",
645
- "delete",
646
- "head",
647
- "options"
648
- ];
649
769
  for (const [path, pathItem] of Object.entries(paths)) {
650
770
  if (!isObject(pathItem)) {
651
771
  result[path] = pathItem;
652
772
  continue;
653
773
  }
654
774
  const normalisedPath = {};
655
- for (const method of METHODS) {
775
+ for (const method of SWAGGER_2_METHODS) {
656
776
  const operation = pathItem[method];
657
777
  if (!isObject(operation)) continue;
658
778
  normalisedPath[method] = normaliseSwaggerOperation(operation, doc, path, method, diagnostics);
659
779
  }
660
780
  const pathParams = pathItem.parameters;
661
- if (Array.isArray(pathParams)) normalisedPath.parameters = pathParams.map((p) => isObject(p) ? normaliseSwaggerParameter(p, doc) : p);
781
+ if (Array.isArray(pathParams)) {
782
+ const paramsPointer = appendPointer(appendPointer(appendPointer("", "paths"), path), "parameters");
783
+ const out = [];
784
+ for (const [index, p] of pathParams.entries()) {
785
+ if (!isObject(p)) {
786
+ out.push(p);
787
+ continue;
788
+ }
789
+ const normalised = normaliseSwaggerParameter(p, doc, diagnostics, appendPointer(paramsPointer, String(index)));
790
+ if (normalised !== void 0) out.push(normalised);
791
+ }
792
+ normalisedPath.parameters = out;
793
+ }
662
794
  result[path] = normalisedPath;
663
795
  }
664
796
  return result;
665
797
  }
666
798
  function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
667
799
  const result = {};
668
- const globalProduces = Array.isArray(doc.produces) ? doc.produces : ["application/json"];
669
- const globalConsumes = Array.isArray(doc.consumes) ? doc.consumes : ["application/json"];
670
- const produces = Array.isArray(operation.produces) ? operation.produces : globalProduces;
671
- const consumes = Array.isArray(operation.consumes) ? operation.consumes : globalConsumes;
800
+ const consumesResolution = resolveSwaggerContentTypes(operation.consumes, doc.consumes);
801
+ const producesResolution = resolveSwaggerContentTypes(operation.produces, doc.produces);
802
+ const produces = producesResolution.types;
803
+ const consumes = consumesResolution.types;
672
804
  for (const [key, value] of Object.entries(operation)) if (key !== "parameters" && key !== "responses" && key !== "produces" && key !== "consumes") result[key] = value;
673
805
  const params = operation.parameters;
674
806
  if (Array.isArray(params)) {
@@ -681,7 +813,17 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
681
813
  nonBodyParams.push(param);
682
814
  continue;
683
815
  }
684
- const resolvedParam = resolveSwaggerParameter(param, doc);
816
+ const paramResolution = resolveSwaggerParameter(param, doc);
817
+ if (paramResolution.kind === "cycle") {
818
+ emitDiagnostic(diagnostics, {
819
+ code: "swagger-cyclic-parameter-ref",
820
+ message: `Cyclic Swagger 2.0 parameter $ref "${paramResolution.ref}"; skipping entry`,
821
+ pointer: appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"), String(index)),
822
+ detail: { ref: paramResolution.ref }
823
+ });
824
+ continue;
825
+ }
826
+ const resolvedParam = paramResolution.param;
685
827
  const location = resolvedParam.in;
686
828
  if (location === "body") {
687
829
  if (bodyParam !== void 0) {
@@ -705,16 +847,69 @@ function normaliseSwaggerOperation(operation, doc, path, method, diagnostics) {
705
847
  bodyParam = buildFormDataBody(resolvedParam, params);
706
848
  usesFormData = true;
707
849
  }
708
- } else nonBodyParams.push(normaliseSwaggerParameter(resolvedParam, doc));
850
+ } else {
851
+ const normalised = normaliseSwaggerParameter(resolvedParam, doc, diagnostics, appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "parameters"), String(index)));
852
+ if (normalised !== void 0) nonBodyParams.push(normalised);
853
+ }
709
854
  }
710
855
  if (nonBodyParams.length > 0) result.parameters = nonBodyParams;
711
- if (bodyParam !== void 0) result.requestBody = buildRequestBody(bodyParam, usesFormData ? formDataContentTypes(consumes) : consumes);
856
+ if (bodyParam !== void 0) {
857
+ const operationPointer = appendPointer(appendPointer(appendPointer("", "paths"), path), method);
858
+ let bodyContentTypes;
859
+ if (usesFormData) bodyContentTypes = formDataContentTypes(consumes);
860
+ else if (consumesResolution.source === "synthesised") {
861
+ bodyContentTypes = consumes;
862
+ emitDiagnostic(diagnostics, {
863
+ code: "swagger-missing-consumes",
864
+ message: "Operation declares a body parameter but neither operation-level nor document-level `consumes` is set; defaulting to application/json",
865
+ pointer: operationPointer,
866
+ detail: {
867
+ level: "operation",
868
+ method
869
+ }
870
+ });
871
+ } else if (consumes.length === 0) {
872
+ bodyContentTypes = [];
873
+ emitDiagnostic(diagnostics, {
874
+ code: "swagger-missing-consumes",
875
+ message: "Operation declares a body parameter but `consumes` is an explicit empty array; preserving an empty content map",
876
+ pointer: operationPointer,
877
+ detail: {
878
+ level: "operation",
879
+ method,
880
+ reason: "explicitly-cleared",
881
+ source: consumesResolution.source
882
+ }
883
+ });
884
+ } else bodyContentTypes = consumes;
885
+ result.requestBody = buildRequestBody(bodyParam, bodyContentTypes);
886
+ }
712
887
  }
713
888
  const responses = operation.responses;
714
- if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces);
889
+ if (isObject(responses)) result.responses = normaliseSwaggerResponses(responses, doc, produces, producesResolution.source, diagnostics, path, method);
715
890
  return result;
716
891
  }
717
892
  /**
893
+ * Resolve a Swagger 2.0 `consumes` or `produces` array, recording
894
+ * where the value came from so callers can decide whether to emit a
895
+ * "missing content type" diagnostic. Per the Swagger 2.0 spec, absence
896
+ * at BOTH levels means no body — not an implicit `application/json`.
897
+ */
898
+ function resolveSwaggerContentTypes(operationLevel, documentLevel) {
899
+ if (Array.isArray(operationLevel)) return {
900
+ types: operationLevel,
901
+ source: "operation"
902
+ };
903
+ if (Array.isArray(documentLevel)) return {
904
+ types: documentLevel,
905
+ source: "document"
906
+ };
907
+ return {
908
+ types: ["application/json"],
909
+ source: "synthesised"
910
+ };
911
+ }
912
+ /**
718
913
  * Determine the request body media type for a Swagger 2.0 formData operation.
719
914
  *
720
915
  * Per the OAS 3 conversion rules, `application/x-www-form-urlencoded` is
@@ -729,51 +924,141 @@ function formDataContentTypes(consumes) {
729
924
  return ["multipart/form-data"];
730
925
  }
731
926
  /**
732
- * Resolve a Swagger parameter that may be a `$ref`.
733
- */
734
- function resolveSwaggerParameter(param, doc, visited = /* @__PURE__ */ new Set()) {
735
- const ref = param.$ref;
736
- if (typeof ref !== "string" || !ref.startsWith("#/parameters/")) return param;
737
- if (visited.has(ref)) return param;
738
- const nextVisited = new Set(visited);
739
- nextVisited.add(ref);
740
- const name = ref.slice(13);
741
- const globalParams = doc.parameters;
742
- if (isObject(globalParams)) {
743
- const resolved = globalParams[name];
744
- if (isObject(resolved)) {
745
- if (typeof resolved.$ref === "string") return resolveSwaggerParameter(resolved, doc, nextVisited);
746
- return resolved;
927
+ * Every JSON-Schema-compatible constraint keyword Swagger 2.0 allows on
928
+ * a Parameter Object or Header Object alongside `type`/`format`. These
929
+ * lift into the synthesised `schema` so consumers see the original
930
+ * validation semantics under OAS 3.x's parameter shape.
931
+ *
932
+ * `allowEmptyValue` is included even though it is a Swagger 2.0
933
+ * parameter-level keyword in the source (not a schema keyword) — OAS
934
+ * 3.x defines it at the Parameter Object root, so the calling function
935
+ * keeps it at the parameter root rather than copying it into `schema`.
936
+ */
937
+ const SWAGGER_PARAM_SCHEMA_KEYWORDS = [
938
+ "enum",
939
+ "default",
940
+ "minimum",
941
+ "maximum",
942
+ "exclusiveMinimum",
943
+ "exclusiveMaximum",
944
+ "multipleOf",
945
+ "minLength",
946
+ "maxLength",
947
+ "pattern",
948
+ "minItems",
949
+ "maxItems",
950
+ "uniqueItems"
951
+ ];
952
+ /**
953
+ * Set of every Swagger 2.0 parameter-root keyword that must be lifted
954
+ * into the synthesised `schema` rather than copied onto the OAS 3.x
955
+ * parameter root. Includes `type`, `format`, `items` (Swagger 2.0
956
+ * parameter-shaped array element descriptor), `collectionFormat`
957
+ * (handled separately by the caller as `style`/`explode`), and every
958
+ * entry from {@link SWAGGER_PARAM_SCHEMA_KEYWORDS}.
959
+ */
960
+ const PARAM_KEYWORDS_LIFTED_INTO_SCHEMA = new Set([
961
+ "type",
962
+ "format",
963
+ "items",
964
+ "collectionFormat",
965
+ ...SWAGGER_PARAM_SCHEMA_KEYWORDS
966
+ ]);
967
+ /**
968
+ * Synthesise an OpenAPI 3.x `schema` object from a Swagger 2.0
969
+ * parameter-shaped node (parameter or header). Copies `type`,
970
+ * `format`, and every JSON-Schema-compatible constraint that Swagger
971
+ * 2.0 places at the parameter root. Nested `items` is recursively
972
+ * synthesised the same way so array element constraints survive.
973
+ */
974
+ function buildSchemaFromSwaggerParameterShape(node) {
975
+ const schema = { type: node.type };
976
+ if (typeof node.format === "string") schema.format = node.format;
977
+ for (const keyword of SWAGGER_PARAM_SCHEMA_KEYWORDS) if (node[keyword] !== void 0) schema[keyword] = node[keyword];
978
+ if (isObject(node.items)) schema.items = buildSchemaFromSwaggerParameterShape(node.items);
979
+ return schema;
980
+ }
981
+ /**
982
+ * Resolve a Swagger parameter that may be a `$ref`. Returns the
983
+ * resolved parameter object, or a cycle marker so the caller can
984
+ * decide how to surface the failure. Non-ref parameters resolve to
985
+ * themselves; ref targets that don't exist also resolve to the input
986
+ * (the caller treats unknown refs the same as bare parameters).
987
+ *
988
+ * Uses the shared {@link resolveRefChain} helper so cycle detection
989
+ * and hop accounting stay consistent with the other OpenAPI $ref
990
+ * walkers.
991
+ */
992
+ function resolveSwaggerParameter(param, doc) {
993
+ let cyclicRef;
994
+ const finalNode = resolveRefChain(param, {
995
+ extractRef: (node) => {
996
+ const ref = node.$ref;
997
+ if (typeof ref !== "string") return void 0;
998
+ return ref.startsWith("#/parameters/") ? ref : void 0;
999
+ },
1000
+ lookup: (ref) => {
1001
+ const name = ref.slice(13);
1002
+ const globalParams = doc.parameters;
1003
+ if (!isObject(globalParams)) return void 0;
1004
+ const resolved = globalParams[name];
1005
+ return isObject(resolved) ? resolved : void 0;
1006
+ },
1007
+ onCycle: (ref) => {
1008
+ cyclicRef = ref;
747
1009
  }
748
- }
749
- return param;
1010
+ });
1011
+ if (cyclicRef !== void 0) return {
1012
+ kind: "cycle",
1013
+ ref: cyclicRef
1014
+ };
1015
+ return {
1016
+ kind: "ok",
1017
+ param: finalNode ?? param
1018
+ };
750
1019
  }
751
1020
  /**
752
1021
  * Normalise a single Swagger parameter to OpenAPI 3.x form.
753
1022
  */
754
- function normaliseSwaggerParameter(param, doc) {
1023
+ function normaliseSwaggerParameter(param, doc, diagnostics, pointer = "") {
755
1024
  if (typeof param.$ref === "string") {
756
- const resolved = resolveSwaggerParameter(param, doc);
757
- if (resolved !== param) return normaliseSwaggerParameter(resolved, doc);
1025
+ const resolution = resolveSwaggerParameter(param, doc);
1026
+ if (resolution.kind === "cycle") {
1027
+ emitDiagnostic(diagnostics, {
1028
+ code: "swagger-cyclic-parameter-ref",
1029
+ message: `Cyclic Swagger 2.0 parameter $ref "${resolution.ref}"; skipping entry`,
1030
+ pointer,
1031
+ detail: { ref: resolution.ref }
1032
+ });
1033
+ return;
1034
+ }
1035
+ const resolved = resolution.param;
1036
+ if (resolved !== param) return normaliseSwaggerParameter(resolved, doc, diagnostics, pointer);
758
1037
  }
759
1038
  const result = {};
760
1039
  for (const [key, value] of Object.entries(param)) {
761
- if (key === "type" || key === "format" || key === "collectionFormat") continue;
1040
+ if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
762
1041
  result[key] = value;
763
1042
  }
764
- if (typeof param.type === "string") {
765
- const schema = { type: param.type };
766
- if (typeof param.format === "string") schema.format = param.format;
767
- if (param.enum !== void 0) schema.enum = param.enum;
768
- if (param.default !== void 0) schema.default = param.default;
769
- if (param.minimum !== void 0) schema.minimum = param.minimum;
770
- if (param.maximum !== void 0) schema.maximum = param.maximum;
771
- result.schema = schema;
772
- }
1043
+ if (typeof param.type === "string") if (param.type === "file" && param.in !== "formData") {
1044
+ emitDiagnostic(diagnostics, {
1045
+ code: "swagger-invalid-file-parameter",
1046
+ message: `Swagger 2.0 type: "file" is only valid under in: formData; converting to { type: "string", format: "binary" }`,
1047
+ pointer,
1048
+ detail: {
1049
+ name: param.name,
1050
+ in: param.in
1051
+ }
1052
+ });
1053
+ result.schema = {
1054
+ type: "string",
1055
+ format: "binary"
1056
+ };
1057
+ } else result.schema = buildSchemaFromSwaggerParameterShape(param);
773
1058
  const cf = param.collectionFormat;
774
1059
  if (typeof cf === "string") switch (cf) {
775
1060
  case "csv":
776
- result.style = "form";
1061
+ result.style = param.in === "path" || param.in === "header" ? "simple" : "form";
777
1062
  result.explode = false;
778
1063
  break;
779
1064
  case "ssv":
@@ -781,8 +1066,15 @@ function normaliseSwaggerParameter(param, doc) {
781
1066
  result.explode = false;
782
1067
  break;
783
1068
  case "tsv":
784
- result.style = "tabDelimited";
785
- result.explode = false;
1069
+ emitDiagnostic(diagnostics, {
1070
+ code: "swagger-collection-format-dropped",
1071
+ message: "Swagger 2.0 collectionFormat: \"tsv\" has no OpenAPI 3.x equivalent; dropping the keyword",
1072
+ pointer,
1073
+ detail: {
1074
+ feature: "collectionFormat:tsv",
1075
+ location: "parameter"
1076
+ }
1077
+ });
786
1078
  break;
787
1079
  case "pipes":
788
1080
  result.style = "pipeDelimited";
@@ -829,12 +1121,18 @@ function buildFormDataBody(param, allParams) {
829
1121
  }
830
1122
  /**
831
1123
  * Build an OpenAPI 3.x request body from a Swagger 2.0 body parameter.
1124
+ *
1125
+ * `consumes` is taken at face value — the caller is responsible for
1126
+ * deciding whether an absent value should fall back to a default
1127
+ * (and emitting `swagger-missing-consumes`) or be preserved as an
1128
+ * empty content map (an explicit clear). Inventing a default here
1129
+ * would mask the difference and silently override the upstream
1130
+ * resolution.
832
1131
  */
833
1132
  function buildRequestBody(bodyParam, consumes) {
834
1133
  const schema = bodyParam.schema;
835
1134
  const content = {};
836
- const contentTypes = consumes.length > 0 ? consumes : ["application/json"];
837
- for (const ct of contentTypes) if (typeof ct === "string") content[ct] = isObject(schema) ? { schema } : {};
1135
+ for (const ct of consumes) if (typeof ct === "string") content[ct] = isObject(schema) ? { schema } : {};
838
1136
  const result = { content };
839
1137
  if (bodyParam.required === true) result.required = true;
840
1138
  if (typeof bodyParam.description === "string") result.description = bodyParam.description;
@@ -842,32 +1140,37 @@ function buildRequestBody(bodyParam, consumes) {
842
1140
  }
843
1141
  /**
844
1142
  * Resolve a Swagger 2.0 response `$ref` (e.g. `#/responses/NotFound`).
1143
+ *
1144
+ * Uses the shared {@link resolveRefChain} helper so cycle detection
1145
+ * and hop accounting stay consistent with the other OpenAPI $ref
1146
+ * walkers. On cycle the original wrapper is returned (legacy
1147
+ * behaviour), preserving the existing response-resolution contract.
845
1148
  */
846
- function resolveSwaggerResponse(response, doc, visited = /* @__PURE__ */ new Set()) {
847
- const ref = response.$ref;
848
- if (typeof ref !== "string" || !ref.startsWith("#/responses/")) return response;
849
- if (visited.has(ref)) return response;
850
- const nextVisited = new Set(visited);
851
- nextVisited.add(ref);
852
- const name = ref.slice(12);
853
- const globalResponses = doc.responses;
854
- if (isObject(globalResponses)) {
855
- const resolved = globalResponses[name];
856
- if (isObject(resolved)) {
857
- if (typeof resolved.$ref === "string") return resolveSwaggerResponse(resolved, doc, nextVisited);
858
- return resolved;
859
- }
860
- }
861
- return response;
862
- }
863
- function normaliseSwaggerResponses(responses, doc, produces) {
1149
+ function resolveSwaggerResponse(response, doc) {
1150
+ return resolveRefChain(response, {
1151
+ extractRef: (node) => {
1152
+ const ref = node.$ref;
1153
+ if (typeof ref !== "string") return void 0;
1154
+ return ref.startsWith("#/responses/") ? ref : void 0;
1155
+ },
1156
+ lookup: (ref) => {
1157
+ const name = ref.slice(12);
1158
+ const globalResponses = doc.responses;
1159
+ if (!isObject(globalResponses)) return void 0;
1160
+ const resolved = globalResponses[name];
1161
+ return isObject(resolved) ? resolved : void 0;
1162
+ },
1163
+ onCycle: () => response
1164
+ }) ?? response;
1165
+ }
1166
+ function normaliseSwaggerResponses(responses, doc, produces, producesSource, diagnostics, path, method) {
864
1167
  const result = {};
865
1168
  for (const [code, response] of Object.entries(responses)) {
866
1169
  if (!isObject(response)) {
867
1170
  result[code] = response;
868
1171
  continue;
869
1172
  }
870
- result[code] = normaliseSwaggerSingleResponse(response, doc, produces);
1173
+ result[code] = normaliseSwaggerSingleResponse(response, doc, produces, producesSource, diagnostics, path, method, code);
871
1174
  }
872
1175
  return result;
873
1176
  }
@@ -880,7 +1183,7 @@ function normaliseSwaggerResponses(responses, doc, produces) {
880
1183
  * operation’s `responses` map or under document-level `responses`
881
1184
  * (now `components.responses`).
882
1185
  */
883
- function normaliseSwaggerSingleResponse(response, doc, produces) {
1186
+ function normaliseSwaggerSingleResponse(response, doc, produces, producesSource = "synthesised", diagnostics, path, method, statusCode) {
884
1187
  const resolved = resolveSwaggerResponse(response, doc);
885
1188
  const normalised = {};
886
1189
  for (const [key, value] of Object.entries(resolved)) if (key !== "schema" && key !== "headers") normalised[key] = value;
@@ -890,11 +1193,24 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
890
1193
  const contentTypes = produces.length > 0 ? produces : ["application/json"];
891
1194
  for (const ct of contentTypes) if (typeof ct === "string") content[ct] = { schema };
892
1195
  normalised.content = content;
1196
+ if (producesSource === "synthesised") emitDiagnostic(diagnostics, {
1197
+ code: "swagger-missing-consumes",
1198
+ message: "Response declares a schema but neither operation-level nor document-level `produces` is set; defaulting to application/json",
1199
+ pointer: path !== void 0 && method !== void 0 && statusCode !== void 0 ? appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "responses"), statusCode) : "",
1200
+ detail: {
1201
+ level: "response",
1202
+ statusCode
1203
+ }
1204
+ });
893
1205
  }
894
1206
  const headers = resolved.headers;
895
1207
  if (isObject(headers)) {
896
1208
  const convertedHeaders = {};
897
- for (const [name, header] of Object.entries(headers)) convertedHeaders[name] = isObject(header) ? normaliseSwaggerHeader(header) : header;
1209
+ const headersPointer = appendPointer(path !== void 0 && method !== void 0 && statusCode !== void 0 ? appendPointer(appendPointer(appendPointer(appendPointer(appendPointer("", "paths"), path), method), "responses"), statusCode) : "", "headers");
1210
+ for (const [name, header] of Object.entries(headers)) {
1211
+ const headerPointer = appendPointer(headersPointer, name);
1212
+ convertedHeaders[name] = isObject(header) ? normaliseSwaggerHeader(header, diagnostics, headerPointer) : header;
1213
+ }
898
1214
  normalised.headers = convertedHeaders;
899
1215
  }
900
1216
  return normalised;
@@ -912,21 +1228,13 @@ function normaliseSwaggerSingleResponse(response, doc, produces) {
912
1228
  * `simple`/`explode: false` rather than the `form` style used for query
913
1229
  * parameters.
914
1230
  */
915
- function normaliseSwaggerHeader(header) {
1231
+ function normaliseSwaggerHeader(header, diagnostics, pointer = "") {
916
1232
  const result = {};
917
1233
  for (const [key, value] of Object.entries(header)) {
918
- if (key === "type" || key === "format" || key === "collectionFormat") continue;
1234
+ if (PARAM_KEYWORDS_LIFTED_INTO_SCHEMA.has(key)) continue;
919
1235
  result[key] = value;
920
1236
  }
921
- if (typeof header.type === "string") {
922
- const schema = { type: header.type };
923
- if (typeof header.format === "string") schema.format = header.format;
924
- if (header.enum !== void 0) schema.enum = header.enum;
925
- if (header.default !== void 0) schema.default = header.default;
926
- if (header.minimum !== void 0) schema.minimum = header.minimum;
927
- if (header.maximum !== void 0) schema.maximum = header.maximum;
928
- result.schema = schema;
929
- }
1237
+ if (typeof header.type === "string") result.schema = buildSchemaFromSwaggerParameterShape(header);
930
1238
  const cf = header.collectionFormat;
931
1239
  if (typeof cf === "string") switch (cf) {
932
1240
  case "csv":
@@ -938,8 +1246,15 @@ function normaliseSwaggerHeader(header) {
938
1246
  result.explode = false;
939
1247
  break;
940
1248
  case "tsv":
941
- result.style = "tabDelimited";
942
- result.explode = false;
1249
+ emitDiagnostic(diagnostics, {
1250
+ code: "swagger-collection-format-dropped",
1251
+ message: "Swagger 2.0 collectionFormat: \"tsv\" has no OpenAPI 3.x equivalent; dropping the keyword",
1252
+ pointer,
1253
+ detail: {
1254
+ feature: "collectionFormat:tsv",
1255
+ location: "header"
1256
+ }
1257
+ });
943
1258
  break;
944
1259
  case "pipes":
945
1260
  result.style = "pipeDelimited";
@@ -949,49 +1264,86 @@ function normaliseSwaggerHeader(header) {
949
1264
  return result;
950
1265
  }
951
1266
  /**
952
- * Mapping of Swagger 2.0 $ref prefixes to OpenAPI 3.x equivalents.
953
- * Applied after document restructuring so all $ref strings point
954
- * to the correct locations in the normalised document.
955
- */
956
- const REF_REWRITES = [
957
- ["#/definitions/", "#/components/schemas/"],
958
- ["#/parameters/", "#/components/parameters/"],
959
- ["#/responses/", "#/components/responses/"]
960
- ];
961
- /**
962
1267
  * Deep-rewrite $ref strings in a normalised Swagger 2.0 document
963
- * from Swagger 2.0 locations to OpenAPI 3.x locations.
964
- * Mutates the object in place \u2014 called only on the fresh clone
965
- * produced by normaliseSwagger2Document.
1268
+ * from Swagger 2.0 locations to OpenAPI 3.x locations using the
1269
+ * shared {@link rewriteSwaggerRefPrefix} mapping. Mutates the object
1270
+ * in place \u2014 called only on the fresh clone produced by
1271
+ * normaliseSwagger2Document.
966
1272
  */
967
1273
  function rewriteSwaggerRefs(node) {
968
1274
  if (!isObject(node)) return;
969
- if (typeof node.$ref === "string") {
970
- for (const [from, to] of REF_REWRITES) if (node.$ref.startsWith(from)) {
971
- node.$ref = to + node.$ref.slice(from.length);
972
- break;
973
- }
974
- }
1275
+ if (typeof node.$ref === "string") node.$ref = rewriteSwaggerRefPrefix(node.$ref);
975
1276
  for (const value of Object.values(node)) if (isObject(value)) rewriteSwaggerRefs(value);
976
1277
  else if (Array.isArray(value)) for (const item of value) rewriteSwaggerRefs(item);
977
1278
  }
978
1279
  /**
979
- * Recursively check whether any node in the supplied subtree carries an
980
- * `xml` annotation. Walks both objects and arrays so the check works for
981
- * schemas (definitions, parameter schemas, response schemas, request body
982
- * schemas) as well as operations and parameters that may carry `xml`
983
- * metadata at any depth.
1280
+ * Map from Swagger 2.0 `oauth2.flow` (singular) to the OAS 3.x flow key
1281
+ * under `flows.<key>`. `application` and `accessCode` were renamed in
1282
+ * OAS 3.x to align with RFC 6749 grant-type names.
1283
+ */
1284
+ const SWAGGER_OAUTH_FLOW_RENAME = {
1285
+ implicit: "implicit",
1286
+ password: "password",
1287
+ application: "clientCredentials",
1288
+ accessCode: "authorizationCode"
1289
+ };
1290
+ /**
1291
+ * Translate a Swagger 2.0 Security Scheme Object into an OpenAPI 3.x
1292
+ * Security Scheme Object. The Swagger 2.0 spec defines three types:
1293
+ *
1294
+ * - `basic` — has no other fields; OAS 3.x represents this as
1295
+ * `{ type: "http", scheme: "basic" }`.
1296
+ * - `apiKey` — carries `name`/`in`; OAS 3.x uses the same shape.
1297
+ * - `oauth2` — carries `flow`/`authorizationUrl`/`tokenUrl`/`scopes` at
1298
+ * the root. OAS 3.x nests these under `flows.<name>` where the flow
1299
+ * name maps via {@link SWAGGER_OAUTH_FLOW_RENAME}.
1300
+ *
1301
+ * Unknown `type` values pass through verbatim — downstream validation
1302
+ * (`unknown-security-scheme-type` diagnostic in the parser) handles
1303
+ * those cases.
984
1304
  */
985
- function hasXmlAnywhere(node) {
986
- if (!isObject(node)) {
987
- if (Array.isArray(node)) {
988
- for (const item of node) if (hasXmlAnywhere(item)) return true;
1305
+ function translateSwaggerSecurityScheme(scheme, diagnostics, pointer = "", name) {
1306
+ const type = scheme.type;
1307
+ if (type === "basic") {
1308
+ const result = {
1309
+ type: "http",
1310
+ scheme: "basic"
1311
+ };
1312
+ if (typeof scheme.description === "string") result.description = scheme.description;
1313
+ return result;
1314
+ }
1315
+ if (type === "oauth2") {
1316
+ const flowName = scheme.flow;
1317
+ if (typeof flowName !== "string") {
1318
+ emitDiagnostic(diagnostics, {
1319
+ code: "swagger-malformed-oauth-flow",
1320
+ message: `Swagger 2.0 oauth2 security scheme${name !== void 0 ? ` "${name}"` : ""} is missing the required \`flow\` field; preserving the original shape verbatim`,
1321
+ pointer,
1322
+ detail: {
1323
+ name,
1324
+ flow: flowName
1325
+ }
1326
+ });
1327
+ return {
1328
+ ...scheme,
1329
+ type: "oauth2"
1330
+ };
989
1331
  }
990
- return false;
1332
+ const renamedFlow = SWAGGER_OAUTH_FLOW_RENAME[flowName] ?? flowName;
1333
+ const flowBody = {};
1334
+ if (typeof scheme.authorizationUrl === "string") flowBody.authorizationUrl = scheme.authorizationUrl;
1335
+ if (typeof scheme.tokenUrl === "string") flowBody.tokenUrl = scheme.tokenUrl;
1336
+ if (typeof scheme.refreshUrl === "string") flowBody.refreshUrl = scheme.refreshUrl;
1337
+ const scopes = scheme.scopes;
1338
+ flowBody.scopes = isObject(scopes) ? { ...scopes } : {};
1339
+ const result = {
1340
+ type: "oauth2",
1341
+ flows: { [renamedFlow]: flowBody }
1342
+ };
1343
+ if (typeof scheme.description === "string") result.description = scheme.description;
1344
+ return result;
991
1345
  }
992
- if ("xml" in node) return true;
993
- for (const value of Object.values(node)) if (hasXmlAnywhere(value)) return true;
994
- return false;
1346
+ return { ...scheme };
995
1347
  }
996
1348
  //#endregion
997
1349
  //#region src/core/normalise.ts
@@ -1075,24 +1427,34 @@ function deepNormalise(schema, transform, visited = /* @__PURE__ */ new WeakSet(
1075
1427
  } else result[key] = value;
1076
1428
  return result;
1077
1429
  }
1430
+ /**
1431
+ * Construct a child {@link NodeContext} that descends to `segment`,
1432
+ * preserving document-level flags (`documentHasDynamicAnchor`,
1433
+ * `documentHasRecursiveAnchor`, `declaredDraft`). Centralising the copy
1434
+ * keeps the recursion in `deepNormaliseWithContext` from drifting from
1435
+ * the {@link NodeContext} shape.
1436
+ */
1437
+ function childContext(ctx, segment) {
1438
+ return {
1439
+ diagnostics: ctx.diagnostics,
1440
+ pointer: appendPointer(ctx.pointer, segment),
1441
+ documentHasDynamicAnchor: ctx.documentHasDynamicAnchor,
1442
+ documentHasRecursiveAnchor: ctx.documentHasRecursiveAnchor,
1443
+ declaredDraft: ctx.declaredDraft
1444
+ };
1445
+ }
1078
1446
  function normaliseArrayWithContext(items, transform, ctx) {
1079
1447
  const result = [];
1080
1448
  for (let i = 0; i < items.length; i++) {
1081
1449
  const item = items[i];
1082
- if (isObject(item)) result.push(deepNormaliseWithContext(item, transform, {
1083
- diagnostics: ctx.diagnostics,
1084
- pointer: appendPointer(ctx.pointer, String(i))
1085
- }));
1450
+ if (isObject(item)) result.push(deepNormaliseWithContext(item, transform, childContext(ctx, String(i))));
1086
1451
  else result.push(item);
1087
1452
  }
1088
1453
  return result;
1089
1454
  }
1090
1455
  function normaliseSubSchemaMapWithContext(map, transform, ctx) {
1091
1456
  const result = {};
1092
- for (const [k, v] of Object.entries(map)) if (isObject(v)) result[k] = deepNormaliseWithContext(v, transform, {
1093
- diagnostics: ctx.diagnostics,
1094
- pointer: appendPointer(ctx.pointer, k)
1095
- });
1457
+ for (const [k, v] of Object.entries(map)) if (isObject(v)) result[k] = deepNormaliseWithContext(v, transform, childContext(ctx, k));
1096
1458
  else result[k] = v;
1097
1459
  return result;
1098
1460
  }
@@ -1108,34 +1470,16 @@ function normaliseSubSchemaMapWithContext(map, transform, ctx) {
1108
1470
  function deepNormaliseWithContext(schema, transform, ctx) {
1109
1471
  const node = transform({ ...schema }, ctx);
1110
1472
  const result = {};
1111
- for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMapWithContext(value, transform, {
1112
- diagnostics: ctx.diagnostics,
1113
- pointer: appendPointer(ctx.pointer, key)
1114
- });
1115
- else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArrayWithContext(value, transform, {
1116
- diagnostics: ctx.diagnostics,
1117
- pointer: appendPointer(ctx.pointer, key)
1118
- });
1119
- else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormaliseWithContext(value, transform, {
1120
- diagnostics: ctx.diagnostics,
1121
- pointer: appendPointer(ctx.pointer, key)
1122
- });
1123
- else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArrayWithContext(value, transform, {
1124
- diagnostics: ctx.diagnostics,
1125
- pointer: appendPointer(ctx.pointer, key)
1126
- });
1127
- else if (isObject(value)) result[key] = deepNormaliseWithContext(value, transform, {
1128
- diagnostics: ctx.diagnostics,
1129
- pointer: appendPointer(ctx.pointer, key)
1130
- });
1473
+ for (const [key, value] of Object.entries(node)) if (isObject(value) && OBJECT_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseSubSchemaMapWithContext(value, transform, childContext(ctx, key));
1474
+ else if (Array.isArray(value) && ARRAY_SUBSCHEMA_KEYS.has(key)) result[key] = normaliseArrayWithContext(value, transform, childContext(ctx, key));
1475
+ else if (isObject(value) && SINGLE_SUBSCHEMA_KEYS.has(key)) result[key] = deepNormaliseWithContext(value, transform, childContext(ctx, key));
1476
+ else if (key === "items") if (Array.isArray(value)) result[key] = normaliseArrayWithContext(value, transform, childContext(ctx, key));
1477
+ else if (isObject(value)) result[key] = deepNormaliseWithContext(value, transform, childContext(ctx, key));
1131
1478
  else result[key] = value;
1132
1479
  else if (key === "dependencies" && isObject(value)) {
1133
1480
  const normalised = {};
1134
- const depsPointer = appendPointer(ctx.pointer, key);
1135
- for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormaliseWithContext(dv, transform, {
1136
- diagnostics: ctx.diagnostics,
1137
- pointer: appendPointer(depsPointer, dk)
1138
- });
1481
+ const depsContext = childContext(ctx, key);
1482
+ for (const [dk, dv] of Object.entries(value)) if (isObject(dv)) normalised[dk] = deepNormaliseWithContext(dv, transform, childContext(depsContext, dk));
1139
1483
  else normalised[dk] = dv;
1140
1484
  result[key] = normalised;
1141
1485
  } else result[key] = value;
@@ -1188,17 +1532,24 @@ function collectDependencyStrings(items, property, keyword, ctx) {
1188
1532
  * After splitting, `dependencies` is removed from the node.
1189
1533
  *
1190
1534
  * When `ctx` is supplied, diagnostics are emitted for:
1191
- * - `legacy-dependencies-split` once per node that contained the
1192
- * deprecated keyword (callers pass this only on draft paths where
1193
- * the keyword is unexpected, e.g. 2020-12).
1535
+ * - The supplied `legacyDiagnostic` code (if any) once per node that
1536
+ * contained the deprecated keyword. Callers pass
1537
+ * `legacy-dependencies-split-2019` on the 2019-09 path (the draft
1538
+ * that introduced the split — authors should already have migrated)
1539
+ * and `legacy-dependencies-split` on the defensive 2020-12 path.
1194
1540
  * - `dependent-required-invalid` for each array entry whose element is
1195
1541
  * not a string.
1542
+ * - `dependencies-conflict` when both the legacy `dependencies` keyword
1543
+ * and a pre-existing `dependentRequired`/`dependentSchemas` declare
1544
+ * the same key with different values. The pre-existing modern
1545
+ * keyword wins and the legacy value is discarded — silently
1546
+ * overwriting would mask the author's bug.
1196
1547
  */
1197
- function splitDependencies(node, ctx, emitLegacyDiagnostic) {
1548
+ function splitDependencies(node, ctx, legacyDiagnostic) {
1198
1549
  const deps = node.dependencies;
1199
1550
  if (!isObject(deps)) return;
1200
- if (emitLegacyDiagnostic && ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
1201
- code: "legacy-dependencies-split",
1551
+ if (legacyDiagnostic !== void 0 && ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
1552
+ code: legacyDiagnostic,
1202
1553
  message: "Legacy `dependencies` keyword was split into `dependentRequired`/`dependentSchemas`; `dependencies` was deprecated in Draft 2019-09",
1203
1554
  pointer: appendPointer(ctx.pointer, "dependencies"),
1204
1555
  detail: { keys: Object.keys(deps) }
@@ -1211,17 +1562,61 @@ function splitDependencies(node, ctx, emitLegacyDiagnostic) {
1211
1562
  } else if (isObject(value)) schemaEntries[key] = value;
1212
1563
  if (Object.keys(requiredEntries).length > 0) {
1213
1564
  const existing = node.dependentRequired;
1214
- if (isObject(existing)) for (const [k, v] of Object.entries(requiredEntries)) existing[k] = v;
1565
+ if (isObject(existing)) for (const [k, v] of Object.entries(requiredEntries)) {
1566
+ if (k in existing) {
1567
+ if (ctx !== void 0 && !arraysEqual(existing[k], v)) emitDiagnostic(ctx.diagnostics, {
1568
+ code: "dependencies-conflict",
1569
+ message: `Legacy \`dependencies.${k}\` conflicts with the pre-existing \`dependentRequired.${k}\`; keeping the modern keyword and discarding the legacy value`,
1570
+ pointer: appendPointer(ctx.pointer, "dependencies"),
1571
+ detail: {
1572
+ key: k,
1573
+ kept: existing[k],
1574
+ discarded: v,
1575
+ keyword: "dependentRequired"
1576
+ }
1577
+ });
1578
+ continue;
1579
+ }
1580
+ existing[k] = v;
1581
+ }
1215
1582
  else node.dependentRequired = requiredEntries;
1216
1583
  }
1217
1584
  if (Object.keys(schemaEntries).length > 0) {
1218
1585
  const existing = node.dependentSchemas;
1219
- if (isObject(existing)) for (const [k, v] of Object.entries(schemaEntries)) existing[k] = v;
1586
+ if (isObject(existing)) for (const [k, v] of Object.entries(schemaEntries)) {
1587
+ if (k in existing) {
1588
+ if (ctx !== void 0) emitDiagnostic(ctx.diagnostics, {
1589
+ code: "dependencies-conflict",
1590
+ message: `Legacy \`dependencies.${k}\` conflicts with the pre-existing \`dependentSchemas.${k}\`; keeping the modern keyword and discarding the legacy value`,
1591
+ pointer: appendPointer(ctx.pointer, "dependencies"),
1592
+ detail: {
1593
+ key: k,
1594
+ kept: existing[k],
1595
+ discarded: v,
1596
+ keyword: "dependentSchemas"
1597
+ }
1598
+ });
1599
+ continue;
1600
+ }
1601
+ existing[k] = v;
1602
+ }
1220
1603
  else node.dependentSchemas = schemaEntries;
1221
1604
  }
1222
1605
  delete node.dependencies;
1223
1606
  }
1224
1607
  /**
1608
+ * Compare two values that should both be string arrays. Returns true
1609
+ * when both are arrays of equal length holding the same strings in
1610
+ * the same order. Used to skip the `dependencies-conflict` diagnostic
1611
+ * when the legacy and modern keywords agree.
1612
+ */
1613
+ function arraysEqual(a, b) {
1614
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
1615
+ if (a.length !== b.length) return false;
1616
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
1617
+ return true;
1618
+ }
1619
+ /**
1225
1620
  * Emit diagnostics for any non-string entries inside a pre-existing
1226
1621
  * `dependentRequired` keyword. Used on draft paths where the author may
1227
1622
  * have already migrated to the Draft 2019-09 form but still produced
@@ -1319,7 +1714,7 @@ function applyDraft04Translations(node, ctx) {
1319
1714
  delete node.id;
1320
1715
  }
1321
1716
  translateTupleItems(node);
1322
- splitDependencies(node, ctx, false);
1717
+ splitDependencies(node, ctx, void 0);
1323
1718
  validateDependentRequired(node, ctx);
1324
1719
  }
1325
1720
  /**
@@ -1372,7 +1767,7 @@ function normaliseDraft04NodeWithContext(node, ctx) {
1372
1767
  */
1373
1768
  function normaliseDraft06Or07NodeWithContext(node, ctx) {
1374
1769
  translateTupleItems(node);
1375
- splitDependencies(node, ctx, false);
1770
+ splitDependencies(node, ctx, void 0);
1376
1771
  validateDependentRequired(node, ctx);
1377
1772
  return node;
1378
1773
  }
@@ -1391,34 +1786,120 @@ function normaliseDraft06Or07NodeWithContext(node, ctx) {
1391
1786
  * `$recursiveAnchor` names are likewise preserved as `$anchor`.
1392
1787
  */
1393
1788
  function normaliseDraft201909NodeWithContext(node, ctx) {
1394
- if (typeof node.$recursiveRef === "string") {
1395
- node.$ref = node.$recursiveRef;
1789
+ if (node.$anchor === "__recursive__" && node.$recursiveAnchor !== true) emitDiagnostic(ctx.diagnostics, {
1790
+ code: "recursive-anchor-collision",
1791
+ message: `User-supplied \`$anchor: "${RECURSIVE_ANCHOR_SENTINEL}"\` collides with the canonical sentinel synthesised by the Draft 2019-09 \`$recursiveAnchor: true\` rewrite; lookups for this anchor may resolve to either declaration`,
1792
+ pointer: appendPointer(ctx.pointer, "$anchor"),
1793
+ detail: { anchor: RECURSIVE_ANCHOR_SENTINEL }
1794
+ });
1795
+ const recursiveRef = node.$recursiveRef;
1796
+ if (typeof recursiveRef === "string") {
1797
+ if (!recursiveRef.startsWith("#")) emitDiagnostic(ctx.diagnostics, {
1798
+ code: "dynamic-ref-degraded",
1799
+ message: `Cross-document \`$recursiveRef\` "${recursiveRef}" rewritten to a static \`$ref\`; dynamic-scope resolution is not preserved`,
1800
+ pointer: appendPointer(ctx.pointer, "$recursiveRef"),
1801
+ detail: {
1802
+ keyword: "$recursiveRef",
1803
+ ref: recursiveRef
1804
+ }
1805
+ });
1806
+ node.$ref = recursiveRef;
1396
1807
  delete node.$recursiveRef;
1397
1808
  }
1398
1809
  if (node.$recursiveAnchor === true) {
1399
- if (typeof node.$anchor !== "string") node.$anchor = "__recursive__";
1810
+ if (typeof node.$anchor !== "string") node.$anchor = RECURSIVE_ANCHOR_SENTINEL;
1400
1811
  delete node.$recursiveAnchor;
1401
1812
  } else if (typeof node.$recursiveAnchor === "string") {
1402
1813
  if (typeof node.$anchor !== "string") node.$anchor = node.$recursiveAnchor;
1403
1814
  delete node.$recursiveAnchor;
1404
1815
  }
1816
+ splitDependencies(node, ctx, "legacy-dependencies-split-2019");
1405
1817
  validateDependentRequired(node, ctx);
1406
1818
  return node;
1407
1819
  }
1408
1820
  function normaliseDynamicRefNodeWithContext(node, ctx) {
1409
- if (typeof node.$dynamicRef === "string") {
1410
- node.$ref = node.$dynamicRef;
1821
+ const dynamicRef = node.$dynamicRef;
1822
+ if (typeof dynamicRef === "string") {
1823
+ const crossDocument = !dynamicRef.startsWith("#");
1824
+ if (crossDocument || ctx.documentHasDynamicAnchor) emitDiagnostic(ctx.diagnostics, {
1825
+ code: "dynamic-ref-degraded",
1826
+ 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`,
1827
+ pointer: appendPointer(ctx.pointer, "$dynamicRef"),
1828
+ detail: {
1829
+ keyword: "$dynamicRef",
1830
+ ref: dynamicRef
1831
+ }
1832
+ });
1833
+ node.$ref = dynamicRef;
1411
1834
  delete node.$dynamicRef;
1412
1835
  }
1413
1836
  if (typeof node.$dynamicAnchor === "string") {
1414
1837
  if (typeof node.$anchor !== "string") node.$anchor = node.$dynamicAnchor;
1415
1838
  delete node.$dynamicAnchor;
1416
1839
  }
1417
- splitDependencies(node, ctx, true);
1840
+ translateTupleItems(node);
1841
+ splitDependencies(node, ctx, "legacy-dependencies-split");
1418
1842
  validateDependentRequired(node, ctx);
1419
1843
  return node;
1420
1844
  }
1421
1845
  /**
1846
+ * Pick the per-node transform that normalises a single Schema Object to
1847
+ * canonical Draft 2020-12 form for the supplied draft. Exposed so the
1848
+ * OpenAPI 3.1 path can honour a non-default `jsonSchemaDialect`
1849
+ * declaration by routing each Schema Object through the matching
1850
+ * transform without re-implementing the dispatch.
1851
+ */
1852
+ function selectDraftTransform(draft) {
1853
+ switch (draft) {
1854
+ case "draft-04": return normaliseDraft04NodeWithContext;
1855
+ case "draft-06":
1856
+ case "draft-07": return normaliseDraft06Or07NodeWithContext;
1857
+ case "draft-2019-09": return normaliseDraft201909NodeWithContext;
1858
+ case "draft-2020-12": return normaliseDynamicRefNodeWithContext;
1859
+ }
1860
+ }
1861
+ /**
1862
+ * Scan a JSON document body for the presence of a named keyword
1863
+ * anywhere in the structure. Walks both arrays and objects without
1864
+ * regard to schema-vs-data position — the caller is responsible for
1865
+ * passing a keyword whose presence is meaningful at any depth.
1866
+ *
1867
+ * Cycle-safe: cyclic references introduced by the OpenAPI bundler's
1868
+ * `structuredClone` of external refs are short-circuited via the
1869
+ * `visited` set so the scan terminates.
1870
+ */
1871
+ function documentContainsKeyword(value, keyword, visited = /* @__PURE__ */ new WeakSet()) {
1872
+ if (Array.isArray(value)) {
1873
+ if (visited.has(value)) return false;
1874
+ visited.add(value);
1875
+ for (const item of value) if (documentContainsKeyword(item, keyword, visited)) return true;
1876
+ return false;
1877
+ }
1878
+ if (isObject(value)) {
1879
+ if (visited.has(value)) return false;
1880
+ visited.add(value);
1881
+ if (keyword in value) return true;
1882
+ for (const v of Object.values(value)) if (documentContainsKeyword(v, keyword, visited)) return true;
1883
+ return false;
1884
+ }
1885
+ return false;
1886
+ }
1887
+ /**
1888
+ * Build a root {@link NodeContext} for a document being normalised.
1889
+ * Pre-scans for `$dynamicAnchor` and `$recursiveAnchor` so per-node
1890
+ * transforms can decide whether a `$dynamicRef`/`$recursiveRef`
1891
+ * rewrite needs a `dynamic-ref-degraded` diagnostic.
1892
+ */
1893
+ function buildRootContext(schema, diagnostics, declaredDraft) {
1894
+ return {
1895
+ diagnostics,
1896
+ pointer: "",
1897
+ documentHasDynamicAnchor: documentContainsKeyword(schema, "$dynamicAnchor"),
1898
+ documentHasRecursiveAnchor: documentContainsKeyword(schema, "$recursiveAnchor"),
1899
+ declaredDraft
1900
+ };
1901
+ }
1902
+ /**
1422
1903
  * Normalise a JSON Schema to canonical Draft 2020-12 form.
1423
1904
  * Deep-clones the input — the original is never mutated.
1424
1905
  *
@@ -1428,27 +1909,8 @@ function normaliseDynamicRefNodeWithContext(node, ctx) {
1428
1909
  * `dependencies` reaching the 2020-12 path).
1429
1910
  */
1430
1911
  function normaliseJsonSchema(schema, draft, diagnostics) {
1431
- const ctx = {
1432
- diagnostics,
1433
- pointer: ""
1434
- };
1435
- let normalised;
1436
- switch (draft) {
1437
- case "draft-04":
1438
- normalised = deepNormaliseWithContext(schema, normaliseDraft04NodeWithContext, ctx);
1439
- break;
1440
- case "draft-2019-09":
1441
- normalised = deepNormaliseWithContext(schema, normaliseDraft201909NodeWithContext, ctx);
1442
- break;
1443
- case "draft-2020-12":
1444
- normalised = deepNormaliseWithContext(schema, normaliseDynamicRefNodeWithContext, ctx);
1445
- break;
1446
- case "draft-06":
1447
- case "draft-07":
1448
- normalised = deepNormaliseWithContext(schema, normaliseDraft06Or07NodeWithContext, ctx);
1449
- break;
1450
- }
1451
- return resolveRelativeRefs(normalised, diagnostics);
1912
+ const ctx = buildRootContext(schema, diagnostics, draft);
1913
+ return resolveRelativeRefs(deepNormaliseWithContext(schema, selectDraftTransform(draft), ctx), diagnostics);
1452
1914
  }
1453
1915
  /**
1454
1916
  * Parse a string as an absolute URI, returning `undefined` when it has
@@ -1486,6 +1948,29 @@ function stripFragment(url) {
1486
1948
  return clone.toString();
1487
1949
  }
1488
1950
  /**
1951
+ * Emit an `invalid-id-fragment` diagnostic when an `$id` value carries
1952
+ * a fragment that will be stripped during base-URI resolution. Per
1953
+ * JSON Schema 2020-12 §8.2.1, the URI in `$id` MUST NOT contain a
1954
+ * non-empty fragment (an empty `#` fragment is permitted for historical
1955
+ * reasons but conveys nothing). Stripping it silently loses authoring
1956
+ * intent — the caller almost certainly meant to declare an `$anchor`
1957
+ * or sibling identifier instead.
1958
+ */
1959
+ function reportFragmentInId(value, url, pointer, diagnostics) {
1960
+ if (diagnostics === void 0) return;
1961
+ if (typeof value !== "string") return;
1962
+ if (url.hash.length === 0) return;
1963
+ emitDiagnostic(diagnostics, {
1964
+ code: "invalid-id-fragment",
1965
+ 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`,
1966
+ pointer: appendPointer(pointer, "$id"),
1967
+ detail: {
1968
+ id: value,
1969
+ fragment: url.hash
1970
+ }
1971
+ });
1972
+ }
1973
+ /**
1489
1974
  * Recursively rewrite relative `$ref`s in a schema so they resolve
1490
1975
  * correctly under the JSON Schema base-URI rules (RFC 3986 + JSON
1491
1976
  * Schema §8.2). Refs that resolve to the document's own `$id` are
@@ -1519,7 +2004,10 @@ function rewriteRelativeRefsNode(node, currentBase, docBase, pointer, diagnostic
1519
2004
  const nodeId = node.$id;
1520
2005
  if (typeof nodeId === "string" && nodeId.length > 0) {
1521
2006
  const resolved = resolveAgainst(nodeId, currentBase);
1522
- if (resolved !== void 0) nextBase = stripFragment(resolved);
2007
+ if (resolved !== void 0) {
2008
+ reportFragmentInId(nodeId, resolved, pointer, diagnostics);
2009
+ nextBase = stripFragment(resolved);
2010
+ }
1523
2011
  }
1524
2012
  const result = {};
1525
2013
  for (const [key, value] of Object.entries(node)) {
@@ -1588,6 +2076,24 @@ function rewriteRef(ref, currentBase, docBase, pointer, diagnostics) {
1588
2076
  function normaliseOpenApiSchemas(doc, version, diagnostics) {
1589
2077
  if (isSwagger2(version)) return normaliseSwagger2Document(doc, deepNormalise, normaliseDraft04Node, diagnostics);
1590
2078
  if (isOpenApi30(version)) return deepNormaliseOpenApi30Doc(applyDiscriminatorAllOfPrepass(doc), deepNormalise);
2079
+ if (!isOpenApi31(version)) {
2080
+ const rawOpenApi = typeof doc.openapi === "string" ? doc.openapi : void 0;
2081
+ const rawSwagger = typeof doc.swagger === "string" ? doc.swagger : void 0;
2082
+ const versionLabel = rawOpenApi ?? rawSwagger ?? `${String(version.major)}.${String(version.minor)}.${String(version.patch)}`;
2083
+ const pointer = rawOpenApi !== void 0 ? "/openapi" : "/swagger";
2084
+ emitDiagnostic(diagnostics, {
2085
+ code: "unknown-openapi-version",
2086
+ message: `Unsupported OpenAPI/Swagger version "${versionLabel}"; falling back to the OpenAPI 3.1 pipeline`,
2087
+ pointer,
2088
+ detail: {
2089
+ version: versionLabel,
2090
+ major: version.major,
2091
+ minor: version.minor,
2092
+ patch: version.patch
2093
+ }
2094
+ });
2095
+ }
2096
+ let dialectDraft;
1591
2097
  if (isOpenApi31(version)) {
1592
2098
  const dialect = readJsonSchemaDialect(doc);
1593
2099
  if (dialect.kind === "unknown") emitDiagnostic(diagnostics, {
@@ -1596,8 +2102,45 @@ function normaliseOpenApiSchemas(doc, version, diagnostics) {
1596
2102
  pointer: "/jsonSchemaDialect",
1597
2103
  detail: { uri: dialect.uri }
1598
2104
  });
2105
+ else if (dialect.kind === "known") dialectDraft = dialect.draft;
2106
+ }
2107
+ return deepNormaliseOpenApiDoc(applyDiscriminatorAllOfPrepass(doc), (schema) => {
2108
+ let intermediate = schema;
2109
+ const effectiveDraft = resolveSchemaLocalDialect(intermediate, diagnostics) ?? dialectDraft;
2110
+ if (effectiveDraft !== void 0 && effectiveDraft !== "draft-2020-12") intermediate = deepNormaliseWithContext(intermediate, selectDraftTransform(effectiveDraft), buildRootContext(intermediate, diagnostics, effectiveDraft));
2111
+ return resolveRelativeRefs(deepNormalise(intermediate, normaliseOpenApi30Discriminator), diagnostics);
2112
+ });
2113
+ }
2114
+ /**
2115
+ * Resolve a Schema-Object-level `$schema` declaration to a
2116
+ * `JsonSchemaDraft`. Returns `undefined` when the keyword is absent or
2117
+ * its value is not a string; emits `unknown-json-schema-dialect` and
2118
+ * returns `undefined` when the value is a string that does not match
2119
+ * any supported draft URI. The caller falls back to the document-level
2120
+ * dialect (or 2020-12) in that case.
2121
+ *
2122
+ * Per OpenAPI 3.1 §4.7.5, the override applies to the Schema Object
2123
+ * and its subschemas. Nested overrides inside individual sub-schemas
2124
+ * are not currently honoured — the dispatch happens once at the top
2125
+ * of each Schema Object surfaced by `deepNormaliseOpenApiDoc`.
2126
+ */
2127
+ function resolveSchemaLocalDialect(schema, diagnostics) {
2128
+ const value = schema.$schema;
2129
+ if (typeof value !== "string") return void 0;
2130
+ const draft = matchJsonSchemaDraftUri(value);
2131
+ if (draft === void 0) {
2132
+ emitDiagnostic(diagnostics, {
2133
+ code: "unknown-json-schema-dialect",
2134
+ message: `Schema Object-level \`$schema\` URI "${value}" does not match a supported JSON Schema draft; falling back to the document dialect`,
2135
+ pointer: "/$schema",
2136
+ detail: {
2137
+ uri: value,
2138
+ level: "schema-object"
2139
+ }
2140
+ });
2141
+ return;
1599
2142
  }
1600
- return deepNormaliseOpenApiDoc(applyDiscriminatorAllOfPrepass(doc), (schema) => resolveRelativeRefs(deepNormalise(schema, normaliseOpenApi30Discriminator), diagnostics));
2143
+ return draft;
1601
2144
  }
1602
2145
  //#endregion
1603
- export { normaliseOpenApiSchemas as a, deepNormaliseOpenApi30Doc as c, normaliseOpenApi30Discriminator as d, normaliseOpenApi30Node as f, normaliseJsonSchema as i, deepNormaliseOpenApiDoc as l, deepNormaliseWithContext as n, normaliseSwagger2Document as o, normaliseDraft04Node as r, applyDiscriminatorAllOfPrepass as s, deepNormalise as t, normaliseOpenApi30Combined as u };
2146
+ export { normaliseJsonSchema as a, normaliseSwagger2Document as c, deepNormaliseOpenApiDoc as d, liftExampleToExamples as f, normaliseOpenApi30Node as h, normaliseDraft04Node as i, applyDiscriminatorAllOfPrepass as l, normaliseOpenApi30Discriminator as m, deepNormaliseWithContext as n, normaliseOpenApiSchemas as o, normaliseOpenApi30Combined as p, documentContainsKeyword as r, selectDraftTransform as s, deepNormalise as t, deepNormaliseOpenApi30Doc as u };