@workos/oagen 0.30.1 → 0.31.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 (46) hide show
  1. package/README.md +4 -12
  2. package/dist/cli/index.d.mts +1 -1
  3. package/dist/cli/index.mjs +3003 -664
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/compat/index.d.mts +85 -36
  6. package/dist/compat/index.d.mts.map +1 -1
  7. package/dist/compat/index.mjs +33 -23
  8. package/dist/compat/index.mjs.map +1 -1
  9. package/dist/{diff-DMFiDAI6.mjs → diff-CAsypxXV.mjs} +109 -89
  10. package/dist/{diff-DMFiDAI6.mjs.map → diff-CAsypxXV.mjs.map} +1 -1
  11. package/dist/differ-B0DSuKUZ.d.mts +72 -0
  12. package/dist/differ-B0DSuKUZ.d.mts.map +1 -0
  13. package/dist/index.d.mts +129 -61
  14. package/dist/index.d.mts.map +1 -1
  15. package/dist/index.mjs +2992 -563
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/{overlay-BhXruu32.mjs → overlay-Dj3WWlNP.mjs} +45 -38
  18. package/dist/overlay-Dj3WWlNP.mjs.map +1 -0
  19. package/dist/scripts/smoke/baseline.d.mts +1 -1
  20. package/dist/scripts/smoke/baseline.mjs +2848 -537
  21. package/dist/scripts/smoke/baseline.mjs.map +1 -1
  22. package/dist/scripts/smoke/sdk-test.d.mts +1 -1
  23. package/dist/scripts/smoke/sdk-test.mjs +4 -2
  24. package/dist/scripts/smoke/sdk-test.mjs.map +1 -1
  25. package/dist/smoke.d.mts +259 -36
  26. package/dist/smoke.d.mts.map +1 -1
  27. package/dist/smoke.mjs +2848 -537
  28. package/dist/smoke.mjs.map +1 -1
  29. package/dist/{tree-sitter-C6HB4OrC.mjs → tree-sitter-C7Hk-NDu.mjs} +36 -7
  30. package/dist/tree-sitter-C7Hk-NDu.mjs.map +1 -0
  31. package/dist/{types-yrBidOg3.d.mts → types-I7W7CNl1.d.mts} +227 -9
  32. package/dist/types-I7W7CNl1.d.mts.map +1 -0
  33. package/dist/verify/index.d.mts +7 -8
  34. package/dist/verify/index.d.mts.map +1 -1
  35. package/dist/verify/index.mjs +24 -14
  36. package/dist/verify/index.mjs.map +1 -1
  37. package/docs/architecture/emitter-contract.md +1 -0
  38. package/docs/architecture/ir-types.md +175 -17
  39. package/docs/cli.md +13 -13
  40. package/package.json +3 -3
  41. package/skills/check-emitter-parity/SKILL.md +15 -1
  42. package/dist/differ-DHldxZMr.d.mts +0 -123
  43. package/dist/differ-DHldxZMr.d.mts.map +0 -1
  44. package/dist/overlay-BhXruu32.mjs.map +0 -1
  45. package/dist/tree-sitter-C6HB4OrC.mjs.map +0 -1
  46. package/dist/types-yrBidOg3.d.mts.map +0 -1
@@ -7,11 +7,11 @@ import * as path$1 from "node:path";
7
7
  import path, { relative, resolve } from "node:path";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { BaseResolver, Oas3_1Types, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
10
+ import { createHash } from "node:crypto";
10
11
  import Parser from "tree-sitter";
11
12
  import { execFile, execFileSync } from "node:child_process";
12
13
  import { promisify } from "node:util";
13
14
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
14
- import { createHash } from "node:crypto";
15
15
  //#region src/ir/sdk-behavior.ts
16
16
  /**
17
17
  * Canonical SDK behavior defaults. Per-language or per-project overrides can
@@ -182,11 +182,15 @@ var RegistryError = class extends OagenError {
182
182
  //#region src/parser/refs.ts
183
183
  async function loadAndBundleSpec(specPath) {
184
184
  const absolutePath = resolve(specPath);
185
+ const content = await readFile(absolutePath, "utf-8");
186
+ const document = makeDocumentFromString(content, pathToFileURL(absolutePath).href);
187
+ const config = await createConfig({});
188
+ const resolver = new BaseResolver();
185
189
  const result = await bundleDocument({
186
- document: makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href),
187
- config: await createConfig({}),
190
+ document,
191
+ config,
188
192
  types: Oas3_1Types,
189
- externalRefResolver: new BaseResolver(),
193
+ externalRefResolver: resolver,
190
194
  dereference: false
191
195
  });
192
196
  if (result.problems.some((p) => p.severity === "error")) throw new SpecParseError(`Failed to parse spec: ${result.problems.filter((p) => p.severity === "error").map((p) => p.message).join("\n")}`, `Check the OpenAPI spec at "${absolutePath}" for syntax errors. Run a linter such as \`npx @redocly/cli lint ${specPath}\` to identify issues.`);
@@ -253,6 +257,14 @@ function walkTypeRef(ref, visitor) {
253
257
  }
254
258
  }
255
259
  /**
260
+ * Collect all model names referenced (directly or transitively) by a TypeRef.
261
+ */
262
+ function collectModelRefs(ref) {
263
+ const names = [];
264
+ walkTypeRef(ref, { model: (r) => names.push(r.name) });
265
+ return names;
266
+ }
267
+ /**
256
268
  * Validate model-placement hints against the FULL spec. Run once at the start of
257
269
  * generation — where the complete model/service universe is known — so typos in
258
270
  * `modelHints` fail loud even though {@link assignModelsToServices} is
@@ -274,6 +286,30 @@ function validateModelHints(models, services, hints) {
274
286
  }
275
287
  }
276
288
  }
289
+ /**
290
+ * Every response header an operation declares: the primary response's, each
291
+ * alternate success entry's, and each error entry's. The primary list is the
292
+ * same array as its `successResponses` entry when both exist, so it is yielded
293
+ * once. Every pass that walks an operation's type refs (reachability, model
294
+ * assignment, ref rewriting and validation, direction, compat filtering, enum
295
+ * collection) goes through this so a type reached only through a header is
296
+ * treated the way a body or parameter type is.
297
+ */
298
+ function collectResponseHeaders(op) {
299
+ const result = [];
300
+ const seen = /* @__PURE__ */ new Set();
301
+ const lists = [
302
+ op.responseHeaders,
303
+ ...(op.successResponses ?? []).map((r) => r.headers),
304
+ ...op.errors.map((e) => e.headers)
305
+ ];
306
+ for (const list of lists) {
307
+ if (!list || seen.has(list)) continue;
308
+ seen.add(list);
309
+ result.push(...list);
310
+ }
311
+ return result;
312
+ }
277
313
  //#endregion
278
314
  //#region src/utils/naming.ts
279
315
  /**
@@ -321,7 +357,7 @@ function splitWords(s) {
321
357
  if (!s) return [];
322
358
  return recombineCompounds(s.replace(/[^a-zA-Z0-9_\-\s.]/g, "_").replace(/([a-z])([A-Z])/g, "$1\0$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1\0$2").replace(/([a-zA-Z])(\d)/g, "$1\0$2").replace(/(\d)([a-zA-Z])/g, "$1\0$2").split(/[\0_\-\s.]+/).filter((w) => w.length > 0));
323
359
  }
324
- const ACRONYM_SET = new Set([
360
+ const ACRONYM_SET = /* @__PURE__ */ new Set([
325
361
  "SSO",
326
362
  "FGA",
327
363
  "SAML",
@@ -331,7 +367,7 @@ const ACRONYM_SET = new Set([
331
367
  "M2M"
332
368
  ]);
333
369
  function toPascalCase(s, acronyms) {
334
- const merged = acronyms ? new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
370
+ const merged = acronyms ? /* @__PURE__ */ new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
335
371
  return splitWords(s).map((w) => {
336
372
  const upper = w.toUpperCase();
337
373
  if (merged.has(upper)) return upper;
@@ -340,7 +376,7 @@ function toPascalCase(s, acronyms) {
340
376
  }).join("");
341
377
  }
342
378
  function toCamelCase(s, acronyms) {
343
- const merged = acronyms ? new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
379
+ const merged = acronyms ? /* @__PURE__ */ new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
344
380
  const words = splitWords(s);
345
381
  if (words.length === 0) return "";
346
382
  return words.map((w, i) => {
@@ -373,7 +409,7 @@ function stripListItemMarkers(name) {
373
409
  return name.replace(/ListItem/g, "").replace(/ByExternalId/g, "").replace(/ByResourceId/g, "").replace(/ForResource/g, "");
374
410
  }
375
411
  /** Words that look plural but must NOT be singularized. */
376
- const SINGULAR_SAFE_LIST = new Set([
412
+ const SINGULAR_SAFE_LIST = /* @__PURE__ */ new Set([
377
413
  "Status",
378
414
  "Address",
379
415
  "Access",
@@ -446,138 +482,598 @@ function qualifyInlineModelName(baseName, parentName) {
446
482
  return `${cleanParent}${baseName}`;
447
483
  }
448
484
  /**
485
+ * The inline model or enum names `schemaToTypeRef` would mint for `schema`
486
+ * typed under `contextName` with `parentModelName`; empty when it mints none
487
+ * (a `$ref`, a primitive, an array or map of those). Mirrors the naming
488
+ * branches of `schemaToTypeRef`: an inline object in any form it recognises
489
+ * (`properties`, an `allOf` with object members) takes
490
+ * `qualifyInlineModelName`; a `oneOf`/`anyOf` mints one model per inline
491
+ * object variant, named exactly as the union references them
492
+ * (`nameInlineVariants`: `DrawingLayer`, `DrawingLayerWithOpacity`, or
493
+ * `CircleDrawingLayer` when every variant is titled); an inline enum, or a
494
+ * `oneOf` of string consts that collapses into one, takes the parent-prefixed
495
+ * enum name, which is never singularized; an array or map claims whatever its
496
+ * items or values would.
497
+ */
498
+ function mintedInlineNames(contextName, schema, parentModelName) {
499
+ return mintedInlineNameEntries(contextName, schema, parentModelName).map((entry) => entry.name);
500
+ }
501
+ /** `mintedInlineNames`, also telling for each name whether it is context-free. */
502
+ function mintedInlineNameEntries(contextName, schema, parentModelName) {
503
+ const s = unwrapNullableSchema(schema);
504
+ if (s.$ref) return [];
505
+ const base = toPascalCase(contextName);
506
+ const modelName = () => [{
507
+ name: qualifyInlineModelName(base, parentModelName),
508
+ contextFree: false
509
+ }];
510
+ const enumName = () => {
511
+ const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
512
+ return [{
513
+ name: cleanParent && !base.startsWith(cleanParent) ? `${cleanParent}${base}` : base,
514
+ contextFree: false
515
+ }];
516
+ };
517
+ if (s.allOf) {
518
+ const only = s.allOf.length === 1 ? s.allOf[0] : void 0;
519
+ if (only) return mintedInlineNameEntries(contextName, only, parentModelName);
520
+ return s.allOf.some((m) => !m.$ref && (m.properties || m.type === "object")) ? modelName() : [];
521
+ }
522
+ const variants = s.oneOf ?? s.anyOf;
523
+ if (variants) {
524
+ const nonNull = variants.filter((v) => !isNullSchema$1(v));
525
+ if (nonNull.length >= 2 && nonNull.every((v) => typeof v.const === "string")) return enumName();
526
+ return [...nameInlineVariantEntries(s, modelName()[0].name)].map(([variant, entry]) => ({
527
+ ...entry,
528
+ schema: variant
529
+ }));
530
+ }
531
+ if (s.const !== void 0) return [];
532
+ if ((s.enum ?? []).filter((v) => v !== null).length >= 2) return enumName();
533
+ if (isInlineObjectSchema(s)) return modelName();
534
+ if (s.items && (s.type === "array" || !s.type)) return mintedInlineNameEntries(contextName, s.items, parentModelName);
535
+ if (s.type === "object" && !s.properties) {
536
+ const value = mapValueSchema(s);
537
+ return value ? mintedInlineNameEntries(contextName, value, parentModelName) : [];
538
+ }
539
+ return [];
540
+ }
541
+ /**
542
+ * The inline names the other properties of an object mint
543
+ * (`mintedInlineNameEntries`) which a `oneOf`/`anyOf` under `fieldName` must
544
+ * not give one of its variants (`nameInlineVariants`): beside a
545
+ * `shape_with_side` object, the `{ side }` variant of `shape` cannot become
546
+ * `DrawingShapeWithSide`. Every context-dependent sibling name is reserved.
547
+ * A context-free name — a title-mapped variant's title — names one shape
548
+ * wherever it appears, so it is shared, not reserved, when this field gives
549
+ * the same title a variant of the identical shape (`schemaShapeKey`:
550
+ * documentation keywords ignored, property names verbatim — the rule array
551
+ * items already use to share a singular sibling's model); a sibling giving
552
+ * the same title a different shape, or one this field does not use, reserves
553
+ * it, so this union's variant falls back to its context-qualified name. Each
554
+ * sibling's names are read in isolation, without the reservations that
555
+ * sibling would itself apply, so the set depends on sibling names and content
556
+ * only, two unions beside each other cannot chase each other's names, and
557
+ * the shape test is symmetric: when `Circle` means `{ radius }` on one side
558
+ * and `{ diameter }` on the other, both fall back (`CircleDrawingShape`,
559
+ * `CircleDrawingMarker`) and neither keeps the bare `Circle`. Empty without a
560
+ * sibling set, as for a request body or response that is itself a union.
561
+ */
562
+ function reservedInlineNames(fieldName, siblings, parentModelName) {
563
+ const reserved = /* @__PURE__ */ new Set();
564
+ if (!siblings) return reserved;
565
+ const ownShapes = /* @__PURE__ */ new Map();
566
+ const fieldSchema = siblings.get(fieldName);
567
+ if (fieldSchema) for (const own of mintedInlineNameEntries(fieldName, fieldSchema, parentModelName)) {
568
+ if (!own.contextFree || !own.schema) continue;
569
+ ownShapes.set(own.name, [...ownShapes.get(own.name) ?? [], schemaShapeKey(own.schema)]);
570
+ }
571
+ for (const [siblingName, siblingSchema] of siblings) {
572
+ if (siblingName === fieldName) continue;
573
+ for (const minted of mintedInlineNameEntries(siblingName, siblingSchema, parentModelName)) {
574
+ if (!minted.contextFree) {
575
+ reserved.add(minted.name);
576
+ continue;
577
+ }
578
+ const shapes = ownShapes.get(minted.name);
579
+ const siblingShape = minted.schema ? schemaShapeKey(minted.schema) : void 0;
580
+ if (!(shapes !== void 0 && siblingShape !== void 0 && shapes.every((s) => s === siblingShape))) reserved.add(minted.name);
581
+ }
582
+ }
583
+ return reserved;
584
+ }
585
+ /** Documentation keywords that do not shape a model; ignored by `schemaShapeKey` at a schema's own level. */
586
+ const DOC_KEYWORDS = /* @__PURE__ */ new Set([
587
+ "description",
588
+ "title",
589
+ "example",
590
+ "examples",
591
+ "summary",
592
+ "externalDocs",
593
+ "deprecated"
594
+ ]);
595
+ /** Keywords whose value maps NAMES (not keywords) to schemas: names are kept verbatim, values recurse as schemas. */
596
+ const NAMED_SCHEMA_MAPS = /* @__PURE__ */ new Set([
597
+ "properties",
598
+ "patternProperties",
599
+ "dependentSchemas",
600
+ "$defs",
601
+ "definitions"
602
+ ]);
603
+ /** Keywords whose value is a single schema. */
604
+ const SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
605
+ "items",
606
+ "additionalProperties",
607
+ "additionalItems",
608
+ "contains",
609
+ "propertyNames",
610
+ "not",
611
+ "if",
612
+ "then",
613
+ "else",
614
+ "unevaluatedProperties",
615
+ "unevaluatedItems"
616
+ ]);
617
+ /** Keywords whose value is an ordered list of schemas. */
618
+ const SCHEMA_LIST_KEYWORDS = /* @__PURE__ */ new Set([
619
+ "allOf",
620
+ "oneOf",
621
+ "anyOf",
622
+ "prefixItems"
623
+ ]);
624
+ /** Keywords whose list value is a set: compared order-insensitively. */
625
+ const UNORDERED_LIST_KEYWORDS = /* @__PURE__ */ new Set(["required", "type"]);
626
+ /**
627
+ * A canonical rendering of a schema's shape, for deciding whether two inline
628
+ * schemas describe the same model: every structural keyword (`type`,
629
+ * `properties`, `required`, `items`, `additionalProperties`,
630
+ * `patternProperties`, `allOf`/`oneOf`/`anyOf`, `enum`, `const`, `format`,
631
+ * `nullable`, ...) recursively, with keywords sorted and the `required` and
632
+ * `type` lists order-insensitive. Documentation keywords (`description`,
633
+ * `title`, `example`, `examples`, `summary`, `externalDocs`, `deprecated`)
634
+ * and vendor extensions (`x-*`) are ignored, and so is `nullable: false`,
635
+ * which the nullable unwrap leaves behind — but only where they are a
636
+ * schema's own keywords. Inside `properties`, `patternProperties`,
637
+ * `dependentSchemas`, `dependentRequired` and `$defs` the keys are property
638
+ * names, kept verbatim: a field named `description` or `title` is part of the
639
+ * shape. Enum, const and default values are compared as plain data, in
640
+ * order, as the IR keeps them.
641
+ */
642
+ function schemaShapeKey(schema) {
643
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
644
+ const dataKey = (value) => {
645
+ if (Array.isArray(value)) return value.map(dataKey);
646
+ if (!isRecord(value)) return value;
647
+ const out = {};
648
+ for (const key of Object.keys(value).sort()) out[key] = dataKey(value[key]);
649
+ return out;
650
+ };
651
+ const schemaKey = (value) => {
652
+ if (!isRecord(value)) return value;
653
+ const out = {};
654
+ for (const keyword of Object.keys(value).sort()) {
655
+ const v = value[keyword];
656
+ if (DOC_KEYWORDS.has(keyword) || keyword.startsWith("x-")) continue;
657
+ if (keyword === "nullable" && v === false) continue;
658
+ if (NAMED_SCHEMA_MAPS.has(keyword) && isRecord(v)) {
659
+ const named = {};
660
+ for (const name of Object.keys(v).sort()) named[name] = schemaKey(v[name]);
661
+ out[keyword] = named;
662
+ } else if (keyword === "dependentRequired" && isRecord(v)) {
663
+ const named = {};
664
+ for (const name of Object.keys(v).sort()) {
665
+ const names = v[name];
666
+ named[name] = Array.isArray(names) ? names.map(String).sort() : dataKey(names);
667
+ }
668
+ out[keyword] = named;
669
+ } else if (SCHEMA_KEYWORDS.has(keyword)) out[keyword] = schemaKey(v);
670
+ else if (SCHEMA_LIST_KEYWORDS.has(keyword) && Array.isArray(v)) out[keyword] = v.map(schemaKey);
671
+ else if (UNORDERED_LIST_KEYWORDS.has(keyword) && Array.isArray(v)) out[keyword] = v.map(String).sort();
672
+ else out[keyword] = dataKey(v);
673
+ }
674
+ return out;
675
+ };
676
+ return JSON.stringify(schemaKey(schema));
677
+ }
678
+ /**
679
+ * The context name an array field's items (`kind: 'item'`) or a map field's
680
+ * values (`kind: 'value'`) are typed and extracted under. By default it is
681
+ * the field itself, so the inline model is the parent plus the singularized
682
+ * field (`layers` on `Drawing` → `DrawingLayer`).
683
+ *
684
+ * When a sibling property of the same object would mint that very name —
685
+ * `layer` as an inline object, an inline enum, an inline union with object
686
+ * variants (any of whose variant names counts), or an array or map of such
687
+ * (`mintedInlineNames`); a primitive `email` beside `emails` claims nothing
688
+ * — the elements step aside to
689
+ * `<field>_item` / `<field>_value`, which qualify to `<Parent><Field>Item` /
690
+ * `<Parent><Field>Value` (`DrawingLayersItem`, `DrawingLayersValue`),
691
+ * matching the `<Variant>Item` convention. Should a sibling mint that name
692
+ * too (a property `layers_item`), the fallback keeps going the way
693
+ * `variantMemberKeys` does for variants: `<field>_array_item`, then
694
+ * `<field>_array_item2`, `<field>_array_item3`, ... for items
695
+ * (`DrawingLayersArrayItem`, ...) and `<field>_member_value`,
696
+ * `<field>_member_value2`, ... for values, taking the first candidate whose
697
+ * minted name no sibling claims.
698
+ *
699
+ * Identical-shape sharing: when every sibling claiming the singular name is
700
+ * an inline object whose shape is structurally identical to the items or
701
+ * values (`schemaShapeKey`: same properties, required, types, nested inline
702
+ * schemas and enum values; description, title and example ignored), the
703
+ * fields share one model under the singular name. They are the same shape,
704
+ * so one model is the right SDK type and a duplicate would only add noise;
705
+ * should a later spec version make the shapes diverge, the split happens
706
+ * then, which is the moment the item type genuinely changes. This
707
+ * deliberately departs from a literal one-model-per-field reading for the
708
+ * identical case.
709
+ *
710
+ * Only context-dependent names take part: a title-mapped variant's title
711
+ * (`MintedInlineName.contextFree`) is the same under every context, so no
712
+ * candidate could step away from it, and such a name is shared by design —
713
+ * two sibling arrays whose items carry the same titled variants both
714
+ * reference `Circle` and `Square` (`nameInlineVariants`). The search always
715
+ * terminates: it stops as soon as a candidate changes nothing about the names
716
+ * (`firstFreeElementContext`'s no-progress rule), and
717
+ * `MAX_ELEMENT_CONTEXT_ATTEMPTS` caps it as a backstop; both return the last
718
+ * candidate tried, deterministically.
719
+ *
720
+ * Decided from sibling names and schema content alone, so document order
721
+ * plays no part. `schemaToTypeRef` and every extractor call this with the
722
+ * same sibling set (`collectProperties`); otherwise the reference and the
723
+ * model disagree.
724
+ */
725
+ function elementContextName(fieldName, kind, siblings, parentModelName, elementSchema) {
726
+ return elementContextDecision(fieldName, kind, siblings, parentModelName, elementSchema).context;
727
+ }
728
+ /** `elementContextName`, with the reason it decided as it did (see `ElementContextReason`). */
729
+ function elementContextDecision(fieldName, kind, siblings, parentModelName, elementSchema) {
730
+ if (!siblings) return {
731
+ context: fieldName,
732
+ reason: "no-siblings"
733
+ };
734
+ const dependentNames = (candidate) => mintedInlineNameEntries(candidate, elementSchema, parentModelName).filter((entry) => !entry.contextFree).map((entry) => entry.name);
735
+ const own = dependentNames(fieldName);
736
+ if (own.length === 0) return {
737
+ context: fieldName,
738
+ reason: "nothing-minted"
739
+ };
740
+ const claimed = /* @__PURE__ */ new Map();
741
+ for (const [siblingName, siblingSchema] of siblings) {
742
+ if (siblingName === fieldName) continue;
743
+ for (const minted of mintedInlineNames(siblingName, siblingSchema, parentModelName)) claimed.set(minted, [...claimed.get(minted) ?? [], siblingSchema]);
744
+ }
745
+ const rivals = [...new Set(own.flatMap((name) => claimed.get(name) ?? []))];
746
+ if (rivals.length === 0) return {
747
+ context: fieldName,
748
+ reason: "unclaimed"
749
+ };
750
+ const element = unwrapNullableSchema(elementSchema);
751
+ const identical = (rival) => {
752
+ const sibling = unwrapNullableSchema(rival);
753
+ return isInlineObjectSchema(sibling) && isInlineObjectSchema(element) && schemaShapeKey(sibling) === schemaShapeKey(element);
754
+ };
755
+ if (rivals.every(identical)) return {
756
+ context: fieldName,
757
+ reason: "shared-shape"
758
+ };
759
+ return firstFreeElementContext(fieldName, kind, new Set(claimed.keys()), dependentNames, own);
760
+ }
761
+ /**
762
+ * The first fallback context under which the elements mint no claimed name:
763
+ * `<field>_item` / `<field>_value`, then `<field>_array_item` /
764
+ * `<field>_member_value`, then `<field>_array_item2`, `…3`, ... (the way
765
+ * `variantMemberKeys` numbers a variant's members). `mint` gives the names
766
+ * under a candidate and `previous` those under the context tried before the
767
+ * first candidate. The search stops with `no-progress` as soon as a
768
+ * candidate mints the same names as the one before it — the names do not
769
+ * depend on the context, so no candidate can avoid the claim — and with
770
+ * `cap` after `maxAttempts` candidates; both keep the last candidate tried.
771
+ */
772
+ function firstFreeElementContext(fieldName, kind, claimed, mint, previous, maxAttempts = 256) {
773
+ const fallback = kind === "item" ? "array_item" : "member_value";
774
+ let last = previous;
775
+ let candidate = fieldName;
776
+ for (let i = 0; i < maxAttempts; i++) {
777
+ candidate = i === 0 ? `${fieldName}_${kind}` : i === 1 ? `${fieldName}_${fallback}` : `${fieldName}_${fallback}${i}`;
778
+ const minted = mint(candidate);
779
+ if (minted.every((name) => !claimed.has(name))) return {
780
+ context: candidate,
781
+ reason: "free"
782
+ };
783
+ if (sameNames(minted, last)) return {
784
+ context: candidate,
785
+ reason: "no-progress"
786
+ };
787
+ last = minted;
788
+ }
789
+ return {
790
+ context: candidate,
791
+ reason: "cap"
792
+ };
793
+ }
794
+ /** The same names, in any order. */
795
+ function sameNames(a, b) {
796
+ if (a.length !== b.length) return false;
797
+ const sortedB = [...b].sort();
798
+ return [...a].sort().every((name, i) => name === sortedB[i]);
799
+ }
800
+ /**
801
+ * Every property the object described by `schema` exposes, by name: its own
802
+ * `properties`, those of its `allOf` members (following a member's `$ref`
803
+ * into `schemas` when given, as `extractAllOfModel` does), and those of
804
+ * inline `oneOf`/`anyOf` variants, whose fields the model extractors flatten
805
+ * in; the first definition of a name wins. This is the sibling set
806
+ * `elementContextName` decides from; the TypeRef builder and every extractor
807
+ * derive it through this one function so they agree.
808
+ */
809
+ function collectProperties(schema, schemas) {
810
+ const properties = /* @__PURE__ */ new Map();
811
+ const seen = /* @__PURE__ */ new Set();
812
+ const visit = (s, followRef) => {
813
+ if (!s || seen.has(s)) return;
814
+ seen.add(s);
815
+ if (s.$ref) {
816
+ if (followRef && schemas) visit(schemas[s.$ref.split("/").pop() ?? ""], true);
817
+ return;
818
+ }
819
+ for (const [key, value] of Object.entries(s.properties ?? {})) if (value && !properties.has(key)) properties.set(key, value);
820
+ for (const sub of s.allOf ?? []) visit(sub, true);
821
+ for (const variant of [...s.oneOf ?? [], ...s.anyOf ?? []]) visit(variant, false);
822
+ };
823
+ visit(schema, false);
824
+ return properties;
825
+ }
826
+ /**
449
827
  * Walk all component schemas and extract inline Model definitions for fields
450
- * that are objects with properties (or arrays of such objects).
828
+ * that are objects with properties, including objects reached through array
829
+ * items, `oneOf` variants, and map (`additionalProperties` /
830
+ * `patternProperties`) values.
451
831
  * These correspond to the ModelRef entries created by schemaToTypeRef.
452
832
  */
453
833
  function extractInlineModelsFromSchemas(schemas) {
454
834
  if (!schemas) return [];
455
835
  const inlineModels = [];
456
- for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, resolveSchemaName(schemaName));
836
+ for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, resolveSchemaName(schemaName), schemas, collectProperties(schema, schemas));
457
837
  return inlineModels;
458
838
  }
459
- function extractInlineModelsFromProperties(schema, results, parentName) {
839
+ /**
840
+ * Walk a schema's properties and materialise every inline object as a model.
841
+ * `componentSchemas` is optional and only feeds `buildFieldFromSchema`, so a
842
+ * field that `$ref`s a component can take its `example`/`examples` from it;
843
+ * callers without the component map at hand simply omit it.
844
+ * `siblingProperties` is the parent object's full property set, for callers
845
+ * that see more of it than this schema shows (see `elementContextName`).
846
+ * `reservedNames` is set only on the re-entry for a field's array items or
847
+ * map values: the inline names that field's siblings mint, which a union
848
+ * among the items or values must step around exactly as `schemaToTypeRef`
849
+ * does when it hands the same set down (`reservedNamesFor`).
850
+ *
851
+ * The recursion follows the document's own inline structure only: a `$ref`
852
+ * is resolved solely to read a component's property list for typing (an
853
+ * augmentation's inherited fields, a sibling set), never descended into, so
854
+ * the depth is bounded by the document's inline nesting and no component
855
+ * graph — self-extending, mutually extending, or reused at several levels —
856
+ * can make it run away.
857
+ */
858
+ function extractInlineModelsFromProperties(schema, results, parentName, componentSchemas, siblingProperties, reservedNames) {
460
859
  const properties = schema.properties ?? {};
461
860
  const allOfSchemas = schema.allOf ?? [];
462
- for (const sub of allOfSchemas) if (sub.properties) extractInlineModelsFromProperties(sub, results, parentName);
463
- for (const [fieldName, fieldSchema] of Object.entries(properties)) {
464
- if (!fieldSchema) continue;
465
- if (fieldSchema.properties && (fieldSchema.type === "object" || !fieldSchema.type)) {
861
+ const siblings = siblingProperties ?? collectProperties(schema);
862
+ const reservedFor = (fieldName) => reservedNames ?? reservedInlineNames(fieldName, siblings, parentName);
863
+ for (const sub of allOfSchemas) if (sub.properties) extractInlineModelsFromProperties(sub, results, parentName, componentSchemas, siblings, reservedNames);
864
+ for (const [fieldName, rawFieldSchema] of Object.entries(properties)) {
865
+ if (!rawFieldSchema) continue;
866
+ const fieldSchema = unwrapNullableField(rawFieldSchema);
867
+ if (isInlineObjectSchema(fieldSchema)) {
466
868
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
467
- results.push(buildInlineModel(modelName, fieldSchema));
468
- extractInlineModelsFromProperties(fieldSchema, results, modelName);
869
+ results.push(buildInlineModel(modelName, fieldSchema, componentSchemas));
870
+ extractInlineModelsFromProperties(fieldSchema, results, modelName, componentSchemas);
469
871
  }
470
- if (fieldSchema.type === "array" && fieldSchema.items) {
471
- const items = fieldSchema.items;
472
- if (items.properties && (items.type === "object" || !items.type)) {
473
- const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
474
- results.push(buildInlineModel(modelName, items));
475
- extractInlineModelsFromProperties(items, results, modelName);
476
- }
872
+ if (fieldSchema.type === "array" && fieldSchema.items) extractInlineModelsFromProperties({ properties: { [elementContextName(fieldName, "item", siblings, parentName, fieldSchema.items)]: fieldSchema.items } }, results, parentName, componentSchemas, void 0, reservedFor(fieldName));
873
+ if (fieldSchema.type === "object" && !fieldSchema.properties) {
874
+ const valueSchema = mapValueSchema(fieldSchema);
875
+ if (valueSchema) extractInlineModelsFromProperties({ properties: { [elementContextName(fieldName, "value", siblings, parentName, valueSchema)]: valueSchema } }, results, parentName, componentSchemas, void 0, reservedFor(fieldName));
477
876
  }
478
- if (fieldSchema.allOf && fieldSchema.allOf.every((s) => !s.$ref && !s.oneOf && !s.anyOf)) {
479
- const merged = mergeAllOfObjectSchema(fieldSchema);
480
- if (Object.keys(merged.properties ?? {}).length > 0) {
877
+ if (fieldSchema.allOf) {
878
+ const members = fieldSchema.allOf;
879
+ const emitMerged = () => {
880
+ const merged = mergeAugmentation(fieldSchema, componentSchemas);
881
+ if (merged.properties.length === 0) return;
481
882
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
482
- results.push(buildInlineModel(modelName, merged));
483
- extractInlineModelsFromProperties(merged, results, modelName);
484
- }
485
- }
486
- if (fieldSchema.oneOf) {
487
- const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
883
+ results.push(buildMergedModel(modelName, merged, componentSchemas));
884
+ const own = {};
885
+ for (const p of merged.properties) if (p.origin === null) own[p.name] = p.schema;
886
+ const mergedSiblings = new Map(merged.properties.map((p) => [p.name, p.schema]));
887
+ extractInlineModelsFromProperties({ properties: own }, results, modelName, componentSchemas, mergedSiblings);
888
+ };
889
+ const only = members.length === 1 ? members[0] : void 0;
890
+ if (members.some((m) => m.$ref)) {
891
+ if (members.some((m) => !m.$ref && (m.properties || m.type === "object"))) emitMerged();
892
+ } else if (only) extractInlineModelsFromProperties({ properties: { [fieldName]: {
893
+ ...only,
894
+ ...extractConstraints(fieldSchema)
895
+ } } }, results, parentName, componentSchemas, siblings, reservedNames);
896
+ else if (members.some((m) => m.properties)) emitMerged();
897
+ }
898
+ const unionVariants = fieldSchema.oneOf ?? fieldSchema.anyOf;
899
+ if (unionVariants) {
900
+ const inlineObjectVariants = unionVariants.filter((v) => isInlineObjectSchema(v));
488
901
  if (inlineObjectVariants.length > 0) {
489
902
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
490
903
  const existingNames = new Set(results.map((r) => r.name));
491
- const namingDiscProp = deriveOneOfNamingDiscriminator(inlineObjectVariants);
492
- const emittedNames = [];
904
+ const variantNames = nameInlineVariants(fieldSchema, modelName, reservedFor(fieldName));
493
905
  for (const variant of inlineObjectVariants) {
494
- const variantName = nameOneOfVariant(variant, modelName, emittedNames, namingDiscProp);
495
- emittedNames.push(variantName);
906
+ const variantName = variantNames.get(variant);
907
+ if (variantName === void 0) continue;
496
908
  if (!existingNames.has(variantName)) {
497
909
  existingNames.add(variantName);
498
- results.push(buildInlineModel(variantName, variant));
499
- extractInlineModelsFromProperties(variant, results, variantName);
910
+ results.push(buildInlineModel(variantName, variant, componentSchemas));
911
+ extractInlineModelsFromProperties(variant, results, variantName, componentSchemas);
912
+ extractInlineModelsFromProperties({ properties: variantMemberSchemas(variant, variantName) }, results, variantName, componentSchemas);
500
913
  }
501
914
  }
502
915
  }
503
916
  }
504
917
  }
505
918
  }
506
- /** Find a single string-const-valued property shared by every variant whose
507
- * values are all distinct — the implicit discriminator. Returns null when no
508
- * such property exists. Mirrors `deriveConstNamingDiscriminator` in schemas.ts. */
509
- function deriveOneOfNamingDiscriminator(variants) {
510
- if (variants.length < 2) return null;
511
- const candidates = Object.keys(variants[0]?.properties ?? {});
512
- for (const propName of candidates) {
513
- const values = variants.map((v) => readConstString(v.properties?.[propName]));
514
- if (values.some((v) => v === null)) continue;
515
- if (new Set(values).size !== values.length) continue;
516
- return propName;
517
- }
518
- return null;
519
- }
520
- function readConstString(prop) {
521
- if (!prop) return null;
522
- if (typeof prop.const === "string") return prop.const;
523
- if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
524
- return null;
525
- }
526
- /** Produce a per-variant model name. Variant 0 keeps the bare parent name;
527
- * later variants are prefixed by the const-derived label. Mirrors
528
- * `nameVariantModel` in schemas.ts. Falls back to a numeric suffix when no
529
- * discriminator is available, the const value PascalCases to nothing, or
530
- * the derived candidate collides with the parent or an already-emitted name. */
531
- function nameOneOfVariant(variant, parentName, alreadyEmitted, discriminatorProperty) {
532
- if (alreadyEmitted.length === 0) return parentName;
533
- if (discriminatorProperty) {
534
- const constValue = readConstString(variant.properties?.[discriminatorProperty]);
535
- if (constValue) {
536
- const prefix = toPascalCase(constValue);
537
- if (prefix) {
538
- const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
539
- if (!(candidate === parentName || alreadyEmitted.includes(candidate))) return candidate;
540
- }
541
- }
542
- }
543
- return `${parentName}${alreadyEmitted.length + 1}`;
544
- }
545
919
  /**
546
- * Merge the object members of an `allOf` (recursively) into a single
547
- * object-shaped schema, so an inline object expressed via `allOf` can be
548
- * materialized as one model — mirroring how schemaToTypeRef collapses such a
549
- * field into a single merged model ref. Earlier members win on key conflicts,
550
- * matching the merge order used elsewhere in the parser.
920
+ * Merge an `allOf` field into one object shape, remembering where each
921
+ * property comes from. A `$ref` member is resolved against `componentSchemas`
922
+ * by its component name, as `extractAllOfModel` does, and contributes its
923
+ * properties — its own nested `allOf` chain included — tagged with that
924
+ * component's model name, so the merged model can type them exactly as the
925
+ * component's own model does; inline members contribute properties tagged as
926
+ * the merged model's own. `required` is collected from every member. Earlier
927
+ * members win on key conflicts, matching the merge order used elsewhere in
928
+ * the parser; an unresolvable ref contributes nothing. The walk reads the
929
+ * components' property lists only — it never descends into a property — so
930
+ * it is finite for any component graph (the `seen` set only short-circuits a
931
+ * component reached twice within one merge).
551
932
  */
552
- function mergeAllOfObjectSchema(schema) {
553
- const properties = {};
933
+ function mergeAugmentation(schema, componentSchemas) {
934
+ const properties = [];
935
+ const seenNames = /* @__PURE__ */ new Set();
554
936
  const required = [];
555
- const visit = (s) => {
556
- if (s.properties) {
557
- for (const [key, value] of Object.entries(s.properties)) if (value && !(key in properties)) properties[key] = value;
937
+ const seen = /* @__PURE__ */ new Set();
938
+ const visit = (s, origin) => {
939
+ if (seen.has(s)) return;
940
+ seen.add(s);
941
+ if (s.$ref) {
942
+ const rawName = s.$ref.split("/").pop() ?? "";
943
+ const target = componentSchemas?.[rawName];
944
+ if (!target) return;
945
+ visit(target, origin ?? {
946
+ name: resolveSchemaName(rawName),
947
+ schema: target
948
+ });
949
+ return;
950
+ }
951
+ for (const [key, value] of Object.entries(s.properties ?? {})) {
952
+ if (!value || seenNames.has(key)) continue;
953
+ seenNames.add(key);
954
+ properties.push({
955
+ name: key,
956
+ schema: value,
957
+ origin
958
+ });
558
959
  }
559
960
  if (s.required) required.push(...s.required);
560
- if (s.allOf) for (const sub of s.allOf) visit(sub);
961
+ for (const sub of s.allOf ?? []) visit(sub, origin);
561
962
  };
562
- visit(schema);
963
+ visit(schema, null);
563
964
  return {
564
- type: "object",
565
965
  properties,
566
966
  required,
567
- description: schema.description
967
+ description: schema.description,
968
+ constraints: extractConstraints(schema)
969
+ };
970
+ }
971
+ /**
972
+ * Build the model for a merged `allOf` field. An inherited property is typed
973
+ * with its component as naming context and the component's own sibling set,
974
+ * so it carries the same TypeRef as in the component's model (`BaseMeta`,
975
+ * `BaseChild`, ...); the merged model's own properties are typed under its
976
+ * name with the full merged property set as siblings.
977
+ */
978
+ function buildMergedModel(name, merged, componentSchemas) {
979
+ const requiredSet = new Set(merged.required);
980
+ const mergedSiblings = new Map(merged.properties.map((p) => [p.name, p.schema]));
981
+ const componentSiblings = /* @__PURE__ */ new Map();
982
+ const fields = merged.properties.map((p) => {
983
+ if (p.origin === null) return buildFieldFromSchema(p.name, p.schema, name, requiredSet, componentSchemas, mergedSiblings);
984
+ let siblings = componentSiblings.get(p.origin.name);
985
+ if (!siblings) {
986
+ siblings = collectProperties(p.origin.schema, componentSchemas);
987
+ componentSiblings.set(p.origin.name, siblings);
988
+ }
989
+ return buildFieldFromSchema(p.name, p.schema, p.origin.name, requiredSet, componentSchemas, siblings);
990
+ });
991
+ return {
992
+ name,
993
+ description: merged.description,
994
+ fields,
995
+ ...merged.constraints ? { constraints: merged.constraints } : {}
568
996
  };
569
997
  }
570
- function buildInlineModel(name, schema) {
998
+ /**
999
+ * Strip the nullable wrappers `schemaToTypeRef` unwraps before it names
1000
+ * anything — the OAS 3.1 `type: [X, 'null']` array, the OAS 3.0
1001
+ * `nullable: true` flag, and a `oneOf`/`anyOf` with a single non-null
1002
+ * variant — so extraction dispatches on the same shape the TypeRef was built
1003
+ * from. The unwrap keeps `contextName`/`parentModelName` semantics intact:
1004
+ * `schemaToTypeRef` passes both through each of these wrappers unchanged.
1005
+ */
1006
+ function unwrapNullableSchema(schema) {
1007
+ if (schema.$ref) return schema;
1008
+ if (Array.isArray(schema.type)) {
1009
+ const nonNullTypes = schema.type.filter((t) => t !== "null");
1010
+ if (nonNullTypes.length !== 1) return schema;
1011
+ return unwrapNullableSchema({
1012
+ ...schema,
1013
+ type: nonNullTypes[0],
1014
+ nullable: false
1015
+ });
1016
+ }
1017
+ if (schema.nullable && schema.type) return unwrapNullableSchema({
1018
+ ...schema,
1019
+ nullable: false
1020
+ });
1021
+ const variants = schema.allOf ? void 0 : schema.oneOf ?? schema.anyOf;
1022
+ if (variants) {
1023
+ const nullVariant = variants.find(isNullSchema$1);
1024
+ const nonNullVariants = variants.filter((v) => v !== nullVariant);
1025
+ const only = nonNullVariants[0];
1026
+ if (nullVariant && nonNullVariants.length === 1 && only) return unwrapNullableSchema(only);
1027
+ }
1028
+ return schema;
1029
+ }
1030
+ /**
1031
+ * `unwrapNullableSchema` for a field, carrying the wrapper's limits onto the
1032
+ * branch it unwraps to. The type-array and `nullable: true` forms unwrap by
1033
+ * spreading, so every keyword survives; the union form swaps in the bare
1034
+ * branch, which would drop a `minProperties` written beside
1035
+ * `anyOf: [{ type: 'object', … }, { type: 'null' }]`. `extractConstraints`
1036
+ * conjoins that wrapper's own keywords with its branch's as the field's
1037
+ * limits, so the model built from the branch must carry the same ones or
1038
+ * `Model.constraints` would omit what the field declares.
1039
+ */
1040
+ function unwrapNullableField(schema) {
1041
+ const inner = unwrapNullableSchema(schema);
1042
+ if (inner === schema || schema.allOf || !(schema.oneOf ?? schema.anyOf)) return inner;
1043
+ return {
1044
+ ...inner,
1045
+ ...extractConstraints(schema)
1046
+ };
1047
+ }
1048
+ /** The `{ type: 'null' }` member that marks a `oneOf`/`anyOf` as nullable — same test as schemaToTypeRef. */
1049
+ function isNullSchema$1(schema) {
1050
+ return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
1051
+ }
1052
+ /**
1053
+ * The schema that types a map's values, in the precedence `schemaToTypeRef`
1054
+ * uses: a non-empty `additionalProperties` object wins, otherwise the first
1055
+ * `patternProperties` entry. `additionalProperties: {}` means "any value"
1056
+ * (typed `unknown`, nothing to extract) and does not fall through to
1057
+ * `patternProperties`, matching the type-ref branch.
1058
+ */
1059
+ function mapValueSchema(schema) {
1060
+ const additional = schema.additionalProperties;
1061
+ if (additional && typeof additional === "object") return Object.keys(additional).length > 0 ? additional : null;
1062
+ return (schema.patternProperties ? Object.values(schema.patternProperties) : [])[0] ?? null;
1063
+ }
1064
+ function buildInlineModel(name, schema, componentSchemas) {
571
1065
  const requiredSet = new Set(schema.required ?? []);
572
1066
  const fields = [];
1067
+ const siblings = collectProperties(schema);
573
1068
  for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
574
1069
  if (!fieldSchema) continue;
575
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
1070
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, componentSchemas, siblings));
576
1071
  }
577
1072
  return {
578
1073
  name,
579
1074
  description: schema.description,
580
- fields
1075
+ fields,
1076
+ ...constraintsEntry(schema)
581
1077
  };
582
1078
  }
583
1079
  //#endregion
@@ -585,9 +1081,21 @@ function buildInlineModel(name, schema) {
585
1081
  /**
586
1082
  * Module-level transform set during extractSchemas(). Used by schemaToTypeRef()
587
1083
  * to apply the same name transform to $ref model/enum references.
1084
+ *
1085
+ * Receives the cleaned PascalCase name and, when the name is a
1086
+ * `components.schemas` key, the raw key exactly as spelled in the document.
1087
+ * Names oagen synthesises (inline enums, request-body variant models,
1088
+ * title-derived names) pass no second argument.
588
1089
  */
589
1090
  let activeSchemaNameTransform = null;
590
1091
  /**
1092
+ * Module-level set of raw `components.schemas` keys for the spec being
1093
+ * extracted. resolveSchemaName() consults it to decide whether the name it was
1094
+ * handed is a real component key (forwarded to the transform as
1095
+ * `originalName`) or a name oagen synthesised (no `originalName`).
1096
+ */
1097
+ let activeComponentSchemaKeys = /* @__PURE__ */ new Set();
1098
+ /**
591
1099
  * Module-level set of resolved schema names whose components/schemas entry is
592
1100
  * an enum (i.e. has `enum:`). Populated by extractSchemas() so schemaToTypeRef()
593
1101
  * can classify a `$ref` to a top-level enum schema as `kind: 'enum'` instead of
@@ -595,33 +1103,308 @@ let activeSchemaNameTransform = null;
595
1103
  * ref, even when the target was a shared enum like `PaginationOrder`.
596
1104
  */
597
1105
  let activeEnumSchemaNames = /* @__PURE__ */ new Set();
598
- /** Apply cleanSchemaName + the active transform (if any) to a raw schema name. */
1106
+ /**
1107
+ * Subset of `activeEnumSchemaNames` whose component schema also admits
1108
+ * `null` — via an OAS 3.1 `type: [..., 'null']` array, the OAS 3.0
1109
+ * `nullable: true` flag, or a literal `null` member in `enum:`. A `$ref` to
1110
+ * one of these resolves to `nullable(enum)` so the component's own
1111
+ * nullability survives the reference, matching the shape an inline
1112
+ * `oneOf: [{ $ref }, { type: 'null' }]` already produces.
1113
+ */
1114
+ let activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
1115
+ /** Drop `null` from an `enum:` list — it marks nullability, it is not a member. */
1116
+ function nonNullEnumValues(values) {
1117
+ return (values ?? []).filter((v) => v !== null);
1118
+ }
1119
+ /** True when an enum schema admits `null` by any OAS 3.0 / 3.1 spelling. */
1120
+ function isNullableEnumSchema(schema) {
1121
+ if (schema.nullable === true) return true;
1122
+ if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
1123
+ return Array.isArray(schema.enum) && schema.enum.includes(null);
1124
+ }
1125
+ /** Wrap `ref` in `nullable` unless it already is one, so a nullable enum `$ref` is never double-wrapped. */
1126
+ function asNullable(ref) {
1127
+ return ref.kind === "nullable" ? ref : {
1128
+ kind: "nullable",
1129
+ inner: ref
1130
+ };
1131
+ }
1132
+ /**
1133
+ * Apply cleanSchemaName + the active transform (if any) to a raw schema name.
1134
+ * When `rawName` is a `components.schemas` key the transform also receives it
1135
+ * as `originalName`, so a consumer can opt out of normalization by returning
1136
+ * the original spelling. Names oagen synthesises (request-body variant
1137
+ * models, title-derived names, ...) are not component keys and get no
1138
+ * `originalName`.
1139
+ */
599
1140
  function resolveSchemaName(rawName) {
600
1141
  let name = cleanSchemaName(toPascalCase(rawName));
601
- if (activeSchemaNameTransform) name = activeSchemaNameTransform(name);
1142
+ if (activeSchemaNameTransform) {
1143
+ const originalName = activeComponentSchemaKeys.has(rawName) ? rawName : void 0;
1144
+ name = activeSchemaNameTransform(name, originalName);
1145
+ }
602
1146
  return name;
603
1147
  }
1148
+ /** Validation keywords copied through unchanged when they hold a number. */
1149
+ const NUMERIC_CONSTRAINT_KEYS = [
1150
+ "multipleOf",
1151
+ "minLength",
1152
+ "maxLength",
1153
+ "minItems",
1154
+ "maxItems",
1155
+ "minContains",
1156
+ "maxContains",
1157
+ "minProperties",
1158
+ "maxProperties"
1159
+ ];
1160
+ /**
1161
+ * Collect the JSON Schema validation keywords declared directly on `schema`
1162
+ * into a `Constraints` object, or `undefined` when it declares none — so a
1163
+ * schema without limits yields no `constraints` key on its IR node at all.
1164
+ *
1165
+ * Numeric bounds are normalized to the OAS 3.1 shape. OAS 3.0 spells an
1166
+ * exclusive bound as `exclusiveMinimum: true` beside `minimum: n`; that pair
1167
+ * becomes `exclusiveMinimum: n` with no `minimum`. A boolean `false`, or a
1168
+ * `true` with no bound to apply to, is dropped. The 3.1 numeric form passes
1169
+ * through, including the legal-but-unusual case of both an inclusive and an
1170
+ * exclusive bound on the same side. `uniqueItems: false` restates the default
1171
+ * and is dropped too.
1172
+ *
1173
+ * Only the schema's own keywords are read: limits nested inside array `items`
1174
+ * or map values are not collected. The one exception is a nullable wrapper —
1175
+ * a `oneOf`/`anyOf` with a single non-null, non-`$ref` branch — that declares
1176
+ * none itself: it takes that branch's keywords, since the IR collapses it to
1177
+ * `nullable<T>`. Values of the wrong type (a string `maxLength`, say) are
1178
+ * ignored rather than carried.
1179
+ */
1180
+ function extractConstraints(schema) {
1181
+ let out = ownConstraints(schema);
1182
+ for (const member of schema.allOf ?? []) out = combineConstraints(out, extractConstraints(member));
1183
+ const branch = singleNonNullBranch(schema);
1184
+ return branch ? combineConstraints(out, extractConstraints(branch)) : out;
1185
+ }
1186
+ /** The keywords declared strictly on `schema`, normalized — without the nullable-branch fallback. */
1187
+ function ownConstraints(schema) {
1188
+ const out = {};
1189
+ const lower = normalizeBound(schema.minimum, schema.exclusiveMinimum);
1190
+ if (lower.inclusive !== void 0) out.minimum = lower.inclusive;
1191
+ if (lower.exclusive !== void 0) out.exclusiveMinimum = lower.exclusive;
1192
+ const upper = normalizeBound(schema.maximum, schema.exclusiveMaximum);
1193
+ if (upper.inclusive !== void 0) out.maximum = upper.inclusive;
1194
+ if (upper.exclusive !== void 0) out.exclusiveMaximum = upper.exclusive;
1195
+ for (const key of NUMERIC_CONSTRAINT_KEYS) {
1196
+ const value = schema[key];
1197
+ if (typeof value === "number") out[key] = value;
1198
+ }
1199
+ if (typeof schema.pattern === "string") out.pattern = schema.pattern;
1200
+ if (schema.uniqueItems === true) out.uniqueItems = true;
1201
+ const dependentRequired = readDependentRequired(schema.dependentRequired);
1202
+ if (dependentRequired) out.dependentRequired = dependentRequired;
1203
+ return Object.keys(out).length > 0 ? out : void 0;
1204
+ }
1205
+ /** The one non-null, non-`$ref` branch of a `oneOf`/`anyOf`, or `undefined` for anything else. */
1206
+ function singleNonNullBranch(schema) {
1207
+ const variants = schema.oneOf ?? schema.anyOf;
1208
+ if (!variants) return void 0;
1209
+ const nonNull = variants.filter((v) => !isNullSchema(v));
1210
+ return nonNull.length === 1 && !nonNull[0].$ref ? nonNull[0] : void 0;
1211
+ }
1212
+ /** True for the `{ type: 'null' }` / `{ type: ['null'] }` branch of a nullable wrapper. */
1213
+ function isNullSchema(schema) {
1214
+ return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
1215
+ }
1216
+ /**
1217
+ * `{ constraints }` when `schema` declares any validation keyword, `{}`
1218
+ * otherwise — spread into an IR node so the key stays absent rather than
1219
+ * present-but-undefined.
1220
+ */
1221
+ function constraintsEntry(schema) {
1222
+ const constraints = extractConstraints(schema);
1223
+ return constraints ? { constraints } : {};
1224
+ }
1225
+ /** Keywords where the stricter of two declarations is the larger value. */
1226
+ const MIN_KEYS = [
1227
+ "minimum",
1228
+ "exclusiveMinimum",
1229
+ "minLength",
1230
+ "minItems",
1231
+ "minContains",
1232
+ "minProperties"
1233
+ ];
1234
+ /** Keywords where the stricter of two declarations is the smaller value. */
1235
+ const MAX_KEYS = [
1236
+ "maximum",
1237
+ "exclusiveMaximum",
1238
+ "maxLength",
1239
+ "maxItems",
1240
+ "maxContains",
1241
+ "maxProperties"
1242
+ ];
1243
+ /**
1244
+ * Conjoin two constraint sets the way `allOf` conjoins schemas: an instance
1245
+ * must satisfy both, so every `min*` takes the larger value, every `max*` the
1246
+ * smaller, `uniqueItems` holds if either side demands it, and
1247
+ * `dependentRequired` merges per property with the union of its names
1248
+ * (deduplicated, `a`'s order first). A keyword declared on one side only is
1249
+ * kept as is, and an absent side yields the other unchanged.
1250
+ *
1251
+ * `multipleOf` and `pattern` keep `a`'s value when the two differ: a true
1252
+ * conjunction would need the least common multiple, or a lookahead regex
1253
+ * joining both patterns, and neither is worth carrying into the IR.
1254
+ *
1255
+ * Not for the parameter `$ref` fallback in extractParams: keywords written
1256
+ * beside a `$ref` override the target's there, which is a different operation.
1257
+ */
1258
+ function combineConstraints(a, b) {
1259
+ if (!a) return b;
1260
+ if (!b) return a;
1261
+ const out = {
1262
+ ...a,
1263
+ ...b
1264
+ };
1265
+ for (const key of MIN_KEYS) {
1266
+ const [x, y] = [a[key], b[key]];
1267
+ if (x !== void 0 && y !== void 0) out[key] = Math.max(x, y);
1268
+ }
1269
+ for (const key of MAX_KEYS) {
1270
+ const [x, y] = [a[key], b[key]];
1271
+ if (x !== void 0 && y !== void 0) out[key] = Math.min(x, y);
1272
+ }
1273
+ if (a.uniqueItems || b.uniqueItems) out.uniqueItems = true;
1274
+ if (a.multipleOf !== void 0 && b.multipleOf !== void 0) out.multipleOf = a.multipleOf;
1275
+ if (a.pattern !== void 0 && b.pattern !== void 0) out.pattern = a.pattern;
1276
+ if (a.dependentRequired && b.dependentRequired) out.dependentRequired = unionDependentRequired(a.dependentRequired, b.dependentRequired);
1277
+ return out;
1278
+ }
1279
+ /** Per-property union of two `dependentRequired` maps, deduplicated and keeping `a`'s order first. */
1280
+ function unionDependentRequired(a, b) {
1281
+ const merged = /* @__PURE__ */ new Map();
1282
+ for (const [property, names] of Object.entries(a)) merged.set(property, [...names]);
1283
+ for (const [property, names] of Object.entries(b)) {
1284
+ const existing = merged.get(property);
1285
+ if (!existing) {
1286
+ merged.set(property, [...names]);
1287
+ continue;
1288
+ }
1289
+ for (const name of names) if (!existing.includes(name)) existing.push(name);
1290
+ }
1291
+ return Object.fromEntries(merged);
1292
+ }
1293
+ /**
1294
+ * Resolve one side's bound from `minimum` / `maximum` plus its `exclusive*`
1295
+ * companion into the OAS 3.1 shape: a numeric companion is the exclusive
1296
+ * bound itself, `true` turns the inclusive bound into the exclusive one, and
1297
+ * `false` or absent leaves the inclusive bound as is.
1298
+ */
1299
+ function normalizeBound(inclusive, exclusive) {
1300
+ const bound = typeof inclusive === "number" ? inclusive : void 0;
1301
+ if (typeof exclusive === "number") return {
1302
+ inclusive: bound,
1303
+ exclusive
1304
+ };
1305
+ if (exclusive === true) return { exclusive: bound };
1306
+ return { inclusive: bound };
1307
+ }
1308
+ /** Copy `dependentRequired` when it is an object whose values are string arrays; anything else is ignored. */
1309
+ function readDependentRequired(value) {
1310
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1311
+ const entries = [];
1312
+ for (const [property, deps] of Object.entries(value)) if (Array.isArray(deps) && deps.every((d) => typeof d === "string")) entries.push([property, [...deps]]);
1313
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
1314
+ }
1315
+ /**
1316
+ * Narrow a raw `examples` value to a non-empty list, or undefined. Guards
1317
+ * against documents that put an OAS Example Object map (or anything else)
1318
+ * where JSON Schema expects an array, and against an empty list, so the IR
1319
+ * carries `examples` only when there is at least one value.
1320
+ */
1321
+ function nonEmptyExamples(value) {
1322
+ return Array.isArray(value) && value.length > 0 ? value : void 0;
1323
+ }
1324
+ /**
1325
+ * Pick the single `example` the IR carries: the document's own `example`
1326
+ * when it is set (including an explicit `null`), otherwise the first entry
1327
+ * of the resolved `examples` list.
1328
+ */
1329
+ function pickExample(example, examples) {
1330
+ return example !== void 0 ? example : examples?.[0];
1331
+ }
1332
+ /**
1333
+ * Resolve `example` and `examples` across ordered precedence levels: for a
1334
+ * parameter its own declaration, its inline schema, then the component its
1335
+ * `$ref` points at; for a field its own schema, then its `$ref` target.
1336
+ *
1337
+ * Each value walks the levels independently. `examples` is the first
1338
+ * non-empty list. `example` comes from the first level that has either an
1339
+ * `example` or a list, preferring that level's own `example` over its first
1340
+ * list entry — so a schema-level `example` can never mask the parameter's
1341
+ * own `examples` map. A level with neither falls through; an explicit
1342
+ * `null` is a value and stops the walk.
1343
+ */
1344
+ function resolveExamples(levels) {
1345
+ let example;
1346
+ let examples;
1347
+ for (const level of levels) {
1348
+ if (examples === void 0) examples = level.examples;
1349
+ if (example === void 0) example = pickExample(level.example, level.examples);
1350
+ }
1351
+ return {
1352
+ example,
1353
+ examples
1354
+ };
1355
+ }
1356
+ /**
1357
+ * If a schema is a `$ref` to `#/components/schemas/X`, return the referenced
1358
+ * component schema. Used to surface schema-level metadata (default, example,
1359
+ * examples, deprecated, description) that would otherwise be invisible at the
1360
+ * field or parameter level.
1361
+ *
1362
+ * Refs are not pre-resolved by the bundler (`dereference: false` in refs.ts),
1363
+ * so without this, a param like `schema: { $ref: '.../PaginationOrder' }`
1364
+ * silently loses the target's `default: desc` and the SDK stops emitting the
1365
+ * spec-mandated default value in generated method signatures.
1366
+ */
1367
+ function resolveComponentSchemaRef(schema, componentSchemas) {
1368
+ if (!schema || !componentSchemas) return void 0;
1369
+ const ref = schema.$ref;
1370
+ if (!ref) return void 0;
1371
+ const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
1372
+ if (!match) return void 0;
1373
+ return componentSchemas[match[1]];
1374
+ }
604
1375
  function extractSchemas(schemas, options) {
605
1376
  const enums = [];
606
1377
  if (!schemas) return {
607
1378
  models: [],
608
1379
  enums
609
1380
  };
1381
+ activeComponentSchemaKeys = new Set(Object.keys(schemas));
610
1382
  if (options?.schemaNameTransform) {
611
1383
  const transform = options.schemaNameTransform;
612
- const cleanedNames = Object.keys(schemas).map((n) => cleanSchemaName(toPascalCase(n)));
613
- const transformedToOriginals = /* @__PURE__ */ new Map();
614
- for (const cleaned of cleanedNames) {
615
- const transformed = transform(cleaned);
616
- if (!transformedToOriginals.has(transformed)) transformedToOriginals.set(transformed, []);
617
- transformedToOriginals.get(transformed).push(cleaned);
618
- }
619
- const unsafeToTransform = /* @__PURE__ */ new Set();
620
- for (const [, originals] of transformedToOriginals) if (originals.length > 1) for (const n of originals) unsafeToTransform.add(n);
621
- activeSchemaNameTransform = (name) => unsafeToTransform.has(name) ? name : transform(name);
1384
+ const transformedToRawKeys = /* @__PURE__ */ new Map();
1385
+ for (const raw of Object.keys(schemas)) {
1386
+ const transformed = transform(cleanSchemaName(toPascalCase(raw)), raw);
1387
+ if (!transformedToRawKeys.has(transformed)) transformedToRawKeys.set(transformed, []);
1388
+ transformedToRawKeys.get(transformed).push(raw);
1389
+ }
1390
+ const unsafeRawKeys = /* @__PURE__ */ new Set();
1391
+ const unsafeCleanedNames = /* @__PURE__ */ new Set();
1392
+ for (const [, rawKeys] of transformedToRawKeys) if (rawKeys.length > 1) for (const raw of rawKeys) {
1393
+ unsafeRawKeys.add(raw);
1394
+ unsafeCleanedNames.add(cleanSchemaName(toPascalCase(raw)));
1395
+ }
1396
+ activeSchemaNameTransform = (name, originalName) => {
1397
+ return (originalName !== void 0 ? unsafeRawKeys.has(originalName) : unsafeCleanedNames.has(name)) ? name : transform(name, originalName);
1398
+ };
622
1399
  } else activeSchemaNameTransform = null;
623
1400
  activeEnumSchemaNames = /* @__PURE__ */ new Set();
624
- for (const [name, schema] of Object.entries(schemas)) if (schema.enum) activeEnumSchemaNames.add(resolveSchemaName(name));
1401
+ activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
1402
+ for (const [name, schema] of Object.entries(schemas)) {
1403
+ if (!schema.enum) continue;
1404
+ const resolved = resolveSchemaName(name);
1405
+ activeEnumSchemaNames.add(resolved);
1406
+ if (isNullableEnumSchema(schema)) activeNullableEnumSchemaNames.add(resolved);
1407
+ }
625
1408
  const modelsByCleanName = /* @__PURE__ */ new Map();
626
1409
  for (const [name, schema] of Object.entries(schemas)) {
627
1410
  const pascalName = resolveSchemaName(name);
@@ -632,7 +1415,7 @@ function extractSchemas(schemas, options) {
632
1415
  if (existing) {
633
1416
  if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
634
1417
  } else modelsByCleanName.set(pascalName, model);
635
- for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName)) {
1418
+ for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName, schemas)) {
636
1419
  const existingInline = modelsByCleanName.get(inlineModel.name);
637
1420
  if (existingInline) {
638
1421
  if (inlineModel.fields.length > existingInline.fields.length) modelsByCleanName.set(inlineModel.name, inlineModel);
@@ -657,6 +1440,8 @@ function extractSchemas(schemas, options) {
657
1440
  function clearSchemaNameTransform() {
658
1441
  activeSchemaNameTransform = null;
659
1442
  activeEnumSchemaNames = /* @__PURE__ */ new Set();
1443
+ activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
1444
+ activeComponentSchemaKeys = /* @__PURE__ */ new Set();
660
1445
  }
661
1446
  /**
662
1447
  * Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
@@ -692,14 +1477,17 @@ function collectInlineEnums(fields, enums) {
692
1477
  * that schemaToTypeRef turns into model refs but aren't extracted from components.
693
1478
  */
694
1479
  function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
1480
+ const directRefs = directModelRefNames(ref);
695
1481
  walkTypeRef(ref, { model: (modelRef) => {
696
1482
  if (modelNames.has(modelRef.name)) return;
1483
+ if (!directRefs.has(modelRef.name)) return;
697
1484
  if (Object.keys(schemas).some((k) => resolveSchemaName(k) === modelRef.name)) return;
698
1485
  const parentSchema = findSchemaByName(parentModelName, schemas);
699
1486
  if (!parentSchema) return;
700
1487
  const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
701
1488
  if (!fieldSchema) return;
702
- const extracted = extractNestedSchema(modelRef.name, fieldSchema);
1489
+ const reserved = reservedInlineNames(fieldName, collectProperties(parentSchema, schemas), parentModelName);
1490
+ const extracted = extractNestedSchema(unionBaseName(modelRef.name, fieldName, fieldSchema, parentModelName, reserved), fieldSchema, schemas, reserved);
703
1491
  for (const m of extracted) if (!modelNames.has(m.name)) {
704
1492
  models.push(m);
705
1493
  modelNames.add(m.name);
@@ -707,6 +1495,35 @@ function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, mod
707
1495
  }
708
1496
  } });
709
1497
  }
1498
+ /**
1499
+ * The base name `extractNestedSchema` should name a field's `oneOf` variants
1500
+ * from: the field's qualified inline name when naming the variants from it
1501
+ * yields `refName` (the ref the walk is resolving), else `refName` itself,
1502
+ * which is right for every non-union field and for a union whose variant 0 is
1503
+ * bare.
1504
+ */
1505
+ function unionBaseName(refName, fieldName, fieldSchema, parentModelName, reserved) {
1506
+ if (!fieldSchema.oneOf) return refName;
1507
+ const fromField = qualifyInlineModelName(toPascalCase(fieldName), parentModelName);
1508
+ return [...nameInlineVariants(fieldSchema, fromField, reserved).values()].includes(refName) ? fromField : refName;
1509
+ }
1510
+ /**
1511
+ * The model refs a field's TypeRef produces directly: the ref itself, the
1512
+ * items of a (possibly nullable) array, or each (possibly nullable) variant of
1513
+ * a union — the shapes `extractNestedSchema` can name from a ref. Refs nested
1514
+ * any deeper are excluded.
1515
+ */
1516
+ function directModelRefNames(ref) {
1517
+ const names = /* @__PURE__ */ new Set();
1518
+ let root = ref;
1519
+ while (root.kind === "nullable" || root.kind === "array") root = root.kind === "nullable" ? root.inner : root.items;
1520
+ const leaves = root.kind === "union" ? root.variants : [root];
1521
+ for (const leaf of leaves) {
1522
+ const inner = leaf.kind === "nullable" ? leaf.inner : leaf;
1523
+ if (inner.kind === "model") names.add(inner.name);
1524
+ }
1525
+ return names;
1526
+ }
710
1527
  /** Find a component schema by PascalCase name. */
711
1528
  function findSchemaByName(pascalName, schemas) {
712
1529
  for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
@@ -735,104 +1552,278 @@ function findNestedFieldSchema(fieldName, parentSchema, schemas) {
735
1552
  }
736
1553
  return null;
737
1554
  }
738
- /** Extract a model (and nested models) from an inline field schema. */
739
- function extractNestedSchema(name, schema) {
1555
+ /**
1556
+ * Extract a model (and nested models) from an inline field schema. `reserved`
1557
+ * holds the inline names the field's sibling properties mint
1558
+ * (`reservedInlineNames`), which a `oneOf` variant must not take.
1559
+ */
1560
+ function extractNestedSchema(name, schema, schemas, reserved = NO_RESERVED) {
740
1561
  if (schema.oneOf) {
741
1562
  const models = [];
742
- const constNamingDiscriminator = deriveConstNamingDiscriminator(schema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type)));
1563
+ const variantNames = nameInlineVariants(schema, name, reserved);
743
1564
  for (const variant of schema.oneOf) {
744
- if (variant.$ref) continue;
745
- if (variant.properties && (variant.type === "object" || !variant.type)) {
746
- const requiredSet = new Set(variant.required ?? []);
747
- const fields = [];
748
- for (const [fn, fs] of Object.entries(variant.properties)) {
749
- if (!fs) continue;
750
- fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
751
- }
752
- const variantName = nameVariantModel(variant, name, models, constNamingDiscriminator);
753
- models.push({
754
- name: variantName,
755
- description: variant.description,
756
- fields
757
- });
1565
+ const variantName = variantNames.get(variant);
1566
+ if (variantName === void 0) continue;
1567
+ const requiredSet = new Set(variant.required ?? []);
1568
+ const siblings = collectProperties(variant);
1569
+ const fields = [];
1570
+ for (const [fn, fs] of Object.entries(variant.properties ?? {})) {
1571
+ if (!fs) continue;
1572
+ fields.push(buildFieldFromSchema(fn, fs, variantName, requiredSet, schemas, siblings));
758
1573
  }
1574
+ models.push({
1575
+ name: variantName,
1576
+ description: variant.description,
1577
+ fields,
1578
+ ...constraintsEntry(variant)
1579
+ });
759
1580
  }
760
1581
  return models;
761
1582
  }
762
1583
  if (schema.properties && (schema.type === "object" || !schema.type)) {
763
1584
  const requiredSet = new Set(schema.required ?? []);
1585
+ const siblings = collectProperties(schema);
764
1586
  const fields = [];
765
1587
  for (const [fn, fs] of Object.entries(schema.properties)) {
766
1588
  if (!fs) continue;
767
- fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
1589
+ fields.push(buildFieldFromSchema(fn, fs, name, requiredSet, schemas, siblings));
768
1590
  }
769
1591
  return [{
770
1592
  name,
771
1593
  description: schema.description,
772
- fields
1594
+ fields,
1595
+ ...constraintsEntry(schema)
773
1596
  }];
774
1597
  }
775
1598
  if (schema.type === "array" && schema.items?.properties) {
776
1599
  const requiredSet = new Set(schema.items.required ?? []);
1600
+ const siblings = collectProperties(schema.items);
777
1601
  const fields = [];
778
1602
  for (const [fn, fs] of Object.entries(schema.items.properties)) {
779
1603
  if (!fs) continue;
780
- fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
1604
+ fields.push(buildFieldFromSchema(fn, fs, name, requiredSet, schemas, siblings));
781
1605
  }
782
1606
  return [{
783
1607
  name,
784
1608
  description: schema.items.description,
785
- fields
1609
+ fields,
1610
+ ...constraintsEntry(schema.items)
786
1611
  }];
787
1612
  }
788
1613
  return [];
789
1614
  }
790
1615
  /**
791
- * Derive a meaningful name for a oneOf object variant past the first,
792
- * falling back to the numeric-suffix scheme (`Foo2`, `Foo3`) when no
793
- * better signal is available. The "better signal" is a const-valued
794
- * discriminator property that distinguishes one variant from the others.
795
- *
796
- * For a schema like `ApiKey.owner` whose oneOf has two variants — one
797
- * with `type: { const: "organization" }` and one with `type: { const:
798
- * "user" }` — this returns `ApiKeyOwner` for the first variant (the
799
- * baseline name that the union TypeRef will reference, so consumer-
800
- * visible typed fields stay backward-compatible) and `UserApiKeyOwner`
801
- * for the second instead of `ApiKeyOwner2`. SDK consumers reading the
802
- * generated types can tell the variants apart at a glance.
803
- *
804
- * The first variant always keeps the bare parent name because
805
- * `schemaToTypeRef` builds union variants whose model refs collapse to
806
- * `qualifyInlineModelName(field, parent)` — so the first model name
807
- * must match for the union's degenerate-collapse to land on a valid
808
- * type. Renaming variant 0 would orphan it.
809
- *
810
- * Falls back to numeric suffix when:
811
- * - The variant has no discriminator property (no shared const
812
- * property across all object variants), OR
813
- * - The const value PascalCases to an empty/whitespace name, OR
814
- * - The derived name collides with the parent name or an already-
815
- * emitted variant name (numeric keeps uniqueness).
816
- */
817
- function nameVariantModel(variant, parentName, alreadyEmitted, discriminator) {
818
- if (alreadyEmitted.length === 0) return parentName;
1616
+ * The prefix a variant's `title` contributes to its name: the title trimmed
1617
+ * and PascalCased, or null when there is no title or it PascalCases to
1618
+ * nothing.
1619
+ */
1620
+ function variantTitlePrefix(variant) {
1621
+ const title = typeof variant["title"] === "string" ? variant["title"].trim() : "";
1622
+ return title ? toPascalCase(title) || null : null;
1623
+ }
1624
+ /** `<Prefix><Base>`, or `base` unchanged when it already starts with the prefix (no doubling). */
1625
+ function prefixedVariantName(prefix, base) {
1626
+ return base.startsWith(prefix) ? base : `${prefix}${base}`;
1627
+ }
1628
+ /**
1629
+ * The properties `variant` declares that no other inline object variant of
1630
+ * the same union declares, in the variant's own `properties` order.
1631
+ */
1632
+ function distinguishingProperties(variant, siblings) {
1633
+ const declaredElsewhere = /* @__PURE__ */ new Set();
1634
+ for (const sibling of siblings) {
1635
+ if (sibling === variant) continue;
1636
+ for (const property of Object.keys(sibling.properties ?? {})) declaredElsewhere.add(property);
1637
+ }
1638
+ return Object.keys(variant.properties ?? {}).filter((property) => !declaredElsewhere.has(property));
1639
+ }
1640
+ /**
1641
+ * Name an inline object variant of a `oneOf`/`anyOf` that has no better
1642
+ * signal: it is not variant 0 (which keeps the union's base name) and no
1643
+ * const discriminator gave it a prefix. The name is derived from the
1644
+ * variant's own content, never from where it sits in the document (beyond
1645
+ * its position among the variants, which only the final counter reads), so
1646
+ * an SDK user can tell the variants apart and reordering them changes nothing
1647
+ * but which one is variant 0. In order, taking the first candidate that is
1648
+ * neither the base name nor a name already given to an earlier variant of
1649
+ * the same union:
1650
+ *
1651
+ * 1. the variant's `title` (trimmed and PascalCased): `<Title><Base>`
1652
+ * (`CircleDrawingShape`), the same prefix form a const discriminator
1653
+ * produces, including the rule that a base already starting with the
1654
+ * prefix is not doubled (that candidate equals the base, so it falls
1655
+ * through); a title an earlier variant already took falls through too;
1656
+ * 2. a distinguishing property — the first property, in the variant's own
1657
+ * order, that it declares and no other inline object variant declares:
1658
+ * `<Base>With<Property>` (`DrawingShapeWithSide`); a property that
1659
+ * PascalCases to nothing is skipped;
1660
+ * 3. the position counter, `<Base><position + 1>` (`DrawingShape2`), which
1661
+ * now remains only for variants whose property sets are identical, and
1662
+ * which keeps counting past a taken name (`DrawingShape3`).
1663
+ *
1664
+ * Every candidate is also checked against `reserved`: the inline names the
1665
+ * parent's other properties mint (`reservedInlineNames`), so a variant can
1666
+ * never take a name another field's model already has — beside a
1667
+ * `shape_with_side` object, the `{ side }` variant of `shape` is not
1668
+ * `DrawingShapeWithSide` but the next candidate. Read from sibling names and
1669
+ * content only, so document order plays no part.
1670
+ *
1671
+ * `siblings` are the inline object variants of the union, this one included;
1672
+ * `emitted` are the names already given to the earlier ones, in order. Every
1673
+ * site that names a variant — the union TypeRef, the component-field,
1674
+ * request-body and response extractors, and `mintedInlineNames` — goes
1675
+ * through this function (via `nameInlineVariant`), so they cannot drift.
1676
+ */
1677
+ function nameUndiscriminatedVariant(variant, siblings, base, emitted, reserved = NO_RESERVED) {
1678
+ const taken = (candidate) => candidate === base || emitted.includes(candidate) || reserved.has(candidate);
1679
+ const titlePrefix = variantTitlePrefix(variant);
1680
+ if (titlePrefix !== null) {
1681
+ const candidate = prefixedVariantName(titlePrefix, base);
1682
+ if (!taken(candidate)) return candidate;
1683
+ }
1684
+ for (const property of distinguishingProperties(variant, siblings)) {
1685
+ const word = toPascalCase(property);
1686
+ if (!word) continue;
1687
+ const candidate = `${base}With${word}`;
1688
+ if (!taken(candidate)) return candidate;
1689
+ }
1690
+ for (let position = emitted.length + 1;; position++) {
1691
+ const candidate = `${base}${position}`;
1692
+ if (!taken(candidate)) return candidate;
1693
+ }
1694
+ }
1695
+ /** No reserved names: for a union with no sibling properties (a request body or response that is itself a union). */
1696
+ const NO_RESERVED = /* @__PURE__ */ new Set();
1697
+ /**
1698
+ * True when the union's inline object variants (two or more) each carry a
1699
+ * title and no two titles PascalCase to the same prefix: the one case in
1700
+ * which variant 0 is named from its title rather than keeping the base name.
1701
+ */
1702
+ function allDistinctlyTitled(siblings) {
1703
+ if (siblings.length < 2) return false;
1704
+ const prefixes = siblings.map(variantTitlePrefix);
1705
+ return prefixes.every((prefix) => prefix !== null) && new Set(prefixes).size === prefixes.length;
1706
+ }
1707
+ /**
1708
+ * The name of one inline object variant of a `oneOf`/`anyOf`, given the
1709
+ * union's base name (the field's qualified inline name, e.g. `ApiKeyOwner`),
1710
+ * the inline object variants of the union (`siblings`, this one included),
1711
+ * the names already given to the earlier variants (`emitted`, in order), and
1712
+ * the const-valued property that distinguishes the variants when there is
1713
+ * one (`deriveConstNamingDiscriminator`).
1714
+ *
1715
+ * Variant 0 keeps the bare base name: the union TypeRef and every extractor
1716
+ * reference it under that name, and a consumer's typed field keeps the name
1717
+ * it always had. The one exception: when the union has no const discriminator
1718
+ * and every variant carries a distinct title, variant 0 is named from its
1719
+ * title like the others (`CircleDrawingShape` beside `SquareDrawingShape`)
1720
+ * instead of being the only one left bare. Whether the union is discriminated
1721
+ * is read from the variants themselves here, not from `discriminator`, so a
1722
+ * caller forcing a property (an explicit `discriminator:` without `mapping:`)
1723
+ * still agrees with the extractors on variant 0.
1724
+ *
1725
+ * A later variant takes `<ConstValue><Base>` from the discriminator
1726
+ * (`UserApiKeyOwner`) when the value PascalCases to something and the name is
1727
+ * neither the base, nor already emitted, nor minted by one of the parent's
1728
+ * other properties (`reserved`), and otherwise falls to
1729
+ * `nameUndiscriminatedVariant`: its title, a distinguishing property, then
1730
+ * the position counter, each checked the same way.
1731
+ */
1732
+ function nameInlineVariant(variant, siblings, base, emitted, discriminator, reserved = NO_RESERVED) {
1733
+ if (emitted.length === 0) {
1734
+ const titlePrefix = deriveConstNamingDiscriminator([...siblings]) === null && allDistinctlyTitled(siblings) ? variantTitlePrefix(variant) : null;
1735
+ return titlePrefix === null ? base : prefixedVariantName(titlePrefix, base);
1736
+ }
819
1737
  if (discriminator) {
820
1738
  const constValue = getConstPropertyValue(variant, discriminator.property);
821
- if (constValue) {
822
- const prefix = toPascalCase(constValue);
823
- if (prefix) {
824
- const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
825
- if (!(candidate === parentName || alreadyEmitted.some((m) => m.name === candidate))) return candidate;
1739
+ const prefix = constValue ? toPascalCase(constValue) : "";
1740
+ if (prefix) {
1741
+ const candidate = prefixedVariantName(prefix, base);
1742
+ if (candidate !== base && !emitted.includes(candidate) && !reserved.has(candidate)) return candidate;
1743
+ }
1744
+ }
1745
+ return nameUndiscriminatedVariant(variant, siblings, base, emitted, reserved);
1746
+ }
1747
+ /**
1748
+ * Name every inline object variant of a `oneOf`/`anyOf` schema from the
1749
+ * union's base name, keyed by variant schema identity; null, non-object and
1750
+ * `$ref` variants get no entry. Positions are local to this variant list, so
1751
+ * the union's TypeRef entries and the extracted models agree no matter what
1752
+ * else has been extracted. `reserved` holds the inline names the parent's
1753
+ * other properties mint (`reservedInlineNames`); every site that has a
1754
+ * sibling set passes the same one, so the reference and the model step
1755
+ * around them identically.
1756
+ *
1757
+ * One union shape is named differently: a `oneOf` with no explicit
1758
+ * `discriminator:` whose every variant carries a `$ref` or a `title` and pins
1759
+ * the same const property (`titleMappedDiscriminatorProperty`). Its
1760
+ * discriminator mapping has always named an inline variant by its title
1761
+ * (`resolveSchemaName(title)`: `Circle`, `Square`, run through the project's
1762
+ * schema-name transform like a component), and `schemaToTypeRef` references
1763
+ * the variant by that mapped name, so the title is the model name here too —
1764
+ * unless it is the base, already emitted, or reserved by a sibling, in which
1765
+ * case the variant takes a context-qualified name (its title as a prefix on
1766
+ * the base, `CircleDrawingMarker`, else a distinguishing property, else the
1767
+ * counter) and the mapping, which is read from this map, follows it. A kept
1768
+ * title is context-free: it is the same whatever field, array or map the
1769
+ * union sits under, so it names one shape wherever it appears, as a
1770
+ * component name would. A second union beside it using the same title for a
1771
+ * variant of the identical shape (`schemaShapeKey`) therefore shares the
1772
+ * model instead of stepping around it; a sibling giving the same title a
1773
+ * different shape reserves it (`reservedInlineNames`), and since both sides
1774
+ * read their siblings in isolation the decision is symmetric: when the
1775
+ * shapes differ, both unions fall back to their context-qualified names
1776
+ * (`CircleDrawingShape`, `CircleDrawingMarker`) and neither keeps the bare
1777
+ * `Circle`; when they are identical, both keep `Circle` and one model is
1778
+ * emitted. `elementContextName` never lets a context-free name drive an
1779
+ * items/values step-aside, which no context could satisfy.
1780
+ * `nameInlineVariantEntries` reports which names are context-free; a
1781
+ * fallen-back title name is context-dependent.
1782
+ *
1783
+ * This is the list-level entry point every site uses: `schemaToTypeRef` (via
1784
+ * `nameInlineObjectVariants`), `extractNestedSchema`,
1785
+ * `extractInlineModelDeep`, `extractInlineModelsFromProperties` and
1786
+ * `mintedInlineNames`.
1787
+ */
1788
+ function nameInlineVariants(union, base, reserved = NO_RESERVED) {
1789
+ const names = /* @__PURE__ */ new Map();
1790
+ for (const [variant, entry] of nameInlineVariantEntries(union, base, reserved)) names.set(variant, entry.name);
1791
+ return names;
1792
+ }
1793
+ /** `nameInlineVariants`, also telling for each name whether it is context-free. */
1794
+ function nameInlineVariantEntries(union, base, reserved = NO_RESERVED) {
1795
+ const nonNullVariants = (union.oneOf ?? union.anyOf ?? []).filter((v) => !isNullVariantSchema(v));
1796
+ const inlineObjectVariants = nonNullVariants.filter((v) => isInlineObjectSchema(v));
1797
+ const discriminator = deriveConstNamingDiscriminator(inlineObjectVariants);
1798
+ const titleMapped = union.oneOf !== void 0 && !union.discriminator && titleMappedDiscriminatorProperty(nonNullVariants) !== null;
1799
+ const emitted = [];
1800
+ const entries = /* @__PURE__ */ new Map();
1801
+ for (const variant of inlineObjectVariants) {
1802
+ let name = null;
1803
+ let contextFree = false;
1804
+ if (titleMapped) {
1805
+ const titled = resolveVariantModelName(variant);
1806
+ if (titled !== null) {
1807
+ if (titled !== base && !emitted.includes(titled) && !reserved.has(titled)) {
1808
+ name = titled;
1809
+ contextFree = true;
1810
+ } else name = nameUndiscriminatedVariant(variant, inlineObjectVariants, base, emitted, reserved);
826
1811
  }
827
1812
  }
1813
+ name ??= nameInlineVariant(variant, inlineObjectVariants, base, emitted, discriminator, reserved);
1814
+ emitted.push(name);
1815
+ entries.set(variant, {
1816
+ name,
1817
+ contextFree
1818
+ });
828
1819
  }
829
- return `${parentName}${alreadyEmitted.length + 1}`;
1820
+ return entries;
830
1821
  }
831
1822
  /**
832
1823
  * Returns a discriminator descriptor when every inline object variant in a
833
1824
  * oneOf has a string-const value on the same property — the signature of a
834
1825
  * discriminated union without an explicit `discriminator:` keyword. Used by
835
- * `nameVariantModel` to derive variant names from the const values.
1826
+ * `nameInlineVariant` to derive variant names from the const values.
836
1827
  *
837
1828
  * Distinct from `detectConstPropertyDiscriminator` (which requires every
838
1829
  * variant to carry a `$ref` or `title` so a name mapping can be built):
@@ -850,18 +1841,96 @@ function deriveConstNamingDiscriminator(variants) {
850
1841
  }
851
1842
  return null;
852
1843
  }
853
- /** Build a single Field from a schema property entry. Shared across all extraction sites. */
854
- function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet) {
1844
+ /**
1845
+ * True for a schema that `schemaToTypeRef` turns into an inline model reference:
1846
+ * it declares `properties` directly (no `$ref`) and is an object — whether by
1847
+ * `type: 'object'`, by omitting `type`, or by an OAS 3.1 nullable array such as
1848
+ * `type: ['object', 'null']`. Every inline-model extractor must use the same
1849
+ * test, or a field's reference points at a model nobody materializes.
1850
+ */
1851
+ function isInlineObjectSchema(schema) {
1852
+ if (!schema || schema.$ref || !schema.properties) return false;
1853
+ const { type } = schema;
1854
+ return type === void 0 || type === "object" || Array.isArray(type) && type.includes("object");
1855
+ }
1856
+ /** The `{ type: 'null' }` member that marks a `oneOf`/`anyOf` as nullable; never a variant to name. */
1857
+ function isNullVariantSchema(schema) {
1858
+ return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
1859
+ }
1860
+ /**
1861
+ * The synthetic context names a multi-type inline variant's non-object
1862
+ * members are typed and extracted under, chosen so the models they name can
1863
+ * never collide with a model named from one of the variant's own properties.
1864
+ * An `array` member's items go under `item` (`<Variant>Item`), falling back
1865
+ * to `array_item` (`<Variant>ArrayItem`) and then `array_item2`,
1866
+ * `array_item3`, ... when a property already claims that name; any other
1867
+ * member goes under `value` (`<Variant>Value`), then `member_value`,
1868
+ * `member_value2`, .... A candidate is skipped when the model name
1869
+ * `qualifyInlineModelName` derives from it equals one derived from a
1870
+ * property — compared after qualification, so a property `items`, whose
1871
+ * name singularizes to `<Variant>Item`, blocks `item` as well.
1872
+ * This is the single source of these names for the TypeRef builder
1873
+ * (`inlineVariantTypeRef`) and the extractors (`variantMemberSchemas`).
1874
+ */
1875
+ function variantMemberKeys(variant, variantName) {
1876
+ const modelNameFor = (key) => qualifyInlineModelName(toPascalCase(key), variantName);
1877
+ const claimed = new Set(Object.keys(variant.properties ?? {}).map(modelNameFor));
1878
+ const pick = (preferred, fallback) => {
1879
+ for (let i = 0;; i++) {
1880
+ const candidate = i === 0 ? preferred : i === 1 ? fallback : `${fallback}${i}`;
1881
+ if (!claimed.has(modelNameFor(candidate))) return candidate;
1882
+ }
1883
+ };
1884
+ return {
1885
+ item: pick("item", "array_item"),
1886
+ value: pick("value", "member_value")
1887
+ };
1888
+ }
1889
+ /**
1890
+ * The schemas carried by the non-object members of a multi-type inline
1891
+ * variant (`type: ['object', 'array']` with `items`), keyed by the synthetic
1892
+ * context name from `variantMemberKeys` that `schemaToTypeRef` types them
1893
+ * under, so an inline object item is `<Variant>Item` (or `<Variant>ArrayItem`
1894
+ * when `item` is taken) and its own nested objects `<Variant>ItemChild`.
1895
+ * Every extractor re-enters with these under the variant's name so the
1896
+ * models line up with the refs by construction. A map member cannot occur
1897
+ * here: a variant with `properties` is typed as a model, never as a map.
1898
+ */
1899
+ function variantMemberSchemas(variant, variantName) {
1900
+ const members = {};
1901
+ if (Array.isArray(variant.type) && variant.type.includes("array") && variant.items) members[variantMemberKeys(variant, variantName).item] = variant.items;
1902
+ return members;
1903
+ }
1904
+ /**
1905
+ * Build a single Field from a schema property entry. Shared across all
1906
+ * extraction sites. Pass `componentSchemas` where they are at hand so a
1907
+ * field whose schema is a bare `$ref` can take `example`/`examples` from the
1908
+ * component it points at, as a parameter does. `siblingProperties` are the
1909
+ * other properties of the object the field belongs to (`collectProperties`);
1910
+ * array items and map values use them to step aside from a sibling that
1911
+ * would take their singular name (`elementContextName`).
1912
+ */
1913
+ function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet, componentSchemas, siblingProperties) {
1914
+ const refTarget = resolveComponentSchemaRef(fieldSchema, componentSchemas);
1915
+ const { example, examples } = resolveExamples([{
1916
+ example: fieldSchema.example,
1917
+ examples: nonEmptyExamples(fieldSchema.examples)
1918
+ }, {
1919
+ example: refTarget?.example,
1920
+ examples: nonEmptyExamples(refTarget?.examples)
1921
+ }]);
855
1922
  return {
856
1923
  name: fieldName,
857
- type: schemaToTypeRef(fieldSchema, fieldName, contextName),
1924
+ type: schemaToTypeRef(fieldSchema, fieldName, contextName, siblingProperties),
858
1925
  required: requiredSet.has(fieldName),
859
1926
  description: fieldSchema.description,
860
1927
  readOnly: fieldSchema.readOnly || void 0,
861
1928
  writeOnly: fieldSchema.writeOnly || void 0,
862
1929
  deprecated: fieldSchema.deprecated || void 0,
863
1930
  default: fieldSchema.default,
864
- example: fieldSchema.example
1931
+ example,
1932
+ ...examples ? { examples } : {},
1933
+ ...constraintsEntry(fieldSchema)
865
1934
  };
866
1935
  }
867
1936
  /**
@@ -898,39 +1967,27 @@ function buildSyntheticEnumRef(values, contextName, parentModelName) {
898
1967
  };
899
1968
  }
900
1969
  /**
901
- * Detect an implicit discriminator on an oneOf where every variant is an
902
- * object schema that pins the same property to a const value. Returns
903
- * { property, mapping } so the IR can represent the union as discriminated
904
- * without needing an explicit `discriminator:` key in the spec.
1970
+ * Detect an implicit discriminator on a `oneOf` whose every variant pins the
1971
+ * same property to a const value AND is nameable on its own — by a `$ref` to
1972
+ * a component or by a `title` — so the IR can represent the union as
1973
+ * discriminated, with a mapping to concrete names, without an explicit
1974
+ * `discriminator:` keyword. Returns the property, or null when any variant
1975
+ * doesn't fit the pattern or when the variants disagree on which property
1976
+ * carries the discriminator. The mapping itself is built by `schemaToTypeRef`
1977
+ * from the names `nameInlineVariants` gives the inline variants (their titles
1978
+ * unless taken) and the component names of `$ref` variants, so it can only
1979
+ * ever point at a model that exists.
905
1980
  *
906
- * Both shapes are supported:
1981
+ * Both const shapes are supported:
907
1982
  * { properties: { event: { const: "x" } }, required: ["event"] }
908
1983
  * { properties: { event: { type: "string", enum: ["x"] } } }
909
- *
910
- * Returns null when any variant doesn't fit the pattern or when variants
911
- * disagree on which property carries the discriminator.
912
1984
  */
913
- function detectConstPropertyDiscriminator(variants) {
1985
+ function titleMappedDiscriminatorProperty(variants) {
914
1986
  if (variants.length < 2) return null;
915
- let candidateProperty = null;
1987
+ if (!variants.every((v) => resolveVariantModelName(v) !== null)) return null;
916
1988
  const candidates = Object.keys(variants[0]?.properties ?? {});
917
- for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) {
918
- candidateProperty = propName;
919
- break;
920
- }
921
- if (!candidateProperty) return null;
922
- const mapping = {};
923
- for (const v of variants) {
924
- const value = getConstPropertyValue(v, candidateProperty);
925
- if (value === null) return null;
926
- const variantName = resolveVariantModelName(v);
927
- if (!variantName) return null;
928
- mapping[value] = variantName;
929
- }
930
- return {
931
- property: candidateProperty,
932
- mapping
933
- };
1989
+ for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) return propName;
1990
+ return null;
934
1991
  }
935
1992
  function getConstPropertyValue(schema, property) {
936
1993
  const prop = schema.properties?.[property];
@@ -956,42 +2013,96 @@ function mapVariantToDiscriminatorEntry(variant, discriminator) {
956
2013
  }
957
2014
  /**
958
2015
  * Derive a discriminator value → variant model name mapping for an inline-
959
- * variant `oneOf` whose explicit `discriminator:` has no `mapping:` clause.
960
- *
961
- * Each variant model name is reproduced via `nameVariantModel` so it matches
962
- * what `extractNestedSchema` registered when it walked the same `oneOf` to
963
- * pull variants out as named models. Variant 0 keeps the bare parent name
964
- * (e.g. `ApiKeyOwner`); subsequent variants get a const-derived prefix
965
- * (e.g. `UserApiKeyOwner` for the `type: const: user` variant).
966
- *
967
- * The "parent name" used here mirrors the one `collectNestedInlineModels`
968
- * passes to `extractNestedSchema`: it's the qualified inline model name
969
- * built from `contextName` (the field name) plus `parentModelName` (the
970
- * containing model). Returns null when any non-object variant is present
971
- * or when a variant lacks a const value on the discriminator property.
972
- */
973
- function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, parentModelName) {
2016
+ * variant `oneOf` from the names `nameInlineVariants` gave its variants
2017
+ * (`inlineVariantNames`): the explicit `discriminator:` without a `mapping:`
2018
+ * clause, and the implicit const discriminator of anonymous inline variants
2019
+ * (`ApiKeyCreatedData.owner`: `ApiKeyCreatedDataOwner`,
2020
+ * `UserApiKeyCreatedDataOwner`). Reading the names rather than deriving them
2021
+ * again is what keeps the mapping pointing at models the extractors create.
2022
+ * Returns null without a context name, when any variant is not an inline
2023
+ * object, or when a variant lacks a const value on the property.
2024
+ */
2025
+ function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, inlineVariantNames) {
974
2026
  if (variants.length === 0 || !contextName) return null;
975
- const inlineObjectVariants = variants.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
976
- if (inlineObjectVariants.length !== variants.length) return null;
977
- const baseInlineName = qualifyInlineModelName(toPascalCase(contextName), parentModelName);
978
- const namingDiscriminator = { property: discriminatorProperty };
979
2027
  const mapping = {};
980
- const emittedSoFar = [];
981
- for (const variant of inlineObjectVariants) {
2028
+ for (const variant of variants) {
2029
+ const variantName = inlineVariantNames.get(variant);
2030
+ if (variantName === void 0) return null;
982
2031
  const constValue = getConstPropertyValue(variant, discriminatorProperty);
983
2032
  if (constValue === null) return null;
984
- const variantName = nameVariantModel(variant, baseInlineName, emittedSoFar, namingDiscriminator);
985
2033
  mapping[constValue] = variantName;
986
- emittedSoFar.push({
987
- name: variantName,
988
- fields: []
989
- });
990
2034
  }
991
2035
  return mapping;
992
2036
  }
2037
+ /**
2038
+ * The names reserved for a union typed under `contextName`: the set an
2039
+ * enclosing field handed down (`reservedNames`, for a union among a field's
2040
+ * array items or map values), else the inline names the field's own sibling
2041
+ * properties mint (`reservedInlineNames`), else nothing. The extractor
2042
+ * derives the same set at each of those levels, so the reference and the
2043
+ * model step around a sibling's name identically.
2044
+ */
2045
+ function reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames) {
2046
+ if (reservedNames) return reservedNames;
2047
+ if (contextName === void 0) return NO_RESERVED;
2048
+ return reservedInlineNames(contextName, siblingProperties, parentModelName);
2049
+ }
2050
+ /**
2051
+ * Name the inline object variants of a field's `oneOf`/`anyOf` exactly as the
2052
+ * inline-model extractors do (`extractInlineModelsFromProperties` in
2053
+ * inline-models.ts, and `extractNestedSchema` / `extractInlineModelDeep`
2054
+ * here): the union's base name is the field's qualified inline name, the
2055
+ * names the field's sibling properties mint — or the set handed down from an
2056
+ * enclosing field — are reserved (`reservedNamesFor`), and
2057
+ * `nameInlineVariants` hands out the variant names from there. Returned keyed
2058
+ * by variant schema identity.
2059
+ */
2060
+ function nameInlineObjectVariants(union, contextName, parentModelName, siblingProperties, reservedNames) {
2061
+ return nameInlineVariants(union, qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName), reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames));
2062
+ }
2063
+ /**
2064
+ * The TypeRef for an inline object variant referenced by the name its
2065
+ * extracted model carries, keeping every other type the variant allows.
2066
+ * Mirrors what `schemaToTypeRef` builds for the same schema — one member per
2067
+ * non-null entry of a `type` array, `union`-wrapped when there is more than
2068
+ * one, `nullable`-wrapped when `null` or OAS 3.0 `nullable: true` is present
2069
+ * — with the `object` member swapped for the named ref:
2070
+ * `type: ['object', 'string']` → `union[model, string]`,
2071
+ * `['object', 'integer', 'null']` → `nullable<union[model, integer]>`,
2072
+ * `['object', 'null']` / `nullable: true` → `nullable<model>`.
2073
+ * A `oneOf: [X, { type: 'null' }]` variant has no `properties` of its own
2074
+ * and never reaches this path; `schemaToTypeRef` types it directly.
2075
+ *
2076
+ * Anything inline under a non-object member is named from the variant, never
2077
+ * from the union's field: an `array` member types its items under the
2078
+ * variant as parent with the context `variantMemberKeys` picks (`item` →
2079
+ * `<Variant>Item`, or `array_item` → `<Variant>ArrayItem` when a property
2080
+ * claims `item`), and any other member under its `value` key
2081
+ * (`<Variant>Value` for an inline enum, else `<Variant>MemberValue`). Naming
2082
+ * them from the field would hand variant 0's model name to every variant's
2083
+ * items.
2084
+ */
2085
+ function inlineVariantTypeRef(variant, name) {
2086
+ const modelRef = {
2087
+ kind: "model",
2088
+ name
2089
+ };
2090
+ const typeList = Array.isArray(variant.type) ? variant.type : [];
2091
+ const nullable = variant.nullable === true || typeList.includes("null");
2092
+ const keys = variantMemberKeys(variant, name);
2093
+ const members = typeList.filter((t) => t !== "null").map((t) => t === "object" ? modelRef : schemaToTypeRef({
2094
+ ...variant,
2095
+ type: t,
2096
+ nullable: false
2097
+ }, t === "array" ? keys.item : keys.value, name));
2098
+ const ref = members.length > 1 ? {
2099
+ kind: "union",
2100
+ variants: members
2101
+ } : modelRef;
2102
+ return nullable ? asNullable(ref) : ref;
2103
+ }
993
2104
  function extractEnum(name, schema) {
994
- const values = (schema.enum ?? []).map((v) => ({
2105
+ const values = nonNullEnumValues(schema.enum).map((v) => ({
995
2106
  name: toUpperSnakeCase(String(v)),
996
2107
  value: typeof v === "number" ? v : String(v),
997
2108
  description: void 0
@@ -1007,9 +2118,10 @@ function extractModel(name, schema, schemas) {
1007
2118
  if (schema.allOf) return extractAllOfModel(name, schema, schemas);
1008
2119
  const requiredSet = new Set(schema.required ?? []);
1009
2120
  const fields = [];
2121
+ const siblings = collectProperties(schema, schemas);
1010
2122
  for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
1011
2123
  if (!fieldSchema) continue;
1012
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
2124
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, schemas, siblings));
1013
2125
  }
1014
2126
  if (fields.length === 0 && (schema.oneOf || schema.anyOf)) {
1015
2127
  const variants = schema.oneOf ?? schema.anyOf ?? [];
@@ -1020,7 +2132,7 @@ function extractModel(name, schema, schemas) {
1020
2132
  for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
1021
2133
  if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
1022
2134
  seenFieldNames.add(fieldName);
1023
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
2135
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
1024
2136
  }
1025
2137
  }
1026
2138
  }
@@ -1041,12 +2153,14 @@ function extractModel(name, schema, schemas) {
1041
2153
  return {
1042
2154
  name,
1043
2155
  description: schema.description,
1044
- fields
2156
+ fields,
2157
+ ...constraintsEntry(schema)
1045
2158
  };
1046
2159
  }
1047
- function extractAllOfModel(name, schema, schemas) {
2160
+ function extractAllOfModel(name, schema, schemas, siblingProperties) {
1048
2161
  const fields = [];
1049
2162
  const requiredSet = /* @__PURE__ */ new Set();
2163
+ const siblings = siblingProperties ?? collectProperties(schema, schemas);
1050
2164
  let resultDiscriminator;
1051
2165
  for (const subSchema of schema.allOf ?? []) {
1052
2166
  let resolved = subSchema;
@@ -1056,7 +2170,7 @@ function extractAllOfModel(name, schema, schemas) {
1056
2170
  if (refName && schemas[refName]) resolved = schemas[refName];
1057
2171
  }
1058
2172
  if (resolved.allOf) {
1059
- const nested = extractAllOfModel(name, resolved, schemas);
2173
+ const nested = extractAllOfModel(name, resolved, schemas, siblings);
1060
2174
  for (const f of nested.fields) {
1061
2175
  fields.push({
1062
2176
  ...f,
@@ -1076,7 +2190,7 @@ function extractAllOfModel(name, schema, schemas) {
1076
2190
  for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
1077
2191
  if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
1078
2192
  seenFieldNames.add(fieldName);
1079
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
2193
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
1080
2194
  }
1081
2195
  }
1082
2196
  }
@@ -1086,7 +2200,7 @@ function extractAllOfModel(name, schema, schemas) {
1086
2200
  const emptyRequired = /* @__PURE__ */ new Set();
1087
2201
  for (const [fieldName, fieldSchema] of Object.entries(resolved.properties)) {
1088
2202
  if (!fieldSchema) continue;
1089
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
2203
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
1090
2204
  }
1091
2205
  }
1092
2206
  }
@@ -1097,7 +2211,8 @@ function extractAllOfModel(name, schema, schemas) {
1097
2211
  name,
1098
2212
  description: schema.description,
1099
2213
  fields,
1100
- ...resultDiscriminator ? { discriminator: resultDiscriminator } : {}
2214
+ ...resultDiscriminator ? { discriminator: resultDiscriminator } : {},
2215
+ ...constraintsEntry(schema)
1101
2216
  };
1102
2217
  }
1103
2218
  /**
@@ -1168,18 +2283,23 @@ function detectAllOfVariantDiscriminator(variants, fallbackName) {
1168
2283
  mapping
1169
2284
  };
1170
2285
  }
1171
- function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
2286
+ function extractDiscriminatedAllOfVariantModels(schema, fallbackName, schemas) {
1172
2287
  const models = [];
1173
2288
  const seenNames = /* @__PURE__ */ new Set();
1174
2289
  const baseProperties = {};
1175
2290
  const baseRequired = /* @__PURE__ */ new Set();
2291
+ let baseConstraints = ownConstraints(schema);
1176
2292
  for (const subSchema of schema.allOf ?? []) {
1177
- if (subSchema.oneOf) continue;
2293
+ if (subSchema.oneOf) {
2294
+ baseConstraints = combineConstraints(baseConstraints, ownConstraints(subSchema));
2295
+ continue;
2296
+ }
1178
2297
  const flat = subSchema.allOf ? flattenVariantForExtraction(subSchema) : subSchema;
1179
2298
  if (flat.properties) {
1180
2299
  for (const [k, v] of Object.entries(flat.properties)) if (!(k in baseProperties) && v) baseProperties[k] = v;
1181
2300
  }
1182
2301
  if (flat.required) for (const r of flat.required) baseRequired.add(r);
2302
+ baseConstraints = combineConstraints(baseConstraints, extractConstraints(subSchema));
1183
2303
  }
1184
2304
  for (const subSchema of schema.allOf ?? []) for (const variant of subSchema.oneOf ?? []) {
1185
2305
  const flat = flattenVariantForExtraction(variant);
@@ -1189,10 +2309,11 @@ function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
1189
2309
  const merged = {
1190
2310
  type: "object",
1191
2311
  properties: mergedProperties,
1192
- required: [...new Set([...baseRequired, ...flat.required ?? []])]
2312
+ required: [.../* @__PURE__ */ new Set([...baseRequired, ...flat.required ?? []])],
2313
+ ...combineConstraints(baseConstraints, extractConstraints(variant))
1193
2314
  };
1194
2315
  const variantName = deriveDiscriminatedVariantName(variant, fallbackName);
1195
- for (const model of extractInlineModelDeep(variantName, merged)) {
2316
+ for (const model of extractInlineModelDeep(variantName, merged, schemas)) {
1196
2317
  if (seenNames.has(model.name)) continue;
1197
2318
  seenNames.add(model.name);
1198
2319
  models.push(model);
@@ -1298,16 +2419,27 @@ function deriveDiscriminatedVariantName(schema, fallbackName) {
1298
2419
  if (pascal.endsWith(fallbackName)) return pascal;
1299
2420
  return `${fallbackName}${pascal}`;
1300
2421
  }
1301
- function extractInlineModelDeep(name, schema) {
2422
+ function extractInlineModelDeep(name, schema, schemas) {
1302
2423
  const requiredSet = new Set(schema.required ?? []);
1303
2424
  const fields = [];
1304
2425
  const properties = schema.properties ?? {};
1305
2426
  const nestedModels = [];
2427
+ const siblings = collectProperties(schema);
1306
2428
  for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1307
2429
  if (!fieldSchema) continue;
1308
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
1309
- if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema));
1310
- if (fieldSchema.type === "array" && fieldSchema.items?.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema.items));
2430
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, schemas, siblings));
2431
+ if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema, schemas));
2432
+ if (fieldSchema.type === "array" && fieldSchema.items?.properties) {
2433
+ const itemContext = elementContextName(fieldName, "item", siblings, name, fieldSchema.items);
2434
+ nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, itemContext), fieldSchema.items, schemas));
2435
+ }
2436
+ if (fieldSchema.type === "object" && !fieldSchema.properties) {
2437
+ const valueSchema = mapValueSchema(fieldSchema);
2438
+ if (valueSchema && !valueSchema.$ref && valueSchema.properties) {
2439
+ const valueContext = elementContextName(fieldName, "value", siblings, name, valueSchema);
2440
+ nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, valueContext), valueSchema, schemas));
2441
+ }
2442
+ }
1311
2443
  if (fieldSchema.allOf) {
1312
2444
  const mergedProperties = {};
1313
2445
  const mergedRequired = [];
@@ -1318,22 +2450,20 @@ function extractInlineModelDeep(name, schema) {
1318
2450
  if (Object.keys(mergedProperties).length > 0) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), {
1319
2451
  type: "object",
1320
2452
  properties: mergedProperties,
1321
- required: mergedRequired
1322
- }));
2453
+ required: mergedRequired,
2454
+ ...extractConstraints(fieldSchema)
2455
+ }, schemas));
1323
2456
  }
1324
2457
  if (fieldSchema.oneOf) {
1325
- const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
2458
+ const inlineObjectVariants = fieldSchema.oneOf.filter((v) => isInlineObjectSchema(v));
1326
2459
  if (inlineObjectVariants.length > 0) {
1327
- const baseQualified = qualifyNestedInlineName(name, fieldName);
1328
- const namingDisc = deriveConstNamingDiscriminator(inlineObjectVariants);
1329
- const emitted = [];
2460
+ const variantNames = nameInlineVariants(fieldSchema, qualifyNestedInlineName(name, fieldName), reservedInlineNames(fieldName, siblings, name));
1330
2461
  for (const variant of inlineObjectVariants) {
1331
- const variantName = nameVariantModel(variant, baseQualified, emitted, namingDisc);
1332
- emitted.push({
1333
- name: variantName,
1334
- fields: []
1335
- });
1336
- nestedModels.push(...extractInlineModelDeep(variantName, variant));
2462
+ const variantName = variantNames.get(variant);
2463
+ if (variantName === void 0) continue;
2464
+ nestedModels.push(...extractInlineModelDeep(variantName, variant, schemas));
2465
+ const memberSchemas = variantMemberSchemas(variant, variantName);
2466
+ if (Object.keys(memberSchemas).length > 0) nestedModels.push(...extractInlineModelDeep(variantName, { properties: memberSchemas }, schemas).slice(1));
1337
2467
  }
1338
2468
  }
1339
2469
  }
@@ -1341,7 +2471,8 @@ function extractInlineModelDeep(name, schema) {
1341
2471
  return [{
1342
2472
  name,
1343
2473
  description: schema.description,
1344
- fields
2474
+ fields,
2475
+ ...constraintsEntry(schema)
1345
2476
  }, ...nestedModels];
1346
2477
  }
1347
2478
  function qualifyNestedInlineName(parentName, fieldName) {
@@ -1352,15 +2483,28 @@ function qualifyNestedInlineName(parentName, fieldName) {
1352
2483
  if (match && match[2]) return match[1] + singularize$1(match[2]);
1353
2484
  return qualified;
1354
2485
  }
1355
- function schemaToTypeRef(schema, contextName, parentModelName) {
2486
+ /**
2487
+ * `elementContextName` for an optional context: with no field name there is
2488
+ * nothing to decide, and the items / values keep an undefined context.
2489
+ */
2490
+ function elementContext(contextName, kind, siblingProperties, parentModelName, elementSchema) {
2491
+ return contextName === void 0 ? void 0 : elementContextName(contextName, kind, siblingProperties, parentModelName, elementSchema);
2492
+ }
2493
+ function schemaToTypeRef(schema, contextName, parentModelName, siblingProperties, reservedNames) {
1356
2494
  if (schema.$ref) {
1357
2495
  const segments = schema.$ref.split("/");
1358
2496
  const rawName = segments[segments.length - 1];
1359
2497
  const resolved = resolveSchemaName(rawName);
1360
- if (activeEnumSchemaNames.has(resolved)) return {
1361
- kind: "enum",
1362
- name: resolved
1363
- };
2498
+ if (activeEnumSchemaNames.has(resolved)) {
2499
+ const enumRef = {
2500
+ kind: "enum",
2501
+ name: resolved
2502
+ };
2503
+ return activeNullableEnumSchemaNames.has(resolved) ? {
2504
+ kind: "nullable",
2505
+ inner: enumRef
2506
+ } : enumRef;
2507
+ }
1364
2508
  return {
1365
2509
  kind: "model",
1366
2510
  name: resolved
@@ -1375,21 +2519,18 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1375
2519
  type: "unknown"
1376
2520
  }
1377
2521
  };
1378
- if (schema.type.includes("null") && nonNullTypes.length === 1) return {
1379
- kind: "nullable",
1380
- inner: schemaToTypeRef({
1381
- ...schema,
1382
- type: nonNullTypes[0],
1383
- nullable: false
1384
- }, contextName)
1385
- };
2522
+ if (schema.type.includes("null") && nonNullTypes.length === 1) return asNullable(schemaToTypeRef({
2523
+ ...schema,
2524
+ type: nonNullTypes[0],
2525
+ nullable: false
2526
+ }, contextName, parentModelName, siblingProperties, reservedNames));
1386
2527
  if (nonNullTypes.length > 1) {
1387
2528
  const ref = {
1388
2529
  kind: "union",
1389
2530
  variants: nonNullTypes.map((t) => schemaToTypeRef({
1390
2531
  ...schema,
1391
2532
  type: t
1392
- }, contextName))
2533
+ }, contextName, parentModelName, siblingProperties, reservedNames))
1393
2534
  };
1394
2535
  if (schema.type.includes("null")) return {
1395
2536
  kind: "nullable",
@@ -1398,13 +2539,10 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1398
2539
  return ref;
1399
2540
  }
1400
2541
  }
1401
- if (schema.nullable && schema.type) return {
1402
- kind: "nullable",
1403
- inner: schemaToTypeRef({
1404
- ...schema,
1405
- nullable: false
1406
- }, contextName)
1407
- };
2542
+ if (schema.nullable && schema.type) return asNullable(schemaToTypeRef({
2543
+ ...schema,
2544
+ nullable: false
2545
+ }, contextName, parentModelName, siblingProperties, reservedNames));
1408
2546
  if (schema.allOf) {
1409
2547
  const refItem = schema.allOf.find((s) => s.$ref);
1410
2548
  if (refItem) {
@@ -1414,7 +2552,7 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1414
2552
  };
1415
2553
  return schemaToTypeRef(refItem, contextName, parentModelName);
1416
2554
  }
1417
- if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName);
2555
+ if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName, siblingProperties, reservedNames);
1418
2556
  if (schema.allOf.some((s) => s.properties)) return {
1419
2557
  kind: "model",
1420
2558
  name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
@@ -1423,12 +2561,9 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1423
2561
  if (schema.oneOf || schema.anyOf) {
1424
2562
  const compositionKind = schema.oneOf ? "oneOf" : "anyOf";
1425
2563
  const rawVariants = schema.oneOf ?? schema.anyOf ?? [];
1426
- const nullVariant = rawVariants.find((v) => v.type === "null" || Array.isArray(v.type) && v.type.length === 1 && v.type[0] === "null");
2564
+ const nullVariant = rawVariants.find(isNullVariantSchema);
1427
2565
  const nonNullVariants = rawVariants.filter((v) => v !== nullVariant);
1428
- if (nullVariant && nonNullVariants.length === 1) return {
1429
- kind: "nullable",
1430
- inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
1431
- };
2566
+ if (nullVariant && nonNullVariants.length === 1) return asNullable(schemaToTypeRef(nonNullVariants[0], contextName, parentModelName, siblingProperties, reservedNames));
1432
2567
  const literalStrings = collectLiteralStringConsts(nonNullVariants);
1433
2568
  if (literalStrings !== null && literalStrings.length >= 2) {
1434
2569
  const enumRef = buildSyntheticEnumRef(literalStrings, contextName, parentModelName);
@@ -1437,13 +2572,14 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1437
2572
  inner: enumRef
1438
2573
  } : enumRef;
1439
2574
  }
2575
+ const inlineVariantNames = nameInlineObjectVariants(schema, contextName, parentModelName, siblingProperties, reservedNames);
1440
2576
  let resolvedDiscriminator;
1441
2577
  if (schema.discriminator) {
1442
2578
  const property = schema.discriminator.propertyName;
1443
2579
  const explicitMapping = schema.discriminator.mapping ?? {};
1444
2580
  let mapping = Object.fromEntries(Object.entries(explicitMapping).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]));
1445
2581
  if (Object.keys(mapping).length === 0) {
1446
- const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName, parentModelName);
2582
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName, inlineVariantNames);
1447
2583
  if (inlineMapping) mapping = inlineMapping;
1448
2584
  }
1449
2585
  resolvedDiscriminator = {
@@ -1451,12 +2587,22 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1451
2587
  mapping
1452
2588
  };
1453
2589
  } else if (compositionKind === "oneOf") {
1454
- const synthetic = detectConstPropertyDiscriminator(nonNullVariants);
1455
- if (synthetic) resolvedDiscriminator = synthetic;
1456
- else {
2590
+ const titleProperty = titleMappedDiscriminatorProperty(nonNullVariants);
2591
+ if (titleProperty) {
2592
+ const mapping = {};
2593
+ for (const v of nonNullVariants) {
2594
+ const value = getConstPropertyValue(v, titleProperty);
2595
+ const mappedName = inlineVariantNames.get(v) ?? resolveVariantModelName(v);
2596
+ if (value !== null && mappedName !== null) mapping[value] = mappedName;
2597
+ }
2598
+ resolvedDiscriminator = {
2599
+ property: titleProperty,
2600
+ mapping
2601
+ };
2602
+ } else {
1457
2603
  const namingDisc = deriveConstNamingDiscriminator(nonNullVariants);
1458
2604
  if (namingDisc) {
1459
- const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName, parentModelName);
2605
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName, inlineVariantNames);
1460
2606
  if (inlineMapping) resolvedDiscriminator = {
1461
2607
  property: namingDisc.property,
1462
2608
  mapping: inlineMapping
@@ -1467,12 +2613,11 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1467
2613
  const variants = [];
1468
2614
  for (const v of rawVariants) {
1469
2615
  if (v === nullVariant) continue;
1470
- const inlineMappingName = resolvedDiscriminator && !v.$ref && v.properties && (v.type === "object" || !v.type) ? mapVariantToDiscriminatorEntry(v, resolvedDiscriminator) : null;
1471
- if (inlineMappingName) variants.push({
1472
- kind: "model",
1473
- name: inlineMappingName
1474
- });
1475
- else variants.push(schemaToTypeRef(v, contextName, parentModelName));
2616
+ const inlineName = inlineVariantNames.get(v);
2617
+ if (inlineName) {
2618
+ const mappedName = resolvedDiscriminator ? mapVariantToDiscriminatorEntry(v, resolvedDiscriminator) : null;
2619
+ variants.push(inlineVariantTypeRef(v, mappedName ?? inlineName));
2620
+ } else variants.push(schemaToTypeRef(v, contextName, parentModelName));
1476
2621
  }
1477
2622
  const hasNull = !!nullVariant;
1478
2623
  const union = {
@@ -1515,30 +2660,30 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1515
2660
  };
1516
2661
  }
1517
2662
  }
1518
- if (schema.enum && schema.enum.length === 1) {
1519
- const v = schema.enum[0];
1520
- return {
2663
+ const enumValues = nonNullEnumValues(schema.enum);
2664
+ const enumAdmitsNull = schema.enum !== void 0 && enumValues.length !== schema.enum.length;
2665
+ if (schema.enum && enumValues.length === 1) {
2666
+ const v = enumValues[0];
2667
+ const literal = {
1521
2668
  kind: "literal",
1522
2669
  value: typeof v === "number" ? v : String(v)
1523
2670
  };
2671
+ return enumAdmitsNull ? asNullable(literal) : literal;
1524
2672
  }
1525
2673
  if (schema.enum) {
1526
2674
  const baseName = toPascalCase(contextName ?? "UnknownEnum");
1527
2675
  const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
1528
2676
  const qualifiedName = cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName;
1529
- return {
2677
+ const enumRef = {
1530
2678
  kind: "enum",
1531
2679
  name: activeSchemaNameTransform ? activeSchemaNameTransform(qualifiedName) : qualifiedName,
1532
- values: schema.enum.map((v) => typeof v === "number" ? v : String(v))
2680
+ values: enumValues.map((v) => typeof v === "number" ? v : String(v))
1533
2681
  };
2682
+ return enumAdmitsNull ? asNullable(enumRef) : enumRef;
1534
2683
  }
1535
- if (schema.type === "array" && schema.items) return {
1536
- kind: "array",
1537
- items: schemaToTypeRef(schema.items, contextName, parentModelName)
1538
- };
1539
- if (!schema.type && schema.items) return {
2684
+ if ((schema.type === "array" || !schema.type) && schema.items) return {
1540
2685
  kind: "array",
1541
- items: schemaToTypeRef(schema.items, contextName, parentModelName)
2686
+ items: schemaToTypeRef(schema.items, elementContext(contextName, "item", siblingProperties, parentModelName, schema.items), parentModelName, void 0, reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames))
1542
2687
  };
1543
2688
  if (schema.type === "object" && schema.properties) return {
1544
2689
  kind: "model",
@@ -1549,12 +2694,8 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1549
2694
  kind: "primitive",
1550
2695
  type: "unknown"
1551
2696
  };
1552
- if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
1553
- if (Object.keys(schema.additionalProperties).length > 0) valueType = schemaToTypeRef(schema.additionalProperties, contextName);
1554
- } else if (schema.patternProperties) {
1555
- const patterns = Object.values(schema.patternProperties);
1556
- if (patterns.length > 0) valueType = schemaToTypeRef(patterns[0], contextName);
1557
- }
2697
+ const valueSchema = mapValueSchema(schema);
2698
+ if (valueSchema) valueType = schemaToTypeRef(valueSchema, elementContext(contextName, "value", siblingProperties, parentModelName, valueSchema), parentModelName, void 0, reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames));
1558
2699
  return {
1559
2700
  kind: "map",
1560
2701
  valueType
@@ -1654,11 +2795,11 @@ function classifyAndExtractResponse(schema, contextName, componentSchemas) {
1654
2795
  isPaginated: false
1655
2796
  };
1656
2797
  const envelope = detectListEnvelope(schema);
1657
- if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath);
1658
- if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName);
2798
+ if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath, componentSchemas);
2799
+ if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName, componentSchemas);
1659
2800
  return extractDirectResource(schema, contextName, componentSchemas);
1660
2801
  }
1661
- const KNOWN_DATA_PATHS = new Set([
2802
+ const KNOWN_DATA_PATHS = /* @__PURE__ */ new Set([
1662
2803
  "data",
1663
2804
  "items",
1664
2805
  "results",
@@ -1716,33 +2857,27 @@ function detectListEnvelope(schema) {
1716
2857
  dataPath: null
1717
2858
  };
1718
2859
  }
1719
- function extractListResponse(schema, contextName, dataPath) {
2860
+ function extractListResponse(schema, contextName, dataPath, componentSchemas) {
1720
2861
  let itemTypeRef = {
1721
2862
  kind: "primitive",
1722
2863
  type: "string"
1723
2864
  };
1724
2865
  const inlineModels = [];
1725
- const propSources = [];
1726
- if (schema.allOf) {
1727
- for (const sub of schema.allOf) if (sub.properties) propSources.push(sub.properties);
1728
- }
1729
- if (schema.properties) propSources.push(schema.properties);
1730
- for (const props of propSources) {
1731
- const dataProp = props[dataPath];
1732
- if (!dataProp) continue;
1733
- if (dataProp.type === "array" && dataProp.items) {
1734
- const items = dataProp.items;
1735
- if (items.$ref) itemTypeRef = schemaToTypeRef(items, contextName);
1736
- else if (items.type === "object" && items.properties) {
1737
- const itemName = contextName.replace(/Response$/, "") + "Item";
1738
- itemTypeRef = {
1739
- kind: "model",
1740
- name: itemName
1741
- };
1742
- inlineModels.push(...extractInlineModel(itemName, items));
1743
- }
2866
+ const { properties, required } = collectEnvelopeProperties(schema, componentSchemas);
2867
+ const dataProp = properties[dataPath];
2868
+ if (dataProp?.type === "array" && dataProp.items) {
2869
+ const items = dataProp.items;
2870
+ if (items.$ref) itemTypeRef = schemaToTypeRef(items, contextName);
2871
+ else if (items.type === "object" && items.properties) {
2872
+ const itemName = contextName.replace(/Response$/, "") + "Item";
2873
+ itemTypeRef = {
2874
+ kind: "model",
2875
+ name: itemName
2876
+ };
2877
+ inlineModels.push(...extractInlineModel(itemName, items, componentSchemas));
1744
2878
  }
1745
2879
  }
2880
+ const { envelopeFields, envelopeCompanions } = extractEnvelopeCompanions(properties, required, dataPath, contextName.replace(/Response$/, ""), componentSchemas);
1746
2881
  return {
1747
2882
  response: {
1748
2883
  kind: "array",
@@ -1751,7 +2886,117 @@ function extractListResponse(schema, contextName, dataPath) {
1751
2886
  inlineModels,
1752
2887
  isPaginated: true,
1753
2888
  dataPath,
1754
- itemType: itemTypeRef
2889
+ itemType: itemTypeRef,
2890
+ envelopeFields,
2891
+ envelopeCompanions: envelopeCompanions.length > 0 ? envelopeCompanions : void 0
2892
+ };
2893
+ }
2894
+ /**
2895
+ * Every property the envelope declares, merged across its `allOf` branches,
2896
+ * with the `required` lists that go with them.
2897
+ *
2898
+ * A `$ref` branch is resolved against the component schemas so fields an
2899
+ * envelope inherits from a shared base (`trace_id` on every response, say)
2900
+ * are not silently dropped. A property declared by more than one branch keeps
2901
+ * both declarations' shape when both are inline objects — `pagination.next`
2902
+ * from one branch and `pagination.total` from another both belong to the
2903
+ * response — and otherwise takes the later declaration, as `allOf` merging
2904
+ * does elsewhere in the parser.
2905
+ */
2906
+ function collectEnvelopeProperties(schema, componentSchemas) {
2907
+ const properties = {};
2908
+ const required = [];
2909
+ const seenRefs = /* @__PURE__ */ new Set();
2910
+ const visit = (s) => {
2911
+ if (s.$ref) {
2912
+ if (seenRefs.has(s.$ref)) return;
2913
+ seenRefs.add(s.$ref);
2914
+ const refName = s.$ref.split("/").pop();
2915
+ const resolved = refName ? componentSchemas?.[refName] : void 0;
2916
+ if (resolved) visit(resolved);
2917
+ return;
2918
+ }
2919
+ for (const sub of s.allOf ?? []) visit(sub);
2920
+ for (const [key, prop] of Object.entries(s.properties ?? {})) {
2921
+ if (!prop) continue;
2922
+ const existing = properties[key];
2923
+ properties[key] = existing ? mergePropertyDeclarations(existing, prop) : prop;
2924
+ }
2925
+ if (s.required) required.push(...s.required);
2926
+ };
2927
+ visit(schema);
2928
+ return {
2929
+ properties,
2930
+ required
2931
+ };
2932
+ }
2933
+ /**
2934
+ * Combine two `allOf` declarations of one property. Inline objects merge their
2935
+ * fields; an array keeps the earlier `items` when the later declaration only
2936
+ * constrains it (`minItems: 1`), so `data: User[]` is not downgraded to
2937
+ * `string[]`; anything else takes the later declaration.
2938
+ */
2939
+ function mergePropertyDeclarations(earlier, later) {
2940
+ if (isInlineObjectSchema(earlier) && isInlineObjectSchema(later)) return {
2941
+ ...earlier,
2942
+ ...later,
2943
+ properties: {
2944
+ ...earlier.properties,
2945
+ ...later.properties
2946
+ },
2947
+ required: [...earlier.required ?? [], ...later.required ?? []]
2948
+ };
2949
+ if (earlier.type === "array" && later.type === "array") return {
2950
+ ...earlier,
2951
+ ...later,
2952
+ items: later.items ?? earlier.items
2953
+ };
2954
+ return later;
2955
+ }
2956
+ /**
2957
+ * Type a list envelope's companion properties as fields, lifting any inline
2958
+ * object among them (and anything nested inside it) to a model.
2959
+ *
2960
+ * Models come from `extractInlineModelsFromProperties`, the extractor that
2961
+ * mirrors `schemaToTypeRef` form for form — plain, nullable and `allOf`
2962
+ * objects, arrays of objects, and every `oneOf`/`anyOf` object variant — so a
2963
+ * field's reference always has a model behind it.
2964
+ *
2965
+ * Each companion is extracted twice: once under the operation's name, which
2966
+ * is how its models enter the IR (`ListUsersListMetadata`), and once under no
2967
+ * parent, which yields the name each model would carry if named from its
2968
+ * property alone (`ListMetadata`). Both runs walk the same code path, so the
2969
+ * two model lists line up index for index. `resolveEnvelopeCompanionNames`
2970
+ * later picks, per companion, which of the two names survives — the bare one
2971
+ * only where every copy of that companion across the API agrees structurally.
2972
+ */
2973
+ function extractEnvelopeCompanions(properties, required, dataPath, operationName, componentSchemas) {
2974
+ const envelopeFields = [];
2975
+ const envelopeCompanions = [];
2976
+ const requiredSet = new Set(required);
2977
+ const siblings = collectProperties({ properties });
2978
+ for (const [key, propSchema] of Object.entries(properties)) {
2979
+ if (key === dataPath) continue;
2980
+ const field = buildFieldFromSchema(key, propSchema, operationName, requiredSet, componentSchemas, siblings);
2981
+ envelopeFields.push(field);
2982
+ const single = {
2983
+ type: "object",
2984
+ properties: { [key]: propSchema }
2985
+ };
2986
+ const models = [];
2987
+ extractInlineModelsFromProperties(single, models, operationName, componentSchemas, siblings);
2988
+ if (models.length === 0) continue;
2989
+ const bare = [];
2990
+ extractInlineModelsFromProperties(single, bare, void 0, componentSchemas, siblings);
2991
+ envelopeCompanions.push({
2992
+ field,
2993
+ models,
2994
+ bareNames: bare.map((m) => m.name)
2995
+ });
2996
+ }
2997
+ return {
2998
+ envelopeFields,
2999
+ envelopeCompanions
1755
3000
  };
1756
3001
  }
1757
3002
  function hasDiscriminantConstField(schema) {
@@ -1797,7 +3042,7 @@ function isSingleResourceWrapper(schema) {
1797
3042
  if (propSchema.oneOf) return propSchema.oneOf.some((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
1798
3043
  return false;
1799
3044
  }
1800
- function extractWrappedResource(schema, contextName) {
3045
+ function extractWrappedResource(schema, contextName, componentSchemas) {
1801
3046
  const props = schema.properties;
1802
3047
  const propSchema = props[schema.required && schema.required.length === 1 ? schema.required[0] : Object.keys(props)[0]];
1803
3048
  const inlineModels = [];
@@ -1806,7 +3051,7 @@ function extractWrappedResource(schema, contextName) {
1806
3051
  const objectVariant = propSchema.oneOf.find((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
1807
3052
  if (objectVariant) {
1808
3053
  const modelName = deriveModelName(objectVariant, resourceName);
1809
- inlineModels.push(...extractInlineModel(modelName, objectVariant));
3054
+ inlineModels.push(...extractInlineModel(modelName, objectVariant, componentSchemas));
1810
3055
  const hasNullVariant = propSchema.oneOf.some((v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null"));
1811
3056
  const modelRef = {
1812
3057
  kind: "model",
@@ -1823,7 +3068,7 @@ function extractWrappedResource(schema, contextName) {
1823
3068
  }
1824
3069
  }
1825
3070
  const modelName = deriveModelName(propSchema, resourceName);
1826
- inlineModels.push(...extractInlineModel(modelName, propSchema));
3071
+ inlineModels.push(...extractInlineModel(modelName, propSchema, componentSchemas));
1827
3072
  return {
1828
3073
  response: {
1829
3074
  kind: "model",
@@ -1837,7 +3082,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1837
3082
  const inlineModels = [];
1838
3083
  if (schema.properties && (schema.type === "object" || !schema.type)) {
1839
3084
  const modelName = deriveModelName(schema, contextName);
1840
- inlineModels.push(...extractInlineModel(modelName, schema));
3085
+ inlineModels.push(...extractInlineModel(modelName, schema, componentSchemas));
1841
3086
  return {
1842
3087
  response: {
1843
3088
  kind: "model",
@@ -1884,10 +3129,11 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1884
3129
  const merged = {
1885
3130
  type: "object",
1886
3131
  properties: mergedProperties,
1887
- required: mergedRequired
3132
+ required: mergedRequired,
3133
+ ...extractConstraints(schema)
1888
3134
  };
1889
3135
  const modelName = mergedRefFields ? toPascalCase(contextName) : deriveModelName(merged, contextName);
1890
- inlineModels.push(...extractInlineModel(modelName, merged));
3136
+ inlineModels.push(...extractInlineModel(modelName, merged, componentSchemas));
1891
3137
  return {
1892
3138
  response: {
1893
3139
  kind: "model",
@@ -1898,12 +3144,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1898
3144
  };
1899
3145
  }
1900
3146
  }
1901
- if (schema.oneOf) {
1902
- for (const variant of schema.oneOf) if (variant.properties && (variant.type === "object" || !variant.type)) {
1903
- const modelName = deriveModelName(variant, contextName);
1904
- if (!new Set(inlineModels.map((m) => m.name)).has(modelName)) inlineModels.push(...extractInlineModel(modelName, variant));
1905
- }
1906
- }
3147
+ if (schema.oneOf) extractInlineModelsFromProperties({ properties: { [contextName]: schema } }, inlineModels, void 0, componentSchemas);
1907
3148
  if (schema.type === "array" && schema.items) {
1908
3149
  const items = schema.items;
1909
3150
  if (items.$ref) return {
@@ -1916,7 +3157,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1916
3157
  };
1917
3158
  if (items.properties && (items.type === "object" || !items.type)) {
1918
3159
  const itemName = contextName.replace(/Response$/, "") + "Item";
1919
- inlineModels.push(...extractInlineModel(itemName, items));
3160
+ inlineModels.push(...extractInlineModel(itemName, items, componentSchemas));
1920
3161
  return {
1921
3162
  response: {
1922
3163
  kind: "array",
@@ -1946,70 +3187,288 @@ function deriveModelName(schema, fallback) {
1946
3187
  return toPascalCase(fallback);
1947
3188
  }
1948
3189
  /**
1949
- * Qualify a nested inline model name with its parent to avoid generic names.
1950
- * e.g., parent="Connection" + field="domains" → "ConnectionDomain" (singularized)
1951
- * Only qualifies when the field name alone would be too generic.
3190
+ * Materialise an inline response object (and every nested inline object
3191
+ * under it) as models. `componentSchemas` only feeds `buildFieldFromSchema`,
3192
+ * so a response field that `$ref`s a component takes the component's
3193
+ * `example`/`examples` exactly as a request-body or component-model field
3194
+ * does; nested models get the same map so the fallback reaches every level.
1952
3195
  */
1953
- function qualifyNestedName(parentName, fieldName) {
1954
- const pascalField = toPascalCase(fieldName);
1955
- if (pascalField.startsWith(parentName)) return pascalField;
1956
- const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
1957
- const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
1958
- if (match && match[2]) {
1959
- const trailing = singularize$1(match[2]);
1960
- return match[1] + trailing;
1961
- }
1962
- return qualified;
1963
- }
1964
- function extractInlineModel(name, schema) {
3196
+ function extractInlineModel(name, schema, componentSchemas) {
1965
3197
  const requiredSet = new Set(schema.required ?? []);
1966
3198
  const fields = [];
1967
3199
  const properties = schema.properties ?? {};
1968
3200
  const nestedModels = [];
3201
+ const siblings = collectProperties(schema);
1969
3202
  for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1970
3203
  if (!fieldSchema) continue;
1971
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
1972
- if (fieldSchema.type === "object" && fieldSchema.properties) {
1973
- const nestedName = qualifyNestedName(name, fieldName);
1974
- nestedModels.push(...extractInlineModel(nestedName, fieldSchema));
1975
- }
1976
- if (fieldSchema.type === "array" && fieldSchema.items) {
1977
- const items = fieldSchema.items;
1978
- if (items.type === "object" && items.properties) {
1979
- const nestedName = qualifyNestedName(name, fieldName);
1980
- nestedModels.push(...extractInlineModel(nestedName, items));
1981
- }
3204
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, componentSchemas, siblings));
3205
+ }
3206
+ extractInlineModelsFromProperties(schema, nestedModels, name, componentSchemas);
3207
+ return [{
3208
+ name,
3209
+ description: schema.description,
3210
+ fields,
3211
+ ...constraintsEntry(schema)
3212
+ }, ...nestedModels];
3213
+ }
3214
+ //#endregion
3215
+ //#region src/parser/security.ts
3216
+ /**
3217
+ * Convert a document- or operation-level `security` list into the IR shape,
3218
+ * preserving what the list says rather than flattening it:
3219
+ *
3220
+ * - absent (or not a list) → `undefined`: nothing declared here.
3221
+ * - `[]` → `[]`: no authentication required.
3222
+ * - `[{ a: [] }, { b: [] }]` → `[[a], [b]]`: either scheme suffices.
3223
+ * - `[{ a: [], b: [] }]` → `[[a, b]]`: both schemes are required together.
3224
+ * - `[{ a: [] }, {}]` → `[[a], []]`: the empty set is an anonymous alternative,
3225
+ * which is how a document marks authentication optional.
3226
+ *
3227
+ * An entry that is not an object (`null`, say) is not a requirement object.
3228
+ * Reading it as `{}` would declare an anonymous alternative and make the
3229
+ * operation look open, so it is dropped with an `[oagen] Warning:` naming
3230
+ * `context` (the operation, or the document). A non-empty list left with no
3231
+ * valid entry is treated as undeclared (`undefined`) rather than as `[]`:
3232
+ * the document never said "no authentication", so the operation keeps
3233
+ * inheriting the default.
3234
+ *
3235
+ * The previous flattening collapsed the alternatives onto one list and turned
3236
+ * `[]` into `undefined`, so "no auth" read the same as "use the default" and
3237
+ * "either of" the same as "all of" (#182).
3238
+ */
3239
+ function extractSecurityRequirements(security, context) {
3240
+ if (!Array.isArray(security)) return void 0;
3241
+ const sets = [];
3242
+ for (const entry of security) {
3243
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
3244
+ console.warn(`[oagen] Warning: Ignoring security requirement ${JSON.stringify(entry)} on ${context} — not a requirement object`);
3245
+ continue;
1982
3246
  }
1983
- if (fieldSchema.allOf) {
1984
- const mergedProperties = {};
1985
- const mergedRequired = [];
1986
- for (const sub of fieldSchema.allOf) {
1987
- if (sub.properties) Object.assign(mergedProperties, sub.properties);
1988
- if (sub.required) mergedRequired.push(...sub.required);
1989
- }
1990
- if (Object.keys(mergedProperties).length > 0) {
1991
- const nestedName = qualifyNestedName(name, fieldName);
1992
- const merged = {
1993
- type: "object",
1994
- properties: mergedProperties,
1995
- required: mergedRequired
1996
- };
1997
- if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, merged));
3247
+ sets.push(Object.entries(entry).map(([schemeName, scopes]) => ({
3248
+ schemeName,
3249
+ scopes: Array.isArray(scopes) ? scopes : []
3250
+ })));
3251
+ }
3252
+ if (security.length > 0 && sets.length === 0) {
3253
+ console.warn(`[oagen] Warning: No valid security requirement on ${context}; treating security as undeclared`);
3254
+ return;
3255
+ }
3256
+ return sets;
3257
+ }
3258
+ //#endregion
3259
+ //#region src/parser/envelope-companion-names.ts
3260
+ /**
3261
+ * Decide the final name of every lifted companion model, across all list
3262
+ * operations at once, and return the models to merge into the IR.
3263
+ *
3264
+ * The same envelope recurs on every list endpoint of an API, so naming each
3265
+ * copy after its operation would mint one identical `ListMetadata` per
3266
+ * endpoint. But naming by property alone is only safe while the copies agree:
3267
+ * `mergeInlineResponseModels` keeps the largest model of a name, so a
3268
+ * `pagination` with three fields would silently replace an unrelated
3269
+ * `Pagination` component, or absorb another endpoint's two-field `pagination`.
3270
+ *
3271
+ * So a companion takes its bare name only when that cannot change any other
3272
+ * type's shape:
3273
+ *
3274
+ * Copies are compared structurally — field names, requiredness and each
3275
+ * field's shape, with nested models resolved and inline enums compared by
3276
+ * value — never by name alone. A copy collapses onto a target shape only when
3277
+ * the target *admits* every value the copy can hold: the same fields with the
3278
+ * same types, where the target may be looser (a field the copy requires may be
3279
+ * optional on the target, a non-null copy field may be nullable there) but
3280
+ * never stricter. Widening a response type loses no data a reader can see;
3281
+ * narrowing it would promise fields the wire may omit.
3282
+ *
3283
+ * - When a component model of that name exists — compared after the parser's
3284
+ * own renaming, so `PaginationDto` → `Pagination` is caught — the copies it
3285
+ * admits collapse onto it (the component stays as written); the others keep
3286
+ * their operation-qualified name.
3287
+ * - Otherwise the most common exact shape (first declared, on a tie) becomes
3288
+ * the target: it and every copy it admits take the bare name; any other
3289
+ * copy keeps its operation-qualified name.
3290
+ *
3291
+ * The comparison covers the companion's whole field type, so a `oneOf`
3292
+ * companion is judged on every variant, not just the first, and a copy folds
3293
+ * among copies only when its lifted models line up name for name with the
3294
+ * target's (same variants, same order); onto a component, only a companion
3295
+ * whose field references exactly its own lifted model can fold.
3296
+ *
3297
+ * Only the target's definition survives a collapse. The admitted copies'
3298
+ * models are dropped rather than handed to the merge pass, which keeps the
3299
+ * first model of a name and would let a stricter copy declared earlier
3300
+ * narrow the shared type for everyone; their field references are repointed
3301
+ * at the target's models instead.
3302
+ *
3303
+ * Enums declared inline in a companion stay operation-qualified, exactly like
3304
+ * enums declared inline in a parameter.
3305
+ */
3306
+ function resolveEnvelopeCompanionNames(companions, componentModels) {
3307
+ const byBareName = /* @__PURE__ */ new Map();
3308
+ /** Copies folded onto another definition; their own models are not emitted. */
3309
+ const folded = /* @__PURE__ */ new Set();
3310
+ for (const companion of companions) {
3311
+ if (companion.models.length === 0) continue;
3312
+ const bare = companion.bareNames[0];
3313
+ const group = byBareName.get(bare) ?? [];
3314
+ group.push(companion);
3315
+ byBareName.set(bare, group);
3316
+ }
3317
+ const componentMap = new Map((componentModels ?? []).map((m) => [m.name, m]));
3318
+ const localMap = (c) => new Map(c.models.map((m) => [m.name, m]));
3319
+ const unwrap = (t) => t.kind === "nullable" ? t.inner : t;
3320
+ for (const [bare, group] of byBareName) {
3321
+ let folds;
3322
+ let finalNames;
3323
+ let owner;
3324
+ const component = componentMap.get(bare);
3325
+ if (component) {
3326
+ const targetType = {
3327
+ kind: "model",
3328
+ name: component.name
3329
+ };
3330
+ finalNames = [component.name];
3331
+ folds = (c) => {
3332
+ const refs = collectModelRefs(c.field.type);
3333
+ if (refs.length !== 1 || refs[0] !== c.models[0].name) return false;
3334
+ return typeAdmits(targetType, componentMap, unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set());
3335
+ };
3336
+ } else {
3337
+ const counts = /* @__PURE__ */ new Map();
3338
+ for (const c of group) {
3339
+ const key = `${fingerprintType$1(unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set())}|${c.bareNames.join(",")}`;
3340
+ const seen = counts.get(key);
3341
+ if (seen) seen.n += 1;
3342
+ else counts.set(key, {
3343
+ n: 1,
3344
+ first: c
3345
+ });
1998
3346
  }
3347
+ let best = {
3348
+ n: 0,
3349
+ first: group[0]
3350
+ };
3351
+ for (const entry of counts.values()) if (entry.n > best.n) best = entry;
3352
+ owner = best.first;
3353
+ finalNames = owner.bareNames;
3354
+ const ownerType = unwrap(owner.field.type);
3355
+ const ownerMap = localMap(owner);
3356
+ const ownerNames = owner.bareNames;
3357
+ folds = (c) => c.bareNames.length === ownerNames.length && c.bareNames.every((name, i) => name === ownerNames[i]) && typeAdmits(ownerType, ownerMap, unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set());
1999
3358
  }
2000
- if (fieldSchema.oneOf) {
2001
- const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
2002
- if (objectVariant) {
2003
- const nestedName = qualifyNestedName(name, fieldName);
2004
- if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, objectVariant));
3359
+ const collapsing = group.filter(folds);
3360
+ for (const companion of collapsing) {
3361
+ renameCompanion(companion, finalNames);
3362
+ if (companion !== owner) folded.add(companion);
3363
+ }
3364
+ }
3365
+ return companions.filter((c) => !folded.has(c)).flatMap((c) => c.models);
3366
+ }
3367
+ /**
3368
+ * Rename a companion's models to `names` (index-aligned with `models`) and
3369
+ * repoint every reference, in its field and inside its models.
3370
+ */
3371
+ function renameCompanion(companion, names) {
3372
+ const renames = /* @__PURE__ */ new Map();
3373
+ companion.models.forEach((model, i) => {
3374
+ const name = names[i];
3375
+ if (name !== void 0 && model.name !== name) renames.set(model.name, name);
3376
+ });
3377
+ if (renames.size === 0) return;
3378
+ const repoint = (ref) => {
3379
+ walkTypeRef(ref, { model: (r) => {
3380
+ const renamed = renames.get(r.name);
3381
+ if (renamed !== void 0) r.name = renamed;
3382
+ } });
3383
+ };
3384
+ repoint(companion.field.type);
3385
+ for (const model of companion.models) {
3386
+ model.name = renames.get(model.name) ?? model.name;
3387
+ for (const field of model.fields) repoint(field.type);
3388
+ }
3389
+ }
3390
+ /**
3391
+ * Whether `target` admits every value `copy` can hold: the same field names,
3392
+ * each field's type admitted (see `typeAdmits`), and no field the target
3393
+ * requires that the copy leaves optional. Nested models resolve through their
3394
+ * respective maps and are compared the same way; a model neither map resolves
3395
+ * is compared by name.
3396
+ */
3397
+ function admits(target, targetMap, copy, copyMap, visiting) {
3398
+ const key = `${target.name}|${copy.name}`;
3399
+ if (visiting.has(key)) return true;
3400
+ visiting.add(key);
3401
+ try {
3402
+ if (target.fields.length !== copy.fields.length) return false;
3403
+ const targetFields = new Map(target.fields.map((f) => [f.name, f]));
3404
+ for (const field of copy.fields) {
3405
+ const expected = targetFields.get(field.name);
3406
+ if (!expected) return false;
3407
+ if (expected.required && !field.required) return false;
3408
+ if (!typeAdmits(expected.type, targetMap, field.type, copyMap, visiting)) return false;
3409
+ }
3410
+ return true;
3411
+ } finally {
3412
+ visiting.delete(key);
3413
+ }
3414
+ }
3415
+ /**
3416
+ * Whether `target` admits every value of `copy`'s type. A nullable target
3417
+ * admits a nullable or a non-null copy; a non-null target admits only a
3418
+ * non-null copy. An enum with known values admits a copy whose values are a
3419
+ * subset of its own. Everything else must match exactly, by fingerprint.
3420
+ */
3421
+ function typeAdmits(target, targetMap, copy, copyMap, visiting) {
3422
+ if (target.kind === "nullable") return typeAdmits(target.inner, targetMap, copy.kind === "nullable" ? copy.inner : copy, copyMap, visiting);
3423
+ if (copy.kind === "nullable") return false;
3424
+ if (target.kind !== copy.kind) return false;
3425
+ switch (target.kind) {
3426
+ case "model": {
3427
+ const t = targetMap.get(target.name);
3428
+ const c = copyMap.get(copy.name);
3429
+ if (t && c) return admits(t, targetMap, c, copyMap, visiting);
3430
+ return target.name === copy.name;
3431
+ }
3432
+ case "array": return typeAdmits(target.items, targetMap, copy.items, copyMap, visiting);
3433
+ case "enum": {
3434
+ const c = copy;
3435
+ if (target.values && c.values) {
3436
+ const allowed = new Set(target.values.map((v) => `${typeof v}:${String(v)}`));
3437
+ return c.values.every((v) => allowed.has(`${typeof v}:${String(v)}`));
2005
3438
  }
3439
+ return target.name === c.name;
2006
3440
  }
3441
+ default: return fingerprintType$1(target, targetMap, /* @__PURE__ */ new Set()) === fingerprintType$1(copy, copyMap, /* @__PURE__ */ new Set());
3442
+ }
3443
+ }
3444
+ /**
3445
+ * Structural fingerprint of a companion model: field names, requiredness, and
3446
+ * each field's shape, with models nested inside the companion compared by
3447
+ * shape rather than by (operation-qualified) name, and inline enums compared
3448
+ * by their values. Models outside the companion are compared by name.
3449
+ */
3450
+ function fingerprintModel(model, local, visiting) {
3451
+ if (visiting.has(model.name)) return `rec:${model.name}`;
3452
+ visiting.add(model.name);
3453
+ const fields = [...model.fields].sort((a, b) => a.name.localeCompare(b.name)).map((f) => `${f.name}${f.required ? "!" : "?"}:${fingerprintType$1(f.type, local, visiting)}`);
3454
+ visiting.delete(model.name);
3455
+ return `{${fields.join(",")}}`;
3456
+ }
3457
+ function fingerprintType$1(ref, local, visiting) {
3458
+ switch (ref.kind) {
3459
+ case "primitive": return `p:${ref.type}${ref.format ? `:${ref.format}` : ""}`;
3460
+ case "literal": return `l:${typeof ref.value}:${String(ref.value)}`;
3461
+ case "nullable": return `n<${fingerprintType$1(ref.inner, local, visiting)}>`;
3462
+ case "array": return `a<${fingerprintType$1(ref.items, local, visiting)}>`;
3463
+ case "map": return `m<${ref.keyType ? `${fingerprintType$1(ref.keyType, local, visiting)},` : ""}${fingerprintType$1(ref.valueType, local, visiting)}>`;
3464
+ case "union": return `u:${ref.compositionKind ?? ""}<${ref.variants.map((v) => fingerprintType$1(v, local, visiting)).sort().join(",")}>`;
3465
+ case "enum": return ref.values ? `e<${[...ref.values].map((v) => `${typeof v}:${String(v)}`).sort().join("|")}>` : `e:${ref.name}`;
3466
+ case "model": {
3467
+ const nested = local.get(ref.name);
3468
+ return nested ? fingerprintModel(nested, local, visiting) : `ref:${ref.name}`;
3469
+ }
3470
+ default: return assertNever(ref);
2007
3471
  }
2008
- return [{
2009
- name,
2010
- description: schema.description,
2011
- fields
2012
- }, ...nestedModels];
2013
3472
  }
2014
3473
  //#endregion
2015
3474
  //#region src/parser/operations.ts
@@ -2037,6 +3496,60 @@ function resolveParameterRef(raw, componentParameters, operationContext) {
2037
3496
  }
2038
3497
  return current;
2039
3498
  }
3499
+ /**
3500
+ * Resolve a raw request body to a concrete Request Body Object, following
3501
+ * `$ref` chains into `components/requestBodies` with a cycle guard. Throws on
3502
+ * an unresolvable ref so a malformed spec fails at parse time instead of
3503
+ * silently losing the body — and with it the body-ownership rule that keeps
3504
+ * duplicated secrets out of the query string.
3505
+ */
3506
+ function resolveRequestBodyRef(raw, componentRequestBodies, operationContext) {
3507
+ if (!raw) return void 0;
3508
+ const seenRefs = /* @__PURE__ */ new Set();
3509
+ let current = raw;
3510
+ while ("$ref" in current) {
3511
+ if (seenRefs.has(current.$ref)) throw new Error(`Circular request body $ref chain starting at "${current.$ref}" in ${operationContext}.`);
3512
+ seenRefs.add(current.$ref);
3513
+ const refName = /^#\/components\/requestBodies\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
3514
+ const resolved = refName ? componentRequestBodies?.[refName] : void 0;
3515
+ if (!resolved) throw new Error(`Unresolved request body $ref "${current.$ref}" in ${operationContext}: no matching entry under components/requestBodies.`);
3516
+ current = resolved;
3517
+ }
3518
+ return current;
3519
+ }
3520
+ /**
3521
+ * Resolve a raw response header to a concrete Header Object, following `$ref`
3522
+ * chains into `components/headers` with a cycle guard.
3523
+ *
3524
+ * Unlike parameters and request bodies, a header that cannot be resolved is
3525
+ * warned about and dropped rather than failing the parse: a response header
3526
+ * is informational, so losing one costs an emitter a typed accessor, not a
3527
+ * wire contract, and should not block generating the rest of the SDK.
3528
+ */
3529
+ function resolveHeaderRef(raw, componentHeaders, headerName, operationContext) {
3530
+ const seenRefs = /* @__PURE__ */ new Set();
3531
+ let current = raw;
3532
+ let description;
3533
+ while ("$ref" in current) {
3534
+ if (description === void 0 && typeof current.description === "string") description = current.description;
3535
+ if (seenRefs.has(current.$ref)) {
3536
+ console.warn(`[oagen] Warning: Circular header $ref chain starting at "${current.$ref}" for response header "${headerName}" in ${operationContext}. Skipping the header.`);
3537
+ return;
3538
+ }
3539
+ seenRefs.add(current.$ref);
3540
+ const refName = /^#\/components\/headers\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
3541
+ const resolved = refName ? componentHeaders?.[refName] : void 0;
3542
+ if (!resolved) {
3543
+ console.warn(`[oagen] Warning: Unresolved header $ref "${current.$ref}" for response header "${headerName}" in ${operationContext}: no matching entry under components/headers. Skipping the header.`);
3544
+ return;
3545
+ }
3546
+ current = resolved;
3547
+ }
3548
+ return description === void 0 ? current : {
3549
+ ...current,
3550
+ description
3551
+ };
3552
+ }
2040
3553
  const HTTP_METHODS = [
2041
3554
  "get",
2042
3555
  "post",
@@ -2047,26 +3560,34 @@ const HTTP_METHODS = [
2047
3560
  "options",
2048
3561
  "trace"
2049
3562
  ];
2050
- function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters) {
3563
+ function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters, componentRequestBodies, componentModels, documentTags, componentExamples, componentHeaders) {
2051
3564
  if (!paths) return {
2052
3565
  services: [],
2053
3566
  inlineModels: []
2054
3567
  };
3568
+ const pathModelBaseNames = resolvePathModelBaseNames(paths, componentModels, componentRequestBodies);
2055
3569
  const serviceMap = /* @__PURE__ */ new Map();
2056
3570
  const inlineModels = [];
3571
+ const envelopeCompanions = [];
3572
+ const tagServiceByOperation = /* @__PURE__ */ new Map();
3573
+ const headerModelRegistry = new Map((componentModels ?? []).map((model) => [model.name, { fingerprint: modelFingerprint(model) }]));
2057
3574
  for (const [path, pathItem] of Object.entries(paths)) {
2058
3575
  const pathLevelParams = pathItem.parameters ?? [];
2059
3576
  for (const method of HTTP_METHODS) {
2060
3577
  const op = pathItem[method];
2061
3578
  if (!op) continue;
2062
- const serviceName = inferServiceName(path, op.tags?.[0]);
2063
- const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters);
3579
+ const tag = op.tags?.[0];
3580
+ const serviceName = inferServiceName(path, tag);
3581
+ const { operation, inlineModels: opModels, envelopeCompanions: opCompanions } = buildOperation(method, path, op, pathLevelParams, pathModelBaseNames.get(`${method} ${path}`) ?? derivePathModelBaseName(path, method), operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies, componentExamples, componentHeaders, headerModelRegistry);
2064
3582
  inlineModels.push(...opModels);
3583
+ if (opCompanions) envelopeCompanions.push(...opCompanions);
3584
+ if (tag) tagServiceByOperation.set(operation, serviceName);
2065
3585
  const ops = serviceMap.get(serviceName) ?? [];
2066
3586
  ops.push(operation);
2067
3587
  serviceMap.set(serviceName, ops);
2068
3588
  }
2069
3589
  }
3590
+ inlineModels.push(...resolveEnvelopeCompanionNames(envelopeCompanions, componentModels));
2070
3591
  for (const [serviceName, operations] of serviceMap) {
2071
3592
  const pathGroups = /* @__PURE__ */ new Map();
2072
3593
  for (const op of operations) {
@@ -2088,14 +3609,73 @@ function extractOperations(paths, operationIdTransform, componentSchemas, compon
2088
3609
  }
2089
3610
  }
2090
3611
  for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
3612
+ const declaredTags = indexDocumentTags(documentTags);
2091
3613
  return {
2092
- services: Array.from(serviceMap.entries()).map(([name, operations]) => ({
2093
- name,
2094
- operations
2095
- })),
3614
+ services: Array.from(serviceMap.entries()).map(([name, operations]) => {
3615
+ const description = resolveServiceDescription(name, operations, tagServiceByOperation, declaredTags);
3616
+ return description === void 0 ? {
3617
+ name,
3618
+ operations
3619
+ } : {
3620
+ name,
3621
+ description,
3622
+ operations
3623
+ };
3624
+ }),
2096
3625
  inlineModels
2097
3626
  };
2098
3627
  }
3628
+ /**
3629
+ * Index the document's top-level `tags[]` by the service name each tag
3630
+ * produces, so a tag spelled `user_management`, `User Management`, or
3631
+ * `user-management` all map to the `UserManagement` service. Every named tag
3632
+ * is indexed — with its description when it declares a non-blank one, and
3633
+ * `undefined` otherwise — because a declared-but-undescribed tag still marks
3634
+ * its service as one that must not borrow another tag's wording. When several
3635
+ * tags normalize to the same service name, the first described one wins.
3636
+ *
3637
+ * `parseSpec` ignored `tags` entirely before descriptions were read, so a
3638
+ * document with a malformed value — `tags` written as an object, a `null`
3639
+ * entry, a numeric `name` — used to parse and must keep parsing. Anything
3640
+ * that cannot be matched is skipped rather than thrown on.
3641
+ */
3642
+ function indexDocumentTags(documentTags) {
3643
+ const index = /* @__PURE__ */ new Map();
3644
+ if (!Array.isArray(documentTags)) return index;
3645
+ for (const tag of documentTags) {
3646
+ if (typeof tag !== "object" || tag === null) continue;
3647
+ const { name, description } = tag;
3648
+ if (typeof name !== "string" || name === "") continue;
3649
+ const serviceName = toPascalCase(name);
3650
+ if (index.get(serviceName) !== void 0) continue;
3651
+ const described = typeof description === "string" && description.trim() !== "";
3652
+ index.set(serviceName, described ? description : void 0);
3653
+ }
3654
+ return index;
3655
+ }
3656
+ /**
3657
+ * Pick a service's description from the document's tags.
3658
+ *
3659
+ * A service whose own name is a declared tag is described by that tag alone:
3660
+ * its description when it has one, nothing otherwise. Another tag's wording is
3661
+ * never borrowed for it — operations tagged `api_keys` that the path-prefix
3662
+ * split merges into `UserManagement` must not label that service "API keys".
3663
+ *
3664
+ * A service whose own name is not declared is one the split carved out of a
3665
+ * tag (e.g. `OrganizationDomains` from operations tagged `organizations`); it
3666
+ * inherits the description of the first described tag its operations carry.
3667
+ * Path-named services whose operations have no tag carry nothing and stay
3668
+ * undescribed.
3669
+ */
3670
+ function resolveServiceDescription(serviceName, operations, tagServiceByOperation, declaredTags) {
3671
+ if (declaredTags.has(serviceName)) return declaredTags.get(serviceName);
3672
+ for (const op of operations) {
3673
+ const tagService = tagServiceByOperation.get(op);
3674
+ if (tagService === void 0) continue;
3675
+ const description = declaredTags.get(tagService);
3676
+ if (description !== void 0) return description;
3677
+ }
3678
+ }
2099
3679
  function inferServiceName(path, tag) {
2100
3680
  if (tag) return toPascalCase(tag);
2101
3681
  const first = path.split("/").filter(Boolean)[0] ?? "Default";
@@ -2114,6 +3694,24 @@ function inferServiceName(path, tag) {
2114
3694
  function normalizeOperationIdForNaming(operationId) {
2115
3695
  return operationId.replace(/\[\d+\]\s*$/, "");
2116
3696
  }
3697
+ /**
3698
+ * The inline request body or response model name of an operation that has an
3699
+ * `operationId`. The single source of that name: extractRequestBody and
3700
+ * deriveResponseName use it to name, resolvePathModelBaseNames to reserve.
3701
+ */
3702
+ function operationIdModelName(operationId, suffix) {
3703
+ return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(operationId)) + suffix);
3704
+ }
3705
+ /**
3706
+ * The stem every model derived from an `operationId` starts with: its
3707
+ * `Request` and `Response` models, a list's `Item`, nested inline models under
3708
+ * those (`…ResponseOwner`) and envelope companions (`…ListMetadata`). Every
3709
+ * step of cleanSchemaName is a global replace or acts on the leading word, so
3710
+ * this is a true prefix of each of those names. Used to reserve them all.
3711
+ */
3712
+ function operationIdModelBase(operationId) {
3713
+ return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(operationId)));
3714
+ }
2117
3715
  function inferOperationName(method, path, operationId, operationIdTransform) {
2118
3716
  if (operationId) {
2119
3717
  const normalized = normalizeOperationIdForNaming(operationId);
@@ -2258,10 +3856,10 @@ function extractParameterGroups(op, allIRParams, operationContext) {
2258
3856
  * the request body. Emitters use the group's presence to generate sum-type
2259
3857
  * interfaces and custom JSON marshalling instead of flat optional fields.
2260
3858
  */
2261
- function extractBodyParameterGroups(op, operationContext, componentSchemas) {
3859
+ function extractBodyParameterGroups(op, requestBody, operationContext, componentSchemas) {
2262
3860
  const raw = op["x-mutually-exclusive-body-groups"];
2263
3861
  if (!raw || typeof raw !== "object") return void 0;
2264
- const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
3862
+ const bodySchema = requestBody?.content?.["application/json"]?.schema;
2265
3863
  const variantFieldSchemas = collectVariantFieldSchemas(bodySchema, componentSchemas);
2266
3864
  const groups = [];
2267
3865
  for (const [groupName, groupDef] of Object.entries(raw)) {
@@ -2301,8 +3899,10 @@ function extractBodyParameterGroups(op, operationContext, componentSchemas) {
2301
3899
  }
2302
3900
  return groups.length > 0 ? groups : void 0;
2303
3901
  }
2304
- function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters) {
2305
- const opLabel = op.operationId ?? `${method.toUpperCase()} ${path}`;
3902
+ function buildOperation(method, path, op, pathLevelParams, pathModelBaseName, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies, componentExamples, componentHeaders, headerModelRegistry = /* @__PURE__ */ new Map()) {
3903
+ const opLabel = operationLabel(op, path, method);
3904
+ const operationName = inferOperationName(method, path, op.operationId, operationIdTransform);
3905
+ const requestBodyObject = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
2306
3906
  const paramsByIdentity = /* @__PURE__ */ new Map();
2307
3907
  for (const raw of [...pathLevelParams, ...op.parameters ?? []]) {
2308
3908
  const param = resolveParameterRef(raw, componentParameters, opLabel);
@@ -2310,14 +3910,28 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2310
3910
  }
2311
3911
  const allParams = [...paramsByIdentity.values()];
2312
3912
  const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
3913
+ const groupedQueryNames = collectParameterGroupMemberNames(op);
3914
+ const bodyOwnedNames = /* @__PURE__ */ new Set();
3915
+ if (BODY_CARRYING_METHODS.has(method)) {
3916
+ const bodySchema = selectRequestBodyContent(requestBodyObject).schema;
3917
+ for (const name of collectBodyPropertyNames(bodySchema, componentSchemas)) if (!groupedQueryNames.has(name)) bodyOwnedNames.add(name);
3918
+ }
3919
+ const wireParams = bodyOwnedNames.size > 0 ? allParams.filter((p) => !(p.in === "query" && bodyOwnedNames.has(p.name))) : allParams;
2313
3920
  const opContext = serviceName;
2314
- const pathParams = extractParams(allParams, "path", opContext, componentSchemas);
2315
- const queryParams = extractParams(allParams, "query", opContext, componentSchemas);
2316
- const headerParams = extractParams(allParams, "header", opContext, componentSchemas).filter((p) => p.name.toLowerCase() !== "idempotency-key");
2317
- const cookieParams = extractParams(allParams, "cookie", opContext, componentSchemas);
3921
+ const pathParams = extractParams(wireParams, "path", opContext, componentSchemas, componentExamples, opLabel);
3922
+ const queryParams = extractParams(wireParams, "query", opContext, componentSchemas, componentExamples, opLabel);
3923
+ const headerParams = extractParams(wireParams, "header", opContext, componentSchemas, componentExamples, opLabel).filter((p) => p.name.toLowerCase() !== "idempotency-key");
3924
+ const cookieParams = extractParams(wireParams, "cookie", opContext, componentSchemas, componentExamples, opLabel);
3925
+ const bodyOwnedQueryParams = bodyOwnedNames.size > 0 ? extractParams(allParams.filter((p) => p.in === "query" && bodyOwnedNames.has(p.name)), "query", opContext, componentSchemas, componentExamples, opLabel) : [];
2318
3926
  const reqBodyModels = [];
2319
- const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
2320
- const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method, componentSchemas);
3927
+ const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(requestBodyObject, op, pathModelBaseName, reqBodyModels, componentSchemas);
3928
+ const { response, responseHeaders, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType, envelopeFields, envelopeCompanions } = extractResponses(op.responses, op, pathModelBaseName, path, method, componentSchemas, {
3929
+ componentHeaders,
3930
+ componentSchemas,
3931
+ serviceName: opContext,
3932
+ operationQualifier: toPascalCase(operationName),
3933
+ registry: headerModelRegistry
3934
+ });
2321
3935
  inlineModels.push(...reqBodyModels);
2322
3936
  const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
2323
3937
  let pagination;
@@ -2328,31 +3942,36 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2328
3942
  param: paginationFromParams?.param ?? "after",
2329
3943
  limitParam: paginationFromParams?.limitParam,
2330
3944
  dataPath: responseDataPath ?? paginationFromParams?.dataPath,
2331
- itemType
3945
+ itemType,
3946
+ envelopeFields
2332
3947
  };
2333
3948
  } else if (paginationFromParams) pagination = paginationFromParams;
2334
- const security = extractOperationSecurity(op.security);
3949
+ const security = extractSecurityRequirements(op.security, opLabel);
2335
3950
  const queryParamGroups = extractParameterGroups(op, [
2336
3951
  ...pathParams,
2337
3952
  ...queryParams,
2338
3953
  ...headerParams,
2339
3954
  ...cookieParams
2340
3955
  ], opLabel);
2341
- const bodyParamGroups = extractBodyParameterGroups(op, opLabel, componentSchemas);
3956
+ const bodyParamGroups = extractBodyParameterGroups(op, requestBodyObject, opLabel, componentSchemas);
2342
3957
  const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
3958
+ const summary = presentText(op.summary);
2343
3959
  return {
2344
3960
  operation: {
2345
- name: inferOperationName(method, path, op.operationId, operationIdTransform),
2346
- description: buildDescription(op.summary, op.description),
3961
+ name: operationName,
3962
+ ...summary !== void 0 ? { summary } : {},
3963
+ description: buildDescription(summary, op.description),
2347
3964
  httpMethod: method,
2348
3965
  path,
2349
3966
  pathParams,
2350
3967
  queryParams,
3968
+ bodyOwnedQueryParams: bodyOwnedQueryParams.length > 0 ? bodyOwnedQueryParams : void 0,
2351
3969
  headerParams,
2352
3970
  cookieParams: cookieParams.length > 0 ? cookieParams : void 0,
2353
3971
  requestBody,
2354
3972
  requestBodyEncoding,
2355
3973
  response,
3974
+ ...responseHeaders && { responseHeaders },
2356
3975
  successResponses,
2357
3976
  errors,
2358
3977
  pagination,
@@ -2362,31 +3981,49 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2362
3981
  security,
2363
3982
  parameterGroups
2364
3983
  },
2365
- inlineModels
3984
+ inlineModels,
3985
+ envelopeCompanions
2366
3986
  };
2367
3987
  }
2368
3988
  /**
2369
- * Extract per-operation security requirements from an OpenAPI security directive.
2370
- * Returns undefined when the operation uses the spec-level default security.
2371
- *
2372
- * OpenAPI security format: `security: [{ schemeName: [scope1, scope2] }]`
3989
+ * Combine an operation's `summary` and `description` into one doc text.
3990
+ * Empty or whitespace-only text counts as absent, so `summary: "List pets"`
3991
+ * with `description: ""` yields `"List pets"` rather than `""`. Text that has
3992
+ * content is kept verbatim, surrounding whitespace included.
2373
3993
  */
2374
- function extractOperationSecurity(security) {
2375
- if (!security || security.length === 0) return void 0;
2376
- const requirements = [];
2377
- for (const entry of security) for (const [schemeName, scopes] of Object.entries(entry)) requirements.push({
2378
- schemeName,
2379
- scopes: scopes ?? []
2380
- });
2381
- return requirements.length > 0 ? requirements : void 0;
2382
- }
2383
3994
  function buildDescription(summary, description) {
2384
- if (summary && description && summary !== description) return `${summary}\n\n${description}`;
2385
- return description ?? summary;
3995
+ const presentSummary = presentText(summary);
3996
+ const presentDescription = presentText(description);
3997
+ if (presentSummary && presentDescription && presentSummary !== presentDescription) return `${presentSummary}\n\n${presentDescription}`;
3998
+ return presentDescription ?? presentSummary;
3999
+ }
4000
+ /** The text itself when it has non-whitespace content, otherwise `undefined`. */
4001
+ function presentText(text) {
4002
+ return typeof text === "string" && text.trim() !== "" ? text : void 0;
2386
4003
  }
2387
- function extractParams(params, location, operationContext, componentSchemas) {
4004
+ function extractParams(params, location, operationContext, componentSchemas, componentExamples, opLabel) {
2388
4005
  return params.filter((p) => p.in === location).map((p) => {
2389
- const refTarget = resolveParamSchemaRef(p.schema, componentSchemas);
4006
+ const refTarget = resolveComponentSchemaRef(p.schema, componentSchemas);
4007
+ const { example, examples } = resolveExamples([
4008
+ {
4009
+ example: p.example,
4010
+ examples: exampleObjectValues(p.examples, componentExamples, `parameter "${p.name}" in ${opLabel ?? location}`)
4011
+ },
4012
+ {
4013
+ example: p.schema?.example,
4014
+ examples: nonEmptyExamples(p.schema?.examples)
4015
+ },
4016
+ {
4017
+ example: refTarget?.example,
4018
+ examples: nonEmptyExamples(refTarget?.examples)
4019
+ }
4020
+ ]);
4021
+ const own = p.schema ? extractConstraints(p.schema) : void 0;
4022
+ const inherited = refTarget ? extractConstraints(refTarget) : void 0;
4023
+ const constraints = own && inherited ? {
4024
+ ...inherited,
4025
+ ...own
4026
+ } : own ?? inherited;
2390
4027
  return {
2391
4028
  name: p.name,
2392
4029
  type: p.schema ? schemaToTypeRef(p.schema, p.name, operationContext ? toPascalCase(operationContext) : void 0) : {
@@ -2394,32 +4031,74 @@ function extractParams(params, location, operationContext, componentSchemas) {
2394
4031
  type: "string"
2395
4032
  },
2396
4033
  required: p.required ?? false,
2397
- description: p.description,
4034
+ description: firstDescription(p.description, p.schema?.description, refTarget?.description),
2398
4035
  deprecated: p.deprecated || p.schema?.deprecated || refTarget?.deprecated || void 0,
2399
4036
  default: p.schema?.default ?? refTarget?.default,
2400
- example: p.example ?? p.schema?.example ?? refTarget?.example,
4037
+ example,
4038
+ ...examples ? { examples } : {},
2401
4039
  style: p.style,
2402
- explode: p.explode
4040
+ explode: p.explode,
4041
+ ...constraints ? { constraints } : {}
2403
4042
  };
2404
4043
  });
2405
4044
  }
2406
4045
  /**
2407
- * If a parameter's schema is a `$ref` to `#/components/schemas/X`, return the
2408
- * referenced component schema. Used to surface schema-level metadata (default,
2409
- * example, deprecated) that wouldn't otherwise be visible at the param level.
2410
- *
2411
- * Refs are not pre-resolved by the bundler (`dereference: false` in refs.ts),
2412
- * so without this, a param like `schema: { $ref: '.../PaginationOrder' }`
2413
- * silently loses the target's `default: desc` and the SDK stops emitting the
2414
- * spec-mandated default value in generated method signatures.
4046
+ * First non-empty description in precedence order. A parameter that declares
4047
+ * `description: ""` is treated as undocumented so its schema's text still
4048
+ * surfaces, and the result is `undefined` rather than `""` when nothing in
4049
+ * the chain is documented.
4050
+ */
4051
+ function firstDescription(...candidates) {
4052
+ return candidates.find((candidate) => typeof candidate === "string" && candidate.length > 0);
4053
+ }
4054
+ /**
4055
+ * Flatten a parameter's `examples` map of Example Objects to the inline
4056
+ * values it carries, in document order. A `$ref` entry is resolved against
4057
+ * `components.examples` first (see `resolveExampleRef`). Entries that point
4058
+ * elsewhere (`externalValue`) or have no `value` are skipped; `null` is a
4059
+ * legitimate value and kept. Returns undefined when nothing remains so the
4060
+ * caller can fall through to the schema's `examples`.
4061
+ */
4062
+ function exampleObjectValues(examples, componentExamples, context) {
4063
+ if (!examples || typeof examples !== "object" || Array.isArray(examples)) return void 0;
4064
+ const values = [];
4065
+ for (const raw of Object.values(examples)) {
4066
+ const entry = resolveExampleRef(raw, componentExamples, context);
4067
+ if (!entry || typeof entry !== "object") continue;
4068
+ if (entry.externalValue !== void 0 || entry.value === void 0) continue;
4069
+ values.push(entry.value);
4070
+ }
4071
+ return values.length > 0 ? values : void 0;
4072
+ }
4073
+ /**
4074
+ * Follow an Example Object's `$ref` into `components.examples`, including a
4075
+ * component that is itself a `$ref`, with a cycle guard. The bundler keeps
4076
+ * refs (`dereference: false`), so without this every `$ref` entry would be
4077
+ * dropped and a map made only of refs would silently yield nothing — the
4078
+ * parameter would then take its `examples` from the schema instead of the
4079
+ * values the document points at. A ref that can't be resolved is skipped
4080
+ * with a warning rather than failing the parse: a missing example is a doc
4081
+ * gap, not a broken API surface.
2415
4082
  */
2416
- function resolveParamSchemaRef(schema, componentSchemas) {
2417
- if (!schema || !componentSchemas) return void 0;
2418
- const ref = schema.$ref;
2419
- if (!ref) return void 0;
2420
- const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
2421
- if (!match) return void 0;
2422
- return componentSchemas[match[1]];
4083
+ function resolveExampleRef(raw, componentExamples, context) {
4084
+ const seenRefs = /* @__PURE__ */ new Set();
4085
+ let current = raw;
4086
+ while (current && typeof current === "object" && current.$ref !== void 0) {
4087
+ const ref = current.$ref;
4088
+ if (seenRefs.has(ref)) {
4089
+ console.warn(`[oagen] Warning: Circular example $ref chain starting at "${ref}" on ${context}; skipping`);
4090
+ return;
4091
+ }
4092
+ seenRefs.add(ref);
4093
+ const refName = /^#\/components\/examples\/(.+)$/.exec(ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
4094
+ const resolved = refName ? componentExamples?.[refName] : void 0;
4095
+ if (!resolved) {
4096
+ console.warn(`[oagen] Warning: Unresolved example $ref "${ref}" on ${context}; skipping`);
4097
+ return;
4098
+ }
4099
+ current = resolved;
4100
+ }
4101
+ return current;
2423
4102
  }
2424
4103
  /**
2425
4104
  * Collect the property schemas of every `oneOf` branch reachable from a request
@@ -2470,26 +4149,91 @@ function resolveSchemaRefObject(schema, componentSchemas) {
2470
4149
  if (!match) return void 0;
2471
4150
  return componentSchemas[match[1]];
2472
4151
  }
2473
- function extractRequestBody(body, op, inlineModels) {
2474
- if (!body?.content) return {};
2475
- let encoding = "json";
2476
- let schema;
2477
- if (body.content["application/json"]?.schema) {
2478
- encoding = "json";
2479
- schema = body.content["application/json"].schema;
2480
- } else if (body.content["multipart/form-data"]?.schema) {
2481
- encoding = "form-data";
2482
- schema = body.content["multipart/form-data"].schema;
2483
- } else if (body.content["application/x-www-form-urlencoded"]?.schema) {
2484
- encoding = "form-urlencoded";
2485
- schema = body.content["application/x-www-form-urlencoded"].schema;
2486
- } else if (body.content["application/octet-stream"]) {
2487
- encoding = "binary";
2488
- schema = body.content["application/octet-stream"].schema;
2489
- } else if (body.content["text/plain"]) {
2490
- encoding = "text";
2491
- schema = body.content["text/plain"].schema;
4152
+ /** HTTP methods whose request body is the primary parameter carrier. */
4153
+ const BODY_CARRYING_METHODS = /* @__PURE__ */ new Set([
4154
+ "post",
4155
+ "put",
4156
+ "patch"
4157
+ ]);
4158
+ /**
4159
+ * Pick the request body's schema and wire encoding from its content map, in
4160
+ * priority order. JSON wins over form encodings; binary and text bodies may
4161
+ * legitimately carry no schema at all.
4162
+ */
4163
+ function selectRequestBodyContent(body) {
4164
+ const content = body?.content;
4165
+ if (!content) return { encoding: "json" };
4166
+ if (content["application/json"]?.schema) return {
4167
+ encoding: "json",
4168
+ schema: content["application/json"].schema
4169
+ };
4170
+ if (content["multipart/form-data"]?.schema) return {
4171
+ encoding: "form-data",
4172
+ schema: content["multipart/form-data"].schema
4173
+ };
4174
+ if (content["application/x-www-form-urlencoded"]?.schema) return {
4175
+ encoding: "form-urlencoded",
4176
+ schema: content["application/x-www-form-urlencoded"].schema
4177
+ };
4178
+ if (content["application/octet-stream"]) return {
4179
+ encoding: "binary",
4180
+ schema: content["application/octet-stream"].schema
4181
+ };
4182
+ if (content["text/plain"]) return {
4183
+ encoding: "text",
4184
+ schema: content["text/plain"].schema
4185
+ };
4186
+ return { encoding: "json" };
4187
+ }
4188
+ /** Parameter names referenced by any `x-mutually-exclusive-parameter-groups` variant. */
4189
+ function collectParameterGroupMemberNames(op) {
4190
+ const names = /* @__PURE__ */ new Set();
4191
+ const raw = op["x-mutually-exclusive-parameter-groups"];
4192
+ if (!raw || typeof raw !== "object") return names;
4193
+ for (const groupDef of Object.values(raw)) {
4194
+ const variants = groupDef && typeof groupDef === "object" ? groupDef.variants : void 0;
4195
+ if (!variants || typeof variants !== "object") continue;
4196
+ for (const members of Object.values(variants)) {
4197
+ if (!Array.isArray(members)) continue;
4198
+ for (const member of members) if (typeof member === "string") names.add(member);
4199
+ }
4200
+ }
4201
+ return names;
4202
+ }
4203
+ /**
4204
+ * Every property name a request body is guaranteed to accept, following
4205
+ * `$ref`s and descending through composition. `allOf` members all apply, so
4206
+ * their properties union. `oneOf` / `anyOf` alternatives are choices, so only
4207
+ * a property every non-null alternative declares counts — otherwise a caller
4208
+ * on another branch would have no slot for the value in either the body or
4209
+ * the query. Used to decide which `in: query` declarations are body-owned.
4210
+ */
4211
+ function collectBodyPropertyNames(schema, componentSchemas, seen = /* @__PURE__ */ new Set()) {
4212
+ if (!schema) return /* @__PURE__ */ new Set();
4213
+ const ref = schema.$ref;
4214
+ if (ref) {
4215
+ if (seen.has(ref)) return /* @__PURE__ */ new Set();
4216
+ return collectBodyPropertyNames(resolveSchemaRefObject(schema, componentSchemas), componentSchemas, /* @__PURE__ */ new Set([...seen, ref]));
4217
+ }
4218
+ const names = new Set(Object.keys(schema.properties ?? {}));
4219
+ for (const sub of schema.allOf ?? []) for (const name of collectBodyPropertyNames(sub, componentSchemas, seen)) names.add(name);
4220
+ for (const alternatives of [schema.oneOf, schema.anyOf]) {
4221
+ const branches = (alternatives ?? []).filter((alt) => alt.type !== "null");
4222
+ if (branches.length === 0) continue;
4223
+ let common;
4224
+ for (const branch of branches) {
4225
+ const branchNames = collectBodyPropertyNames(branch, componentSchemas, seen);
4226
+ common = common ? new Set([...common].filter((name) => branchNames.has(name))) : branchNames;
4227
+ }
4228
+ for (const name of common ?? []) names.add(name);
2492
4229
  }
4230
+ return names;
4231
+ }
4232
+ function extractRequestBody(body, op, pathModelBaseName, inlineModels, componentSchemas) {
4233
+ if (!body?.content) return {};
4234
+ const selected = selectRequestBodyContent(body);
4235
+ const encoding = selected.encoding;
4236
+ let schema = selected.schema;
2493
4237
  if (!schema) {
2494
4238
  if (encoding === "binary") return {
2495
4239
  body: {
@@ -2508,24 +4252,26 @@ function extractRequestBody(body, op, inlineModels) {
2508
4252
  };
2509
4253
  return {};
2510
4254
  }
2511
- const contextName = cleanSchemaName(op?.operationId ? toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Request" : "RequestBody");
4255
+ const contextName = op?.operationId ? operationIdModelName(op.operationId, "Request") : pathModelBaseName + "Request";
2512
4256
  if (schema.oneOf && schema.discriminator) {
2513
4257
  const merged = flattenDiscriminatedUnionBody(schema);
2514
4258
  if (merged) schema = merged;
2515
4259
  }
2516
4260
  if (schema.properties && (schema.type === "object" || !schema.type)) {
2517
4261
  const requiredSet = new Set(schema.required ?? []);
4262
+ const siblings = collectProperties(schema);
2518
4263
  const fields = [];
2519
4264
  for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
2520
4265
  if (!fieldSchema) continue;
2521
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet));
4266
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet, componentSchemas, siblings));
2522
4267
  }
2523
4268
  inlineModels.push({
2524
4269
  name: contextName,
2525
4270
  description: void 0,
2526
- fields
4271
+ fields,
4272
+ ...constraintsEntry(schema)
2527
4273
  });
2528
- extractInlineModelsFromProperties(schema, inlineModels, contextName);
4274
+ extractInlineModelsFromProperties(schema, inlineModels, contextName, componentSchemas);
2529
4275
  return {
2530
4276
  body: {
2531
4277
  kind: "model",
@@ -2536,23 +4282,29 @@ function extractRequestBody(body, op, inlineModels) {
2536
4282
  }
2537
4283
  if (schema.oneOf) {
2538
4284
  const variants = [];
4285
+ const inlineObjectVariants = schema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
4286
+ const contentNamed = [];
2539
4287
  for (const variant of schema.oneOf) if (variant.$ref) {
2540
4288
  const ref = schemaToTypeRef(variant, contextName);
2541
4289
  variants.push(ref);
2542
4290
  } else if (variant.type === "null") {} else if (variant.properties && (variant.type === "object" || !variant.type)) {
2543
- const variantName = deriveOneOfVariantName(variant, contextName, inlineModels);
4291
+ const named = deriveOneOfVariantName(variant, contextName, inlineModels, inlineObjectVariants, contentNamed, Boolean(op?.operationId));
4292
+ const variantName = named.name;
4293
+ if (named.fromContent) contentNamed.push(variantName);
2544
4294
  const requiredSet = new Set(variant.required ?? []);
4295
+ const siblings = collectProperties(variant);
2545
4296
  const fields = [];
2546
4297
  for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
2547
4298
  if (!fieldSchema) continue;
2548
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet));
4299
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet, componentSchemas, siblings));
2549
4300
  }
2550
4301
  inlineModels.push({
2551
4302
  name: variantName,
2552
4303
  description: variant.description,
2553
- fields
4304
+ fields,
4305
+ ...constraintsEntry(variant)
2554
4306
  });
2555
- extractInlineModelsFromProperties(variant, inlineModels, variantName);
4307
+ extractInlineModelsFromProperties(variant, inlineModels, variantName, componentSchemas);
2556
4308
  variants.push({
2557
4309
  kind: "model",
2558
4310
  name: variantName
@@ -2638,38 +4390,451 @@ function flattenDiscriminatedUnionBody(schema) {
2638
4390
  type: "object",
2639
4391
  properties,
2640
4392
  required: order.filter((name) => name === discProp || requiredCounts[name] === variantCount),
2641
- ...schema.description ? { description: schema.description } : {}
4393
+ ...schema.description ? { description: schema.description } : {},
4394
+ ...extractConstraints(schema)
2642
4395
  };
2643
4396
  }
2644
- /** Derive a unique variant name for a oneOf inline model. */
2645
- function deriveOneOfVariantName(variant, baseName, existingModels) {
4397
+ /**
4398
+ * Derive a unique variant name for a request-body `oneOf` inline model.
4399
+ *
4400
+ * A variant with a string-const property is named after it
4401
+ * (`RadarEmailChallengeCodeSessionAuthenticateRequest`). Any other variant is
4402
+ * named from its content by the same rule every inline union variant follows
4403
+ * (`nameInlineVariant`): the first const-less variant keeps the body's name
4404
+ * (or its title, when every inline variant of the union is titled), and each
4405
+ * later one takes its title, a distinguishing property
4406
+ * (`CreateThingRequestWithB`) or, when its property set repeats an earlier
4407
+ * variant's, the position counter (`CreateThingRequest2`). `siblings` are the
4408
+ * union's inline object variants and `contentNamed` the names its const-less
4409
+ * variants have taken so far; `fromContent` tells the caller to record this
4410
+ * one there. A clash with a model minted elsewhere still bumps the counter:
4411
+ * on the base for a content-named variant (`CreateThingRequest2`), on the
4412
+ * const-derived name for a const-named one (`FooCreateThingRequest2`) — so a
4413
+ * variant titled `Foo` and a later one pinning `kind: foo` in the same body
4414
+ * never share a model.
4415
+ *
4416
+ * `fromOperationId` says where `baseName` came from. An operationId-derived
4417
+ * base is already in cleanSchemaName form and its variants go through
4418
+ * resolveSchemaName, so they follow the same project-level naming policy as
4419
+ * component schemas. A path-derived base is used verbatim: it is deliberately
4420
+ * not cleaned (see derivePathModelBaseName) and resolvePathModelBaseNames
4421
+ * reserves names by that exact prefix, so cleaning a variant here would let it
4422
+ * slip past the reservation — `NotesPostRequest` became `NotePostRequest` and
4423
+ * collided with a component of that name.
4424
+ */
4425
+ function deriveOneOfVariantName(variant, baseName, existingModels, siblings, contentNamed, fromOperationId) {
4426
+ const finalize = (name) => fromOperationId ? resolveSchemaName(name) : name;
4427
+ const existingNames = new Set(existingModels.map((m) => m.name));
4428
+ const unique = (name, stem) => {
4429
+ let candidate = name;
4430
+ for (let suffix = 2; existingNames.has(candidate); suffix++) candidate = `${stem}${suffix}`;
4431
+ return candidate;
4432
+ };
2646
4433
  if (variant.properties) {
2647
- for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string") return resolveSchemaName(toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request");
4434
+ for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string") {
4435
+ const derivedName = finalize(toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request");
4436
+ return {
4437
+ name: unique(derivedName, derivedName),
4438
+ fromContent: false
4439
+ };
4440
+ }
2648
4441
  }
2649
- const existingNames = new Set(existingModels.map((m) => m.name));
2650
- const transformedBaseName = resolveSchemaName(baseName);
2651
- let name = transformedBaseName;
2652
- let suffix = 2;
2653
- while (existingNames.has(name)) {
2654
- name = `${transformedBaseName}${suffix}`;
2655
- suffix++;
4442
+ const finalBaseName = finalize(baseName);
4443
+ return {
4444
+ name: unique(nameInlineVariant(variant, siblings, finalBaseName, contentNamed, null), finalBaseName),
4445
+ fromContent: true
4446
+ };
4447
+ }
4448
+ /** Human-readable label for an operation in diagnostics: its operationId, else `METHOD /path`. */
4449
+ function operationLabel(op, path, method) {
4450
+ return op?.operationId ?? `${method.toUpperCase()} ${path}`;
4451
+ }
4452
+ /** Shape of a model, independent of field order: name, requiredness, and type of every field. */
4453
+ function modelFingerprint(model) {
4454
+ return model.fields.map((f) => `${f.name}${f.required ? "!" : "?"}:${stableStringify(f.type)}`).sort().join("|");
4455
+ }
4456
+ /** `JSON.stringify` with object keys sorted, so equal shapes serialize equally. */
4457
+ function stableStringify(value) {
4458
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
4459
+ if (value !== null && typeof value === "object") {
4460
+ const record = value;
4461
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
2656
4462
  }
2657
- return name;
4463
+ return JSON.stringify(value) ?? "undefined";
4464
+ }
4465
+ /**
4466
+ * Type a header's schema and materialize the inline models it names, under a
4467
+ * property name that does not collide with a different shape anywhere in the
4468
+ * spec.
4469
+ *
4470
+ * Header models are named from the service and header name alone, so an
4471
+ * inline `X-Meta` on a `Documents` operation is `DocumentsXMeta` — the same
4472
+ * name a component schema, or the same header on a 429 or on another
4473
+ * `Documents` operation, may already own with a different shape. Left alone,
4474
+ * the later merge keeps one definition for the name and every other
4475
+ * reference silently gets the wrong fields (a request body using the
4476
+ * component would even change type without any emitter reading headers).
4477
+ *
4478
+ * The first owner of a name keeps it: component models are seeded before any
4479
+ * operation, operations are visited in path order, and a single operation's
4480
+ * responses in ascending status-code order (integer-like keys iterate that
4481
+ * way), so the lowest code wins regardless of where it appears in the
4482
+ * document. A later header with a different shape — at any depth, a nested
4483
+ * object that differs counts — is re-derived under the first free candidate:
4484
+ *
4485
+ * 1. the bare header name `DocumentsXMeta`
4486
+ * 2. when the name is taken inside this operation, the status code
4487
+ * (`DocumentsXMeta429`); when taken by a component or another
4488
+ * operation, the operation (`DocumentsCreateDocumentXMeta`)
4489
+ * 3. the other of those two
4490
+ * 4. operation and status code `DocumentsCreateDocumentXMeta429`
4491
+ * 5. a numeric suffix `DocumentsXMeta2`, `DocumentsXMeta3`, ...
4492
+ *
4493
+ * Both the type and the models come from the same property name, so nested
4494
+ * models (`DocumentsXMeta429Origin`) line up by construction. A shape
4495
+ * identical to the name's current owner shares it and materializes nothing
4496
+ * new, which is also how a header reuses a matching component model.
4497
+ *
4498
+ * Treating the header as a one-property object reuses the field-level
4499
+ * extractor, whose names come from the same `qualifyInlineModelName(name,
4500
+ * parent)` rule `schemaToTypeRef` applies — the trick the inline request-body
4501
+ * path uses.
4502
+ */
4503
+ function typeHeaderSchema(schema, name, statusCode, ctx, models) {
4504
+ const parentName = ctx.serviceName ? toPascalCase(ctx.serviceName) : void 0;
4505
+ const byStatus = `${name}${statusCode}`;
4506
+ const byOperation = `${ctx.operationQualifier}${name}`;
4507
+ const byBoth = `${ctx.operationQualifier}${name}${statusCode}`;
4508
+ let candidates = [
4509
+ name,
4510
+ byStatus,
4511
+ byOperation,
4512
+ byBoth
4513
+ ];
4514
+ for (let attempt = 0;; attempt++) {
4515
+ const propertyName = attempt < candidates.length ? candidates[attempt] : `${name}${attempt - candidates.length + 2}`;
4516
+ const trial = [];
4517
+ extractInlineModelsFromProperties({ properties: { [propertyName]: schema } }, trial, parentName, ctx.componentSchemas);
4518
+ const fingerprints = trial.map((model) => [model, modelFingerprint(model)]);
4519
+ const collisions = fingerprints.flatMap(([model, fingerprint]) => {
4520
+ const registered = ctx.registry.get(model.name);
4521
+ return registered !== void 0 && registered.fingerprint !== fingerprint ? [registered] : [];
4522
+ });
4523
+ if (collisions.length > 0) {
4524
+ if (attempt === 0 && !collisions.every((c) => c.operation === ctx.operationQualifier)) candidates = [
4525
+ name,
4526
+ byOperation,
4527
+ byStatus,
4528
+ byBoth
4529
+ ];
4530
+ continue;
4531
+ }
4532
+ for (const [model, fingerprint] of fingerprints) {
4533
+ if (ctx.registry.has(model.name)) continue;
4534
+ ctx.registry.set(model.name, {
4535
+ fingerprint,
4536
+ operation: ctx.operationQualifier
4537
+ });
4538
+ models.push(model);
4539
+ }
4540
+ return schemaToTypeRef(schema, propertyName, parentName);
4541
+ }
4542
+ }
4543
+ /**
4544
+ * Read a Response Object's `headers` map into IR response headers, resolving
4545
+ * `components/headers` refs. Returns `undefined` rather than `[]` when nothing
4546
+ * survives so callers can leave the `headers` key off entirely.
4547
+ *
4548
+ * An object-shaped header schema (directly, inside an array, or as a oneOf
4549
+ * variant) becomes a `ModelRef`, so the model it names is appended to
4550
+ * `models` — otherwise the ref would dangle with nothing to emit.
4551
+ */
4552
+ function extractResponseHeaders(headers, operationLabel, statusCode, ctx, models) {
4553
+ if (!headers) return void 0;
4554
+ const result = [];
4555
+ for (const [name, raw] of Object.entries(headers)) {
4556
+ if (name.toLowerCase() === "content-type") continue;
4557
+ const header = resolveHeaderRef(raw, ctx.componentHeaders, name, operationLabel);
4558
+ if (!header) continue;
4559
+ const contentSchema = header.content ? Object.values(header.content)[0]?.schema : void 0;
4560
+ const schema = header.schema ?? contentSchema;
4561
+ const type = schema ? typeHeaderSchema(schema, name, statusCode, ctx, models) : {
4562
+ kind: "primitive",
4563
+ type: "string"
4564
+ };
4565
+ result.push({
4566
+ name,
4567
+ type,
4568
+ required: header.required ?? false,
4569
+ description: header.description,
4570
+ deprecated: header.deprecated || void 0
4571
+ });
4572
+ }
4573
+ return result.length > 0 ? result : void 0;
2658
4574
  }
2659
- function deriveResponseName(op, path, method) {
2660
- if (op?.operationId) return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Response");
2661
- return toPascalCase(path.split("/").filter(Boolean)[0] ?? "Unknown") + toPascalCase(method) + "Response";
4575
+ /**
4576
+ * Build the prefix (`NotesByIdPinPost`) that names both inline models — the
4577
+ * request body (`…Request`) and the response (`…Response`) — of an operation
4578
+ * that has no `operationId`, from the only thing that identifies it: its path
4579
+ * and method.
4580
+ *
4581
+ * Every non-parameter segment is PascalCased in order and every parameter
4582
+ * segment becomes `By` + PascalCase(param name); then the optional
4583
+ * disambiguator and the method are appended. The whole is normalized with
4584
+ * toPascalCase so it is a fixed point of that function: responses.ts re-cases
4585
+ * the response name in `deriveModelName`, and without this step `POST /a/b`
4586
+ * named its body `ABPostRequest` but its response `AbPostResponse`.
4587
+ *
4588
+ * GET /widgets → WidgetsGet
4589
+ * POST /notes → NotesPost
4590
+ * POST /notes/{id}/pin → NotesByIdPinPost
4591
+ * GET /users/{user_id}/sessions → UsersByUserIdSessionsGet
4592
+ *
4593
+ * This tells the common path shapes apart (list vs. item, sibling and nested
4594
+ * sub-resources) but is not unique by construction: `/foo-bar` and `/foo/bar`,
4595
+ * `/notes/by-id` and `/notes/{id}`, `/notes` and `/notes/` all flatten to the
4596
+ * same words. `resolvePathModelBaseNames` detects such collisions: the path
4597
+ * spelled most plainly keeps the plain name and every other one gets a
4598
+ * `disambiguator` derived from its raw path.
4599
+ *
4600
+ * A single-segment path keeps the name the old first-segment rule produced.
4601
+ * The result is deliberately not passed through cleanSchemaName (the old
4602
+ * fallback wasn't either): the segments are the resource names as the API
4603
+ * spells them.
4604
+ */
4605
+ function derivePathModelBaseName(path, method, disambiguator = "") {
4606
+ const parts = path.split("/").filter(Boolean).map((segment) => segment.startsWith("{") ? "By" + toPascalCase(segment.replace(/[{}]/g, "")) : toPascalCase(segment));
4607
+ return toPascalCase([
4608
+ parts.length > 0 ? parts.join("") : "Unknown",
4609
+ disambiguator,
4610
+ method
4611
+ ].filter(Boolean).join("_"));
4612
+ }
4613
+ /**
4614
+ * Decide the path-derived model prefix of every operation that has no
4615
+ * `operationId`, keyed by `${method} ${path}`.
4616
+ *
4617
+ * A prefix collides when two distinct raw paths with the same method flatten
4618
+ * to it (see derivePathModelBaseName), or when a reserved name starts with
4619
+ * it. The reserved names are every resolved component model name and, for
4620
+ * every operation with an `operationId`, its model stem
4621
+ * (operationIdModelBase). A prefix is the stem of several emitted names —
4622
+ * `…Request`, `…Response`, a list's `…Item`, nested inline models such as
4623
+ * `…ResponseOwner`, envelope companions such as `…ListMetadata` — so the
4624
+ * check is on the stem, not on an enumeration of suffixes: an unnamed
4625
+ * `GET /audit/events` must not overwrite `components/schemas/AuditEventsGetItem`
4626
+ * any more than an unnamed `POST /audit/events` may overwrite the models of
4627
+ * a `POST /legacy-audit` whose operationId is `auditEventsPost`. Reserved
4628
+ * names never change.
4629
+ *
4630
+ * This is deliberately a superset: a component named `AuditEventsGetter`, or
4631
+ * an operationId `auditEventsGetSummary`, also pushes `GET /audit/events`
4632
+ * onto a hashed name although no emitted name would coincide. Determinism
4633
+ * and never sharing a name wrongly matter more than the rare extra hash, and
4634
+ * the reservation is unconditional — an inline body is never merged into a
4635
+ * same-named component even when the shapes agree — so the rule stays
4636
+ * simple and order-free. Every name derived from a path-based prefix uses it
4637
+ * verbatim (body `oneOf` variants included; see deriveOneOfVariantName), and
4638
+ * as a second line of defence the check also runs on the cleaned spellings
4639
+ * of the prefix (see prefixSpellings), so `GET /notes` yields to a component
4640
+ * `NoteGetItem` although its own item model is `NotesGetItem`. Names that
4641
+ * embed the prefix behind a const value — a body `oneOf` variant pinning
4642
+ * `kind: email` is `EmailTagsPostRequest`, a nested variant
4643
+ * `<Const><Parent><Field>` — are checked too, with each const the operation
4644
+ * can mint prepended to every spelling (see collectOperationConstPrefixes);
4645
+ * the hash then lands inside those names as well (`EmailTags151Dd5PostRequest`).
4646
+ *
4647
+ * These names are types SDK users read and import, so a collision costs the
4648
+ * plain name only where it must. Within a group of paths that flatten to one
4649
+ * prefix, the path spelled most plainly (see pathDeviation: no trailing
4650
+ * slash, `{id}` rather than a literal `by-id`) is canonical and keeps the
4651
+ * plain prefix; every other path gets a six-character disambiguator, the
4652
+ * start of the SHA-1 of its raw path, inserted before the method
4653
+ * (`POST /foo-bar` next to `POST /foo/bar` → `FooBar546F12Post`). When two
4654
+ * paths tie for plainest, or the prefix is reserved, nobody is canonical and
4655
+ * every path in the group is disambiguated. The outcome depends only on the
4656
+ * document's paths, operationIds and component names, never on their order,
4657
+ * and a path that stops colliding returns to its plain name.
4658
+ */
4659
+ function resolvePathModelBaseNames(paths, componentModels, componentRequestBodies) {
4660
+ const candidates = [];
4661
+ const rawPathsByPlainName = /* @__PURE__ */ new Map();
4662
+ const constPrefixesByPlainName = /* @__PURE__ */ new Map();
4663
+ const reservedNames = /* @__PURE__ */ new Set();
4664
+ for (const model of componentModels ?? []) reservedNames.add(model.name);
4665
+ for (const [path, pathItem] of Object.entries(paths)) for (const method of HTTP_METHODS) {
4666
+ const op = pathItem[method];
4667
+ if (!op) continue;
4668
+ if (op.operationId) {
4669
+ reservedNames.add(operationIdModelBase(op.operationId));
4670
+ continue;
4671
+ }
4672
+ const plain = derivePathModelBaseName(path, method);
4673
+ candidates.push({
4674
+ key: `${method} ${path}`,
4675
+ path,
4676
+ method,
4677
+ plain
4678
+ });
4679
+ const rawPaths = rawPathsByPlainName.get(plain) ?? /* @__PURE__ */ new Set();
4680
+ rawPaths.add(path);
4681
+ rawPathsByPlainName.set(plain, rawPaths);
4682
+ const constPrefixes = constPrefixesByPlainName.get(plain) ?? /* @__PURE__ */ new Set();
4683
+ for (const prefix of collectOperationConstPrefixes(op, componentRequestBodies, operationLabel(op, path, method))) constPrefixes.add(prefix);
4684
+ constPrefixesByPlainName.set(plain, constPrefixes);
4685
+ }
4686
+ const reservedList = [...reservedNames];
4687
+ const canonicalByPlainName = /* @__PURE__ */ new Map();
4688
+ for (const [plain, rawPaths] of rawPathsByPlainName) {
4689
+ const spellings = prefixSpellings(plain);
4690
+ const reserved = ["", ...constPrefixesByPlainName.get(plain) ?? []].flatMap((constPrefix) => spellings.map((spelling) => constPrefix + spelling)).some((stem) => reservedList.some((name) => name.startsWith(stem)));
4691
+ canonicalByPlainName.set(plain, reserved ? void 0 : canonicalPath(rawPaths));
4692
+ }
4693
+ const resolved = /* @__PURE__ */ new Map();
4694
+ for (const { key, path, method, plain } of candidates) {
4695
+ const keepsPlainName = canonicalByPlainName.get(plain) === path;
4696
+ resolved.set(key, keepsPlainName ? plain : derivePathModelBaseName(path, method, pathDisambiguator(path)));
4697
+ }
4698
+ return resolved;
4699
+ }
4700
+ /**
4701
+ * The one path in a group that is spelled most plainly by pathDeviation, or
4702
+ * undefined when two or more tie for it, so the choice never depends on the
4703
+ * order the paths were written in.
4704
+ */
4705
+ function canonicalPath(rawPaths) {
4706
+ let best;
4707
+ let bestScore;
4708
+ let tied = false;
4709
+ for (const path of rawPaths) {
4710
+ const score = pathDeviation(path);
4711
+ const cmp = bestScore ? compareDeviation(score, bestScore) : -1;
4712
+ if (cmp < 0) {
4713
+ best = path;
4714
+ bestScore = score;
4715
+ tied = false;
4716
+ } else if (cmp === 0) tied = true;
4717
+ }
4718
+ return tied ? void 0 : best;
4719
+ }
4720
+ /**
4721
+ * How far a raw path's spelling is from the plain prefix it flattens to:
4722
+ * `[trailing slash, literal segments with any character outside a-z0-9]`,
4723
+ * compared lexicographically. `/notes` scores [0, 0] and `/notes/` [1, 0];
4724
+ * `/notes/{id}` scores [0, 0] and `/notes/by-id` [0, 1].
4725
+ */
4726
+ function pathDeviation(rawPath) {
4727
+ return [rawPath.length > 1 && rawPath.endsWith("/") ? 1 : 0, rawPath.split("/").filter(Boolean).filter((segment) => !segment.startsWith("{") && /[^a-z0-9]/.test(segment)).length];
4728
+ }
4729
+ function compareDeviation(a, b) {
4730
+ return a[0] - b[0] || a[1] - b[1];
4731
+ }
4732
+ /** The first six hex characters of the SHA-1 of a raw path. */
4733
+ function pathDisambiguator(rawPath) {
4734
+ return createHash("sha1").update(rawPath).digest("hex").slice(0, 6);
4735
+ }
4736
+ /**
4737
+ * The spellings a path-based prefix could take if a name derived from it were
4738
+ * run through one of the parser's cleaning steps: the prefix itself (every
4739
+ * path-derived name uses it verbatim), cleanSchemaName's form (lead-word
4740
+ * singularization plus backend and ListItem-marker stripping),
4741
+ * resolveSchemaName's form (the same plus the project's schemaNameTransform),
4742
+ * and stripListItemMarkers alone, which nested-model and inline-enum
4743
+ * qualification apply to a parent name. Reserving by every spelling is the
4744
+ * second line of defence behind "use the base verbatim": a cleaning step added
4745
+ * later cannot let a derived name slip past the reservation. Empty spellings
4746
+ * are dropped so a degenerate prefix cannot match every reserved name.
4747
+ */
4748
+ function prefixSpellings(prefix) {
4749
+ const spellings = [
4750
+ prefix,
4751
+ cleanSchemaName(prefix),
4752
+ resolveSchemaName(prefix),
4753
+ stripListItemMarkers(prefix)
4754
+ ];
4755
+ return [...new Set(spellings)].filter((spelling) => spelling.length > 0);
4756
+ }
4757
+ /**
4758
+ * The PascalCase const values an operationId-less operation can prepend to a
4759
+ * name minted from its prefix, read from the schemas its request body and 2xx
4760
+ * responses declare inline. A body `oneOf` variant pinning `kind: email`
4761
+ * yields `EmailTagsPostRequest` (deriveOneOfVariantName); a nested
4762
+ * `oneOf`/`anyOf` object variant yields `<Const><Parent><Field>`
4763
+ * (nameOneOfVariant, nameVariantModel). Every string const or single-value
4764
+ * enum of every inline object variant is collected, top level and nested — a
4765
+ * superset of whichever property each of those picks as the discriminator, so
4766
+ * the reservation can never be narrower than the naming. `$ref` targets are
4767
+ * components with names of their own and are not followed. A request body
4768
+ * `$ref` that cannot be resolved is left for buildOperation to report.
4769
+ *
4770
+ * Names that do not embed the prefix at all — a response `oneOf` variant
4771
+ * named from its bare const (`Email`, see deriveModelName in responses.ts)
4772
+ * or a `title`-mapped variant — are outside what a disambiguator can
4773
+ * separate and are not reserved here.
4774
+ */
4775
+ function collectOperationConstPrefixes(op, componentRequestBodies, opLabel) {
4776
+ const prefixes = /* @__PURE__ */ new Set();
4777
+ let body;
4778
+ try {
4779
+ body = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
4780
+ } catch {
4781
+ body = void 0;
4782
+ }
4783
+ collectConstPrefixes(selectRequestBodyContent(body).schema, prefixes, 0);
4784
+ for (const [statusCode, response] of Object.entries(op.responses ?? {})) {
4785
+ const code = parseInt(statusCode, 10);
4786
+ if (code < 200 || code >= 300) continue;
4787
+ for (const media of Object.values(response.content ?? {})) collectConstPrefixes(media?.schema, prefixes, 0);
4788
+ }
4789
+ return prefixes;
4790
+ }
4791
+ function collectConstPrefixes(schema, out, depth) {
4792
+ if (!schema || schema.$ref || depth > 16) return;
4793
+ for (const variants of [schema.oneOf, schema.anyOf]) for (const variant of variants ?? []) {
4794
+ if (variant.$ref || !variant.properties) continue;
4795
+ for (const property of Object.values(variant.properties)) {
4796
+ const value = constStringOf(property);
4797
+ const prefix = value === null ? "" : toPascalCase(value);
4798
+ if (prefix) out.add(prefix);
4799
+ }
4800
+ }
4801
+ const children = [
4802
+ ...schema.oneOf ?? [],
4803
+ ...schema.anyOf ?? [],
4804
+ ...schema.allOf ?? [],
4805
+ schema.items,
4806
+ ...Object.values(schema.properties ?? {}),
4807
+ typeof schema.additionalProperties === "object" ? schema.additionalProperties : void 0
4808
+ ];
4809
+ for (const child of children) collectConstPrefixes(child, out, depth + 1);
4810
+ }
4811
+ /** A property's pinned string value: a string `const`, or a single-value string `enum`. */
4812
+ function constStringOf(property) {
4813
+ if (!property) return null;
4814
+ if (typeof property.const === "string") return property.const;
4815
+ if (Array.isArray(property.enum) && property.enum.length === 1 && typeof property.enum[0] === "string") return property.enum[0];
4816
+ return null;
4817
+ }
4818
+ function deriveResponseName(op, pathModelBaseName) {
4819
+ if (op?.operationId) return operationIdModelName(op.operationId, "Response");
4820
+ return pathModelBaseName + "Response";
2662
4821
  }
2663
- function extractResponses(responses, op, path, method, componentSchemas) {
4822
+ function extractResponses(responses, op, pathModelBaseName, path, method, componentSchemas, headerContext = {
4823
+ operationQualifier: "",
4824
+ registry: /* @__PURE__ */ new Map()
4825
+ }) {
2664
4826
  const errors = [];
2665
4827
  let response = {
2666
4828
  kind: "primitive",
2667
4829
  type: "string"
2668
4830
  };
4831
+ let responseHeaders;
2669
4832
  let inlineModels = [];
2670
4833
  let isPaginated = false;
2671
4834
  let dataPath;
2672
4835
  let itemType;
4836
+ let envelopeFields;
4837
+ let envelopeCompanions;
2673
4838
  if (!responses) return {
2674
4839
  response,
2675
4840
  errors,
@@ -2677,8 +4842,11 @@ function extractResponses(responses, op, path, method, componentSchemas) {
2677
4842
  isPaginated
2678
4843
  };
2679
4844
  const allSuccessResponses = [];
4845
+ const opLabel = operationLabel(op, path ?? "/", method ?? "get");
4846
+ const headerModels = [];
2680
4847
  for (const [statusCode, resp] of Object.entries(responses)) {
2681
4848
  const code = parseInt(statusCode, 10);
4849
+ const headers = code >= 200 ? extractResponseHeaders(resp.headers, opLabel, code, headerContext, headerModels) : void 0;
2682
4850
  if (code >= 200 && code < 300) {
2683
4851
  let extractedType = {
2684
4852
  kind: "primitive",
@@ -2686,62 +4854,75 @@ function extractResponses(responses, op, path, method, componentSchemas) {
2686
4854
  };
2687
4855
  const jsonContent = resp.content?.["application/json"];
2688
4856
  if (jsonContent?.schema) {
2689
- const contextName = deriveResponseName(op, path ?? "/", method ?? "get");
4857
+ const contextName = deriveResponseName(op, pathModelBaseName);
2690
4858
  const result = classifyAndExtractResponse(jsonContent.schema, contextName, componentSchemas);
2691
4859
  extractedType = result.response;
2692
4860
  inlineModels = result.inlineModels;
2693
4861
  isPaginated = result.isPaginated;
2694
4862
  dataPath = result.dataPath;
2695
4863
  itemType = result.itemType;
2696
- } else if (resp.content) if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) extractedType = {
2697
- kind: "primitive",
2698
- type: "string",
2699
- format: "binary"
2700
- };
2701
- else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) extractedType = {
2702
- kind: "primitive",
2703
- type: "string"
2704
- };
2705
- else {
2706
- const firstKey = Object.keys(resp.content)[0];
2707
- if (firstKey) {
2708
- const firstContent = resp.content[firstKey];
2709
- if (firstContent?.schema) extractedType = schemaToTypeRef(firstContent.schema, deriveResponseName(op, path ?? "/", method ?? "get"));
4864
+ envelopeFields = result.envelopeFields;
4865
+ envelopeCompanions = result.envelopeCompanions;
4866
+ } else if (resp.content) {
4867
+ if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) extractedType = {
4868
+ kind: "primitive",
4869
+ type: "string",
4870
+ format: "binary"
4871
+ };
4872
+ else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) extractedType = {
4873
+ kind: "primitive",
4874
+ type: "string"
4875
+ };
4876
+ else {
4877
+ const firstKey = Object.keys(resp.content)[0];
4878
+ if (firstKey) {
4879
+ const firstContent = resp.content[firstKey];
4880
+ if (firstContent?.schema) extractedType = schemaToTypeRef(firstContent.schema, deriveResponseName(op, pathModelBaseName));
4881
+ }
2710
4882
  }
2711
4883
  }
2712
4884
  allSuccessResponses.push({
2713
4885
  statusCode: code,
2714
- type: extractedType
4886
+ type: extractedType,
4887
+ ...headers && { headers }
2715
4888
  });
2716
4889
  } else if (code >= 300 && code < 400) allSuccessResponses.push({
2717
4890
  statusCode: code,
2718
4891
  type: {
2719
4892
  kind: "primitive",
2720
4893
  type: "unknown"
2721
- }
4894
+ },
4895
+ ...headers && { headers }
2722
4896
  });
2723
4897
  else if (code >= 400) {
2724
4898
  const jsonContent = resp.content?.["application/json"];
2725
4899
  const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
2726
4900
  errors.push({
2727
4901
  statusCode: code,
2728
- type
4902
+ type,
4903
+ ...headers && { headers }
2729
4904
  });
2730
4905
  }
2731
4906
  }
2732
4907
  if (allSuccessResponses.length > 0) {
2733
4908
  const sorted = [...allSuccessResponses].sort((a, b) => a.statusCode - b.statusCode);
2734
- response = (sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0]).type;
4909
+ const primary = sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0];
4910
+ response = primary.type;
4911
+ responseHeaders = primary.headers;
2735
4912
  }
2736
4913
  const successResponses = allSuccessResponses.length > 1 ? allSuccessResponses : void 0;
4914
+ if (headerModels.length > 0) inlineModels = [...inlineModels, ...headerModels];
2737
4915
  return {
2738
4916
  response,
4917
+ responseHeaders,
2739
4918
  successResponses,
2740
4919
  errors,
2741
4920
  inlineModels,
2742
4921
  isPaginated,
2743
4922
  dataPath,
2744
- itemType
4923
+ itemType,
4924
+ envelopeFields,
4925
+ envelopeCompanions
2745
4926
  };
2746
4927
  }
2747
4928
  //#endregion
@@ -2750,10 +4931,25 @@ function keepLargestByName(models) {
2750
4931
  const byName = /* @__PURE__ */ new Map();
2751
4932
  for (const model of models) {
2752
4933
  const existing = byName.get(model.name);
4934
+ if (existing && !haveSameFields(existing, model)) {
4935
+ const kept = model.fields.length === existing.fields.length ? "keeping the first definition" : "keeping the one with more fields";
4936
+ console.warn(`[oagen] Warning: Inline model "${model.name}" has more than one definition with different fields or field types (${model.fields.length} vs ${existing.fields.length}) — ${kept}`);
4937
+ }
2753
4938
  if (!existing || model.fields.length > existing.fields.length) byName.set(model.name, model);
2754
4939
  }
2755
4940
  return [...byName.values()];
2756
4941
  }
4942
+ /**
4943
+ * Same field names with the same types, in any order. Types are compared by
4944
+ * their JSON serialization: the parser builds a given shape's TypeRef with the
4945
+ * same key order every time, so equal shapes serialize identically.
4946
+ */
4947
+ function haveSameFields(a, b) {
4948
+ const signatures = (m) => new Set(m.fields.map((f) => `${f.name}:${JSON.stringify(f.type)}`));
4949
+ const aSignatures = signatures(a);
4950
+ const bSignatures = signatures(b);
4951
+ return aSignatures.size === bSignatures.size && [...aSignatures].every((s) => bSignatures.has(s));
4952
+ }
2757
4953
  function rewriteModelRef(ref, oldName, newName) {
2758
4954
  walkTypeRef(ref, { model: (r) => {
2759
4955
  if (r.name === oldName) r.name = newName;
@@ -2764,6 +4960,8 @@ function rewriteModelRefs(models, services, oldName, newName) {
2764
4960
  for (const service of services) for (const op of service.operations) {
2765
4961
  rewriteModelRef(op.response, oldName, newName);
2766
4962
  if (op.requestBody) rewriteModelRef(op.requestBody, oldName, newName);
4963
+ for (const field of op.pagination?.envelopeFields ?? []) rewriteModelRef(field.type, oldName, newName);
4964
+ for (const header of collectResponseHeaders(op)) rewriteModelRef(header.type, oldName, newName);
2767
4965
  }
2768
4966
  }
2769
4967
  function mergeInlineResponseModels(schemaModels, inlineModels) {
@@ -2786,15 +4984,25 @@ function mergeInlineResponseModels(schemaModels, inlineModels) {
2786
4984
  });
2787
4985
  return [...mergedSchemaModels, ...keepLargestByName(deduplicatedInlineModels)];
2788
4986
  }
2789
- function mergeFieldInlineModels(existingModels, fieldInlineModels) {
4987
+ /**
4988
+ * Fold the field extractor's inline models into the models gathered so far,
4989
+ * keeping the earlier definition on a name clash. `componentNames` tells a
4990
+ * clash with a real component schema apart from one between two inline
4991
+ * models, so the warning can say which happened and name both shapes.
4992
+ */
4993
+ function mergeFieldInlineModels(existingModels, fieldInlineModels, componentNames) {
2790
4994
  const modelNames = new Set(existingModels.map((m) => m.name));
2791
4995
  const modelsByName = new Map(existingModels.map((m) => [m.name, m]));
4996
+ const fieldList = (m) => m.fields.map((f) => f.name).join(", ") || "(no fields)";
2792
4997
  const deduplicatedFieldModels = fieldInlineModels.filter((model) => {
2793
4998
  if (!modelNames.has(model.name)) return true;
2794
4999
  const existing = modelsByName.get(model.name);
2795
5000
  const existingFieldNames = new Set(existing.fields.map((f) => f.name));
2796
5001
  const inlineFieldNames = new Set(model.fields.map((f) => f.name));
2797
- if (model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name))) console.warn(`[oagen] Warning: Inline model "${model.name}" has different fields than component schema — using component schema`);
5002
+ if (model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name))) {
5003
+ if (componentNames && !componentNames.has(model.name)) console.warn(`[oagen] Warning: Two inline models are named "${model.name}" (fields: ${fieldList(existing)} | ${fieldList(model)}) — keeping the first, the second is dropped`);
5004
+ else console.warn(`[oagen] Warning: Inline model "${model.name}" has different fields than component schema — using component schema`);
5005
+ }
2798
5006
  return false;
2799
5007
  });
2800
5008
  return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
@@ -2826,7 +5034,7 @@ function collapseJsonSuffixModels(models, services) {
2826
5034
  if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
2827
5035
  if (!baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name))) continue;
2828
5036
  if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
2829
- if (isModelReferencedByOthers(model.name, normalizedModels, new Set([model.name, baseName]))) continue;
5037
+ if (isModelReferencedByOthers(model.name, normalizedModels, /* @__PURE__ */ new Set([model.name, baseName]))) continue;
2830
5038
  mergeCandidates.push({
2831
5039
  jsonModel: model,
2832
5040
  baseModel
@@ -2835,6 +5043,10 @@ function collapseJsonSuffixModels(models, services) {
2835
5043
  const toRemove = /* @__PURE__ */ new Set();
2836
5044
  for (const { jsonModel, baseModel } of mergeCandidates) {
2837
5045
  baseModel.fields = jsonModel.fields;
5046
+ if (jsonModel.constraints || baseModel.constraints) baseModel.constraints = {
5047
+ ...baseModel.constraints,
5048
+ ...jsonModel.constraints
5049
+ };
2838
5050
  toRemove.add(jsonModel.name);
2839
5051
  rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
2840
5052
  console.warn(`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`);
@@ -2870,16 +5082,82 @@ function collectInlineEnumsFromModels(models, enums) {
2870
5082
  }
2871
5083
  /**
2872
5084
  * Collect inline enum definitions from operation parameters (query, path, header)
2873
- * and promote them to top-level enums so emitters can generate typed enum files.
5085
+ * list-envelope companion fields, and response headers, and promote them to
5086
+ * top-level enums so emitters can generate typed enum files.
2874
5087
  */
2875
5088
  function collectInlineEnumsFromOperations(services, enums) {
2876
5089
  const enumNames = new Set(enums.map((e) => e.name));
2877
- for (const service of services) for (const op of service.operations) for (const param of [
2878
- ...op.pathParams,
2879
- ...op.queryParams,
2880
- ...op.headerParams,
2881
- ...op.cookieParams ?? []
2882
- ]) collectInlineEnumFromRef(param.type, enums, enumNames);
5090
+ for (const service of services) for (const op of service.operations) {
5091
+ for (const param of [
5092
+ ...op.pathParams,
5093
+ ...op.queryParams,
5094
+ ...op.bodyOwnedQueryParams ?? [],
5095
+ ...op.headerParams,
5096
+ ...op.cookieParams ?? []
5097
+ ]) collectInlineEnumFromRef(param.type, enums, enumNames);
5098
+ for (const field of op.pagination?.envelopeFields ?? []) collectInlineEnumFromRef(field.type, enums, enumNames);
5099
+ for (const { headers, qualifier } of responseHeaderGroups(op)) for (const header of headers) collectHeaderEnums(header, qualifier, enums, enumNames);
5100
+ }
5101
+ }
5102
+ /**
5103
+ * Header lists to walk, each with the status-code qualifier that keeps a
5104
+ * colliding enum name apart. When `successResponses` exists the primary list
5105
+ * is the same array as its entry there, so the entries are walked with their
5106
+ * own codes instead of `responseHeaders`; a lone 2xx carries no status code
5107
+ * in the IR, so its headers get no qualifier.
5108
+ */
5109
+ function responseHeaderGroups(op) {
5110
+ const groups = [];
5111
+ if (op.successResponses) {
5112
+ for (const entry of op.successResponses) if (entry.headers) groups.push({
5113
+ headers: entry.headers,
5114
+ qualifier: String(entry.statusCode)
5115
+ });
5116
+ } else if (op.responseHeaders) groups.push({ headers: op.responseHeaders });
5117
+ for (const error of op.errors) if (error.headers) groups.push({
5118
+ headers: error.headers,
5119
+ qualifier: String(error.statusCode)
5120
+ });
5121
+ return groups;
5122
+ }
5123
+ /**
5124
+ * Promote a header's inline enums. A header enum is named from the service
5125
+ * and header name alone, so `X-Policy` allowing `fast`/`slow` on a 200 and
5126
+ * `retry`/`stop` on a 429 would share one name and whichever was collected
5127
+ * first would silently win; a request parameter named like the header
5128
+ * collides the same way. When the name is already taken by a different value
5129
+ * set, the header's ref is renamed to a distinct name before it is promoted.
5130
+ * A taken name with the same values is shared, as before.
5131
+ */
5132
+ function collectHeaderEnums(header, qualifier, enums, enumNames) {
5133
+ walkTypeRef(header.type, { enum: (ref) => {
5134
+ if (!ref.values) return;
5135
+ const existing = enums.find((e) => e.name === ref.name);
5136
+ if (existing && !sameEnumValues(existing, ref.values)) ref.name = distinctEnumName(ref.name, ref.values, qualifier, enums);
5137
+ collectInlineEnumFromRef(ref, enums, enumNames);
5138
+ } });
5139
+ }
5140
+ /** Order-insensitive: a reordering of the same members is the same type. */
5141
+ function sameEnumValues(existing, values) {
5142
+ if (existing.values.length !== values.length) return false;
5143
+ const have = new Set(existing.values.map((v) => v.value));
5144
+ return values.every((v) => have.has(v));
5145
+ }
5146
+ /**
5147
+ * The first name not taken by a different value set: the base qualified by
5148
+ * the status code (`DocumentsXPolicy429`), then numeric suffixes
5149
+ * (`DocumentsXPolicy2`, `DocumentsXPolicy3`, ...). Deterministic for a given
5150
+ * spec, since it depends only on declaration order.
5151
+ */
5152
+ function distinctEnumName(base, values, qualifier, enums) {
5153
+ const isUsable = (name) => {
5154
+ const taken = enums.find((e) => e.name === name);
5155
+ return !taken || sameEnumValues(taken, values);
5156
+ };
5157
+ if (qualifier && isUsable(`${base}${qualifier}`)) return `${base}${qualifier}`;
5158
+ let suffix = 2;
5159
+ while (!isUsable(`${base}${suffix}`)) suffix++;
5160
+ return `${base}${suffix}`;
2883
5161
  }
2884
5162
  //#endregion
2885
5163
  //#region src/parser/parameter-group-names.ts
@@ -3153,10 +5431,13 @@ function validateModelRefs(spec) {
3153
5431
  for (const p of [
3154
5432
  ...op.pathParams,
3155
5433
  ...op.queryParams,
5434
+ ...op.bodyOwnedQueryParams ?? [],
3156
5435
  ...op.headerParams
3157
5436
  ]) walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
3158
5437
  if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
3159
5438
  walkRef(op.response, `${service.name}.${op.name}.response`);
5439
+ for (const field of op.pagination?.envelopeFields ?? []) walkRef(field.type, `${service.name}.${op.name}.pagination.${field.name}`);
5440
+ for (const header of collectResponseHeaders(op)) walkRef(header.type, `${service.name}.${op.name}.headers.${header.name}`);
3160
5441
  }
3161
5442
  }
3162
5443
  //#endregion
@@ -3167,11 +5448,12 @@ async function parseSpec(specPath, options) {
3167
5448
  const version = spec.openapi ?? "";
3168
5449
  if (!version.startsWith("3.")) throw new SpecParseError(`Unsupported OpenAPI version: ${version}. oagen requires OpenAPI 3.x`, `Update the spec to OpenAPI 3.0 or 3.1. If you are using Swagger 2.x, convert it first with \`npx swagger2openapi ${specPath}\`.`);
3169
5450
  const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
3170
- const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters);
5451
+ const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters, spec.components?.requestBodies, models, spec.tags, spec.components?.examples, spec.components?.headers);
3171
5452
  const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
3172
5453
  const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
5454
+ const componentNames = new Set(Object.keys(spec.components?.schemas ?? {}).map((schemaName) => resolveSchemaName(schemaName)));
3173
5455
  clearSchemaNameTransform();
3174
- const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels), services);
5456
+ const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels, componentNames), services);
3175
5457
  applyFieldHints(finalModels, options?.fieldHints);
3176
5458
  collectInlineEnumsFromModels(finalModels, enums);
3177
5459
  collectInlineEnumsFromOperations(services, enums);
@@ -3190,6 +5472,7 @@ async function parseSpec(specPath, options) {
3190
5472
  models: finalModels,
3191
5473
  enums,
3192
5474
  auth,
5475
+ security: extractSecurityRequirements(spec.security, "the document"),
3193
5476
  sdk: defaultSdkBehavior()
3194
5477
  };
3195
5478
  assignParameterGroupWrapperNames(result);
@@ -3212,20 +5495,47 @@ function applyFieldHints(models, fieldHints) {
3212
5495
  }
3213
5496
  }
3214
5497
  }
3215
- /** Extract authentication schemes from OpenAPI securitySchemes. */
5498
+ /**
5499
+ * Extract authentication schemes from OpenAPI securitySchemes.
5500
+ *
5501
+ * Document order is preserved. Each entry carries its `securitySchemes` key as
5502
+ * `schemeName` so emitters can join the requirements under `ApiSpec.security`
5503
+ * and `Operation.security` back to it.
5504
+ * Schemes the IR cannot represent are omitted with a warning rather than silently.
5505
+ */
3216
5506
  function extractAuthSchemes(securitySchemes) {
3217
5507
  if (!securitySchemes) return void 0;
3218
5508
  const schemes = [];
3219
- for (const [, scheme] of Object.entries(securitySchemes)) if (scheme.type === "http" && scheme.scheme === "bearer") schemes.push({ kind: "bearer" });
3220
- else if (scheme.type === "apiKey" && scheme.in && scheme.name) schemes.push({
3221
- kind: "apiKey",
3222
- in: scheme.in,
3223
- name: scheme.name
3224
- });
3225
- else if (scheme.type === "oauth2" && scheme.flows) schemes.push({
3226
- kind: "oauth2",
3227
- flows: scheme.flows
3228
- });
5509
+ for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
5510
+ const common = scheme.description !== void 0 ? {
5511
+ schemeName,
5512
+ description: scheme.description
5513
+ } : { schemeName };
5514
+ const httpScheme = scheme.type === "http" ? scheme.scheme?.toLowerCase() : void 0;
5515
+ if (httpScheme === "bearer") schemes.push({
5516
+ kind: "bearer",
5517
+ ...common
5518
+ });
5519
+ else if (httpScheme === "basic") schemes.push({
5520
+ kind: "basic",
5521
+ ...common
5522
+ });
5523
+ else if (scheme.type === "apiKey" && scheme.in && scheme.name) schemes.push({
5524
+ kind: "apiKey",
5525
+ ...common,
5526
+ in: scheme.in,
5527
+ name: scheme.name
5528
+ });
5529
+ else if (scheme.type === "oauth2" && scheme.flows) schemes.push({
5530
+ kind: "oauth2",
5531
+ ...common,
5532
+ flows: scheme.flows
5533
+ });
5534
+ else {
5535
+ const detail = scheme.type === "http" ? `type: http, scheme: ${scheme.scheme}` : `type: ${scheme.type}`;
5536
+ console.warn(`[oagen] Warning: Unsupported security scheme "${schemeName}" (${detail}) omitted from spec.auth`);
5537
+ }
5538
+ }
3229
5539
  return schemes.length > 0 ? schemes : void 0;
3230
5540
  }
3231
5541
  //#endregion
@@ -3326,7 +5636,7 @@ const DOTNET_URL_FINGERPRINT_CONFIG = {
3326
5636
  contentNodeTypes: ["string_content", "string_literal_content"],
3327
5637
  interpolationNodeTypes: ["interpolation"]
3328
5638
  };
3329
- const DECLARATION_TYPES = new Set([
5639
+ const DECLARATION_TYPES = /* @__PURE__ */ new Set([
3330
5640
  "class_declaration",
3331
5641
  "struct_declaration",
3332
5642
  "interface_declaration",
@@ -3346,11 +5656,12 @@ function collectPrecedingXmlDocComments(children, index, source) {
3346
5656
  let firstIdx = -1;
3347
5657
  for (let k = index - 1; k >= 0; k--) {
3348
5658
  const prev = children[k];
3349
- if (prev.type === "comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
3350
- if (lastIdx === -1) lastIdx = k;
3351
- firstIdx = k;
5659
+ if (prev.type === "comment") {
5660
+ if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
5661
+ if (lastIdx === -1) lastIdx = k;
5662
+ firstIdx = k;
5663
+ } else break;
3352
5664
  } else break;
3353
- else break;
3354
5665
  }
3355
5666
  if (firstIdx === -1) return null;
3356
5667
  const first = children[firstIdx];
@@ -3665,11 +5976,12 @@ function collectPrecedingGoComments(children, index, source) {
3665
5976
  let firstIdx = -1;
3666
5977
  for (let k = index - 1; k >= 0; k--) {
3667
5978
  const prev = children[k];
3668
- if (prev.type === "comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("//")) {
3669
- if (lastIdx === -1) lastIdx = k;
3670
- firstIdx = k;
5979
+ if (prev.type === "comment") {
5980
+ if (source.slice(prev.startIndex, prev.endIndex).startsWith("//")) {
5981
+ if (lastIdx === -1) lastIdx = k;
5982
+ firstIdx = k;
5983
+ } else break;
3671
5984
  } else break;
3672
- else break;
3673
5985
  }
3674
5986
  if (firstIdx === -1) return null;
3675
5987
  const first = children[firstIdx];
@@ -4033,9 +6345,9 @@ function findPrecedingDocstring(children, index, source) {
4033
6345
  }
4034
6346
  return null;
4035
6347
  }
4036
- const CLASS_MEMBER_TYPES = new Set(["method_definition", "public_field_definition"]);
4037
- const INTERFACE_MEMBER_TYPES = new Set(["property_signature", "method_signature"]);
4038
- const ENUM_MEMBER_TYPES = new Set(["enum_assignment", "property_identifier"]);
6348
+ const CLASS_MEMBER_TYPES = /* @__PURE__ */ new Set(["method_definition", "public_field_definition"]);
6349
+ const INTERFACE_MEMBER_TYPES = /* @__PURE__ */ new Set(["property_signature", "method_signature"]);
6350
+ const ENUM_MEMBER_TYPES = /* @__PURE__ */ new Set(["enum_assignment", "property_identifier"]);
4039
6351
  const NODE_URL_FINGERPRINT_CONFIG = {
4040
6352
  stringNodeTypes: ["string", "template_string"],
4041
6353
  contentNodeTypes: ["string_fragment"],
@@ -4447,11 +6759,12 @@ function collectPrecedingRubyComments(children, index, source) {
4447
6759
  let firstIdx = -1;
4448
6760
  for (let k = index - 1; k >= 0; k--) {
4449
6761
  const prev = children[k];
4450
- if ((prev.grammarType ?? prev.type) === "comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("#")) {
4451
- if (lastIdx === -1) lastIdx = k;
4452
- firstIdx = k;
6762
+ if ((prev.grammarType ?? prev.type) === "comment") {
6763
+ if (source.slice(prev.startIndex, prev.endIndex).startsWith("#")) {
6764
+ if (lastIdx === -1) lastIdx = k;
6765
+ firstIdx = k;
6766
+ } else break;
4453
6767
  } else break;
4454
- else break;
4455
6768
  }
4456
6769
  if (firstIdx === -1) return null;
4457
6770
  const first = children[firstIdx];
@@ -4579,11 +6892,12 @@ function collectPrecedingRustDocComments(children, index, source) {
4579
6892
  let firstIdx = -1;
4580
6893
  for (let k = index - 1; k >= 0; k--) {
4581
6894
  const prev = children[k];
4582
- if (prev.type === "line_comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
4583
- if (lastIdx === -1) lastIdx = k;
4584
- firstIdx = k;
6895
+ if (prev.type === "line_comment") {
6896
+ if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
6897
+ if (lastIdx === -1) lastIdx = k;
6898
+ firstIdx = k;
6899
+ } else break;
4585
6900
  } else break;
4586
- else break;
4587
6901
  }
4588
6902
  if (firstIdx === -1) return null;
4589
6903
  const first = children[firstIdx];
@@ -4596,7 +6910,7 @@ function collectPrecedingRustDocComments(children, index, source) {
4596
6910
  }
4597
6911
  //#endregion
4598
6912
  //#region src/engine/merge-adapters/index.ts
4599
- const adapters = new Map([
6913
+ const adapters = /* @__PURE__ */ new Map([
4600
6914
  ["dotnet", dotnetMergeAdapter],
4601
6915
  ["elixir", elixirMergeAdapter],
4602
6916
  ["go", goMergeAdapter],
@@ -5177,7 +7491,7 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
5177
7491
  if (names.length === 0) continue;
5178
7492
  if (!names.some((n) => referencesIdentifier(prunedText, n))) continue;
5179
7493
  const line = imp.text.trim();
5180
- const withoutImport = removeLines(codeOnly, new Set([line]));
7494
+ const withoutImport = removeLines(codeOnly, /* @__PURE__ */ new Set([line]));
5181
7495
  if (names.some((n) => referencesIdentifier(withoutImport, n))) continue;
5182
7496
  orphaned.add(line);
5183
7497
  }
@@ -5197,26 +7511,28 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
5197
7511
  if (!existInfo) continue;
5198
7512
  if (docIgnoredSymbols.has(symbolName)) continue;
5199
7513
  const genDoc = genInfo.docstring && genInfo.docstring.text.trim() !== headerLine ? genInfo.docstring : null;
5200
- if (genDoc) if (existInfo.docstring) {
5201
- if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
5202
- const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
5203
- const range = resolveDocRange(result, existInfo.docstring.text, existInfo.declStartIndex, existInfo.docstring.startIndex, existInfo.docstring.endIndex);
7514
+ if (genDoc) {
7515
+ if (existInfo.docstring) {
7516
+ if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
7517
+ const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
7518
+ const range = resolveDocRange(result, existInfo.docstring.text, existInfo.declStartIndex, existInfo.docstring.startIndex, existInfo.docstring.endIndex);
7519
+ edits.push({
7520
+ start: range.start,
7521
+ end: range.end,
7522
+ newText
7523
+ });
7524
+ docstringUpdates++;
7525
+ }
7526
+ } else {
7527
+ const lineStart = declarationLineStart(result, existInfo.declStartIndex);
7528
+ const indent = " ".repeat(existInfo.declColumn);
5204
7529
  edits.push({
5205
- start: range.start,
5206
- end: range.end,
5207
- newText
7530
+ start: lineStart,
7531
+ end: lineStart,
7532
+ newText: indent + genDoc.text + "\n"
5208
7533
  });
5209
7534
  docstringUpdates++;
5210
7535
  }
5211
- } else {
5212
- const lineStart = declarationLineStart(result, existInfo.declStartIndex);
5213
- const indent = " ".repeat(existInfo.declColumn);
5214
- edits.push({
5215
- start: lineStart,
5216
- end: lineStart,
5217
- newText: indent + genDoc.text + "\n"
5218
- });
5219
- docstringUpdates++;
5220
7536
  }
5221
7537
  const matchedExistMembers = /* @__PURE__ */ new Set();
5222
7538
  for (const [memberName, genMember] of genInfo.members) {
@@ -5556,8 +7872,10 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
5556
7872
  if (outcome.edited) regionDeclarationEdits++;
5557
7873
  if (!alreadyPlaced && !outcome.placed) remaining.push(entry);
5558
7874
  }
5559
- if (!alreadyPlaced) if (remaining.length === 0) blocksByClass.delete("");
5560
- else blocksByClass.set("", remaining);
7875
+ if (!alreadyPlaced) {
7876
+ if (remaining.length === 0) blocksByClass.delete("");
7877
+ else blocksByClass.set("", remaining);
7878
+ }
5561
7879
  }
5562
7880
  }
5563
7881
  let effectiveClassEndLines = classEndLines;
@@ -5576,19 +7894,21 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
5576
7894
  break;
5577
7895
  }
5578
7896
  }
5579
- if (insertLine === void 0) if (info) insertLine = info.bodyEndLine;
5580
- else {
5581
- const scopeEndLine = entry.containingScope ? findDescribeScopeEndLine(result, entry.containingScope) : void 0;
5582
- const blockHasImportsFallback = entry.lines.some((l) => /^\s*(import|from)\s/.test(l));
5583
- if (scopeEndLine !== void 0) insertLine = scopeEndLine;
5584
- else if (blockHasImportsFallback || hasTopLevelDeclaration(entry.lines)) {
5585
- let lastImportLine = 0;
5586
- for (let li = 0; li < result.length; li++) {
5587
- const trimmed = result[li].trimStart();
5588
- if (/^(import|from)\s/.test(trimmed)) lastImportLine = li + 1;
5589
- }
5590
- insertLine = lastImportLine;
5591
- } else insertLine = result.length;
7897
+ if (insertLine === void 0) {
7898
+ if (info) insertLine = info.bodyEndLine;
7899
+ else {
7900
+ const scopeEndLine = entry.containingScope ? findDescribeScopeEndLine(result, entry.containingScope) : void 0;
7901
+ const blockHasImportsFallback = entry.lines.some((l) => /^\s*(import|from)\s/.test(l));
7902
+ if (scopeEndLine !== void 0) insertLine = scopeEndLine;
7903
+ else if (blockHasImportsFallback || hasTopLevelDeclaration(entry.lines)) {
7904
+ let lastImportLine = 0;
7905
+ for (let li = 0; li < result.length; li++) {
7906
+ const trimmed = result[li].trimStart();
7907
+ if (/^(import|from)\s/.test(trimmed)) lastImportLine = li + 1;
7908
+ }
7909
+ insertLine = lastImportLine;
7910
+ } else insertLine = result.length;
7911
+ }
5592
7912
  }
5593
7913
  if (entry.lines.some((l) => /^\s*import\s/.test(l))) while (insertLine >= 2 && result[insertLine - 1]?.trim() === "" && /^\s*(import|using|from)\s/.test(result[insertLine - 2] ?? "")) insertLine--;
5594
7914
  const prevLine = insertLine > 0 ? result[insertLine - 1]?.trimStart() ?? "" : "";
@@ -6198,7 +8518,7 @@ function toSnake(s) {
6198
8518
  * When a path ends with one of these, we use it as the verb instead of
6199
8519
  * deriving a CRUD verb from the HTTP method.
6200
8520
  */
6201
- const ACTION_VERBS = new Set([
8521
+ const ACTION_VERBS = /* @__PURE__ */ new Set([
6202
8522
  "accept",
6203
8523
  "activate",
6204
8524
  "authorize",
@@ -6513,7 +8833,7 @@ function aggregateSurfaceServices(spec, ctx) {
6513
8833
  /**
6514
8834
  * Collect model and enum names transitively referenced by service operations.
6515
8835
  * Walks operation parameters, request bodies, responses, and pagination item
6516
- * types, then chases model field references until the set stabilizes.
8836
+ * and envelope types, then chases model field references until the set stabilizes.
6517
8837
  */
6518
8838
  function collectReferencedNames(services, models, opts = {}) {
6519
8839
  const preserveAllDiscriminated = opts.preserveAllDiscriminated !== false;
@@ -6529,12 +8849,17 @@ function collectReferencedNames(services, models, opts = {}) {
6529
8849
  for (const p of [
6530
8850
  ...op.pathParams,
6531
8851
  ...op.queryParams,
8852
+ ...op.bodyOwnedQueryParams ?? [],
6532
8853
  ...op.headerParams,
6533
8854
  ...op.cookieParams ?? []
6534
8855
  ]) collectFromTypeRef(p.type);
6535
8856
  if (op.requestBody) collectFromTypeRef(op.requestBody);
6536
8857
  collectFromTypeRef(op.response);
6537
- if (op.pagination) collectFromTypeRef(op.pagination.itemType);
8858
+ if (op.pagination) {
8859
+ collectFromTypeRef(op.pagination.itemType);
8860
+ for (const field of op.pagination.envelopeFields ?? []) collectFromTypeRef(field.type);
8861
+ }
8862
+ for (const header of collectResponseHeaders(op)) collectFromTypeRef(header.type);
6538
8863
  for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
6539
8864
  if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
6540
8865
  }
@@ -6696,7 +9021,7 @@ async function treeShakeFiles(files, language) {
6696
9021
  }
6697
9022
  deps.set(file.path, fileDeps);
6698
9023
  }
6699
- const codeExtensions = new Set([
9024
+ const codeExtensions = /* @__PURE__ */ new Set([
6700
9025
  ".ts",
6701
9026
  ".js",
6702
9027
  ".tsx",
@@ -6778,11 +9103,10 @@ async function resolveFileToDirectoryConflicts(files, targetDir) {
6778
9103
  content: moduleContent,
6779
9104
  overwriteExisting: true
6780
9105
  });
6781
- const compatImport = "from ._compat import * # noqa: F401,F403 # pyright: ignore[reportUnusedImport]";
6782
9106
  const existingContent = file.content.endsWith("\n") ? file.content : file.content + "\n";
6783
9107
  result[i] = {
6784
9108
  ...file,
6785
- content: existingContent + compatImport + "\n"
9109
+ content: existingContent + "from ._compat import * # noqa: F401,F403 # pyright: ignore[reportUnusedImport]\n"
6786
9110
  };
6787
9111
  removals.push(fullModulePath);
6788
9112
  }
@@ -6833,7 +9157,7 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
6833
9157
  try {
6834
9158
  await execFileAsync(formatCmd.cmd, [...formatCmd.args, ...batch], {
6835
9159
  cwd: targetDir,
6836
- maxBuffer: 10 * 1024 * 1024,
9160
+ maxBuffer: 10485760,
6837
9161
  timeout: 12e4
6838
9162
  });
6839
9163
  } catch (err) {
@@ -6929,7 +9253,7 @@ function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
6929
9253
  files: scopedFileList,
6930
9254
  operations: scopedOperations
6931
9255
  };
6932
- const files = [...new Set([...prev.files, ...scopedFileList])].sort();
9256
+ const files = [.../* @__PURE__ */ new Set([...prev.files, ...scopedFileList])].sort();
6933
9257
  const priorOps = prev.operations;
6934
9258
  return {
6935
9259
  files,
@@ -7160,7 +9484,7 @@ function getEmitter(language) {
7160
9484
  //#endregion
7161
9485
  //#region src/compat/language-hints.ts
7162
9486
  /** Untyped map patterns — hoisted to avoid allocation on every isTypeEquivalent call. */
7163
- const UNTYPED_MAP_PATTERNS = new Set([
9487
+ const UNTYPED_MAP_PATTERNS = /* @__PURE__ */ new Set([
7164
9488
  "Record<string, unknown>",
7165
9489
  "Record<string, any>",
7166
9490
  "{ [key: string]: any; }",
@@ -7625,7 +9949,7 @@ function buildModelNameMap(surface, spec, lookup, hints, options) {
7625
9949
  }
7626
9950
  }
7627
9951
  /** Categories that can be patched by the overlay retry loop. */
7628
- const PATCHABLE_CATEGORIES = new Set(["symbol_removed", "symbol_renamed"]);
9952
+ const PATCHABLE_CATEGORIES = /* @__PURE__ */ new Set(["symbol_removed", "symbol_renamed"]);
7629
9953
  /** Check if a classified change is patchable by the overlay loop. */
7630
9954
  function isPatchableChange(change) {
7631
9955
  return PATCHABLE_CATEGORIES.has(change.category);
@@ -7784,6 +10108,7 @@ function expandDocUrls(spec, docUrl) {
7784
10108
  description: expand(service.description),
7785
10109
  operations: service.operations.map((op) => ({
7786
10110
  ...op,
10111
+ ...op.summary ? { summary: expand(op.summary) } : {},
7787
10112
  description: expand(op.description),
7788
10113
  pathParams: op.pathParams.map((p) => ({
7789
10114
  ...p,
@@ -7793,6 +10118,10 @@ function expandDocUrls(spec, docUrl) {
7793
10118
  ...p,
7794
10119
  description: expand(p.description)
7795
10120
  })),
10121
+ bodyOwnedQueryParams: op.bodyOwnedQueryParams?.map((p) => ({
10122
+ ...p,
10123
+ description: expand(p.description)
10124
+ })),
7796
10125
  headerParams: op.headerParams.map((p) => ({
7797
10126
  ...p,
7798
10127
  description: expand(p.description)
@@ -7886,10 +10215,13 @@ function buildDirectionIndex(...specs) {
7886
10215
  for (const param of [
7887
10216
  ...op.pathParams,
7888
10217
  ...op.queryParams,
10218
+ ...op.bodyOwnedQueryParams ?? [],
7889
10219
  ...op.headerParams,
7890
10220
  ...op.cookieParams ?? []
7891
10221
  ]) requestSeeds.push(param.type);
7892
10222
  responseSeeds.push(op.response);
10223
+ for (const field of op.pagination?.envelopeFields ?? []) responseSeeds.push(field.type);
10224
+ for (const header of collectResponseHeaders(op)) responseSeeds.push(header.type);
7893
10225
  for (const success of op.successResponses ?? []) responseSeeds.push(success.type);
7894
10226
  for (const error of op.errors) if (error.type) responseSeeds.push(error.type);
7895
10227
  }
@@ -7937,12 +10269,7 @@ function reachableModels(seeds, models) {
7937
10269
  case "map":
7938
10270
  queue.push(ref.valueType);
7939
10271
  break;
7940
- case "union":
7941
- queue.push(...ref.variants);
7942
- break;
7943
- case "primitive":
7944
- case "enum":
7945
- case "literal": break;
10272
+ case "union": queue.push(...ref.variants);
7946
10273
  }
7947
10274
  }
7948
10275
  return found;
@@ -8071,16 +10398,18 @@ function diffFields(oldModel, newModel, direction = "unknown") {
8071
10398
  for (const [name, newField] of newByName) {
8072
10399
  const oldField = oldByName.get(name);
8073
10400
  if (!oldField) continue;
8074
- if (!typeRefsEqual(oldField.type, newField.type)) if (primitivesDifferOnlyByFormat(oldField.type, newField.type)) changes.push({
8075
- ...classifyFieldChange("field-format-changed", name),
8076
- details: formatChangeDetails(oldField.type, newField.type)
8077
- });
8078
- else changes.push({
8079
- kind: "field-type-changed",
8080
- fieldName: name,
8081
- classification: "breaking",
8082
- details: `type changed`
8083
- });
10401
+ if (!typeRefsEqual(oldField.type, newField.type)) {
10402
+ if (primitivesDifferOnlyByFormat(oldField.type, newField.type)) changes.push({
10403
+ ...classifyFieldChange("field-format-changed", name),
10404
+ details: formatChangeDetails(oldField.type, newField.type)
10405
+ });
10406
+ else changes.push({
10407
+ kind: "field-type-changed",
10408
+ fieldName: name,
10409
+ classification: "breaking",
10410
+ details: `type changed`
10411
+ });
10412
+ }
8084
10413
  if (oldField.required !== newField.required) changes.push(classifyFieldChange("field-required-changed", name, newField.required));
8085
10414
  if (!!oldField.readOnly !== !!newField.readOnly || !!oldField.writeOnly !== !!newField.writeOnly) changes.push(classifyFieldChange("field-access-changed", name));
8086
10415
  }
@@ -8225,11 +10554,13 @@ function diffParams(oldOp, newOp) {
8225
10554
  for (const [name, newEntry] of newByName) {
8226
10555
  const oldEntry = oldByName.get(name);
8227
10556
  if (!oldEntry) continue;
8228
- if (!typeRefsEqual(oldEntry.p.type, newEntry.p.type)) if (primitivesDifferOnlyByFormat(oldEntry.p.type, newEntry.p.type)) {
8229
- const change = classifyParamChange("param-format-changed", name);
8230
- change.details = formatChangeDetails(oldEntry.p.type, newEntry.p.type);
8231
- changes.push(change);
8232
- } else changes.push(classifyParamChange("param-type-changed", name));
10557
+ if (!typeRefsEqual(oldEntry.p.type, newEntry.p.type)) {
10558
+ if (primitivesDifferOnlyByFormat(oldEntry.p.type, newEntry.p.type)) {
10559
+ const change = classifyParamChange("param-format-changed", name);
10560
+ change.details = formatChangeDetails(oldEntry.p.type, newEntry.p.type);
10561
+ changes.push(change);
10562
+ } else changes.push(classifyParamChange("param-type-changed", name));
10563
+ }
8233
10564
  if (oldEntry.p.required !== newEntry.p.required) changes.push(classifyParamChange("param-required-changed", name, newEntry.p.required));
8234
10565
  const oldDefault = serializeDefault(oldEntry.p.default);
8235
10566
  const newDefault = serializeDefault(newEntry.p.default);
@@ -8519,6 +10850,7 @@ function mergePolicy(defaults, overrides) {
8519
10850
  ...overrides
8520
10851
  };
8521
10852
  }
10853
+ Object.keys(LANGUAGE_DEFAULTS);
8522
10854
  //#endregion
8523
10855
  //#region src/compat/schema.ts
8524
10856
  /** Validate the basic structure of a parsed compat snapshot. */
@@ -8856,7 +11188,7 @@ function enrichWithSpecContext(snapshot, spec) {
8856
11188
  }
8857
11189
  //#endregion
8858
11190
  //#region src/compat/config.ts
8859
- const BREAKING_CATEGORIES = new Set([
11191
+ const BREAKING_CATEGORIES = /* @__PURE__ */ new Set([
8860
11192
  "symbol_removed",
8861
11193
  "symbol_renamed",
8862
11194
  "parameter_removed",
@@ -8874,7 +11206,7 @@ const BREAKING_CATEGORIES = new Set([
8874
11206
  "enum_member_value_changed",
8875
11207
  "staticness_changed"
8876
11208
  ]);
8877
- const SOFT_RISK_CATEGORIES = new Set([
11209
+ const SOFT_RISK_CATEGORIES = /* @__PURE__ */ new Set([
8878
11210
  "parameter_added_non_terminal_optional",
8879
11211
  "constructor_reordered_named_friendly",
8880
11212
  "default_value_changed",
@@ -9157,37 +11489,38 @@ function classifyParameterChanges(baseline, candidate, policy, specRef, language
9157
11489
  specRef
9158
11490
  }));
9159
11491
  if (baseParam.position !== candParam.position) {
9160
- if (isConstructor) if (policy.constructorOrderMatters) changes.push(makeChange({
9161
- category: "constructor_position_changed_order_sensitive",
9162
- symbol: baseline.fqName,
9163
- old: {
9164
- parameter: baseParam.publicName,
9165
- position: String(baseParam.position)
9166
- },
9167
- new: {
9168
- parameter: candParam.publicName,
9169
- position: String(candParam.position)
9170
- },
9171
- message: `Parameter "${baseParam.publicName}" moved from position ${baseParam.position} to ${candParam.position} on "${baseline.displayName}"`,
9172
- policy,
9173
- specRef
9174
- }));
9175
- else changes.push(makeChange({
9176
- category: "constructor_reordered_named_friendly",
9177
- symbol: baseline.fqName,
9178
- old: {
9179
- parameter: baseParam.publicName,
9180
- position: String(baseParam.position)
9181
- },
9182
- new: {
9183
- parameter: candParam.publicName,
9184
- position: String(candParam.position)
9185
- },
9186
- message: `Parameter "${baseParam.publicName}" reordered on "${baseline.displayName}" (named-friendly language)`,
9187
- policy,
9188
- specRef
9189
- }));
9190
- else if (baseParam.passing === "positional") changes.push(makeChange({
11492
+ if (isConstructor) {
11493
+ if (policy.constructorOrderMatters) changes.push(makeChange({
11494
+ category: "constructor_position_changed_order_sensitive",
11495
+ symbol: baseline.fqName,
11496
+ old: {
11497
+ parameter: baseParam.publicName,
11498
+ position: String(baseParam.position)
11499
+ },
11500
+ new: {
11501
+ parameter: candParam.publicName,
11502
+ position: String(candParam.position)
11503
+ },
11504
+ message: `Parameter "${baseParam.publicName}" moved from position ${baseParam.position} to ${candParam.position} on "${baseline.displayName}"`,
11505
+ policy,
11506
+ specRef
11507
+ }));
11508
+ else changes.push(makeChange({
11509
+ category: "constructor_reordered_named_friendly",
11510
+ symbol: baseline.fqName,
11511
+ old: {
11512
+ parameter: baseParam.publicName,
11513
+ position: String(baseParam.position)
11514
+ },
11515
+ new: {
11516
+ parameter: candParam.publicName,
11517
+ position: String(candParam.position)
11518
+ },
11519
+ message: `Parameter "${baseParam.publicName}" reordered on "${baseline.displayName}" (named-friendly language)`,
11520
+ policy,
11521
+ specRef
11522
+ }));
11523
+ } else if (baseParam.passing === "positional") changes.push(makeChange({
9191
11524
  category: "parameter_position_changed_order_sensitive",
9192
11525
  symbol: baseline.fqName,
9193
11526
  old: {
@@ -9330,9 +11663,12 @@ function specDerivedNames(spec, hints) {
9330
11663
  for (const op of service.operations) {
9331
11664
  collectTypeRefNames(op.response, names, hints, topLevelEnumNames);
9332
11665
  if (op.requestBody) collectTypeRefNames(op.requestBody, names, hints, topLevelEnumNames);
11666
+ for (const field of op.pagination?.envelopeFields ?? []) collectTypeRefNames(field.type, names, hints, topLevelEnumNames);
11667
+ for (const header of collectResponseHeaders(op)) collectTypeRefNames(header.type, names, hints, topLevelEnumNames);
9333
11668
  for (const p of [
9334
11669
  ...op.pathParams,
9335
11670
  ...op.queryParams,
11671
+ ...op.bodyOwnedQueryParams ?? [],
9336
11672
  ...op.headerParams,
9337
11673
  ...op.cookieParams ?? []
9338
11674
  ]) collectTypeRefNames(p.type, names, hints, topLevelEnumNames);
@@ -10447,11 +12783,12 @@ function resolveScript(scriptRelPath) {
10447
12783
  }
10448
12784
  function runPackagedScript(scriptRelPath, args) {
10449
12785
  const { bin, script } = resolveScript(scriptRelPath);
10450
- execFileSync(bin, bin === "npx" ? [
12786
+ const fullArgs = bin === "npx" ? [
10451
12787
  "tsx",
10452
12788
  script,
10453
12789
  ...args
10454
- ] : [script, ...args], {
12790
+ ] : [script, ...args];
12791
+ execFileSync(bin, fullArgs, {
10455
12792
  stdio: "inherit",
10456
12793
  env: process.env
10457
12794
  });
@@ -10483,11 +12820,12 @@ function runSmokeCheck(opts) {
10483
12820
  try {
10484
12821
  if (smokeRunner) {
10485
12822
  const bin = smokeRunner.endsWith(".ts") ? "npx" : "node";
10486
- execFileSync(bin, bin === "npx" ? [
12823
+ const fullArgs = bin === "npx" ? [
10487
12824
  "tsx",
10488
12825
  smokeRunner,
10489
12826
  ...smokeArgs
10490
- ] : [smokeRunner, ...smokeArgs], {
12827
+ ] : [smokeRunner, ...smokeArgs];
12828
+ execFileSync(bin, fullArgs, {
10491
12829
  stdio: "inherit",
10492
12830
  env: process.env
10493
12831
  });
@@ -10603,9 +12941,10 @@ async function verifyCommand(opts) {
10603
12941
  diagData.compatCheck = compatSummary;
10604
12942
  if (parsedSpec && maxRetries > 0) setRetryDiagnostics(diagData, compatFlow.attempts, compatFlow.status === "passed", compatSummary.preservationScore, compatFlow.patchedPerIteration);
10605
12943
  }
10606
- if (compatFlow.status === "passed" && compatFlow.compatResult.passed) if (compatFlow.attempts > 0) console.log(`Compat: converged after ${compatFlow.attempts} retry iteration(s)`);
10607
- else console.log("Compat: passed");
10608
- else {
12944
+ if (compatFlow.status === "passed" && compatFlow.compatResult.passed) {
12945
+ if (compatFlow.attempts > 0) console.log(`Compat: converged after ${compatFlow.attempts} retry iteration(s)`);
12946
+ else console.log("Compat: passed");
12947
+ } else {
10609
12948
  if (diagnostics) {
10610
12949
  writeDiagnostics(diagData);
10611
12950
  console.log("Diagnostics written to verify-diagnostics.json");