@workos/oagen 0.0.1

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 (64) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +197 -0
  4. package/dist/chunk-CIX5NOBF.js +263 -0
  5. package/dist/chunk-IJ5E5AE7.js +1040 -0
  6. package/dist/chunk-P5XUDUGY.js +1748 -0
  7. package/dist/cli/index.js +5603 -0
  8. package/dist/compat/index.d.ts +195 -0
  9. package/dist/compat/index.js +4453 -0
  10. package/dist/index.d.ts +300 -0
  11. package/dist/index.js +1733 -0
  12. package/dist/scripts/smoke/baseline.js +1751 -0
  13. package/dist/scripts/smoke/sdk-test.js +93 -0
  14. package/dist/smoke.js +1963 -0
  15. package/dist/types-B5t9O9Wm.d.ts +367 -0
  16. package/dist/verify/index.d.ts +76 -0
  17. package/dist/verify/index.js +336 -0
  18. package/docs/advanced/index.md +11 -0
  19. package/docs/advanced/oagen-ignore.md +144 -0
  20. package/docs/agents/architecture.md +53 -0
  21. package/docs/agents/emitters.md +69 -0
  22. package/docs/agents/testing.md +60 -0
  23. package/docs/architecture/dependency-layers.md +75 -0
  24. package/docs/architecture/emitter-contract.md +160 -0
  25. package/docs/architecture/extractor-contract.md +227 -0
  26. package/docs/architecture/ir-types.md +275 -0
  27. package/docs/architecture/non-additive-changes.md +76 -0
  28. package/docs/architecture/pipeline.md +110 -0
  29. package/docs/architecture/workflows.md +215 -0
  30. package/docs/cli.md +156 -0
  31. package/docs/contributor/index.md +10 -0
  32. package/docs/contributor/public-api.md +59 -0
  33. package/docs/contributor/versioning.md +48 -0
  34. package/docs/core/index.md +12 -0
  35. package/docs/core/minimal-emitter.md +134 -0
  36. package/docs/core/quickstart.md +142 -0
  37. package/docs/diagrams/generate-sdk-workflow.md +50 -0
  38. package/docs/index.md +40 -0
  39. package/examples/reference-emitter/README.md +41 -0
  40. package/examples/reference-emitter/oagen.config.ts +7 -0
  41. package/examples/reference-emitter/spec/github-subset.yml +400 -0
  42. package/examples/reference-emitter/src/emitter.ts +52 -0
  43. package/examples/reference-emitter/src/generators/client.ts +37 -0
  44. package/examples/reference-emitter/src/generators/config.ts +19 -0
  45. package/examples/reference-emitter/src/generators/enums.ts +16 -0
  46. package/examples/reference-emitter/src/generators/errors.ts +39 -0
  47. package/examples/reference-emitter/src/generators/models.ts +29 -0
  48. package/examples/reference-emitter/src/generators/resources.ts +60 -0
  49. package/examples/reference-emitter/src/generators/tests.ts +7 -0
  50. package/examples/reference-emitter/src/index.ts +1 -0
  51. package/examples/reference-emitter/src/naming.ts +21 -0
  52. package/examples/reference-emitter/src/type-mapper.ts +39 -0
  53. package/examples/reference-emitter/tsconfig.json +19 -0
  54. package/package.json +102 -0
  55. package/skills/generate-emitter/SKILL.md +393 -0
  56. package/skills/generate-emitter/references/common-pitfalls.md +23 -0
  57. package/skills/generate-emitter/references/generator-guide.md +201 -0
  58. package/skills/generate-emitter/references/project-scaffold.md +138 -0
  59. package/skills/generate-extractor/SKILL.md +200 -0
  60. package/skills/generate-sdk/SKILL.md +155 -0
  61. package/skills/generate-smoke-test/SKILL.md +185 -0
  62. package/skills/integrate/SKILL.md +108 -0
  63. package/skills/verify-compat/SKILL.md +151 -0
  64. package/skills/verify-smoke-test/SKILL.md +176 -0
@@ -0,0 +1,201 @@
1
+ # Generator Implementation Guide
2
+
3
+ Detailed instructions for implementing each generator file in a language emitter. Read this when implementing Steps 2–4 of the main skill.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Type Mapping (type-map.ts)](#type-mapping)
8
+ - [Naming Conventions (naming.ts)](#naming-conventions)
9
+ - [Models (models.ts)](#models)
10
+ - [Enums (enums.ts)](#enums)
11
+ - [Resources (resources.ts)](#resources)
12
+ - [Serializers (serializers.ts)](#serializers)
13
+ - [Client (client.ts)](#client)
14
+ - [Errors (errors.ts)](#errors)
15
+ - [Config (config.ts)](#config)
16
+ - [Common/Utilities (common.ts)](#common-utilities)
17
+ - [Type Signatures (types-\*.ts)](#type-signatures)
18
+ - [Tests (tests.ts) and Fixtures (fixtures.ts)](#tests-and-fixtures)
19
+
20
+ ---
21
+
22
+ ## Type Mapping
23
+
24
+ Foundation file. Map every IR `TypeRef` kind to the target language's type representation.
25
+
26
+ ### Required Mappings
27
+
28
+ | IR TypeRef | What to Map |
29
+ | ------------------------------------------------------------ | ------------------------------------ |
30
+ | `{ kind: "primitive", type: "string" }` | String type |
31
+ | `{ kind: "primitive", type: "string", format: "date" }` | Date type |
32
+ | `{ kind: "primitive", type: "string", format: "date-time" }` | DateTime type |
33
+ | `{ kind: "primitive", type: "string", format: "uuid" }` | String or UUID type |
34
+ | `{ kind: "primitive", type: "integer" }` | Integer type |
35
+ | `{ kind: "primitive", type: "number" }` | Float/double type |
36
+ | `{ kind: "primitive", type: "boolean" }` | Boolean type |
37
+ | `{ kind: "array", items: ... }` | Array/list of mapped item type |
38
+ | `{ kind: "model", name: "Foo" }` | Reference to model class |
39
+ | `{ kind: "enum", name: "Foo" }` | Reference to enum type |
40
+ | `{ kind: "nullable", inner: ... }` | Optional/nullable wrapper |
41
+ | `{ kind: "union", variants: [...] }` | Union/sum type |
42
+ | `{ kind: "map", valueType: ... }` | Map/dict/Record of mapped value type |
43
+
44
+ Export a `mapTypeRef(typeRef: TypeRef, namespacePascal: string): string` function.
45
+
46
+ **For Scenario A:** The type mapping must produce types that match the existing SDK's conventions. If the SDK uses `Date` objects instead of ISO strings for date-time, map accordingly. If the SDK wraps nullable types in `Optional<T>`, do that. Check the design doc's type mapping table.
47
+
48
+ If the language has a separate type annotation system, also export mapping functions for those (e.g., `mapTypeRefForRbs`, `mapTypeRefForSorbet`).
49
+
50
+ ---
51
+
52
+ ## Naming Conventions
53
+
54
+ Map oagen's PascalCase IR names to the target language's conventions.
55
+
56
+ ### Required Functions
57
+
58
+ ```typescript
59
+ export function className(name: string): string;
60
+ export function fileName(name: string): string;
61
+ export function methodName(name: string): string;
62
+ export function modulePath(
63
+ namespace: string,
64
+ category: string,
65
+ name: string,
66
+ ): string;
67
+ ```
68
+
69
+ ### Examples by Language
70
+
71
+ | IR Name | Ruby | Python | Go | Kotlin |
72
+ | --------------------- | ----------------- | ----------------- | ----------------- | ---------------- |
73
+ | `UserProfile` (class) | `UserProfile` | `UserProfile` | `UserProfile` | `UserProfile` |
74
+ | `UserProfile` (file) | `user_profile.rb` | `user_profile.py` | `user_profile.go` | `UserProfile.kt` |
75
+ | `listUsers` (method) | `list_users` | `list_users` | `ListUsers` | `listUsers` |
76
+ | `user_id` (field) | `user_id` | `user_id` | `UserID` | `userId` |
77
+
78
+ **For Scenario A:** Verify naming conventions against the existing SDK. If the SDK uses non-standard naming (e.g., preserves acronyms like `SSO` instead of converting to `Sso`, or uses `ID` instead of `Id`), the naming functions must reproduce that behavior. Check 5–10 actual names from the SDK and verify the naming functions produce identical output.
79
+
80
+ Use the shared utilities from `@workos/oagen` (`toPascalCase`, `toSnakeCase`, `toCamelCase`, `toKebabCase`, `toUpperSnakeCase`) as building blocks.
81
+
82
+ ---
83
+
84
+ ## Models
85
+
86
+ - Input: `Model[]` (each has `name`, `description?`, `fields[]`)
87
+ - Each `Field` has `name`, `type: TypeRef`, `required: boolean`, `description?`
88
+ - Output: One file per model, using the exact model pattern from the design doc
89
+ - Use `type-map.ts` to convert field types
90
+
91
+ **For Scenario A:** If the existing SDK uses a dual type system (e.g., separate `Organization` and `OrganizationResponse` types), this generator must produce BOTH types. If the SDK has specific decorators, base classes, or factory methods on models, replicate them.
92
+
93
+ ---
94
+
95
+ ## Enums
96
+
97
+ - Input: `Enum[]` (each has `name`, `values[]`)
98
+ - Each `EnumValue` has `name`, `value` (string), `description?`
99
+ - Output: One file per enum, using the exact enum pattern from the design doc
100
+
101
+ ---
102
+
103
+ ## Resources
104
+
105
+ - Input: `Service[]` (each has `name`, `operations[]`)
106
+ - Each `Operation` has: `name`, `httpMethod`, `path`, `pathParams`, `queryParams`, `requestBody?`, `response`, `paginated`, `injectIdempotencyKey`
107
+ - Output: One file per service/resource
108
+ - Handle:
109
+ - Path parameter interpolation (e.g., `/orgs/{id}` to target language's format string mechanism)
110
+ - Strip leading `/` from paths (use `"organizations"` not `"/organizations"`)
111
+ - Paginated responses — use the exact pagination pattern from the design doc (NOT a generic page type)
112
+ - Idempotent POST methods (include an idempotency key as a standalone parameter, not embedded in params)
113
+ - Query parameters for list methods
114
+ - A per-request options parameter on every method (last parameter, using the language's idiomatic calling convention)
115
+ - Documentation for parameters and return types using the target language's standard docstring format
116
+ - Delete methods signal "no response body" using the language's null/void type and return nil/null/unit
117
+
118
+ **For Scenario A:** The method signatures, parameter ordering, and return types must match the existing SDK's patterns exactly. If the SDK takes an options object as the first parameter and an ID separately, replicate that. If it uses keyword arguments, replicate that. Read the resource pattern section of the design doc carefully.
119
+
120
+ ---
121
+
122
+ ## Serializers
123
+
124
+ **Only create this file if the design doc documents a serialization pattern.**
125
+
126
+ - Input: `Model[]` — generates serialize/deserialize functions for each model
127
+ - Maps between wire format (API responses) and domain types (what the SDK exposes)
128
+ - Common in SDKs that have separate Response and Domain type layers
129
+
130
+ ---
131
+
132
+ ## Client
133
+
134
+ Mostly static code with namespace interpolation. Must include:
135
+
136
+ - HTTP request method accepting: HTTP method, path, optional query params, optional body, a model type for deserialization, a page type for paginated responses, an optional idempotency key, and per-request options. Use the target language's idiomatic parameter passing convention.
137
+ - Model and page parameters for response deserialization
138
+ - Response handling: null/void model type returns nil/null, page type creates paginated response, model type creates model instance
139
+ - HTTP request method dispatching (GET/POST/PUT/PATCH/DELETE)
140
+ - Exponential backoff retry with jitter, capped at a reasonable maximum (see the language's SDK design doc for the specific formula)
141
+ - Retryable statuses: 429, 500, 502, 503, 504
142
+ - Respect `Retry-After` headers
143
+ - Auto-generated UUID idempotency keys for POST, reused across retries
144
+ - Resource accessor methods (one per service) with return type documentation
145
+ - Per-request options support for extra headers and timeout overrides
146
+
147
+ ### SDK Scaffolding Files
148
+
149
+ `generateClient` must also emit the project scaffolding that makes the generated SDK self-contained and analyzable:
150
+
151
+ - **Barrel entry point** (e.g., `src/index.ts`) — re-exports all public types, models, enums, exceptions, and the main client. This is what `oagen verify` (via the extractor) uses to discover the SDK's public surface.
152
+ - **Project config** (e.g., `tsconfig.json`) — so the SDK can be type-checked and built.
153
+ - **Package manifest** (e.g., `package.json`) — with correct `main`, `types`, and `exports` fields so tooling resolves the entry point.
154
+
155
+ Without these, `oagen verify` will fail with "No entry point found" because the extractor cannot discover the SDK's public surface.
156
+
157
+ **For Scenario A:** The client architecture must match the existing SDK. If the client uses constructor overloads, has a specific initialization pattern, or delegates to a separate HTTP transport layer, replicate that. The design doc's client architecture section is authoritative.
158
+
159
+ ---
160
+
161
+ ## Errors
162
+
163
+ Mostly static code with namespace interpolation. Required error types:
164
+
165
+ - `APIError` (base), `AuthenticationError` (401), `NotFoundError` (404), `UnprocessableEntityError` (422), `RateLimitExceededError` (429), `ServerError` (500+), `NetworkError` (connection failures), `ConfigurationError` (missing API key)
166
+
167
+ **For Scenario A:** Match the existing SDK's error class names and hierarchy exactly. If the SDK names it `BadRequestException` instead of `UnprocessableEntityError`, use the SDK's name.
168
+
169
+ ---
170
+
171
+ ## Config
172
+
173
+ Configuration class/module with: `api_key`, `base_url`, `max_retries`, `timeout`. Environment variable default for API key.
174
+
175
+ ---
176
+
177
+ ## Common/Utilities
178
+
179
+ **Create this file if the design doc documents shared utilities.** Generates shared types, pagination base classes, barrel exports, or helper functions. Common in SDKs that have a `common/` directory with shared infrastructure.
180
+
181
+ ---
182
+
183
+ ## Type Signatures
184
+
185
+ Language-specific. May be 0, 1, or 2 files depending on the language's type system. Examples: `.pyi` stubs for Python, `.d.ts` for JavaScript, nothing for Go/Rust (types are inline). Must include request options and idempotency key parameters on resource method signatures, enum type signatures using the target language's idiomatic enum representation, and delete operations returning the language's void/null type.
186
+
187
+ ---
188
+
189
+ ## Tests and Fixtures
190
+
191
+ - `generateTests` is the only Emitter interface method — it should internally call `generateFixtures` from `fixtures.ts` and combine both into a single `GeneratedFile[]` return
192
+ - Generate one test file per resource/service
193
+ - Test file paths are language-specific — use the target language's idiomatic convention (e.g., Ruby uses `test/{namespace}/resources/{name}_test.rb`, Node uses `src/{service}/tests/{name}.test.ts`)
194
+ - Test classes/modules nested in namespace modules
195
+ - Each test: stub HTTP request, call method, assert response type using the language's idiomatic assertion style
196
+ - Include error tests (404, 401), retry tests (429 with Retry-After), and idempotency tests (explicit + auto-generated keys)
197
+ - Fixture JSON file paths are also language-specific — organize them near the test files using the target language's convention
198
+ - Use a `load_fixture` helper for reading fixtures
199
+ - Generate fixture JSON files from IR model schemas
200
+
201
+ **For Scenario A:** Use the exact testing framework and mocking library from the existing SDK. If the SDK uses Jest + jest-fetch-mock, generate Jest tests with jest-fetch-mock, not Vitest + msw. If the SDK uses a specific test structure (describe/it vs test functions, specific setup/teardown patterns), replicate it. Read the test pattern section of the design doc.
@@ -0,0 +1,138 @@
1
+ # Emitter Project Scaffold
2
+
3
+ When `{project}/package.json` does **NOT** exist, create the following boilerplate files to initialize the project. If `package.json` already exists, skip this entirely.
4
+
5
+ ## `package.json`
6
+
7
+ ```json
8
+ {
9
+ "name": "@workos/oagen-emitters-{language}",
10
+ "version": "0.0.1",
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }
16
+ },
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "typecheck": "tsc --noEmit",
22
+ "sdk:generate": "oagen generate --lang {language} --output ./sdk",
23
+ "sdk:verify": "oagen verify --lang {language} --output ./sdk",
24
+ "sdk:extract": "oagen extract --lang {language} --output ./sdk-{language}-surface.json"
25
+ },
26
+ "dependencies": {
27
+ "@workos/oagen": "^0.0.1"
28
+ },
29
+ "devDependencies": {
30
+ "tsup": "^8.4.0",
31
+ "tsx": "^4.19.0",
32
+ "vitest": "^3.0.0",
33
+ "@types/node": "^25.3.3"
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## `tsconfig.json`
39
+
40
+ Mirror oagen core's config: ES2022, ESNext modules, bundler resolution, strict mode, declaration output.
41
+
42
+ ```json
43
+ {
44
+ "compilerOptions": {
45
+ "target": "ES2022",
46
+ "module": "ESNext",
47
+ "moduleResolution": "bundler",
48
+ "strict": true,
49
+ "declaration": true,
50
+ "declarationMap": true,
51
+ "sourceMap": true,
52
+ "outDir": "dist",
53
+ "rootDir": ".",
54
+ "esModuleInterop": true,
55
+ "skipLibCheck": true,
56
+ "forceConsistentCasingInFileNames": true,
57
+ "resolveJsonModule": true,
58
+ "isolatedModules": true
59
+ },
60
+ "include": ["src", "test"],
61
+ "exclude": ["node_modules", "dist"]
62
+ }
63
+ ```
64
+
65
+ ## `vitest.config.ts`
66
+
67
+ ```ts
68
+ import { defineConfig } from "vitest/config";
69
+ export default defineConfig({
70
+ test: { globals: true, include: ["test/**/*.test.ts"] },
71
+ });
72
+ ```
73
+
74
+ ## `tsup.config.ts`
75
+
76
+ ```ts
77
+ import { defineConfig } from "tsup";
78
+ export default defineConfig({
79
+ entry: { index: "src/index.ts" },
80
+ format: ["esm"],
81
+ dts: true,
82
+ clean: true,
83
+ target: "node20",
84
+ });
85
+ ```
86
+
87
+ ## `oagen.config.ts`
88
+
89
+ ```ts
90
+ import type { OagenConfig } from "@workos/oagen";
91
+ const config: OagenConfig = { emitters: [] };
92
+ export default config;
93
+ ```
94
+
95
+ (Step 6 of the main skill adds the emitter to this config.)
96
+
97
+ ## `src/index.ts`
98
+
99
+ ```ts
100
+ // Barrel export — re-exports all emitters
101
+ ```
102
+
103
+ (Step 6 adds the re-export.)
104
+
105
+ ## `.gitignore`
106
+
107
+ ```
108
+ node_modules/
109
+ dist/
110
+ ```
111
+
112
+ After creating these files, run `npm install` in the emitter project directory.
113
+
114
+ ## SDK generation scripts
115
+
116
+ The `sdk:*` scripts bake in `--lang` and `--output` so you only need to pass the remaining flags via `--`:
117
+
118
+ ```bash
119
+ # Fresh generation
120
+ npm run sdk:generate -- --spec ../openapi.yaml --namespace workos
121
+
122
+ # Verify generated output
123
+ npm run sdk:verify -- --spec ../openapi.yaml
124
+
125
+ # Extract API surface from an existing SDK (Scenario A)
126
+ npm run sdk:extract -- --sdk-path ../path-to-live-sdk
127
+
128
+ # Generate with compat overlay (Scenario A)
129
+ npm run sdk:generate -- --spec ../openapi.yaml --namespace workos --api-surface ./sdk-{language}-surface.json
130
+
131
+ # Verify with compat check (Scenario A)
132
+ npm run sdk:verify -- --spec ../openapi.yaml --api-surface ./sdk-{language}-surface.json
133
+
134
+ # Integrate into live SDK (Scenario A)
135
+ npm run sdk:generate -- --spec ../openapi.yaml --namespace workos --api-surface ./sdk-{language}-surface.json --target ../path-to-live-sdk
136
+ ```
137
+
138
+ The `--spec` flag can be replaced by setting `OPENAPI_SPEC_PATH` in the environment.
@@ -0,0 +1,200 @@
1
+ ---
2
+ name: generate-extractor
3
+ description: Scaffold a new language extractor for oagen's compat system, implementing the Extractor interface to extract a live SDK's public API surface. Use when the user wants to add backwards-compatibility support, build an extractor, check for breaking changes or regressions, or asks about extracting an API surface for any language — even if they don't use the word "extractor" explicitly. Also triggers for "compat support", "API surface analysis", or "public surface extraction".
4
+ ---
5
+
6
+ # /generate-extractor
7
+
8
+ Scaffold a language extractor for oagen's compat verification system. An extractor analyzes a live SDK and returns its public API surface as a canonical `ApiSurface` JSON, which the differ then compares against generated output to detect breaking changes.
9
+
10
+ ## Overview
11
+
12
+ ```
13
+ Live SDK → Extractor → ApiSurface JSON → Differ ← Generated SDK → Violations
14
+ ```
15
+
16
+ Each language needs its own extractor because public surface detection is language-specific (e.g., TypeScript exports vs. Ruby public methods vs. Python `__all__` vs. Go capitalized identifiers). An extractor implements the `Extractor` interface and is registered via `oagen.config.ts` in the emitter project.
17
+
18
+ ## Reference Docs
19
+
20
+ - [Extractor Contract](../../docs/architecture/extractor-contract.md) — `Extractor` interface, `ApiSurface` type, language-specific strategies
21
+ - [Dependency Layers](../../docs/architecture/dependency-layers.md) — where compat fits in the import hierarchy
22
+
23
+ ## Resolve Paths
24
+
25
+ **Emitter project:** Use the `project` argument if provided, otherwise use `AskUserQuestion`.
26
+
27
+ **oagen core:** Check for `node_modules/@workos/oagen/`, or `src/engine/types.ts` in the current directory, otherwise ask.
28
+
29
+ ## Prerequisites
30
+
31
+ Read and understand these files before starting:
32
+
33
+ 1. **oagen core types** (from `@workos/oagen`): `Extractor`, `ApiSurface`, `ApiClass`, `ApiMethod`, `ApiParam`, `ApiProperty`, `ApiInterface`, `ApiField`, `ApiTypeAlias`, `ApiEnum`
34
+ 2. **Reference extractor:** `{oagen}/src/compat/extractors/node.ts` — study the structure, not the TypeScript-specific analysis
35
+ 3. **Reference tests:** `{oagen}/test/compat/extractors/node.test.ts`
36
+ 4. **Fixture SDK:** `{oagen}/test/fixtures/sample-sdk/`
37
+ 5. **Contract spec:** `{oagen}/docs/architecture/extractor-contract.md` — includes language-specific strategies
38
+
39
+ If an `sdk_path` argument is provided, **delegate SDK exploration to a subagent** to keep file-reading noise out of the main context:
40
+
41
+ Use the `Agent` tool with `subagent_type: Explore` and a prompt like:
42
+
43
+ > Explore the SDK at `{sdk_path}`. Focus on: entry point discovery (main module/package), public surface detection (how public vs private symbols are distinguished), type information sources (compiler API, stubs, annotations), class/method extraction patterns, and file/directory layout. Return structured findings with real code snippets and file paths. Only report what you actually find.
44
+
45
+ The real SDK is the ground truth.
46
+
47
+ ## Step 1: Determine Analysis Strategy
48
+
49
+ Before writing any code, determine how to analyze the target language's SDK:
50
+
51
+ | Decision | What it controls |
52
+ | ---------------------------- | -------------------------------------------------------- |
53
+ | **Entry point discovery** | How to find the SDK's main module/package |
54
+ | **Public surface detection** | How to distinguish public from private symbols |
55
+ | **Type information source** | Where types come from (compiler API, stubs, annotations) |
56
+ | **Class/method extraction** | How to extract classes, methods, params, return types |
57
+ | **Analysis tooling** | What npm packages or subprocess calls are needed |
58
+
59
+ ### Implementation Strategy
60
+
61
+ Extractors run as TypeScript code. For non-TS/JS SDKs, choose one:
62
+
63
+ 1. **Native analysis (preferred):** Use a TS/JS parser or AST library that can analyze the target language
64
+ 2. **Subprocess delegation:** Shell out to a target-language script that performs analysis and returns JSON
65
+ 3. **Static file parsing:** Parse type annotation files (`.rbi`, `.rbs`, `.pyi`) or source files directly
66
+
67
+ Present your proposed strategy to the user for confirmation, including any runtime dependencies.
68
+
69
+ ## Step 2: Create the Extractor
70
+
71
+ Create `src/compat/extractors/{language}.ts` **in the emitter project**.
72
+
73
+ ```typescript
74
+ import type { ApiSurface, Extractor } from '@workos/oagen';
75
+ import { resolveHints } from '@workos/oagen';
76
+
77
+ export const {language}Extractor: Extractor = {
78
+ language: '{language}',
79
+ hints: resolveHints({
80
+ // Override only the hints that differ from Node/TypeScript defaults.
81
+ // See docs/architecture/extractor-contract.md for the full reference.
82
+ // stripNullable: (type) => ...,
83
+ // isExtractionArtifact: (type) => ...,
84
+ // tolerateCategoryMismatch: false,
85
+ // derivedModelNames: (name) => [...],
86
+ }),
87
+ async extract(sdkPath: string): Promise<ApiSurface> {
88
+ // 1. Discover entry point
89
+ // 2. Load and analyze public surface
90
+ // 3. Extract classes, interfaces, type aliases, enums
91
+ // 4. Build export map
92
+ // 5. Return sorted, deterministic ApiSurface
93
+ return {
94
+ language: '{language}',
95
+ extractedFrom: sdkPath,
96
+ extractedAt: new Date().toISOString(),
97
+ classes: sortRecord(classes),
98
+ interfaces: sortRecord(interfaces),
99
+ typeAliases: sortRecord(typeAliases),
100
+ enums: sortRecord(enums),
101
+ exports: sortRecord(exports),
102
+ };
103
+ },
104
+ };
105
+ ```
106
+
107
+ ### Implementation Rules
108
+
109
+ 1. **Deterministic output** — Running twice on the same SDK must produce identical JSON. Sort all record keys and array members.
110
+ 2. **Public surface only** — Extract only public/exported symbols. Each language defines "public" differently.
111
+ 3. **Preserve fidelity** — Capture method signatures as they appear in the live SDK, not as the IR would generate them.
112
+ 4. **Handle missing infrastructure gracefully** — Throw a descriptive error if required files are missing.
113
+ 5. **Populate all ApiSurface fields** — `classes`, `interfaces`, `typeAliases`, `enums`, `exports`. Map as closely as possible even if the language doesn't have a direct equivalent for every category.
114
+ 6. **Provide `hints`** — Every extractor must include a `hints: LanguageHints` field. Use `resolveHints({...})` to start from Node defaults and override only the language-specific methods. Test that your hints produce correct results for the target language's type strings.
115
+
116
+ ## Step 3: Register the Extractor
117
+
118
+ Add to `oagen.config.ts`:
119
+
120
+ ```typescript
121
+ import { {language}Extractor } from './src/compat/extractors/{language}.js';
122
+ const config: OagenConfig = {
123
+ emitters: [/* ... */],
124
+ extractors: [{language}Extractor],
125
+ };
126
+ ```
127
+
128
+ ## Step 4: Create a Fixture SDK
129
+
130
+ Create a minimal but representative fixture at `test/fixtures/sample-sdk-{language}/` **in the emitter project**. It must include:
131
+
132
+ 1. A client class with at least 3 methods (list, get, delete) — required/optional params, different return types
133
+ 2. At least one model/data class (e.g., `Organization` with `id`, `name`)
134
+ 3. At least one enum with 2+ members
135
+ 4. At least one interface/protocol/type alias (if the language supports them)
136
+ 5. A clear entry point that re-exports all public symbols
137
+ 6. At least one readonly property on the client class
138
+
139
+ Mirror the Node fixture at `{oagen}/test/fixtures/sample-sdk/` in terms of what it tests, using the target language's idioms.
140
+
141
+ ## Step 5: Create Tests
142
+
143
+ Create `test/compat/extractors/{language}.test.ts` **in the emitter project** covering:
144
+
145
+ 1. Extracts classes with methods and properties
146
+ 2. Extracts method params and return types
147
+ 3. Extracts optional params and readonly properties
148
+ 4. Extracts interfaces/protocols and enums
149
+ 5. Extracts type aliases and barrel exports
150
+ 6. Produces deterministic output (extract twice, compare)
151
+ 7. Sets metadata correctly
152
+
153
+ Use `toMatchObject` for partial assertions and `toMatchInlineSnapshot()` for at least one representative snapshot per category.
154
+
155
+ ## Step 6: Validate
156
+
157
+ ```bash
158
+ # All tests pass
159
+ cd {project} && npx vitest run
160
+
161
+ # Manual extraction against fixture
162
+ npx tsx -e "
163
+ import { {language}Extractor } from './src/compat/extractors/{language}.js';
164
+ const surface = await {language}Extractor.extract('test/fixtures/sample-sdk-{language}');
165
+ console.log(JSON.stringify(surface, null, 2));
166
+ " > /tmp/test-{language}-surface.json
167
+
168
+ # Determinism check — extract twice, diff
169
+ ```
170
+
171
+ If `sdk_path` was provided, also test against the real SDK and verify all public classes are captured, method signatures match, and no private symbols leak through.
172
+
173
+ ## Verification Report
174
+
175
+ ```
176
+ === Extractor: {language} ===
177
+ Files created: src/compat/extractors/{language}.ts, test suite, fixture SDK
178
+ Modified: oagen.config.ts
179
+ Validation: Tests / Fixture extract / Determinism / Real SDK extract
180
+ ApiSurface: {N} classes, {N} interfaces, {N} type aliases, {N} enums, {N} exports
181
+ ```
182
+
183
+ ## Output
184
+
185
+ This skill produces, in the emitter project:
186
+
187
+ - `src/compat/extractors/{language}.ts` — extractor implementing the `Extractor` interface with `LanguageHints`
188
+ - `test/compat/extractors/{language}.test.ts` — extraction tests against fixture SDK
189
+ - `test/fixtures/sample-sdk-{language}/` — minimal fixture SDK for testing
190
+ - Updated `oagen.config.ts` with the new extractor registered
191
+
192
+ ## Common Pitfalls
193
+
194
+ 1. **Non-deterministic output** — Always sort record keys and array members
195
+ 2. **Missing error on bad input** — Throw descriptive errors for missing entry points or type infrastructure
196
+ 3. **Extracting private symbols** — Only extract what consumers can actually use
197
+ 4. **Subprocess encoding** — Ensure UTF-8 and handle stderr/stdout correctly
198
+ 5. **Hardcoded paths** — Use the `sdkPath` parameter, never hardcode fixture paths
199
+ 6. **Ignoring generics** — Capture generic type parameters (e.g., `ListResponse<Organization>`)
200
+ 7. **Forgetting the export map** — Map the entry point to its public symbols even if the language doesn't have explicit barrel exports