@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
@@ -1,6 +1,6 @@
1
- import { m as CommandError } from "../tree-sitter-C6HB4OrC.mjs";
2
- import { i as getEmitter, o as generate, s as MANIFEST_FILENAME, t as diffSpecs } from "../diff-DMFiDAI6.mjs";
3
- import { A as getDefaultPolicy, T as apiSurfaceToSnapshot, c as filterSurface, f as specDerivedMethodPaths, i as getExtractor, j as mergePolicy, l as specDerivedEnumValues, n as isPatchableChange, o as diffSnapshots, p as specDerivedNames, r as patchOverlay, t as buildOverlayLookup, u as specDerivedFieldPaths } from "../overlay-BhXruu32.mjs";
1
+ import { m as CommandError } from "../tree-sitter-C7Hk-NDu.mjs";
2
+ import { i as getEmitter, o as generate, s as MANIFEST_FILENAME, t as diffSpecs } from "../diff-CAsypxXV.mjs";
3
+ import { A as getDefaultPolicy, T as apiSurfaceToSnapshot, c as filterSurface, f as specDerivedMethodPaths, i as getExtractor, j as mergePolicy, l as specDerivedEnumValues, n as isPatchableChange, o as diffSnapshots, p as specDerivedNames, r as patchOverlay, t as buildOverlayLookup, u as specDerivedFieldPaths } from "../overlay-Dj3WWlNP.mjs";
4
4
  import * as path$1 from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { execFileSync } from "node:child_process";
@@ -13,10 +13,14 @@ async function runCompatCheck(baseline, outputDir, lang, spec, policyOverrides)
13
13
  let scopedToSpec = false;
14
14
  let scopedSymbolCount;
15
15
  if (spec) {
16
- scopedBaseline = filterSurface(baseline, specDerivedNames(spec, extractor.hints), {
17
- fieldPaths: specDerivedFieldPaths(spec, extractor.hints),
18
- methodPaths: specDerivedMethodPaths(spec),
19
- enumValues: specDerivedEnumValues(spec)
16
+ const allowed = specDerivedNames(spec, extractor.hints);
17
+ const fieldPaths = specDerivedFieldPaths(spec, extractor.hints);
18
+ const methodPaths = specDerivedMethodPaths(spec);
19
+ const enumVals = specDerivedEnumValues(spec);
20
+ scopedBaseline = filterSurface(baseline, allowed, {
21
+ fieldPaths,
22
+ methodPaths,
23
+ enumValues: enumVals
20
24
  });
21
25
  scopedToSpec = true;
22
26
  scopedSymbolCount = Object.keys(scopedBaseline.interfaces).length + Object.keys(scopedBaseline.classes).length + Object.keys(scopedBaseline.typeAliases).length + Object.keys(scopedBaseline.enums).length;
@@ -24,7 +28,8 @@ async function runCompatCheck(baseline, outputDir, lang, spec, policyOverrides)
24
28
  const baseSnap = apiSurfaceToSnapshot(scopedBaseline);
25
29
  const candSnap = apiSurfaceToSnapshot(candidate);
26
30
  const langId = lang;
27
- const diff = diffSnapshots(baseSnap, candSnap, policyOverrides ? mergePolicy(getDefaultPolicy(langId), policyOverrides) : getDefaultPolicy(langId));
31
+ const policy = policyOverrides ? mergePolicy(getDefaultPolicy(langId), policyOverrides) : getDefaultPolicy(langId);
32
+ const diff = diffSnapshots(baseSnap, candSnap, policy);
28
33
  return {
29
34
  passed: diff.changes.every((c) => c.severity !== "breaking"),
30
35
  diff,
@@ -123,7 +128,9 @@ function cleanGeneratedFiles(dir) {
123
128
  }
124
129
  async function runOverlayRetryLoop(opts) {
125
130
  const { baseline, parsedSpec, outputDir, lang, maxRetries, namespace, onRetry } = opts;
126
- let overlay = buildOverlayLookup(baseline, readManifestSync(outputDir), parsedSpec, getExtractor(lang).hints);
131
+ const manifest = readManifestSync(outputDir);
132
+ const extractor = getExtractor(lang);
133
+ let overlay = buildOverlayLookup(baseline, manifest, parsedSpec, extractor.hints);
127
134
  let prevPreserved = -1;
128
135
  const patchedPerIteration = [];
129
136
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
@@ -159,7 +166,8 @@ async function runOverlayRetryLoop(opts) {
159
166
  onRetry?.(attempt + 1, maxRetries, patchable.length);
160
167
  overlay = patchOverlay(overlay, patchable, baseline);
161
168
  cleanGeneratedFiles(outputDir);
162
- await generate(parsedSpec, getEmitter(lang), {
169
+ const emitter = getEmitter(lang);
170
+ await generate(parsedSpec, emitter, {
163
171
  namespace: namespace ?? parsedSpec.name,
164
172
  outputDir,
165
173
  overlayLookup: overlay,
@@ -273,11 +281,12 @@ function resolveScript(scriptRelPath) {
273
281
  }
274
282
  function runPackagedScript(scriptRelPath, args) {
275
283
  const { bin, script } = resolveScript(scriptRelPath);
276
- execFileSync(bin, bin === "npx" ? [
284
+ const fullArgs = bin === "npx" ? [
277
285
  "tsx",
278
286
  script,
279
287
  ...args
280
- ] : [script, ...args], {
288
+ ] : [script, ...args];
289
+ execFileSync(bin, fullArgs, {
281
290
  stdio: "inherit",
282
291
  env: process.env
283
292
  });
@@ -309,11 +318,12 @@ function runSmokeCheck(opts) {
309
318
  try {
310
319
  if (smokeRunner) {
311
320
  const bin = smokeRunner.endsWith(".ts") ? "npx" : "node";
312
- execFileSync(bin, bin === "npx" ? [
321
+ const fullArgs = bin === "npx" ? [
313
322
  "tsx",
314
323
  smokeRunner,
315
324
  ...smokeArgs
316
- ] : [smokeRunner, ...smokeArgs], {
325
+ ] : [smokeRunner, ...smokeArgs];
326
+ execFileSync(bin, fullArgs, {
317
327
  stdio: "inherit",
318
328
  env: process.env
319
329
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["path","path","path"],"sources":["../../src/verify/run-compat-check.ts","../../src/engine/manifest-reader.ts","../../src/verify/run-overlay-retry-loop.ts","../../src/compat/staleness.ts","../../src/verify/run-staleness-check.ts","../../src/verify/run-smoke-check.ts"],"sourcesContent":["import { getExtractor } from '../compat/extractor-registry.js';\nimport {\n diffSnapshots,\n specDerivedNames,\n specDerivedFieldPaths,\n specDerivedMethodPaths,\n specDerivedEnumValues,\n filterSurface,\n} from '../compat/differ.js';\nimport { apiSurfaceToSnapshot } from '../compat/ir.js';\nimport type { LanguageId } from '../compat/ir.js';\nimport { getDefaultPolicy, mergePolicy } from '../compat/policy.js';\nimport type { CompatPolicyHints } from '../compat/policy.js';\nimport type { ApiSpec } from '../ir/types.js';\nimport type { ApiSurface } from '../compat/types.js';\nimport type { CompatCheckResult } from './types.js';\n\nexport async function runCompatCheck(\n baseline: ApiSurface,\n outputDir: string,\n lang: string,\n spec?: ApiSpec,\n policyOverrides?: Partial<CompatPolicyHints>,\n): Promise<CompatCheckResult> {\n const extractor = getExtractor(lang);\n const candidate = await extractor.extract(outputDir);\n\n let scopedBaseline = baseline;\n let scopedToSpec = false;\n let scopedSymbolCount: number | undefined;\n if (spec) {\n const allowed = specDerivedNames(spec, extractor.hints);\n const fieldPaths = specDerivedFieldPaths(spec, extractor.hints);\n const methodPaths = specDerivedMethodPaths(spec);\n const enumVals = specDerivedEnumValues(spec);\n scopedBaseline = filterSurface(baseline, allowed, { fieldPaths, methodPaths, enumValues: enumVals });\n scopedToSpec = true;\n scopedSymbolCount =\n Object.keys(scopedBaseline.interfaces).length +\n Object.keys(scopedBaseline.classes).length +\n Object.keys(scopedBaseline.typeAliases).length +\n Object.keys(scopedBaseline.enums).length;\n }\n\n const baseSnap = apiSurfaceToSnapshot(scopedBaseline);\n const candSnap = apiSurfaceToSnapshot(candidate);\n\n const langId = lang as LanguageId;\n const policy = policyOverrides ? mergePolicy(getDefaultPolicy(langId), policyOverrides) : getDefaultPolicy(langId);\n\n const diff = diffSnapshots(baseSnap, candSnap, policy);\n const passed = diff.changes.every((c) => c.severity !== 'breaking');\n\n return { passed, diff, scopedToSpec, scopedSymbolCount };\n}\n","import { readFileSync, existsSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { MANIFEST_FILENAME } from './manifest.js';\n\n/** Minimal shape matching compat/overlay.ts ManifestEntry for cross-layer use. */\ninterface ManifestEntry {\n operationId: string;\n sdkResourceProperty: string;\n sdkMethodName: string;\n httpMethod: string;\n path: string;\n pathParams: string[];\n bodyFields: string[];\n queryFields: string[];\n}\n\n/**\n * Read the operations map from `.oagen-manifest.json` in the given directory\n * and convert it to `ManifestEntry[]` for the overlay builder.\n *\n * Returns undefined if the manifest is absent or has no operations field.\n */\nexport function readManifestSync(dir: string): ManifestEntry[] | undefined {\n const manifestPath = path.join(dir, MANIFEST_FILENAME);\n if (!existsSync(manifestPath)) return undefined;\n try {\n const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));\n const operations = parsed?.operations;\n if (!operations || typeof operations !== 'object') return undefined;\n return convertOperationsToManifestEntries(operations);\n } catch {\n return undefined;\n }\n}\n\n/** Convert the operations map from .oagen-manifest.json into ManifestEntry[]. */\nfunction convertOperationsToManifestEntries(operations: Record<string, unknown>): ManifestEntry[] {\n const entries: ManifestEntry[] = [];\n for (const [httpKey, value] of Object.entries(operations)) {\n const spaceIdx = httpKey.indexOf(' ');\n const httpMethod = httpKey.slice(0, spaceIdx);\n const httpPath = httpKey.slice(spaceIdx + 1);\n\n const values = Array.isArray(value) ? value : [value];\n for (const v of values) {\n const entry = v as { sdkMethod?: string; service?: string };\n entries.push({\n operationId: '',\n sdkResourceProperty: entry.service ?? '',\n sdkMethodName: entry.sdkMethod ?? '',\n httpMethod,\n path: httpPath,\n pathParams: [],\n bodyFields: [],\n queryFields: [],\n });\n }\n }\n return entries;\n}\n","import { readdirSync, unlinkSync, statSync, openSync, readSync, closeSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { buildOverlayLookup, patchOverlay, isPatchableChange } from '../compat/overlay.js';\nimport { getExtractor } from '../compat/extractor-registry.js';\nimport type { ApiSurface } from '../compat/types.js';\nimport type { ApiSpec } from '../ir/types.js';\nimport { getEmitter } from '../engine/registry.js';\nimport { generate } from '../engine/orchestrator.js';\nimport { runCompatCheck } from './run-compat-check.js';\nimport type { OverlayRetryResult } from './types.js';\nimport { readManifestSync } from '../engine/manifest-reader.js';\n\n/** Generated file header markers used by various emitters. */\nconst GENERATED_MARKERS = ['Code generated by oagen', 'auto-generated by oagen', 'This file is auto-generated'];\n\n/**\n * Delete oagen-generated source files so the retry loop can regenerate them\n * with overlay-corrected names. Only deletes files that contain an oagen\n * generation marker, preserving hand-written files.\n */\nfunction cleanGeneratedFiles(dir: string): void {\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return;\n }\n for (const entry of entries) {\n const full = path.join(dir, entry);\n let stat;\n try {\n stat = statSync(full);\n } catch {\n continue;\n }\n if (stat.isDirectory()) {\n cleanGeneratedFiles(full);\n continue;\n }\n // Skip non-source files and manifests\n if (entry.endsWith('.json') || entry.endsWith('.lock')) continue;\n // Read only the first 200 bytes to check for generation marker\n try {\n const buf = Buffer.alloc(200);\n const fd = openSync(full, 'r');\n try {\n readSync(fd, buf, 0, 200, 0);\n } finally {\n closeSync(fd);\n }\n const header = buf.toString('utf-8');\n if (GENERATED_MARKERS.some((m) => header.includes(m))) {\n unlinkSync(full);\n }\n } catch {\n // ignore read/delete errors\n }\n }\n}\n\nexport async function runOverlayRetryLoop(opts: {\n baseline: ApiSurface;\n parsedSpec: ApiSpec;\n outputDir: string;\n lang: string;\n maxRetries: number;\n namespace?: string;\n onRetry?: (attemptNumber: number, maxRetries: number, patchableCount: number) => void;\n}): Promise<OverlayRetryResult> {\n const { baseline, parsedSpec, outputDir, lang, maxRetries, namespace, onRetry } = opts;\n\n const manifest = readManifestSync(outputDir);\n // Pass language-specific hints so property-to-class matching works correctly\n // (e.g., Ruby snake_case \"api_keys\" → PascalCase \"ApiKeys\").\n const extractor = getExtractor(lang);\n let overlay = buildOverlayLookup(baseline, manifest, parsedSpec, extractor.hints);\n let prevPreserved = -1;\n const patchedPerIteration: number[] = [];\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const compatResult = await runCompatCheck(baseline, outputDir, lang, parsedSpec);\n if (compatResult.passed) {\n return {\n status: 'passed',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n\n if (attempt === maxRetries) {\n return {\n status: 'max-retries',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n\n const patchable = compatResult.diff.changes.filter(isPatchableChange);\n if (patchable.length === 0) {\n return {\n status: 'no-patchable',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n\n const currentBreaking = compatResult.diff.summary.breaking;\n // Stall detection: if breaking count hasn't decreased, we're not making progress\n if (attempt > 0 && currentBreaking >= prevPreserved) {\n return {\n status: 'stalled',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n prevPreserved = currentBreaking;\n patchedPerIteration.push(patchable.length);\n onRetry?.(attempt + 1, maxRetries, patchable.length);\n\n overlay = patchOverlay(overlay, patchable, baseline);\n\n // Pre-delete generated source files so the writer creates them fresh\n // with overlay-corrected names. Without this, the AST merger would preserve\n // old method/class names from the existing files.\n // Only delete files that look like oagen-generated source files (have the header).\n cleanGeneratedFiles(outputDir);\n\n const emitter = getEmitter(lang);\n await generate(parsedSpec, emitter, {\n namespace: namespace ?? parsedSpec.name,\n outputDir,\n overlayLookup: overlay,\n apiSurface: baseline,\n });\n }\n\n const compatResult = await runCompatCheck(baseline, outputDir, lang, parsedSpec);\n return {\n status: 'max-retries',\n attempts: maxRetries,\n patchedPerIteration,\n compatResult,\n };\n}\n","import type { ApiSurface, LanguageHints, Violation } from './types.js';\nimport type { ApiSpec } from '../ir/types.js';\nimport { specDerivedNames, specDerivedFieldPaths } from './spec-filter.js';\nimport { diffSpecs } from '../differ/diff.js';\n\n/**\n * Detect symbols that exist in the live SDK surface but are no longer defined\n * in the current OpenAPI spec. These are \"stale\" — they compile and run but\n * represent dead code from a previous spec version.\n *\n * Requires both the old and new spec so we can distinguish hand-written SDK\n * symbols (never in any spec) from genuinely removed spec symbols.\n */\nexport function detectStaleSymbols(\n liveSurface: ApiSurface,\n oldSpec: ApiSpec,\n newSpec: ApiSpec,\n hints: LanguageHints,\n): Violation[] {\n const violations: Violation[] = [];\n\n // Step 1-3: Name-set difference for top-level symbols\n const oldNames = specDerivedNames(oldSpec, hints);\n const newNames = specDerivedNames(newSpec, hints);\n\n const removedNames = new Set<string>();\n for (const name of oldNames) {\n if (!newNames.has(name)) removedNames.add(name);\n }\n\n for (const name of removedNames) {\n if (liveSurface.classes[name]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: name,\n baseline: name,\n candidate: '(removed from spec)',\n message: `Class \"${name}\" is no longer defined in the OpenAPI spec`,\n });\n }\n if (liveSurface.interfaces[name]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: name,\n baseline: name,\n candidate: '(removed from spec)',\n message: `Interface \"${name}\" is no longer defined in the OpenAPI spec`,\n });\n }\n if (liveSurface.typeAliases[name]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: name,\n baseline: name,\n candidate: '(removed from spec)',\n message: `Type alias \"${name}\" is no longer defined in the OpenAPI spec`,\n });\n }\n if (liveSurface.enums[name]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: name,\n baseline: name,\n candidate: '(removed from spec)',\n message: `Enum \"${name}\" is no longer defined in the OpenAPI spec`,\n });\n }\n }\n\n // Step 4-6: Field-level staleness for models that still exist in new spec\n const oldFieldPaths = specDerivedFieldPaths(oldSpec, hints);\n const newFieldPaths = specDerivedFieldPaths(newSpec, hints);\n\n for (const fieldPath of oldFieldPaths) {\n if (newFieldPaths.has(fieldPath)) continue;\n\n const [modelName, fieldName] = fieldPath.split('.');\n // Only flag field-level staleness for models that still exist in the new spec\n // (fully removed models are already caught above)\n if (removedNames.has(modelName)) continue;\n\n const iface = liveSurface.interfaces[modelName];\n if (iface?.fields[fieldName]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: fieldPath,\n baseline: fieldName,\n candidate: '(removed from spec)',\n message: `Field \"${fieldPath}\" is no longer defined in the OpenAPI spec`,\n });\n }\n }\n\n // Step 7: Operation-level staleness via diffSpecs\n const diff = diffSpecs(oldSpec, newSpec);\n for (const change of diff.changes) {\n if (change.kind === 'operation-removed') {\n const cls = liveSurface.classes[change.serviceName];\n if (cls?.methods[change.operationName]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: `${change.serviceName}.${change.operationName}`,\n baseline: change.operationName,\n candidate: '(removed from spec)',\n message: `Method \"${change.serviceName}.${change.operationName}\" is no longer defined in the OpenAPI spec`,\n });\n }\n }\n }\n\n return violations;\n}\n","import { getExtractor } from '../compat/extractor-registry.js';\nimport { detectStaleSymbols } from '../compat/staleness.js';\nimport type { ApiSpec } from '../ir/types.js';\nimport type { ApiSurface } from '../compat/types.js';\nimport type { StalenessCheckResult } from './types.js';\n\nexport function runStalenessCheck(\n baseline: ApiSurface,\n oldSpec: ApiSpec,\n newSpec: ApiSpec,\n lang: string,\n): StalenessCheckResult {\n const extractor = getExtractor(lang);\n const violations = detectStaleSymbols(baseline, oldSpec, newSpec, extractor.hints);\n return { violations };\n}\n","import { execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport * as path from 'node:path';\nimport { CommandError } from '../errors.js';\nimport type { SmokeCheckResult } from './types.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nfunction resolveScript(scriptRelPath: string): { bin: string; script: string } {\n const distScript = path.resolve(__dirname, '..', '..', 'dist', 'scripts', scriptRelPath.replace(/\\.ts$/, '.mjs'));\n if (existsSync(distScript)) {\n return { bin: 'node', script: distScript };\n }\n const srcScript = path.resolve(__dirname, '..', '..', 'scripts', scriptRelPath);\n return { bin: 'npx', script: srcScript };\n}\n\nfunction runPackagedScript(scriptRelPath: string, args: string[]): void {\n const { bin, script } = resolveScript(scriptRelPath);\n const fullArgs = bin === 'npx' ? ['tsx', script, ...args] : [script, ...args];\n execFileSync(bin, fullArgs, { stdio: 'inherit', env: process.env });\n}\n\nexport function runSmokeCheck(opts: {\n spec?: string;\n lang: string;\n output: string;\n rawResults?: string;\n smokeConfig?: string;\n smokeRunner?: string;\n}): SmokeCheckResult {\n const { spec, lang, output, rawResults, smokeConfig, smokeRunner } = opts;\n\n let baselinePath = rawResults ?? 'smoke-results-raw.json';\n let generatedBaseline = false;\n\n if (!rawResults && !existsSync('smoke-results-raw.json')) {\n if (!spec) {\n throw new CommandError(\n 'error: --spec <path> or OPENAPI_SPEC_PATH env var is required when no raw baseline exists',\n '',\n 1,\n );\n }\n\n try {\n runPackagedScript('smoke/baseline.ts', ['--spec', spec]);\n } catch {\n throw new CommandError('Baseline generation failed', '', 1);\n }\n baselinePath = 'smoke-results-spec-baseline.json';\n generatedBaseline = true;\n }\n\n const smokeArgs = ['--lang', lang, '--sdk-path', output, '--raw-results', baselinePath];\n if (spec) smokeArgs.push('--spec', spec);\n if (smokeConfig) smokeArgs.push('--smoke-config', smokeConfig);\n\n try {\n if (smokeRunner) {\n const bin = smokeRunner.endsWith('.ts') ? 'npx' : 'node';\n const fullArgs = bin === 'npx' ? ['tsx', smokeRunner, ...smokeArgs] : [smokeRunner, ...smokeArgs];\n execFileSync(bin, fullArgs, { stdio: 'inherit', env: process.env });\n } else {\n runPackagedScript('smoke/sdk-test.ts', smokeArgs);\n }\n\n return { passed: true, baselinePath, generatedBaseline };\n } catch {\n if (existsSync('smoke-compile-errors.json')) {\n return { passed: false, compileErrors: true, baselinePath, generatedBaseline };\n }\n\n const findingsCount = existsSync('smoke-diff-findings.json')\n ? (JSON.parse(readFileSync('smoke-diff-findings.json', 'utf-8')) as unknown[]).length\n : undefined;\n\n return { passed: false, findingsCount, baselinePath, generatedBaseline };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["path","path","path"],"sources":["../../src/verify/run-compat-check.ts","../../src/engine/manifest-reader.ts","../../src/verify/run-overlay-retry-loop.ts","../../src/compat/staleness.ts","../../src/verify/run-staleness-check.ts","../../src/verify/run-smoke-check.ts"],"sourcesContent":["import { getExtractor } from '../compat/extractor-registry.js';\nimport {\n diffSnapshots,\n specDerivedNames,\n specDerivedFieldPaths,\n specDerivedMethodPaths,\n specDerivedEnumValues,\n filterSurface,\n} from '../compat/differ.js';\nimport { apiSurfaceToSnapshot } from '../compat/ir.js';\nimport type { LanguageId } from '../compat/ir.js';\nimport { getDefaultPolicy, mergePolicy } from '../compat/policy.js';\nimport type { CompatPolicyHints } from '../compat/policy.js';\nimport type { ApiSpec } from '../ir/types.js';\nimport type { ApiSurface } from '../compat/types.js';\nimport type { CompatCheckResult } from './types.js';\n\nexport async function runCompatCheck(\n baseline: ApiSurface,\n outputDir: string,\n lang: string,\n spec?: ApiSpec,\n policyOverrides?: Partial<CompatPolicyHints>,\n): Promise<CompatCheckResult> {\n const extractor = getExtractor(lang);\n const candidate = await extractor.extract(outputDir);\n\n let scopedBaseline = baseline;\n let scopedToSpec = false;\n let scopedSymbolCount: number | undefined;\n if (spec) {\n const allowed = specDerivedNames(spec, extractor.hints);\n const fieldPaths = specDerivedFieldPaths(spec, extractor.hints);\n const methodPaths = specDerivedMethodPaths(spec);\n const enumVals = specDerivedEnumValues(spec);\n scopedBaseline = filterSurface(baseline, allowed, { fieldPaths, methodPaths, enumValues: enumVals });\n scopedToSpec = true;\n scopedSymbolCount =\n Object.keys(scopedBaseline.interfaces).length +\n Object.keys(scopedBaseline.classes).length +\n Object.keys(scopedBaseline.typeAliases).length +\n Object.keys(scopedBaseline.enums).length;\n }\n\n const baseSnap = apiSurfaceToSnapshot(scopedBaseline);\n const candSnap = apiSurfaceToSnapshot(candidate);\n\n const langId = lang as LanguageId;\n const policy = policyOverrides ? mergePolicy(getDefaultPolicy(langId), policyOverrides) : getDefaultPolicy(langId);\n\n const diff = diffSnapshots(baseSnap, candSnap, policy);\n const passed = diff.changes.every((c) => c.severity !== 'breaking');\n\n return { passed, diff, scopedToSpec, scopedSymbolCount };\n}\n","import { readFileSync, existsSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { MANIFEST_FILENAME } from './manifest.js';\n\n/** Minimal shape matching compat/overlay.ts ManifestEntry for cross-layer use. */\ninterface ManifestEntry {\n operationId: string;\n sdkResourceProperty: string;\n sdkMethodName: string;\n httpMethod: string;\n path: string;\n pathParams: string[];\n bodyFields: string[];\n queryFields: string[];\n}\n\n/**\n * Read the operations map from `.oagen-manifest.json` in the given directory\n * and convert it to `ManifestEntry[]` for the overlay builder.\n *\n * Returns undefined if the manifest is absent or has no operations field.\n */\nexport function readManifestSync(dir: string): ManifestEntry[] | undefined {\n const manifestPath = path.join(dir, MANIFEST_FILENAME);\n if (!existsSync(manifestPath)) return undefined;\n try {\n const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));\n const operations = parsed?.operations;\n if (!operations || typeof operations !== 'object') return undefined;\n return convertOperationsToManifestEntries(operations);\n } catch {\n return undefined;\n }\n}\n\n/** Convert the operations map from .oagen-manifest.json into ManifestEntry[]. */\nfunction convertOperationsToManifestEntries(operations: Record<string, unknown>): ManifestEntry[] {\n const entries: ManifestEntry[] = [];\n for (const [httpKey, value] of Object.entries(operations)) {\n const spaceIdx = httpKey.indexOf(' ');\n const httpMethod = httpKey.slice(0, spaceIdx);\n const httpPath = httpKey.slice(spaceIdx + 1);\n\n const values = Array.isArray(value) ? value : [value];\n for (const v of values) {\n const entry = v as { sdkMethod?: string; service?: string };\n entries.push({\n operationId: '',\n sdkResourceProperty: entry.service ?? '',\n sdkMethodName: entry.sdkMethod ?? '',\n httpMethod,\n path: httpPath,\n pathParams: [],\n bodyFields: [],\n queryFields: [],\n });\n }\n }\n return entries;\n}\n","import { readdirSync, unlinkSync, statSync, openSync, readSync, closeSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { buildOverlayLookup, patchOverlay, isPatchableChange } from '../compat/overlay.js';\nimport { getExtractor } from '../compat/extractor-registry.js';\nimport type { ApiSurface } from '../compat/types.js';\nimport type { ApiSpec } from '../ir/types.js';\nimport { getEmitter } from '../engine/registry.js';\nimport { generate } from '../engine/orchestrator.js';\nimport { runCompatCheck } from './run-compat-check.js';\nimport type { OverlayRetryResult } from './types.js';\nimport { readManifestSync } from '../engine/manifest-reader.js';\n\n/** Generated file header markers used by various emitters. */\nconst GENERATED_MARKERS = ['Code generated by oagen', 'auto-generated by oagen', 'This file is auto-generated'];\n\n/**\n * Delete oagen-generated source files so the retry loop can regenerate them\n * with overlay-corrected names. Only deletes files that contain an oagen\n * generation marker, preserving hand-written files.\n */\nfunction cleanGeneratedFiles(dir: string): void {\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return;\n }\n for (const entry of entries) {\n const full = path.join(dir, entry);\n let stat;\n try {\n stat = statSync(full);\n } catch {\n continue;\n }\n if (stat.isDirectory()) {\n cleanGeneratedFiles(full);\n continue;\n }\n // Skip non-source files and manifests\n if (entry.endsWith('.json') || entry.endsWith('.lock')) continue;\n // Read only the first 200 bytes to check for generation marker\n try {\n const buf = Buffer.alloc(200);\n const fd = openSync(full, 'r');\n try {\n readSync(fd, buf, 0, 200, 0);\n } finally {\n closeSync(fd);\n }\n const header = buf.toString('utf-8');\n if (GENERATED_MARKERS.some((m) => header.includes(m))) {\n unlinkSync(full);\n }\n } catch {\n // ignore read/delete errors\n }\n }\n}\n\nexport async function runOverlayRetryLoop(opts: {\n baseline: ApiSurface;\n parsedSpec: ApiSpec;\n outputDir: string;\n lang: string;\n maxRetries: number;\n namespace?: string;\n onRetry?: (attemptNumber: number, maxRetries: number, patchableCount: number) => void;\n}): Promise<OverlayRetryResult> {\n const { baseline, parsedSpec, outputDir, lang, maxRetries, namespace, onRetry } = opts;\n\n const manifest = readManifestSync(outputDir);\n // Pass language-specific hints so property-to-class matching works correctly\n // (e.g., Ruby snake_case \"api_keys\" → PascalCase \"ApiKeys\").\n const extractor = getExtractor(lang);\n let overlay = buildOverlayLookup(baseline, manifest, parsedSpec, extractor.hints);\n let prevPreserved = -1;\n const patchedPerIteration: number[] = [];\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n const compatResult = await runCompatCheck(baseline, outputDir, lang, parsedSpec);\n if (compatResult.passed) {\n return {\n status: 'passed',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n\n if (attempt === maxRetries) {\n return {\n status: 'max-retries',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n\n const patchable = compatResult.diff.changes.filter(isPatchableChange);\n if (patchable.length === 0) {\n return {\n status: 'no-patchable',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n\n const currentBreaking = compatResult.diff.summary.breaking;\n // Stall detection: if breaking count hasn't decreased, we're not making progress\n if (attempt > 0 && currentBreaking >= prevPreserved) {\n return {\n status: 'stalled',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n prevPreserved = currentBreaking;\n patchedPerIteration.push(patchable.length);\n onRetry?.(attempt + 1, maxRetries, patchable.length);\n\n overlay = patchOverlay(overlay, patchable, baseline);\n\n // Pre-delete generated source files so the writer creates them fresh\n // with overlay-corrected names. Without this, the AST merger would preserve\n // old method/class names from the existing files.\n // Only delete files that look like oagen-generated source files (have the header).\n cleanGeneratedFiles(outputDir);\n\n const emitter = getEmitter(lang);\n await generate(parsedSpec, emitter, {\n namespace: namespace ?? parsedSpec.name,\n outputDir,\n overlayLookup: overlay,\n apiSurface: baseline,\n });\n }\n\n const compatResult = await runCompatCheck(baseline, outputDir, lang, parsedSpec);\n return {\n status: 'max-retries',\n attempts: maxRetries,\n patchedPerIteration,\n compatResult,\n };\n}\n","import type { ApiSurface, LanguageHints, Violation } from './types.js';\nimport type { ApiSpec } from '../ir/types.js';\nimport { specDerivedNames, specDerivedFieldPaths } from './spec-filter.js';\nimport { diffSpecs } from '../differ/diff.js';\n\n/**\n * Detect symbols that exist in the live SDK surface but are no longer defined\n * in the current OpenAPI spec. These are \"stale\" — they compile and run but\n * represent dead code from a previous spec version.\n *\n * Requires both the old and new spec so we can distinguish hand-written SDK\n * symbols (never in any spec) from genuinely removed spec symbols.\n */\nexport function detectStaleSymbols(\n liveSurface: ApiSurface,\n oldSpec: ApiSpec,\n newSpec: ApiSpec,\n hints: LanguageHints,\n): Violation[] {\n const violations: Violation[] = [];\n\n // Step 1-3: Name-set difference for top-level symbols\n const oldNames = specDerivedNames(oldSpec, hints);\n const newNames = specDerivedNames(newSpec, hints);\n\n const removedNames = new Set<string>();\n for (const name of oldNames) {\n if (!newNames.has(name)) removedNames.add(name);\n }\n\n for (const name of removedNames) {\n if (liveSurface.classes[name]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: name,\n baseline: name,\n candidate: '(removed from spec)',\n message: `Class \"${name}\" is no longer defined in the OpenAPI spec`,\n });\n }\n if (liveSurface.interfaces[name]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: name,\n baseline: name,\n candidate: '(removed from spec)',\n message: `Interface \"${name}\" is no longer defined in the OpenAPI spec`,\n });\n }\n if (liveSurface.typeAliases[name]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: name,\n baseline: name,\n candidate: '(removed from spec)',\n message: `Type alias \"${name}\" is no longer defined in the OpenAPI spec`,\n });\n }\n if (liveSurface.enums[name]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: name,\n baseline: name,\n candidate: '(removed from spec)',\n message: `Enum \"${name}\" is no longer defined in the OpenAPI spec`,\n });\n }\n }\n\n // Step 4-6: Field-level staleness for models that still exist in new spec\n const oldFieldPaths = specDerivedFieldPaths(oldSpec, hints);\n const newFieldPaths = specDerivedFieldPaths(newSpec, hints);\n\n for (const fieldPath of oldFieldPaths) {\n if (newFieldPaths.has(fieldPath)) continue;\n\n const [modelName, fieldName] = fieldPath.split('.');\n // Only flag field-level staleness for models that still exist in the new spec\n // (fully removed models are already caught above)\n if (removedNames.has(modelName)) continue;\n\n const iface = liveSurface.interfaces[modelName];\n if (iface?.fields[fieldName]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: fieldPath,\n baseline: fieldName,\n candidate: '(removed from spec)',\n message: `Field \"${fieldPath}\" is no longer defined in the OpenAPI spec`,\n });\n }\n }\n\n // Step 7: Operation-level staleness via diffSpecs\n const diff = diffSpecs(oldSpec, newSpec);\n for (const change of diff.changes) {\n if (change.kind === 'operation-removed') {\n const cls = liveSurface.classes[change.serviceName];\n if (cls?.methods[change.operationName]) {\n violations.push({\n category: 'staleness',\n severity: 'warning',\n symbolPath: `${change.serviceName}.${change.operationName}`,\n baseline: change.operationName,\n candidate: '(removed from spec)',\n message: `Method \"${change.serviceName}.${change.operationName}\" is no longer defined in the OpenAPI spec`,\n });\n }\n }\n }\n\n return violations;\n}\n","import { getExtractor } from '../compat/extractor-registry.js';\nimport { detectStaleSymbols } from '../compat/staleness.js';\nimport type { ApiSpec } from '../ir/types.js';\nimport type { ApiSurface } from '../compat/types.js';\nimport type { StalenessCheckResult } from './types.js';\n\nexport function runStalenessCheck(\n baseline: ApiSurface,\n oldSpec: ApiSpec,\n newSpec: ApiSpec,\n lang: string,\n): StalenessCheckResult {\n const extractor = getExtractor(lang);\n const violations = detectStaleSymbols(baseline, oldSpec, newSpec, extractor.hints);\n return { violations };\n}\n","import { execFileSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport * as path from 'node:path';\nimport { CommandError } from '../errors.js';\nimport type { SmokeCheckResult } from './types.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nfunction resolveScript(scriptRelPath: string): { bin: string; script: string } {\n const distScript = path.resolve(__dirname, '..', '..', 'dist', 'scripts', scriptRelPath.replace(/\\.ts$/, '.mjs'));\n if (existsSync(distScript)) {\n return { bin: 'node', script: distScript };\n }\n const srcScript = path.resolve(__dirname, '..', '..', 'scripts', scriptRelPath);\n return { bin: 'npx', script: srcScript };\n}\n\nfunction runPackagedScript(scriptRelPath: string, args: string[]): void {\n const { bin, script } = resolveScript(scriptRelPath);\n const fullArgs = bin === 'npx' ? ['tsx', script, ...args] : [script, ...args];\n execFileSync(bin, fullArgs, { stdio: 'inherit', env: process.env });\n}\n\nexport function runSmokeCheck(opts: {\n spec?: string;\n lang: string;\n output: string;\n rawResults?: string;\n smokeConfig?: string;\n smokeRunner?: string;\n}): SmokeCheckResult {\n const { spec, lang, output, rawResults, smokeConfig, smokeRunner } = opts;\n\n let baselinePath = rawResults ?? 'smoke-results-raw.json';\n let generatedBaseline = false;\n\n if (!rawResults && !existsSync('smoke-results-raw.json')) {\n if (!spec) {\n throw new CommandError(\n 'error: --spec <path> or OPENAPI_SPEC_PATH env var is required when no raw baseline exists',\n '',\n 1,\n );\n }\n\n try {\n runPackagedScript('smoke/baseline.ts', ['--spec', spec]);\n } catch {\n throw new CommandError('Baseline generation failed', '', 1);\n }\n baselinePath = 'smoke-results-spec-baseline.json';\n generatedBaseline = true;\n }\n\n const smokeArgs = ['--lang', lang, '--sdk-path', output, '--raw-results', baselinePath];\n if (spec) smokeArgs.push('--spec', spec);\n if (smokeConfig) smokeArgs.push('--smoke-config', smokeConfig);\n\n try {\n if (smokeRunner) {\n const bin = smokeRunner.endsWith('.ts') ? 'npx' : 'node';\n const fullArgs = bin === 'npx' ? ['tsx', smokeRunner, ...smokeArgs] : [smokeRunner, ...smokeArgs];\n execFileSync(bin, fullArgs, { stdio: 'inherit', env: process.env });\n } else {\n runPackagedScript('smoke/sdk-test.ts', smokeArgs);\n }\n\n return { passed: true, baselinePath, generatedBaseline };\n } catch {\n if (existsSync('smoke-compile-errors.json')) {\n return { passed: false, compileErrors: true, baselinePath, generatedBaseline };\n }\n\n const findingsCount = existsSync('smoke-diff-findings.json')\n ? (JSON.parse(readFileSync('smoke-diff-findings.json', 'utf-8')) as unknown[]).length\n : undefined;\n\n return { passed: false, findingsCount, baselinePath, generatedBaseline };\n }\n}\n"],"mappings":"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"}
@@ -112,6 +112,7 @@ Emitters should handle these additional variants introduced for open-source read
112
112
  - **Cookie params:** Optional `cookieParams` array on operations that use cookie parameters
113
113
  - **Servers:** Optional `servers: ServerEntry[]` on `ApiSpec` alongside `baseUrl`
114
114
  - **Multiple responses:** Optional `successResponses: SuccessResponse[]` on `Operation` when more than one 2xx exists
115
+ - **Response headers:** Optional `responseHeaders: ResponseHeader[]` on `Operation` (the primary response's headers) and `headers: ResponseHeader[]` on each `SuccessResponse` / `ErrorResponse`, present only when the spec declares at least one
115
116
  - **Idempotency:** `injectIdempotencyKey` is now spec-driven (true only when the spec declares an `Idempotency-Key` header)
116
117
 
117
118
  ## OperationPlan
@@ -22,12 +22,15 @@ interface ApiSpec {
22
22
  models: Model[]; // Schema objects
23
23
  enums: Enum[]; // String enums
24
24
  auth?: AuthScheme[]; // Authentication schemes
25
+ security?: SecurityRequirementSet[]; // Document-level default requirements (see Operation.security)
25
26
  sdk: SdkBehavior; // Runtime policies (retry, errors, telemetry, etc.)
26
27
  }
27
28
  ```
28
29
 
29
30
  The `servers` array contains all server entries from the OpenAPI spec. `baseUrl` is always set to the first server's URL for backward compatibility.
30
31
 
32
+ `security` is the document-level `security` list, in the same shape as `Operation.security`; every operation that declares no `security` of its own inherits it. It is `undefined` when the document declares none.
33
+
31
34
  ### SdkBehavior
32
35
 
33
36
  Source: `src/ir/sdk-behavior.ts`
@@ -104,6 +107,10 @@ interface MapType {
104
107
 
105
108
  The optional `keyType` allows non-string map keys (e.g., enum-keyed maps). When absent, emitters should assume `string` keys.
106
109
 
110
+ A map's value schema is typed the same way an array's items are: with the owning field's name and parent model. When the value is an inline object (or an array or `oneOf` of inline objects), the parser extracts it as a model named by the parent plus the singularized field name, e.g. `layers` on `CreateDrawingRequest` → `CreateDrawingRequestLayer`, matching `shapes` → `CreateDrawingRequestShape` for array items. `additionalProperties: {}` or `true` keeps the value type `unknown`. The singular form is taken only when no sibling property of the same object would itself mint that name — an inline object, an inline enum, an inline union with object variants, or an array or map of such (a primitive `email` beside `emails` claims nothing); otherwise array items are `<Parent><Field>Item` (`DrawingLayersItem` beside a `layer` object with different fields) and map values `<Parent><Field>Value`; if a sibling claims that name too (a property `layers_item`), the fallback continues to `<Parent><Field>ArrayItem`, then `<Parent><Field>ArrayItem2`, and so on (`<Parent><Field>MemberValue`, `…MemberValue2` for maps), taking the first name no sibling mints. One deliberate exception: when the claiming sibling is an inline object structurally identical to the items or values (same properties, required, types, nested inline schemas and enum values; `description`, `title` and `example` ignored), both fields share one model under the singular name, since one type is right for both and a duplicate would only add noise; the split happens only once the shapes diverge. The choice reads sibling names and schema content only, so document order plays no part.
111
+
112
+ Inline object variants of a `oneOf`/`anyOf` are extracted as their own models. The first keeps the field's inline name (`DrawingShape`); later ones take a const-discriminator prefix when every variant pins one (`SquareDrawingShape`). A variant with neither is named from its own content, so an SDK user can tell the variants apart and reordering them changes nothing but which one is first: its `title`, trimmed and PascalCased, as the same prefix form (`CircleDrawingShape`; a title the base name already starts with is not doubled, and a title an earlier variant already took is not reused); else the first property, in the variant's own order, that no other inline object variant of the union declares (`DrawingShapeWithSide`, `DrawingShapeWithRadius`); else, only when its property set adds nothing to another variant's, the position counter (`DrawingShape2`). Every candidate also steps around the inline names the parent's other properties mint, so a variant never overwrites another field's model: beside a `shape_with_side` object (`DrawingShapeWithSide`), the `{ side }` variant of `shape` takes the next candidate (`DrawingShape2`), and beside a `shape2` object the counter keeps counting (`DrawingShape3`); each sibling's names are read in isolation, so the choice depends on sibling names and content only. The same reservation is handed down to a union among a field's array items or map values. A `oneOf` without an explicit `discriminator:` whose every variant carries a `title` and pins the same const property is the one shape named differently: its discriminator mapping has always named each variant by its title (`Circle`, `Square`), so the extracted models carry those titles too, and the mapping is read from the same names — such a title is context-free and names one shape wherever it appears, as a component name would, so a second union beside it (at field level, or among a sibling array's items or map's values) that gives the same title a variant of the identical shape (same properties, required, types and nested schemas; `description`, `title` and `example` ignored — the rule array items already use to share a singular sibling's model) shares the model instead of stepping around it. A sibling that gives the same title a different shape reserves it, and because each side reads its siblings in isolation the decision is symmetric: when `Circle` is `{ radius }` on `shape` and `{ diameter }` on `marker`, both fall back to their context-qualified names (`CircleDrawingShape`, `CircleDrawingMarker`: the title as a prefix on the base, else a distinguishing property, else the counter) and neither keeps the bare `Circle`, with the mappings following; when the shapes are identical, both keep `Circle` and one model is emitted. A title that coincides with another field's context-dependent model name falls back the same way. When the union has no const discriminator and every variant carries a distinct title, the first variant takes its title too (`CircleDrawingShape` beside `SquareDrawingShape`) instead of being the only one left bare. The decision reads the variants' content only, and the union TypeRef and every extractor (component fields, request bodies, inline responses, array items, map values) make it through one function, so a reference always has a model behind it. A request body that is itself a `oneOf` of inline objects follows the same rule for its const-less variants (`CreateThingRequest`, `CreateThingRequestWithB`); a variant with a const string property is still named after it, and either kind steps around a name an earlier variant of the body already took (a variant titled `Foo` beside one pinning `kind: foo` gives `FooCreateThingRequest` and `FooCreateThingRequest2`). A response that is itself a `oneOf` goes through the field extractor under the response name, so every inline object variant — const-prefixed, titled, property-named, or nullable (`type: ['object', 'null']`) — is materialized under exactly the name the union references (`GetThingResponse`, `GetThingResponseWithSide`, `SquareGetThingResponse`). Everything nested under a variant is named from the variant, never from the union's field: an inline object under the second variant is `DrawingShapeWithSideCenter` (or `SquareDrawingShapeCenter` under a titled one); a multi-type variant such as `type: ['object', 'array']` keeps its other types beside the model (`union[model, array]`), and the inline items of that array member are `<Variant>Item` (`DrawingShapeWithSideItem`, with nested objects `DrawingShapeWithSideItemChild`); any other non-object member is named `<Variant>Value` (an inline enum, for instance). If a property of the variant already claims that model name (`item`, or `items`, which singularizes to the same `<Variant>Item`), the member falls back to `<Variant>ArrayItem` / `<Variant>MemberValue`, then `<Variant>ArrayItem2`, `<Variant>ArrayItem3`, and so on. A variant that admits null (`['object', 'null']` or `nullable: true`) keeps its `nullable` wrapper around the model.
113
+
107
114
  ### UnionType
108
115
 
109
116
  ```typescript
@@ -131,26 +138,25 @@ Generic type parameters on models. Example: `DirectoryUser<TCustomAttributes = R
131
138
  ## AuthScheme
132
139
 
133
140
  ```typescript
134
- type AuthScheme =
141
+ type AuthScheme = { description?: string; schemeName: string } & (
142
+ | { kind: "basic" }
135
143
  | { kind: "bearer" }
136
144
  | { kind: "apiKey"; in: "header" | "query" | "cookie"; name: string }
137
- | { kind: "oauth2"; flows: Record<string, unknown> };
145
+ | { kind: "oauth2"; flows: Record<string, unknown> }
146
+ );
138
147
  ```
139
148
 
140
149
  Emitters use `AuthScheme` to generate authentication configuration and client constructors. The `cookie` variant supports cookie-based API key authentication.
141
150
 
151
+ `schemeName` is the key under `components.securitySchemes`, spelled exactly as in the document, and `description` is present only when the document declares one; entries keep the document's order. The requirements under `ApiSpec.security` and `Operation.security` refer to the same key, so an emitter joins a requirement to its scheme with `spec.auth?.find((a) => a.schemeName === requirement.schemeName)`. The optional chaining matters: `auth` is `undefined` when the document declares no scheme the IR can represent, and a requirement can still name a scheme that was omitted (see below), so a missing match means "no SDK-level auth is known for this operation", not an error.
152
+
153
+ HTTP scheme names are matched case-insensitively (`Bearer`, `bearer`, and `BEARER` all yield `kind: "bearer"`). Security schemes the IR cannot represent (other `http` schemes such as `digest`, plus `openIdConnect` and `mutualTLS`) are omitted from `auth`, and the parser prints an `[oagen] Warning:` diagnostic naming the scheme.
154
+
142
155
  ## HttpMethod
143
156
 
144
157
  ```typescript
145
158
  type HttpMethod =
146
- | "get"
147
- | "post"
148
- | "put"
149
- | "patch"
150
- | "delete"
151
- | "head"
152
- | "options"
153
- | "trace";
159
+ "get" | "post" | "put" | "patch" | "delete" | "head" | "options" | "trace";
154
160
  ```
155
161
 
156
162
  All standard HTTP methods are supported. Emitters should handle `head`, `options`, and `trace` appropriately (e.g., HEAD returns no body).
@@ -164,17 +170,46 @@ interface PaginationMeta {
164
170
  limitParam?: string; // Limit param name (offset strategy only, e.g., "limit")
165
171
  dataPath?: string; // JSON path to the data array (e.g., "data", "results") — undefined means the response IS the data
166
172
  itemType: TypeRef; // Type of each item in the paginated list
173
+ envelopeFields?: Field[]; // Every envelope property besides the data array, in document order
167
174
  }
168
175
  ```
169
176
 
170
177
  Present on `Operation` when pagination is detected. The `strategy` discriminant tells emitters whether to generate cursor-based, offset-based, or link-header pagination helpers. The `dataPath` is dynamically detected from the response envelope — when `undefined`, emitters should treat the entire response as the data array or use their own default.
171
178
 
179
+ When the parser recognizes an inline list envelope — one array property plus a companion that looks like pagination metadata — `Operation.response` becomes the bare array, and `envelopeFields` keeps everything else the envelope carries: the pagination companion (`list_metadata`, `pagination`, `has_more`, `total`, …) and any discriminant such as `object: "list"`. Each entry is an ordinary `Field` (wire name, type, `required`, description), so `dataPath` + `itemType` + `envelopeFields` together describe the full shape of what the endpoint returns, and an emitter can both type the response and read the page details it needs for the next request:
180
+
181
+ ```typescript
182
+ // GET /customers → { customers: Customer[], pagination: Pagination }
183
+ op.pagination = {
184
+ strategy: "offset",
185
+ param: "page",
186
+ limitParam: "limit",
187
+ dataPath: "customers",
188
+ itemType: { kind: "model", name: "Customer" },
189
+ envelopeFields: [
190
+ {
191
+ name: "pagination",
192
+ type: { kind: "model", name: "Pagination" },
193
+ required: true,
194
+ },
195
+ ],
196
+ };
197
+ ```
198
+
199
+ A companion that is an inline object is lifted to a model. The same envelope recurs on every list endpoint of an API, so copies that agree structurally share one model named from the property alone (`list_metadata` → `ListMetadata`, `pagination` → `Pagination`) rather than one per operation as list items are (`ListUsersItem`). The bare name is taken only where it cannot change another type's shape. A copy collapses onto a target only when the target admits every value the copy can hold: the same fields with the same types, where the target may be looser (optional where the copy requires, nullable where the copy is not) but never stricter. When a component model of that name exists, the copies it admits collapse onto the component as written; otherwise the most common shape becomes the target. Any other copy keeps its operation-qualified name (`ListUsersPagination`). An inline enum in a companion is qualified by operation exactly like a parameter enum (`ListUsersSortOrder`). Companions declared through an `allOf` `$ref` branch are resolved against the component schemas, and a companion declared by more than one branch merges both declarations. Models and enums reached only through `envelopeFields` count as referenced for emission, service assignment, compat scoping, and the differ's request/response direction.
200
+
201
+ `envelopeFields` is absent when the response is a bare array, when the response is a `$ref` to a component schema (the wrapper model itself carries the companions), or when pagination was inferred from query parameters alone.
202
+
172
203
  The `link-header` strategy is for APIs that use RFC 5988 `Link` headers for pagination (e.g., GitHub API).
173
204
 
174
205
  ## Service & Operation
175
206
 
176
207
  Service names are derived from OpenAPI tags when present — the first tag on an operation is converted to PascalCase (e.g., `tags: ["multi-factor-auth"]` → service `MultiFactorAuth`). When no tag is present, the parser falls back to the first path segment (e.g., `/organizations/{id}` → `Organizations`). Emitters can further remap service names via an overlay (e.g., `MultiFactorAuth` → `Mfa`) to match existing SDK class names.
177
208
 
209
+ `Service.description` comes from the document's top-level `tags` list, matched by name: a tag entry applies to the service whose PascalCase name its `name` converts to, so `user-management`, `User Management`, and `user_management` all map to `UserManagement`. A service whose own name is a declared tag is described by that tag alone — its description when it has one, nothing otherwise — so another tag's wording is never borrowed for it (operations tagged `api_keys` that the path-prefix split merges into `UserManagement` do not label it "API keys"). A service whose own name is not declared, which is what the split produces when it carves `OrganizationDomains` out of operations tagged `organizations`, inherits the description of the first described tag its operations carry. The key is omitted when neither rule yields a description, including for path-named services whose operations have no tag. Malformed `tags` values (a non-array, `null` entries, non-string names) are skipped rather than failing the parse.
210
+
211
+ An operation carries its OpenAPI `summary` and `description` in two fields. `Operation.description` is the combined doc text: `summary + "\n\n" + description` when the document declares both with content, or whichever one has content (a summary identical to the description is not repeated). An empty or whitespace-only `summary` or `description` is treated as absent — `summary: "List pets"` with `description: ""` yields `description: "List pets"` — and text that has content is kept verbatim, surrounding whitespace included. An emitter that prints `description` as the doc comment therefore sees the same text whether or not the document used a summary. `Operation.summary` is the document's `summary` alone and is present only when the operation declares one with content, so an emitter that wants a one-line label (and the description without it) reads `summary` rather than guessing whether the first paragraph of `description` is one. When a doc URL base is configured, relative links are expanded in `summary` exactly as in `description`.
212
+
178
213
  ```typescript
179
214
  interface Service {
180
215
  name: string; // e.g., "Organizations"
@@ -184,32 +219,67 @@ interface Service {
184
219
 
185
220
  interface Operation {
186
221
  name: string; // e.g., "listUsers", "getUser", "createUser"
187
- description?: string;
222
+ summary?: string; // The document's summary alone; only when it has non-whitespace content
223
+ description?: string; // summary + "\n\n" + description when both have content, else whichever does
188
224
  httpMethod: HttpMethod; // All 8 standard HTTP methods
189
225
  path: string; // e.g., "/organizations/{id}"
190
226
  pathParams: Parameter[];
227
+ // Excludes `in: query` params that a POST/PUT/PATCH request body also declares;
228
+ // those travel body-only and are kept in bodyOwnedQueryParams instead.
191
229
  queryParams: Parameter[];
230
+ // Query declarations the body owns (see above). Emitters never serialize
231
+ // these to the URL; enum collection and the compat surface still walk them.
232
+ bodyOwnedQueryParams?: Parameter[];
192
233
  headerParams: Parameter[];
193
234
  cookieParams?: Parameter[]; // Present only when cookie params exist
194
235
  requestBody?: TypeRef;
195
236
  requestBodyEncoding?:
196
- | "json"
197
- | "form-data"
198
- | "form-urlencoded"
199
- | "binary"
200
- | "text";
237
+ "json" | "form-data" | "form-urlencoded" | "binary" | "text";
201
238
  response: TypeRef; // Primary response (lowest 2xx with body)
239
+ responseHeaders?: ResponseHeader[]; // Headers declared on the primary response (only when any exist)
202
240
  successResponses?: SuccessResponse[]; // All 2xx responses (only when multiple exist)
203
241
  errors: ErrorResponse[];
204
242
  pagination?: PaginationMeta;
205
243
  injectIdempotencyKey: boolean; // true when spec declares Idempotency-Key header
206
244
  deprecated?: boolean;
207
245
  async?: boolean;
246
+ security?: SecurityRequirementSet[]; // Per-operation requirements; undefined inherits ApiSpec.security
208
247
  }
209
248
 
249
+ interface SecurityRequirement {
250
+ schemeName: string; // Matches AuthScheme.schemeName in ApiSpec.auth
251
+ scopes: string[];
252
+ }
253
+
254
+ // One alternative: every scheme in the set is required together.
255
+ type SecurityRequirementSet = SecurityRequirement[];
256
+ ```
257
+
258
+ `security` mirrors the OpenAPI document instead of flattening it. Each requirement object in the document becomes one `SecurityRequirementSet` whose schemes must all be satisfied together ("all of"); the list of sets is satisfied by any one of them ("either of"); an empty set is an alternative that needs no credentials, which is how a document marks authentication optional. The field therefore distinguishes cases a flat list could not:
259
+
260
+ | Document | `Operation.security` | Meaning |
261
+ | --------------------------------------------------- | ------------------------------------- | ---------------------------------------------------- |
262
+ | no `security` key | `undefined` | inherit `ApiSpec.security` |
263
+ | `security: []` | `[]` | no authentication |
264
+ | `security: [{ userToken: [] }, { partnerKey: [] }]` | `[[{ userToken }], [{ partnerKey }]]` | either scheme |
265
+ | `security: [{ userToken: [], partnerKey: [] }]` | `[[{ userToken }, { partnerKey }]]` | both schemes together |
266
+ | `security: [{ userToken: [] }, {}]` | `[[{ userToken }], []]` | `userToken`, or nothing (authentication is optional) |
267
+
268
+ To resolve what an operation accepts, take `op.security ?? spec.security`; a result of `undefined` means the document declares no requirements at all. The same list shape applies to `ApiSpec.security`.
269
+
270
+ ```typescript
210
271
  interface SuccessResponse {
211
272
  statusCode: number;
212
273
  type: TypeRef;
274
+ headers?: ResponseHeader[]; // Only when the spec declares at least one
275
+ }
276
+
277
+ interface ResponseHeader {
278
+ name: string; // As written in the spec, e.g. "Content-Disposition"
279
+ type: TypeRef;
280
+ required: boolean;
281
+ description?: string;
282
+ deprecated?: boolean;
213
283
  }
214
284
 
215
285
  interface Parameter {
@@ -219,10 +289,24 @@ interface Parameter {
219
289
  description?: string;
220
290
  deprecated?: boolean;
221
291
  default?: unknown;
222
- example?: unknown;
292
+ example?: unknown; // The document's `example`, else `examples[0]`
293
+ examples?: unknown[]; // Every example value, in document order; only when non-empty
294
+ style?: "form" | "simple" | "label" | "matrix";
295
+ explode?: boolean;
296
+ constraints?: Constraints; // Validation keywords; see Constraints below
223
297
  }
224
298
  ```
225
299
 
300
+ `Parameter.description` is the first non-empty `description` found on, in order, the parameter itself, its inline `schema`, and the `$ref`'d component schema — the same fallback chain `deprecated`, `default`, and `example` use. An empty string at any level counts as absent, and the field is `undefined` (never `""`) when nothing in the chain is documented.
301
+
302
+ A parameter's `constraints` come from its own schema. When that schema is a `$ref`, the target's keywords are merged underneath the parameter's own — `{ $ref: Limit, minimum: 5 }` against `Limit: { type: integer, minimum: 1, maximum: 100 }` gives `{ minimum: 5, maximum: 100 }` — the per-value way `default` and `example` fall back, so a parameter can be read on its own without chasing the fieldless alias model the target also becomes. The key is absent when neither declares any.
303
+
304
+ ### Parameter Examples
305
+
306
+ `example` and `examples` are read from three levels in order: the parameter itself, then its inline `schema`, then the component schema its `$ref` points at. A parameter-level `examples` is an OpenAPI map of Example Objects, so each entry's `value` is taken in document order; a `$ref` entry is resolved against `components.examples` (a dangling ref is skipped with a warning), and an `externalValue` entry is skipped. A schema-level `examples` is the JSON Schema list and is taken as-is.
307
+
308
+ Each value walks the levels on its own. `examples` is the first level's non-empty list. `example` comes from the first level that has either an `example` or a list, preferring that level's own `example` over its first list entry, so a schema-level `example` can never mask the parameter's own `examples` map. A level with neither falls through; an explicit `null` is a value and is kept. Emitters that read only `example` therefore still see a value for an OAS 3.1 document that uses `examples` alone.
309
+
226
310
  ### Idempotency Key
227
311
 
228
312
  The `injectIdempotencyKey` flag is spec-driven: it is `true` only when the operation explicitly declares an `Idempotency-Key` header parameter in the OpenAPI spec. This means APIs that don't use idempotency keys (e.g., GitHub) get `false`, while APIs that do (e.g., Stripe, billing APIs) already declare the header in their spec and get `true`.
@@ -243,6 +327,16 @@ The `requestBodyEncoding` field tells emitters how to serialize the request body
243
327
 
244
328
  When an operation has multiple 2xx status codes, `successResponses` contains all of them. The `response` field always points to the primary response (lowest 2xx with a body schema). This is useful for emitters that need to handle different status codes differently (e.g., 200 vs 204).
245
329
 
330
+ ### Response Headers
331
+
332
+ Headers a response declares (`Content-Disposition` on a download, `Location` after a create, `ETag`, rate-limit headers) are kept with the status code they belong to as `headers: ResponseHeader[]` on each `SuccessResponse` and `ErrorResponse`. Because `successResponses` is only set when more than one 2xx exists, the primary response's headers are also exposed directly as `Operation.responseHeaders`.
333
+
334
+ Each header is typed like a parameter via its `schema` (or the first media type of its `content` map); a header with neither is a `string`. `$ref`s into `components/headers` are resolved, and a `description` written next to the `$ref` overrides the component's (OpenAPI 3.1); a ref that cannot be resolved is warned about and dropped rather than failing the parse. A header named `Content-Type` is ignored, as OpenAPI requires. All three fields are present only when at least one header survives, so operations without headers keep their shape.
335
+
336
+ An object-shaped header (directly, inside an array, or as a oneOf variant) is materialized as an inline model named from the service and header name (`Documents` + `X-Meta` → `DocumentsXMeta`), nested objects included (`DocumentsXMetaOrigin`). Because that name can already belong to a differently shaped component schema, to the same header on another status code, or to the same header on another operation of the service, the parser keeps a spec-wide registry of model shapes: the first owner of a name keeps it (component models first, then operations in path order, then a single operation's responses in ascending status-code order), and a later header with a different shape is re-derived under the first free candidate — bare name; then the status code (`DocumentsXMeta429`) when the clash is inside the same operation, or the operation (`DocumentsCreateDocumentXMeta`) when it is with a component or another operation; then the other of those two; then both (`DocumentsCreateDocumentXMeta429`); then a numeric suffix (`DocumentsXMeta2`). A header whose shape matches the name's owner shares it and materializes nothing new, so a header matching a component schema simply references that schema. Inline header enums follow the same idea with their own, status-code-based rule.
337
+
338
+ `collectResponseHeaders(op)` (exported from `@workos/oagen`) yields every header list once — the primary list is the same array as its `successResponses` entry — and is what reachability, model assignment, ref rewriting/validation, the differ, and the compat filter use, so a model or enum reached only through a header is emitted like any other.
339
+
246
340
  ## Model & Field
247
341
 
248
342
  ```typescript
@@ -251,6 +345,7 @@ interface Model {
251
345
  description?: string;
252
346
  fields: Field[];
253
347
  typeParams?: TypeParam[]; // Generic type parameters
348
+ constraints?: Constraints; // The object schema's own minProperties / maxProperties / dependentRequired
254
349
  }
255
350
 
256
351
  interface Field {
@@ -262,9 +357,71 @@ interface Field {
262
357
  writeOnly?: boolean;
263
358
  deprecated?: boolean;
264
359
  default?: unknown;
360
+ example?: unknown; // The schema's `example`, else `examples[0]`
361
+ examples?: unknown[]; // The JSON Schema `examples` list; only when non-empty
362
+ constraints?: Constraints; // Validation keywords on the field's schema, siblings of a $ref included; a bare $ref yields none
363
+ }
364
+ ```
365
+
366
+ ### Inline model naming
367
+
368
+ Models come from `components/schemas` (named as written, after `cleanSchemaName` and any `schemaNameTransform`) and from inline object schemas in operations, which the parser materializes as models so emitters have something to reference. An inline request body or response is named from its operation:
369
+
370
+ - **With an `operationId`:** `PascalCase(operationId)` + `Request` / `Response`, passed through `cleanSchemaName`. `createNote` → `CreateNoteRequest` / `CreateNoteResponse`.
371
+ - **Without an `operationId`:** from the full path and method. Each non-parameter segment is PascalCased in order, each `{param}` segment becomes `By` + `PascalCase(param)`, then the method and `Request` / `Response` are appended, and the whole name is normalized with `toPascalCase` so an operation's body and response share a prefix (`POST /a/b` → `AbPostRequest` / `AbPostResponse`). `POST /notes` → `NotesPostRequest` / `NotesPostResponse`; `POST /notes/{id}/pin` → `NotesByIdPinPostResponse`; `GET /users/{user_id}/sessions` → `UsersByUserIdSessionsGetResponse`. This tells the common path shapes apart (list vs. item, sibling and nested sub-resources) but is not unique by construction: `/foo-bar` and `/foo/bar`, `/notes/by-id` and `/notes/{id}`, or `/notes` and `/notes/` flatten to the same words. Names already taken are reserved first: every component schema name and, for every operation with an `operationId`, the stem all of its models start with (`PascalCase(operationId)` after `cleanSchemaName`). A path-based prefix is treated as colliding when any reserved name starts with it, because one prefix is the stem of several emitted names (`…Request`, `…Response`, a list's `…Item`, nested inline models such as `…ResponseOwner`, envelope companions such as `…ListMetadata`). Names derived from a path-based prefix use it verbatim — body `oneOf` variants included (`NotesPostRequest`, `NotesPostRequest2`, `EmailNotesPostRequest`) — and are never re-cleaned, so no later step can move them out from under the reservation; as a second line of defence the check also runs on the cleaned spellings of the prefix (`cleanSchemaName`, the project's `schemaNameTransform`, `ListItem`-marker stripping), so an unnamed `GET /notes` also yields to a component `NoteGetItem`. Names that embed the prefix behind a const value are covered as well: a body `oneOf` variant pinning `kind: email` is `EmailTagsPostRequest` and a nested `oneOf`/`anyOf` variant is `<Const><Parent><Field>`, so every string `const` (or single-value `enum`) found in the operation's inline body and 2xx response variants, top level and nested, is prepended to every spelling before the check, and a hashed prefix lands inside those names too (`EmailTags151Dd5PostRequest`). Names that do not embed the prefix at all — a response `oneOf` variant named from its bare const (`Email`) or a `title`-mapped variant — are outside what a disambiguator can separate and are left as they are. This is deliberately a superset: a component `AuditEventsGetter`, or an operationId `auditEventsGetSummary`, also pushes an unnamed `GET /audit/events` onto a hashed name although no emitted name would coincide; determinism and never sharing a name wrongly matter more than the rare extra hash. The reservation is unconditional, so an inline body is never merged into a same-named component even when their shapes agree. When two distinct paths with the same method would collide, the path spelled most plainly keeps the plain name (no trailing slash, `{id}` rather than a literal `by-id`; precisely, the lowest `[trailing slash, literal segments with a character outside a-z0-9]`), and every other path in the group gets a six-character disambiguator derived from its raw path (the start of its SHA-1, inserted before the method): `POST /foo/bar` next to `POST /foo-bar` → `FooBarPostResponse` and `FooBar546F12PostResponse`; `GET /notes/{id}` next to `GET /notes/by-id` → `NotesByIdGetResponse` and `NotesByIdBa41B5GetResponse`. When two paths tie for plainest (`/foo-bar` and `/foo_bar`), or the path-based name matches a reserved one (an unnamed `POST /audit/events` next to `operationId: auditEventsPost` or next to `components/schemas/AuditEventsPostRequest`), nobody keeps the plain name and every path in the group is disambiguated. The result depends only on the document's paths, `operationId`s and component names and never on their order; `operationId`-derived and component names never change, and paths that do not collide keep the plain name.
372
+
373
+ A response object with a string `const` discriminant (`object: "note"`) is named from that value instead (`Note`); a list envelope's inline item model replaces the `Response` suffix with `Item`; nested inline objects are qualified with their parent model's name.
374
+
375
+ If two inline models still resolve to the same name with different fields or field types (two `operationId`s that PascalCase to the same word, for example), the parser keeps the one with more fields — the first definition on a tie — and prints `[oagen] Warning: Inline model "…" has more than one definition with different fields or field types`.
376
+
377
+ ### Field Examples
378
+
379
+ OpenAPI 3.1 deprecates a schema's `example` in favour of the JSON Schema `examples` list. The parser reads both: `examples` carries every value the document gives, and `example` is the document's `example` when present, otherwise the first entry of `examples`. A field whose schema is a `$ref` to a component falls back to that component's `example`/`examples`, the same way a parameter does. A field whose schema (and `$ref` target, if any) gives neither has no `example` and no `examples` key.
380
+
381
+ ### Constraints
382
+
383
+ JSON Schema's validation keywords are carried on `Field`, `Parameter` and `Model` so an emitter can document a value's limits ("at most 100"), check a value before sending it, or keep a generated example within range. `type`, `enum`, `const` and `required` already have their own place in the IR and are not repeated here.
384
+
385
+ ```typescript
386
+ interface Constraints {
387
+ // numbers
388
+ minimum?: number;
389
+ maximum?: number;
390
+ exclusiveMinimum?: number;
391
+ exclusiveMaximum?: number;
392
+ multipleOf?: number;
393
+ // strings
394
+ minLength?: number;
395
+ maxLength?: number;
396
+ pattern?: string; // ECMA-262 regex, verbatim from the document
397
+ // arrays
398
+ minItems?: number;
399
+ maxItems?: number;
400
+ uniqueItems?: boolean; // present only when true
401
+ minContains?: number;
402
+ maxContains?: number;
403
+ // objects
404
+ minProperties?: number;
405
+ maxProperties?: number;
406
+ dependentRequired?: Record<string, string[]>;
265
407
  }
266
408
  ```
267
409
 
410
+ Every member is optional and present only when the document declares it. A schema with no validation keyword yields no `constraints` key at all, so `field.constraints?.maxLength` is the idiomatic read. The parser ignores a keyword whose value has the wrong type (a string `maxLength`, say) rather than carrying it.
411
+
412
+ **OAS 3.0 → 3.1 normalization.** OpenAPI 3.1 (JSON Schema 2020-12) gives `exclusiveMinimum` / `exclusiveMaximum` as the bound itself; OpenAPI 3.0 spells them as booleans that make `minimum` / `maximum` exclusive. Both versions are parsed, and the IR always uses the 3.1 shape:
413
+
414
+ | Document (3.0) | IR |
415
+ | ------------------------------------------ | ------------------------------------------ |
416
+ | `minimum: 0, exclusiveMinimum: true` | `{ exclusiveMinimum: 0 }` (no `minimum`) |
417
+ | `maximum: 100, exclusiveMaximum: true` | `{ exclusiveMaximum: 100 }` (no `maximum`) |
418
+ | `minimum: 0, exclusiveMinimum: false` | `{ minimum: 0 }` |
419
+ | `exclusiveMinimum: true` with no `minimum` | nothing |
420
+
421
+ A 3.1 document's numeric form passes through unchanged, including the legal-but-unusual case of an inclusive and an exclusive bound on the same side.
422
+
423
+ **Scope.** The keywords declared on the field, parameter or model schema are collected, conjoined with those of its inline `allOf` members: the parser unwraps a single-member `allOf`, so `{ allOf: [{ type: string, maxLength: 3 }] }` is a string field carrying `{ maxLength: 3 }`, and a `$ref` member contributes the keywords written beside the reference while the referenced model keeps its own. Limits nested inside array `items` or map values (e.g. `items: { maxLength: 10 }`) are not carried. The one exception is a nullable wrapper — a `oneOf`/`anyOf` with exactly one non-null, non-`$ref` branch, which the IR collapses to `nullable<T>` — whose branch keywords are conjoined with the wrapper's own (and its `allOf` members'), so `anyOf: [{ type: string, maxLength: 8 }, { type: "null" }]` gives `{ maxLength: 8 }` and `{ allOf: [{ maxLength: 30 }], anyOf: [{ type: string, minLength: 20 }, { type: "null" }] }` gives `{ maxLength: 30, minLength: 20 }`, and an inline model built from that branch carries the same conjoined keywords, so `minProperties` written beside `anyOf: [{ type: object, … }, { type: "null" }]` reaches both the field and its model. Limits on a general multi-variant union are not carried. Keywords written beside a `$ref` (OpenAPI 3.1 allows them) are the field's own and are kept; a bare `$ref` field gets no `constraints`, because its limits belong to the referenced model. A `Model` carries whatever its own schema declares — for an object schema that is `minProperties`, `maxProperties` and `dependentRequired`; for an `allOf` model — a component, or an inline response, request body or field the parser rebuilds from `allOf` members — it is the wrapper's keywords conjoined with its members' (the stricter bound wins; a `$ref` member contributes the keywords written beside the reference, while the referenced model keeps its own). A variant model of a discriminated `allOf` + `oneOf` union conjoins the shared keywords — the outer wrapper's, the base members' and any written beside the `oneOf` — with the variant's own the way `allOf` does — an instance must satisfy both, so the larger `min*` and the smaller `max*` win and `dependentRequired` maps are unioned per property.
424
+
268
425
  ## Enum
269
426
 
270
427
  ```typescript
@@ -287,6 +444,7 @@ interface EnumValue {
287
444
  interface ErrorResponse {
288
445
  statusCode: number; // e.g., 401, 404, 422
289
446
  type?: TypeRef; // Response body type, if any
447
+ headers?: ResponseHeader[]; // e.g. Retry-After on a 429; only when the spec declares at least one
290
448
  }
291
449
  ```
292
450