@workos/oagen 0.6.0 → 0.8.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.
- package/dist/cli/index.mjs +1339 -451
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +144 -57
- package/dist/compat/index.d.mts.map +1 -1
- package/dist/compat/index.mjs +365 -35
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-CltJMcwG.mjs → diff-BaH9qa8k.mjs} +100 -23
- package/dist/diff-BaH9qa8k.mjs.map +1 -0
- package/dist/differ-CTl98yoS.d.mts +116 -0
- package/dist/differ-CTl98yoS.d.mts.map +1 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +177 -4
- package/dist/index.mjs.map +1 -1
- package/dist/{differ-CG5ZErKl.mjs → overlay-TYxHOCow.mjs} +1112 -553
- package/dist/overlay-TYxHOCow.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.mjs +176 -3
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +43 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +176 -3
- package/dist/smoke.mjs.map +1 -1
- package/dist/tree-sitter-C53w_0WE.mjs.map +1 -1
- package/dist/{types-C7g5sLuX.d.mts → types-mTtkgy_h.d.mts} +213 -2
- package/dist/types-mTtkgy_h.d.mts.map +1 -0
- package/dist/verify/index.d.mts +4 -3
- package/dist/verify/index.d.mts.map +1 -1
- package/dist/verify/index.mjs +12 -10
- package/dist/verify/index.mjs.map +1 -1
- package/docs/architecture/emitter-contract.md +2 -2
- package/docs/architecture/extractor-contract.md +6 -5
- package/docs/architecture/pipeline.md +4 -3
- package/docs/architecture/workflows.md +5 -3
- package/docs/cli.md +148 -6
- package/docs/contributor/public-api.md +1 -1
- package/docs/core/compatibility-ir.md +200 -0
- package/docs/core/compatibility-policy.md +170 -0
- package/docs/core/compatibility.md +83 -0
- package/docs/core/index.md +5 -0
- package/docs/core/manifest-schema.md +76 -0
- package/docs/core/quickstart.md +9 -2
- package/docs/core/verify-compat.md +235 -0
- package/package.json +1 -1
- package/skills/generate-emitter/SKILL.md +12 -7
- package/skills/generate-emitter/references/project-scaffold.md +27 -6
- package/skills/generate-extractor/SKILL.md +14 -8
- package/skills/generate-sdk/SKILL.md +2 -2
- package/skills/generate-smoke-test/SKILL.md +9 -5
- package/skills/review-operations/SKILL.md +2 -2
- package/skills/verify-compat/SKILL.md +1 -1
- package/dist/diff-CltJMcwG.mjs.map +0 -1
- package/dist/differ-CG5ZErKl.mjs.map +0 -1
- package/dist/types-C7g5sLuX.d.mts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["path","path"],"sources":["../../src/verify/run-compat-check.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 diffSurfaces,\n specDerivedNames,\n specDerivedFieldPaths,\n specDerivedMethodPaths,\n specDerivedEnumValues,\n filterSurface,\n} from '../compat/differ.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): 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 diff = diffSurfaces(scopedBaseline, candidate, extractor.hints);\n const passed = diff.violations.every((v) => v.severity !== 'breaking');\n\n return { passed, diff, scopedToSpec, scopedSymbolCount };\n}\n","import { existsSync, readFileSync, readdirSync, unlinkSync, statSync, openSync, readSync, closeSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { buildOverlayLookup, patchOverlay } from '../compat/overlay.js';\nimport type { ManifestEntry } from '../compat/overlay.js';\nimport { getExtractor } from '../compat/extractor-registry.js';\nimport type { ApiSurface, ViolationCategory } 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';\n\nconst PATCHABLE_CATEGORIES: Set<ViolationCategory> = new Set(['public-api', 'export-structure']);\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\n/**\n * Load a smoke-manifest.json from the generated SDK directory and convert\n * it to ManifestEntry[] for the overlay builder.\n */\nfunction loadManifestFromDir(outputDir: string): ManifestEntry[] | undefined {\n const manifestPath = path.join(outputDir, 'smoke-manifest.json');\n if (!existsSync(manifestPath)) return undefined;\n try {\n const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));\n if (Array.isArray(parsed)) return parsed as ManifestEntry[];\n if (typeof parsed === 'object' && parsed !== null) {\n return Object.entries(parsed).map(([httpKey, value]) => {\n const spaceIdx = httpKey.indexOf(' ');\n const httpMethod = httpKey.slice(0, spaceIdx);\n const httpPath = httpKey.slice(spaceIdx + 1);\n const v = value as { sdkMethod?: string; service?: string };\n return {\n operationId: '',\n sdkResourceProperty: v.service ?? '',\n sdkMethodName: v.sdkMethod ?? '',\n httpMethod,\n path: httpPath,\n pathParams: [],\n bodyFields: [],\n queryFields: [],\n };\n });\n }\n } catch {\n // ignore parse errors\n }\n return undefined;\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 = loadManifestFromDir(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.violations.filter((v) => PATCHABLE_CATEGORIES.has(v.category));\n if (patchable.length === 0) {\n return {\n status: 'no-patchable',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n\n const currentPreserved = compatResult.diff.preservedSymbols;\n if (attempt > 0 && currentPreserved <= prevPreserved) {\n return {\n status: 'stalled',\n attempts: attempt,\n patchedPerIteration,\n compatResult,\n };\n }\n prevPreserved = currentPreserved;\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"],"sources":["../../src/verify/run-compat-check.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 { existsSync, readFileSync, readdirSync, unlinkSync, statSync, openSync, readSync, closeSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { buildOverlayLookup, patchOverlay, isPatchableChange } from '../compat/overlay.js';\nimport type { ManifestEntry } 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';\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\n/**\n * Load a smoke-manifest.json from the generated SDK directory and convert\n * it to ManifestEntry[] for the overlay builder.\n */\nfunction loadManifestFromDir(outputDir: string): ManifestEntry[] | undefined {\n const manifestPath = path.join(outputDir, 'smoke-manifest.json');\n if (!existsSync(manifestPath)) return undefined;\n try {\n const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));\n if (Array.isArray(parsed)) return parsed as ManifestEntry[];\n if (typeof parsed === 'object' && parsed !== null) {\n return Object.entries(parsed).map(([httpKey, value]) => {\n const spaceIdx = httpKey.indexOf(' ');\n const httpMethod = httpKey.slice(0, spaceIdx);\n const httpPath = httpKey.slice(spaceIdx + 1);\n const v = value as { sdkMethod?: string; service?: string };\n return {\n operationId: '',\n sdkResourceProperty: v.service ?? '',\n sdkMethodName: v.sdkMethod ?? '',\n httpMethod,\n path: httpPath,\n pathParams: [],\n bodyFields: [],\n queryFields: [],\n };\n });\n }\n } catch {\n // ignore parse errors\n }\n return undefined;\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 = loadManifestFromDir(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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"}
|
|
@@ -154,9 +154,9 @@ Production emitters live in the separate `oagen-emitters` project and import typ
|
|
|
154
154
|
1. Create design doc at `docs/sdk-architecture/{language}.md` in the emitter project
|
|
155
155
|
2. Scaffold files under `src/{language}/` in the emitter project
|
|
156
156
|
3. Implement all `Emitter` methods
|
|
157
|
-
4.
|
|
157
|
+
4. Export through the emitter project's plugin bundle (e.g. `src/plugin.ts`)
|
|
158
158
|
5. Add tests under `test/{language}/` in the emitter project
|
|
159
159
|
|
|
160
160
|
Or use: `/generate-emitter <language>`
|
|
161
161
|
|
|
162
|
-
Emitters are registered via `oagen.config.ts`
|
|
162
|
+
Emitters are registered via the plugin bundle exported from the emitter package. The consumer project's `oagen.config.ts` imports the bundle and layers spec interpretation policy on top. See the [CLI Reference](../cli.md#configuration-oagenconfigts) for details.
|
|
@@ -98,12 +98,13 @@ const goHints = resolveHints({
|
|
|
98
98
|
Extractors are registered via `oagen.config.ts` in the consumer project. The CLI loads the config at startup and registers all extractors automatically before any command runs.
|
|
99
99
|
|
|
100
100
|
```typescript
|
|
101
|
-
// oagen.config.ts
|
|
102
|
-
import {
|
|
101
|
+
// oagen.config.ts (consumer project)
|
|
102
|
+
import { workosEmittersPlugin } from "@workos/oagen-emitters";
|
|
103
103
|
import type { OagenConfig } from "@workos/oagen";
|
|
104
104
|
|
|
105
105
|
const config: OagenConfig = {
|
|
106
|
-
extractors
|
|
106
|
+
...workosEmittersPlugin, // includes all extractors
|
|
107
|
+
// spec interpretation policy...
|
|
107
108
|
};
|
|
108
109
|
export default config;
|
|
109
110
|
```
|
|
@@ -212,8 +213,8 @@ All tree-sitter parsers use `safeParse()` from `src/utils/tree-sitter.ts` to han
|
|
|
212
213
|
3. Provide `hints` as a `LanguageHints` object. Every extractor must have hints — the differ and overlay delegate all language-specific logic through them.
|
|
213
214
|
4. Split into `{language}-parser.ts` and `{language}-surface.ts` if either: (a) the language separates type definitions from method implementations (structural joining needed), or (b) the classification logic for mapping parsed types to `ApiClass`/`ApiInterface`/`ApiEnum` is non-trivial. If the AST co-locates symbols and classification is straightforward, a single `{language}-parser.ts` returning `ApiClass[]` directly is sufficient.
|
|
214
215
|
5. Use `safeParse()` from `src/utils/tree-sitter.ts` instead of calling `parser.parse()` directly.
|
|
215
|
-
6. Export from `src/index.ts`
|
|
216
|
-
7.
|
|
216
|
+
6. Export from `src/index.ts` and include in the plugin bundle
|
|
217
|
+
7. The consumer project's `oagen.config.ts` picks it up through the plugin bundle
|
|
217
218
|
8. Add tests in `test/compat/extractors/{language}.test.ts`
|
|
218
219
|
9. Create a fixture SDK in `test/fixtures/sample-sdk-{language}/` with known classes, methods, and exports
|
|
219
220
|
10. Verify deterministic output: extracting twice produces identical JSON
|
|
@@ -96,11 +96,12 @@ Every generator method receives:
|
|
|
96
96
|
Simple map from language name → `Emitter` instance. Emitters are registered at startup via `oagen.config.ts`:
|
|
97
97
|
|
|
98
98
|
```typescript
|
|
99
|
-
// In oagen.config.ts (
|
|
100
|
-
import {
|
|
99
|
+
// In oagen.config.ts (consumer project)
|
|
100
|
+
import { workosEmittersPlugin } from "@workos/oagen-emitters";
|
|
101
101
|
|
|
102
102
|
export default {
|
|
103
|
-
|
|
103
|
+
...workosEmittersPlugin,
|
|
104
|
+
// spec interpretation policy...
|
|
104
105
|
};
|
|
105
106
|
```
|
|
106
107
|
|
|
@@ -12,7 +12,7 @@ gets written or fixed.
|
|
|
12
12
|
|
|
13
13
|
### Skill sequence
|
|
14
14
|
|
|
15
|
-
**Project setup:** Run `oagen init --lang {language}` to
|
|
15
|
+
**Project setup:** Run `oagen init --lang {language}` to scaffold an emitter project, then implement the emitter methods. Export the emitter through a plugin bundle so the consumer project can register it via its `oagen.config.ts`.
|
|
16
16
|
|
|
17
17
|
**Scenario A** (existing SDK to preserve):
|
|
18
18
|
|
|
@@ -106,8 +106,10 @@ to preserve backwards compatibility:
|
|
|
106
106
|
3. **Verify:** `oagen verify --spec v2.yml --lang {lang} --output ./sdk --api-surface sdk-{lang}-surface.json`
|
|
107
107
|
4. **Ship** if verify exits 0
|
|
108
108
|
|
|
109
|
-
**
|
|
110
|
-
|
|
109
|
+
**Consumer projects** register emitters and extractors by importing plugin bundles
|
|
110
|
+
from emitter packages into their `oagen.config.ts`. Spec interpretation policy
|
|
111
|
+
(transforms, hints, mount rules) also lives in the consumer config. See the
|
|
112
|
+
[CLI Reference](../cli.md#configuration-oagenconfigts) for details.
|
|
111
113
|
|
|
112
114
|
### If verify fails on a spec update
|
|
113
115
|
|
package/docs/cli.md
CHANGED
|
@@ -52,6 +52,85 @@ oagen extract --sdk-path ./existing-sdk --lang ruby --output my-surface.json
|
|
|
52
52
|
| `--lang <language>` | Yes | | Target language |
|
|
53
53
|
| `--output <path>` | No | `sdk-{lang}-surface.json` | Output file path (recommend writing to your output dir, e.g. `./sdk/sdk-ruby-surface.json`, and gitignoring it) |
|
|
54
54
|
|
|
55
|
+
## `oagen compat-extract`
|
|
56
|
+
|
|
57
|
+
Extract a compat snapshot from a live SDK and write it to a JSON file. The snapshot is a machine-readable record of the SDK's public API surface, used as input to `compat-diff`.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
oagen compat-extract --sdk-path ./existing-sdk --lang node --output .
|
|
61
|
+
oagen compat-extract --sdk-path ./existing-sdk --lang php --output ./sdk --spec openapi.yml
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Argument | Required | Default | Description |
|
|
65
|
+
| ------------------- | -------- | ----------------------- | ---------------------------------------------------------------------------------- |
|
|
66
|
+
| `--sdk-path <path>` | Yes | | Path to the live SDK |
|
|
67
|
+
| `--lang <language>` | Yes | | Target language |
|
|
68
|
+
| `--output <dir>` | Yes | | Directory to write `.oagen-compat-snapshot.json` into |
|
|
69
|
+
| `--spec <path>` | No | `OPENAPI_SPEC_PATH` env | Path to OpenAPI spec — enriches symbols with `operationId`, `route`, and `specSha` |
|
|
70
|
+
|
|
71
|
+
Always writes `.oagen-compat-snapshot.json` in the specified directory. The snapshot file is meant to be committed to the repository and updated on each release.
|
|
72
|
+
|
|
73
|
+
When `--spec` is provided, the command parses the spec and enriches callable symbols with `operationId` and `route` (HTTP method + path) by matching against spec operations. It also computes a SHA-256 of the spec file and stores it as `source.specSha` in the snapshot.
|
|
74
|
+
|
|
75
|
+
## `oagen compat-diff`
|
|
76
|
+
|
|
77
|
+
Diff two compat snapshot files and produce a classified change report. No extraction or spec required — pure file-in, report-out.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Basic diff with terminal output
|
|
81
|
+
oagen compat-diff --baseline .oagen-compat-snapshot.json --candidate /tmp/candidate.json
|
|
82
|
+
|
|
83
|
+
# With machine-readable report and failure threshold
|
|
84
|
+
oagen compat-diff \
|
|
85
|
+
--baseline .oagen-compat-snapshot.json \
|
|
86
|
+
--candidate /tmp/candidate.json \
|
|
87
|
+
--output compat-report.json \
|
|
88
|
+
--fail-on breaking
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Argument | Required | Default | Description |
|
|
92
|
+
| -------------------- | -------- | ---------- | -------------------------------------------------- |
|
|
93
|
+
| `--baseline <path>` | Yes | | Path to the baseline compat snapshot JSON |
|
|
94
|
+
| `--candidate <path>` | Yes | | Path to the candidate compat snapshot JSON |
|
|
95
|
+
| `--output <path>` | No | | Write machine-readable JSON report to this path |
|
|
96
|
+
| `--fail-on <level>` | No | `breaking` | Fail threshold: `none`, `breaking`, or `soft-risk` |
|
|
97
|
+
| `--explain` | No | `false` | Include provenance explanations in terminal output |
|
|
98
|
+
|
|
99
|
+
**Exit codes:** 0 = no changes exceed threshold, 1 = changes exceed threshold.
|
|
100
|
+
|
|
101
|
+
## `oagen compat-summary`
|
|
102
|
+
|
|
103
|
+
Format a compat report as a markdown PR comment. Reads the JSON report produced by `compat-diff --output` and outputs markdown to stdout or a file.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Single language
|
|
107
|
+
oagen compat-summary --report compat-report.json | gh pr comment --body-file -
|
|
108
|
+
|
|
109
|
+
# Cross-language rollup (multiple reports)
|
|
110
|
+
oagen compat-summary --report php.json --report python.json --report go.json
|
|
111
|
+
|
|
112
|
+
# Write to a file
|
|
113
|
+
oagen compat-summary --report compat-report.json --output summary.md
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Argument | Required | Default | Description |
|
|
117
|
+
| -------------------- | -------- | ------- | -------------------------------------------------------------------------- |
|
|
118
|
+
| `--report <path...>` | Yes | | Path(s) to compat report JSON(s) — pass multiple for cross-language rollup |
|
|
119
|
+
| `--output <path>` | No | stdout | Write markdown to this file instead of stdout |
|
|
120
|
+
|
|
121
|
+
**Single report** output includes:
|
|
122
|
+
|
|
123
|
+
- Status header (pass/warning/fail)
|
|
124
|
+
- Summary table with breaking, soft-risk, and additive counts
|
|
125
|
+
- Breaking changes table (always visible)
|
|
126
|
+
- Soft-risk and additive changes in collapsible `<details>` sections
|
|
127
|
+
|
|
128
|
+
**Multiple reports** produce a cross-language rollup:
|
|
129
|
+
|
|
130
|
+
- Per-language summary table (breaking/soft-risk/additive per language)
|
|
131
|
+
- Conceptual changes table showing per-language severity for each change
|
|
132
|
+
- Same conceptual change across languages is shown as one row, not N separate rows
|
|
133
|
+
|
|
55
134
|
## `oagen verify`
|
|
56
135
|
|
|
57
136
|
Verify an already-generated SDK — run smoke tests and optional compat checks. Use after `oagen generate`.
|
|
@@ -103,9 +182,10 @@ Creates:
|
|
|
103
182
|
|
|
104
183
|
- `package.json` with `@workos/oagen` dependency and `sdk:generate`/`sdk:verify`/`sdk:extract` scripts
|
|
105
184
|
- `tsconfig.json`, `vitest.config.ts`, `tsup.config.ts` — build/test tooling
|
|
106
|
-
- `oagen.config.ts` —
|
|
185
|
+
- `oagen.config.ts` — minimal config for local development, imports the plugin bundle
|
|
186
|
+
- `src/plugin.ts` — plugin bundle export registering the stub emitter
|
|
107
187
|
- `src/{language}/index.ts` — stub emitter implementing the full `Emitter` interface
|
|
108
|
-
- `src/index.ts` — barrel export
|
|
188
|
+
- `src/index.ts` — barrel export including the plugin bundle
|
|
109
189
|
- Empty directories: `test/`, `smoke/`, `docs/sdk-architecture/`
|
|
110
190
|
|
|
111
191
|
After initialization, `npm run typecheck` passes immediately. Implement your emitter methods in `src/{language}/` to start generating SDK code.
|
|
@@ -141,19 +221,80 @@ oagen resolve --spec openapi.yml --format json
|
|
|
141
221
|
|
|
142
222
|
When `oagen.config.ts` defines `operationHints` and/or `mountRules`, they are applied automatically. The table format flags operations that have no hint and are resolved purely by algorithm.
|
|
143
223
|
|
|
224
|
+
## `--config <path>`
|
|
225
|
+
|
|
226
|
+
All commands accept a global `--config <path>` option to load a specific config file instead of the default cwd-based discovery.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# Explicit config
|
|
230
|
+
oagen generate --config ../openapi-spec/oagen.config.ts --spec openapi.yml --lang node --output ./sdk
|
|
231
|
+
|
|
232
|
+
# Default: searches for oagen.config.ts in the current directory
|
|
233
|
+
oagen generate --spec openapi.yml --lang node --output ./sdk
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
When `--config` is omitted, the CLI searches the current working directory for `oagen.config.ts`, `oagen.config.js`, or `oagen.config.mjs` in that order.
|
|
237
|
+
|
|
144
238
|
## Configuration (`oagen.config.ts`)
|
|
145
239
|
|
|
146
|
-
Place an `oagen.config.ts` (or `.js`/`.mjs`) in
|
|
240
|
+
Place an `oagen.config.ts` (or `.js`/`.mjs`) in the project that drives generation. The CLI loads this file at startup before any command runs.
|
|
241
|
+
|
|
242
|
+
The config file belongs to the **consumer project** (the project that owns spec interpretation policy), not the emitter library. Emitter packages export plugin bundles that the consumer config composes.
|
|
243
|
+
|
|
244
|
+
### Consumer config (recommended)
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import type { OagenConfig } from "@workos/oagen";
|
|
248
|
+
import { workosEmittersPlugin } from "@workos/oagen-emitters";
|
|
249
|
+
|
|
250
|
+
const config: OagenConfig = {
|
|
251
|
+
...workosEmittersPlugin,
|
|
252
|
+
docUrl: "https://workos.com/docs",
|
|
253
|
+
operationIdTransform: (id) => id.replace(/Controller_/, ""),
|
|
254
|
+
operationHints,
|
|
255
|
+
mountRules,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export default config;
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Standalone emitter config
|
|
262
|
+
|
|
263
|
+
For standalone emitter projects that also serve as the consumer:
|
|
147
264
|
|
|
148
265
|
```ts
|
|
149
266
|
import type { OagenConfig } from "@workos/oagen";
|
|
150
267
|
import { nodeEmitter } from "./src/node/index.js";
|
|
151
|
-
import { nodeExtractor } from "./src/node/extractor.js";
|
|
152
268
|
|
|
153
269
|
const config: OagenConfig = {
|
|
154
270
|
emitters: [nodeEmitter],
|
|
155
|
-
|
|
156
|
-
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export default config;
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Composing multiple plugin bundles
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import type { OagenConfig } from "@workos/oagen";
|
|
280
|
+
import { workosEmittersPlugin } from "@workos/oagen-emitters";
|
|
281
|
+
import { experimentalPlugin } from "@workos/oagen-experimental";
|
|
282
|
+
|
|
283
|
+
const config: OagenConfig = {
|
|
284
|
+
emitters: [
|
|
285
|
+
...(workosEmittersPlugin.emitters ?? []),
|
|
286
|
+
...(experimentalPlugin.emitters ?? []),
|
|
287
|
+
],
|
|
288
|
+
extractors: [
|
|
289
|
+
...(workosEmittersPlugin.extractors ?? []),
|
|
290
|
+
...(experimentalPlugin.extractors ?? []),
|
|
291
|
+
],
|
|
292
|
+
smokeRunners: {
|
|
293
|
+
...(workosEmittersPlugin.smokeRunners ?? {}),
|
|
294
|
+
...(experimentalPlugin.smokeRunners ?? {}),
|
|
295
|
+
},
|
|
296
|
+
operationHints,
|
|
297
|
+
mountRules,
|
|
157
298
|
};
|
|
158
299
|
|
|
159
300
|
export default config;
|
|
@@ -168,6 +309,7 @@ export default config;
|
|
|
168
309
|
| `emitterProject` | `string` | Path to the emitter project directory. Used by skills to scaffold new emitters, tests, and smoke runners in the correct location. |
|
|
169
310
|
| `smokeRunners` | `Record<string, string>` | Map from language key to custom smoke runner script path. Overrides the built-in `sdk-test.ts` for `oagen verify`. Can also be set per-invocation with `--smoke-runner`. |
|
|
170
311
|
| `operationIdTransform` | `(id: string) => string` | Custom transform for operation IDs. Receives the raw `operationId` from the spec; return the desired operation name. No additional casing conversion is applied. When omitted, `operationId` values are converted to `camelCase`. |
|
|
312
|
+
| `schemaNameTransform` | `(name: string) => string` | Custom transform for schema (model/enum) names. Applied after the built-in `cleanSchemaName` normalization. Receives the cleaned PascalCase name; return the desired name. |
|
|
171
313
|
| `docUrl` | `string` | Base URL for documentation links. When set, relative markdown paths in descriptions (e.g. `[User](/reference/authkit/user)`) are expanded to full URLs (e.g. `[User](https://workos.com/docs/reference/authkit/user)`). |
|
|
172
314
|
| `operationHints` | `Record<string, OperationHint>` | Per-operation overrides keyed by `"METHOD /path"`. Override derived method names, remount to a different service, or split union-body operations into typed wrappers. See [Operation Resolution](architecture/ir-types.md#operation-resolution). |
|
|
173
315
|
| `mountRules` | `Record<string, string>` | Service-level remounting. Maps IR service name to target service/namespace (PascalCase). All operations in the source service are mounted on the target unless overridden per-operation in `operationHints`. |
|
|
@@ -15,7 +15,7 @@ It includes:
|
|
|
15
15
|
- spec diffing APIs such as `diffSpecs`
|
|
16
16
|
- naming utilities used by emitters
|
|
17
17
|
- `planOperation`
|
|
18
|
-
- config typing needed by emitter projects
|
|
18
|
+
- config typing needed by consumer and emitter projects
|
|
19
19
|
|
|
20
20
|
Changes to this surface require a deliberate compatibility review.
|
|
21
21
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Compatibility IR
|
|
2
|
+
|
|
3
|
+
The compatibility IR defines the types used to represent, diff, and classify SDK public API surfaces.
|
|
4
|
+
|
|
5
|
+
## CompatSnapshot
|
|
6
|
+
|
|
7
|
+
A full snapshot of an SDK's public API surface.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
interface CompatSnapshot {
|
|
11
|
+
schemaVersion: string;
|
|
12
|
+
source: {
|
|
13
|
+
specSha?: string;
|
|
14
|
+
extractedAt: string;
|
|
15
|
+
};
|
|
16
|
+
policies: CompatPolicyHints;
|
|
17
|
+
symbols: CompatSymbol[];
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## CompatSymbol
|
|
22
|
+
|
|
23
|
+
A single public API symbol.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
interface CompatSymbol {
|
|
27
|
+
id: string;
|
|
28
|
+
kind: CompatSymbolKind;
|
|
29
|
+
fqName: string;
|
|
30
|
+
ownerFqName?: string;
|
|
31
|
+
displayName: string;
|
|
32
|
+
visibility: "public" | "protected" | "internal";
|
|
33
|
+
stability: "stable" | "unstable" | "deprecated";
|
|
34
|
+
sourceKind: CompatSourceKind;
|
|
35
|
+
operationId?: string;
|
|
36
|
+
schemaName?: string;
|
|
37
|
+
route?: { method: string; path: string };
|
|
38
|
+
parameters?: CompatParameter[];
|
|
39
|
+
returns?: CompatTypeRef;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Symbol Kinds
|
|
44
|
+
|
|
45
|
+
| Kind | Description |
|
|
46
|
+
| ------------------ | ---------------------------------------- |
|
|
47
|
+
| `service_accessor` | A service class (e.g., `UserManagement`) |
|
|
48
|
+
| `callable` | A method on a service class |
|
|
49
|
+
| `constructor` | A constructor/initializer |
|
|
50
|
+
| `field` | A field on an interface/model |
|
|
51
|
+
| `property` | A property on a class |
|
|
52
|
+
| `enum` | An enum type |
|
|
53
|
+
| `enum_member` | A member of an enum |
|
|
54
|
+
| `alias` | A type alias or interface name |
|
|
55
|
+
|
|
56
|
+
### Source Kinds
|
|
57
|
+
|
|
58
|
+
| Source Kind | Description |
|
|
59
|
+
| -------------------------------- | -------------------------------- |
|
|
60
|
+
| `generated_service_wrapper` | Generated from an API service |
|
|
61
|
+
| `generated_model_constructor` | Generated model constructor |
|
|
62
|
+
| `generated_resource_constructor` | Generated resource constructor |
|
|
63
|
+
| `generated_enum` | Generated from an enum schema |
|
|
64
|
+
| `compat_alias` | Added for backward compatibility |
|
|
65
|
+
|
|
66
|
+
## CompatParameter
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
interface CompatParameter {
|
|
70
|
+
publicName: string;
|
|
71
|
+
wireName?: string;
|
|
72
|
+
position: number;
|
|
73
|
+
required: boolean;
|
|
74
|
+
nullable: boolean;
|
|
75
|
+
hasDefault: boolean;
|
|
76
|
+
passing: CompatPassingStyle;
|
|
77
|
+
type: CompatTypeRef;
|
|
78
|
+
sensitivity: ParameterSensitivity;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Passing Styles
|
|
83
|
+
|
|
84
|
+
| Style | Description | Languages |
|
|
85
|
+
| ----------------------- | ----------------------- | ------------------------------ |
|
|
86
|
+
| `positional` | Position-only arguments | Go, Rust |
|
|
87
|
+
| `keyword` | Keyword arguments | Python, Ruby, Elixir |
|
|
88
|
+
| `named` | Named arguments | PHP, Kotlin, C# |
|
|
89
|
+
| `keyword_or_positional` | Either style | Python (positional-or-keyword) |
|
|
90
|
+
| `options_object` | Object destructuring | Node/TypeScript |
|
|
91
|
+
| `builder` | Builder pattern | Java-style |
|
|
92
|
+
|
|
93
|
+
### Parameter Sensitivity
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
interface ParameterSensitivity {
|
|
97
|
+
order: boolean; // Position changes are breaking
|
|
98
|
+
publicName: boolean; // Name changes are breaking
|
|
99
|
+
requiredness: boolean; // Requiredness changes are breaking
|
|
100
|
+
type: boolean; // Type changes are breaking
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Change Categories
|
|
105
|
+
|
|
106
|
+
### Breaking
|
|
107
|
+
|
|
108
|
+
| Category | Description |
|
|
109
|
+
| ---------------------------------------------- | --------------------------------------------------- |
|
|
110
|
+
| `symbol_removed` | A public symbol was removed |
|
|
111
|
+
| `symbol_renamed` | A public symbol was renamed |
|
|
112
|
+
| `parameter_removed` | A parameter was removed |
|
|
113
|
+
| `parameter_renamed` | A parameter was renamed |
|
|
114
|
+
| `parameter_requiredness_increased` | Optional → required |
|
|
115
|
+
| `parameter_type_narrowed` | Parameter type changed |
|
|
116
|
+
| `parameter_position_changed_order_sensitive` | Parameter moved in order-sensitive language |
|
|
117
|
+
| `constructor_position_changed_order_sensitive` | Constructor param moved in order-sensitive language |
|
|
118
|
+
| `named_arg_name_removed` | Named argument removed |
|
|
119
|
+
| `keyword_name_removed` | Keyword argument removed |
|
|
120
|
+
| `overload_removed` | Method overload removed |
|
|
121
|
+
| `union_wrapper_migration_without_compat_alias` | Union wrapper migration without backward compat |
|
|
122
|
+
|
|
123
|
+
### Soft-Risk
|
|
124
|
+
|
|
125
|
+
| Category | Description |
|
|
126
|
+
| ----------------------------------------------------- | ------------------------------------------------ |
|
|
127
|
+
| `parameter_added_non_terminal_optional` | Optional param added (not at end) |
|
|
128
|
+
| `constructor_reordered_named_friendly` | Constructor reordered in named-friendly language |
|
|
129
|
+
| `default_value_changed` | Default value changed |
|
|
130
|
+
| `wrapper_stricter_than_previous_sdk_but_matches_spec` | Stricter but spec-correct |
|
|
131
|
+
| `doc_surface_drift` | Documentation-only drift |
|
|
132
|
+
|
|
133
|
+
### Additive
|
|
134
|
+
|
|
135
|
+
| Category | Description |
|
|
136
|
+
| ----------------------------------- | --------------------------- |
|
|
137
|
+
| `symbol_added` | New symbol added |
|
|
138
|
+
| `parameter_added_optional_terminal` | Optional param added at end |
|
|
139
|
+
| `new_constructor_overload_added` | New constructor overload |
|
|
140
|
+
| `new_wrapper_alias_added` | New compatibility alias |
|
|
141
|
+
|
|
142
|
+
## Provenance Buckets
|
|
143
|
+
|
|
144
|
+
Every classified change includes a provenance field explaining the source of drift:
|
|
145
|
+
|
|
146
|
+
| Provenance | Description |
|
|
147
|
+
| ------------------------- | ---------------------------------------------- |
|
|
148
|
+
| `spec_shape_change` | The OpenAPI spec changed shape |
|
|
149
|
+
| `spec_ordering_change` | The spec changed property ordering |
|
|
150
|
+
| `emitter_template_change` | The emitter template was modified |
|
|
151
|
+
| `compat_extractor_change` | The extractor logic changed |
|
|
152
|
+
| `operation_hint_change` | An operation hint changed |
|
|
153
|
+
| `manual_override_change` | A manual override was applied |
|
|
154
|
+
| `normalization_change` | Naming normalization produced different output |
|
|
155
|
+
| `unknown` | Source not determined |
|
|
156
|
+
|
|
157
|
+
## Extracting Snapshots
|
|
158
|
+
|
|
159
|
+
Every extractor implements `extractSnapshot()`, which returns a `CompatSnapshot` directly:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const extractor = getExtractor("node");
|
|
163
|
+
const snapshot: CompatSnapshot = await extractor.extractSnapshot(sdkPath);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
This is the primary method used by `oagen compat-extract`. Internally, extractors call `extract()` to produce an `ApiSurface`, then convert it to a `CompatSnapshot` via the bridge function.
|
|
167
|
+
|
|
168
|
+
### Extractor Interface
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
interface Extractor {
|
|
172
|
+
language: string;
|
|
173
|
+
extract(sdkPath: string): Promise<ApiSurface>;
|
|
174
|
+
extractSnapshot(sdkPath: string): Promise<CompatSnapshot>;
|
|
175
|
+
hints: LanguageHints;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Both `extract()` and `extractSnapshot()` are available. Use `extractSnapshot()` for the compat pipeline. Use `extract()` when you need the raw `ApiSurface` for overlay or legacy workflows.
|
|
180
|
+
|
|
181
|
+
### Spec Enrichment
|
|
182
|
+
|
|
183
|
+
When `oagen compat-extract` receives `--spec`, it enriches callable symbols with spec-derived metadata:
|
|
184
|
+
|
|
185
|
+
- **`operationId`** — the operation name from the spec, matched by service + method name
|
|
186
|
+
- **`route`** — the HTTP method and path (e.g., `{ method: "POST", path: "/users" }`)
|
|
187
|
+
- **`source.specSha`** — SHA-256 of the spec file
|
|
188
|
+
|
|
189
|
+
This enrichment happens after extraction, as a post-step in the CLI. Extractors themselves don't need spec awareness.
|
|
190
|
+
|
|
191
|
+
### Bridge: ApiSurface → CompatSnapshot
|
|
192
|
+
|
|
193
|
+
The `apiSurfaceToSnapshot()` function converts an `ApiSurface` to a `CompatSnapshot`, applying language-appropriate passing styles and sensitivity flags. This is what `extractSnapshot()` uses internally.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { apiSurfaceToSnapshot } from "@workos/oagen";
|
|
197
|
+
|
|
198
|
+
const surface: ApiSurface = await extractor.extract(sdkPath);
|
|
199
|
+
const snapshot: CompatSnapshot = apiSurfaceToSnapshot(surface);
|
|
200
|
+
```
|