@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,75 @@
1
+ # Dependency Layers
2
+
3
+ ## Layer Hierarchy (one-way imports only)
4
+
5
+ ```
6
+ Layer 0: ir/types ← Pure type definitions, no imports from other layers
7
+ Layer 1: utils ← Only imports from Layer 0 (naming, tree-sitter helpers)
8
+ Layer 2: parser ← Imports from Layers 0-1
9
+ Layer 3: engine, differ ← Imports from Layers 0-1
10
+ Layer 3: compat ← Imports from Layers 0-1 (extractors, overlay, staleness, differ)
11
+ Layer 4: verify ← Imports from Layers 0-3
12
+ Layer 5: cli ← Imports from Layers 0-4
13
+
14
+ Top-level entrypoints (`src/index.ts`, `src/errors.ts`) are exempt from these directional rules because they exist to re-export public APIs and shared error types.
15
+ ```
16
+
17
+ ## Allowed Imports
18
+
19
+ | File in... | Can import from... | Cannot import from... |
20
+ | ------------- | ---------------------------------------------------------------------- | ----------------------------------- |
21
+ | `src/ir/` | (nothing in `src/`) | everything |
22
+ | `src/utils/` | `src/ir/`, `src/errors.ts` | parser, engine, differ, compat... |
23
+ | `src/parser/` | `src/ir/`, `src/utils/`, `src/errors.ts` | engine, differ, compat, verify, cli |
24
+ | `src/engine/` | `src/ir/`, `src/utils/`, `src/errors.ts`, `src/differ/` | parser, verify, cli |
25
+ | `src/differ/` | `src/ir/`, `src/utils/`, `src/errors.ts` | parser, compat, verify, cli |
26
+ | `src/compat/` | `src/ir/`, `src/utils/`, `src/errors.ts`, `src/differ/` | parser, verify, cli |
27
+ | `src/verify/` | `src/ir/`, `src/utils/`, `src/errors.ts`, `src/engine/`, `src/compat/` | parser, cli |
28
+ | `src/cli/` | anything in `src/` | (top level, can import all) |
29
+
30
+ `src/utils/` contains naming utilities (`naming.ts`) and tree-sitter helpers (`tree-sitter.ts`). The `safeParse()` function in `tree-sitter.ts` is used by all tree-sitter-based extractors and the engine merger to work around a 32KB buffer limit in the tree-sitter 0.21.x native binding.
31
+
32
+ `src/compat/` contains the backwards-compatibility system: extractors (per-language API surface extraction), the compat differ, overlay system, and staleness detection. Extractors use tree-sitter grammars for source parsing (via `src/utils/tree-sitter.ts`).
33
+
34
+ ## Emitters Are External
35
+
36
+ Emitters live in a separate project (`oagen-emitters`) and import from `@workos/oagen`. They receive IR nodes via their method signatures and never call the parser directly. Each emitter is self-contained with no cross-emitter dependencies.
37
+
38
+ ## Violation Examples
39
+
40
+ A violation occurs when a lower-layer module imports from a higher layer:
41
+
42
+ ```typescript
43
+ // BAD: src/parser/schemas.ts importing from engine (Layer 2 → Layer 3)
44
+ import { generate } from "../engine/orchestrator.js";
45
+
46
+ // BAD: src/ir/types.ts importing from utils (Layer 0 → Layer 1)
47
+ import { toSnakeCase } from "../utils/naming.js";
48
+
49
+ // BAD: src/engine/orchestrator.ts importing from cli (Layer 3 → Layer 4)
50
+ import { loadConfig } from "../cli/config-loader.js";
51
+ ```
52
+
53
+ **Remediation:** Move the shared logic to a lower layer, or extract a type-only import.
54
+
55
+ ## Cross-Layer Exceptions
56
+
57
+ Two type-only exceptions are currently allowed:
58
+
59
+ - `differ -> engine/types` for shared generation types such as `EmitterContext` and `GeneratedFile`
60
+ - `engine -> compat/types` because overlay-aware generation threads compat types through `EmitterContext` and generation options without depending on compat implementation modules
61
+
62
+ One runtime exception is currently allowed:
63
+
64
+ - `engine -> differ` for incremental generation, which depends on spec diffing and file mapping
65
+
66
+ If you add a new exception, keep it narrow, make it type-only when possible, and document the reason in both code and docs.
67
+
68
+ ## Enforcement
69
+
70
+ These layer rules are enforced by `test/architecture/dependency-layers.test.ts`.
71
+
72
+ If you intentionally cross a layer boundary, either:
73
+
74
+ 1. refactor the shared code to a lower layer, or
75
+ 2. add a narrow documented exception here and update the enforcement test
@@ -0,0 +1,160 @@
1
+ # Emitter Contract
2
+
3
+ Source: `src/engine/types.ts`
4
+
5
+ If you want the smallest possible implementation first, start with [Minimal Emitter](../core/minimal-emitter.md). This document is the full contract reference.
6
+
7
+ ## Interface
8
+
9
+ Every language emitter must implement the `Emitter` interface:
10
+
11
+ ```typescript
12
+ interface Emitter {
13
+ language: string;
14
+
15
+ generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[];
16
+ generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[];
17
+ generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[];
18
+ generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[];
19
+ generateErrors(ctx: EmitterContext): GeneratedFile[];
20
+ generateConfig(ctx: EmitterContext): GeneratedFile[];
21
+ generateTypeSignatures(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[];
22
+ generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[];
23
+ generateManifest?(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[];
24
+ fileHeader(): string;
25
+ }
26
+ ```
27
+
28
+ ## GeneratedFile
29
+
30
+ ```typescript
31
+ interface GeneratedFile {
32
+ path: string; // Relative path within output directory
33
+ content: string; // File content (header prepended by orchestrator)
34
+ header?: string; // Optional override of the default file header
35
+ skipIfExists?: boolean; // Don't overwrite if file already exists on disk (defaults to false)
36
+ }
37
+ ```
38
+
39
+ ## EmitterContext
40
+
41
+ Every generator method receives an `EmitterContext`:
42
+
43
+ ```typescript
44
+ interface EmitterContext {
45
+ namespace: string; // snake_case namespace for file paths
46
+ namespacePascal: string; // PascalCase namespace for code identifiers
47
+ spec: ApiSpec; // The full parsed IR spec
48
+ outputDir?: string; // Output directory path
49
+ apiSurface?: ApiSurface; // Baseline API surface (when --api-surface is provided)
50
+ overlayLookup?: OverlayLookup; // Name preservation overlay (when --api-surface is provided)
51
+ }
52
+ ```
53
+
54
+ ## Overlay Integration
55
+
56
+ When a user passes `--api-surface` to `oagen generate` or `oagen diff`, the engine builds an `OverlayLookup` and passes it to emitters via `ctx.overlayLookup`. Emitters should check the overlay before generating default names to preserve backwards compatibility with an existing SDK.
57
+
58
+ **When it's present:** The user has an existing live SDK and wants the generated output to preserve its public API (method names, type names, exports).
59
+
60
+ **How emitters should use it:**
61
+
62
+ 1. **Method names:** Before generating a method name, check `methodByOperation` using the HTTP key:
63
+
64
+ ```typescript
65
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
66
+ const existing = ctx.overlayLookup?.methodByOperation.get(httpKey);
67
+ if (existing) {
68
+ // Use existing.methodName instead of generating a new name
69
+ }
70
+ ```
71
+
72
+ 2. **Type names:** Check `interfaceByName` and `typeAliasByName` for existing names before applying naming conventions.
73
+
74
+ 3. **Barrel exports:** Check `requiredExports` to ensure all expected symbols are re-exported.
75
+
76
+ **`OverlayLookup` fields:**
77
+
78
+ | Field | Type | Purpose |
79
+ | ------------------- | ---------------------------- | ------------------------------------------------ |
80
+ | `methodByOperation` | `Map<string, MethodOverlay>` | HTTP key → existing method info (name, params) |
81
+ | `httpKeyByMethod` | `Map<string, string>` | Reverse map: "Class.method" → HTTP key |
82
+ | `interfaceByName` | `Map<string, string>` | IR interface name → existing interface name |
83
+ | `typeAliasByName` | `Map<string, string>` | IR type alias name → existing type alias name |
84
+ | `requiredExports` | `Map<string, Set<string>>` | Barrel file path → symbols that must be exported |
85
+
86
+ The `httpKeyByMethod` reverse map is only populated when a manifest (`smoke-manifest.json`) is available. Without it, method-level violations cannot be auto-patched in the self-correcting loop. Emitters that support compat verification should implement `generateManifest`.
87
+
88
+ ## Rules
89
+
90
+ 1. **Pure functions** — Generator methods receive IR nodes and return `GeneratedFile[]`. No I/O, no side effects, no network calls.
91
+
92
+ 2. **Empty input = empty output** — If `models` is `[]`, `generateModels` must return `[]` without errors.
93
+
94
+ 3. **Namespace threading** — All generated code must use `ctx.namespace` (snake_case) for file paths and `ctx.namespacePascal` for code identifiers.
95
+
96
+ 4. **Tests include fixtures** — `generateTests` should internally call fixture generation and return both test files and fixture files in the combined `GeneratedFile[]`.
97
+
98
+ 5. **Inapplicable methods return `[]`** — If a language doesn't need type signature files, `generateTypeSignatures` returns `[]`. Optional methods like `generateManifest` can simply be omitted.
99
+
100
+ 6. **Composable generators** — Interface methods can compose multiple internal generators. Example: Node's `generateConfig` returns config files plus shared utility files.
101
+
102
+ ## New IR Variants for Emitters
103
+
104
+ Emitters should handle these additional variants introduced for open-source readiness:
105
+
106
+ - **Auth:** `'cookie'` in the `apiKey` auth scheme `in` field
107
+ - **Pagination:** `'link-header'` strategy for RFC 5988 Link header pagination; `dataPath` may be `undefined` (meaning the response IS the data)
108
+ - **Encoding:** `'form-urlencoded'` for `application/x-www-form-urlencoded` request bodies
109
+ - **HTTP methods:** `head`, `options`, `trace` — HEAD returns no body, OPTIONS/TRACE are informational
110
+ - **Cookie params:** Optional `cookieParams` array on operations that use cookie parameters
111
+ - **Servers:** Optional `servers: ServerEntry[]` on `ApiSpec` alongside `baseUrl`
112
+ - **Multiple responses:** Optional `successResponses: SuccessResponse[]` on `Operation` when more than one 2xx exists
113
+ - **Idempotency:** `injectIdempotencyKey` is now spec-driven (true only when the spec declares an `Idempotency-Key` header)
114
+
115
+ ## OperationPlan
116
+
117
+ Source: `src/engine/operation-plan.ts`
118
+
119
+ Emitters use `planOperation(op)` to compute an `OperationPlan` for each operation, rather than duplicating decision logic inline:
120
+
121
+ ```typescript
122
+ interface OperationPlan {
123
+ operation: Operation; // back-reference
124
+ isDelete: boolean;
125
+ hasBody: boolean;
126
+ isIdempotentPost: boolean;
127
+ pathParamsInOptions: boolean;
128
+ isPaginated: boolean;
129
+ responseModelName: string | null; // null = void/primitive
130
+ isModelResponse: boolean;
131
+ hasQueryParams: boolean;
132
+ }
133
+
134
+ function planOperation(op: Operation): OperationPlan;
135
+ ```
136
+
137
+ - `responseModelName` is `null` for void/primitive responses. Each emitter maps this to its own fallback (`'void'` in Node, `'Object'` in Ruby).
138
+ - `resolveResponseModelName(op)` is also exported for cases that only need the model name.
139
+
140
+ ## Existing Emitters
141
+
142
+ Production emitters live in the separate `oagen-emitters` project and import types from `@workos/oagen`.
143
+
144
+ | Language | Location | Notes |
145
+ | ---------------------- | ------------------------------- | ------------------------------------------ |
146
+ | TypeScript (reference) | `examples/reference-emitter/` | Ships with oagen — minimal working example |
147
+ | Ruby | `src/ruby/` (in oagen-emitters) | Production emitter |
148
+ | Node | `src/node/` (in oagen-emitters) | Production emitter |
149
+
150
+ ## Adding a New Emitter
151
+
152
+ 1. Create design doc at `docs/sdk-architecture/{language}.md` in the emitter project
153
+ 2. Scaffold files under `src/{language}/` in the emitter project
154
+ 3. Implement all `Emitter` methods
155
+ 4. Register in the emitter project's `oagen.config.ts`
156
+ 5. Add tests under `test/{language}/` in the emitter project
157
+
158
+ Or use: `/generate-emitter <language>`
159
+
160
+ Emitters are registered via `oagen.config.ts` — no need to modify CLI source. See the Configuration section in the README.
@@ -0,0 +1,227 @@
1
+ # Extractor Contract
2
+
3
+ Source: `src/compat/types.ts`
4
+
5
+ ## Interface
6
+
7
+ Every language extractor must implement the `Extractor` interface:
8
+
9
+ ```typescript
10
+ interface Extractor {
11
+ language: string;
12
+ extract(sdkPath: string): Promise<ApiSurface>;
13
+ hints: LanguageHints;
14
+ }
15
+ ```
16
+
17
+ - `language` — the language identifier (e.g., `"node"`, `"ruby"`, `"python"`). Must match the emitter's `language` field.
18
+ - `extract` — analyzes a live SDK at `sdkPath` and returns its public API surface. Must be deterministic: same input produces the same output.
19
+ - `hints` — a `LanguageHints` object that tells the differ and overlay how to interpret type strings for this language. Required — every extractor must explicitly declare its conventions.
20
+
21
+ ## ApiSurface
22
+
23
+ ```typescript
24
+ interface ApiSurface {
25
+ language: string;
26
+ extractedFrom: string;
27
+ extractedAt: string;
28
+ classes: Record<string, ApiClass>;
29
+ interfaces: Record<string, ApiInterface>;
30
+ typeAliases: Record<string, ApiTypeAlias>;
31
+ enums: Record<string, ApiEnum>;
32
+ exports: Record<string, string[]>;
33
+ }
34
+ ```
35
+
36
+ Each sub-type captures a different kind of public symbol:
37
+
38
+ | Type | Fields | What it captures |
39
+ | -------------- | ---------------------------------------------------- | ------------------------------------------------ |
40
+ | `ApiClass` | `name`, `methods`, `properties`, `constructorParams` | Public classes with their methods and properties |
41
+ | `ApiMethod` | `name`, `params`, `returnType`, `async` | Method signatures |
42
+ | `ApiParam` | `name`, `type`, `optional` | Method and constructor parameters |
43
+ | `ApiProperty` | `name`, `type`, `readonly` | Class properties |
44
+ | `ApiInterface` | `name`, `fields`, `extends` | Interface declarations |
45
+ | `ApiField` | `name`, `type`, `optional` | Interface fields |
46
+ | `ApiTypeAlias` | `name`, `value` | Type alias declarations |
47
+ | `ApiEnum` | `name`, `members` | Enum declarations with member values |
48
+
49
+ The `exports` field maps file paths to their exported symbol names, capturing the barrel export structure.
50
+
51
+ ## Rules
52
+
53
+ 1. **Deterministic output** — Running the extractor twice on the same SDK must produce identical JSON. Sort keys and members consistently.
54
+
55
+ 2. **Public surface only** — Extract only public/exported symbols. Internal implementation details, private methods, and unexported types are excluded.
56
+
57
+ 3. **Preserve fidelity** — Method signatures should capture parameter names, types, and optionality as they appear in the live SDK, not as the IR would generate them.
58
+
59
+ 4. **Handle missing infrastructure gracefully** — If the language's type system files are missing (e.g., no `tsconfig.json`, no `.pyi` stubs), throw a descriptive error rather than returning an empty surface.
60
+
61
+ ## Language Hints
62
+
63
+ Every extractor must provide a `hints: LanguageHints` object that tells the differ and overlay how to interpret language-specific type strings. The `LanguageHints` interface includes:
64
+
65
+ | Method/Property | Purpose | Node example | Go example |
66
+ | ----------------------------------- | --------------------------------------------- | ------------------------------------------------ | ------------------------------------ |
67
+ | `stripNullable(type)` | Strip nullable wrapper | `"string \| null"` → `"string"` | `"*Organization"` → `"Organization"` |
68
+ | `isNullableOnlyDifference(a, b)` | True if a and b differ only by nullability | `"string"` vs `"string \| null"` → true | |
69
+ | `isUnionReorder(a, b)` | True if same union members in different order | `"a" \| "b"` vs `"b" \| "a"` → true | Always false (Go has no unions) |
70
+ | `isGenericTypeParam(type)` | True if type is an unresolvable generic param | `"T"`, `"TCustomAttributes"` → true | |
71
+ | `isExtractionArtifact(type)` | True if type is an extraction artifact | `"any"` → true | `"interface{}"` → true |
72
+ | `tolerateCategoryMismatch` | Allow type alias ↔ interface/class mismatch | `true` (TS allows both forms) | `false` |
73
+ | `extractReturnTypeName(returnType)` | Extract innermost type from return type | `"Promise<Organization>"` → `"Organization"` | |
74
+ | `extractParamTypeName(paramType)` | Extract type from param (null for primitives) | `"string"` → null | |
75
+ | `propertyMatchesClass(prop, class)` | True if property maps to class name | camelCase: `"organizations"` → `"Organizations"` | |
76
+ | `derivedModelNames(modelName)` | Additional names a model produces | `["FooResponse", "SerializedFoo"]` | `["FooResponse"]` |
77
+
78
+ Use `resolveHints({...overrides})` to start from Node defaults and override only the methods that differ for your language:
79
+
80
+ ```typescript
81
+ import { resolveHints } from "@workos/oagen";
82
+
83
+ const goHints = resolveHints({
84
+ stripNullable: (type) => (type.startsWith("*") ? type.slice(1) : null),
85
+ isExtractionArtifact: (type) => type === "interface{}",
86
+ tolerateCategoryMismatch: false,
87
+ derivedModelNames: (name) => [`${name}Response`],
88
+ });
89
+ ```
90
+
91
+ ## Registration
92
+
93
+ 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.
94
+
95
+ ```typescript
96
+ // oagen.config.ts
97
+ import { myExtractor } from "./src/compat/extractors/my-language.js";
98
+ import type { OagenConfig } from "@workos/oagen";
99
+
100
+ const config: OagenConfig = {
101
+ extractors: [myExtractor],
102
+ };
103
+ export default config;
104
+ ```
105
+
106
+ Under the hood, the config loader calls `registerExtractor()` for each entry, populating a Map-based registry (`src/compat/extractor-registry.ts`):
107
+
108
+ ```typescript
109
+ registerExtractor(extractor: Extractor): void
110
+ getExtractor(language: string): Extractor
111
+ ```
112
+
113
+ ## File Structure: Parser vs Surface
114
+
115
+ Extractors use tree-sitter for source parsing (except Node, which uses the TypeScript compiler API). The internal file layout depends on the complexity of getting from parsed AST nodes to `ApiSurface` types. There are two reasons a language needs a separate `*-surface.ts` file:
116
+
117
+ 1. **Structural joining** — the language separates type definitions from method implementations (e.g., Go receiver functions, Rust impl blocks), so parsed symbols must be joined before they can become `ApiClass` etc.
118
+ 2. **Classification** — the language co-locates methods inside classes, but deciding _which_ API surface type a class maps to requires non-trivial heuristics (e.g., PHP resource classes → `ApiInterface`, Python Protocols → `ApiClass`).
119
+
120
+ ### Parser only
121
+
122
+ When the AST co-locates methods/properties inside their owning declaration **and** the mapping to API surface types is straightforward enough to handle inline.
123
+
124
+ - **Ruby**: `class Foo` contains `def bar` and `attr_accessor :baz` as direct children. The parser has separate functions (`extractClasses`, `extractServiceModules`, `extractEnumModules`) that each return the appropriate `Api*` type directly.
125
+ - **Node**: The TypeScript compiler API resolves classes with their members in one pass.
126
+
127
+ ### Parser + Surface (structural joining)
128
+
129
+ When the language separates type definitions from method implementations. The parser extracts raw symbols (structs, functions, consts) independently, then a surface builder joins them by matching receivers to types, consts to type declarations, etc.
130
+
131
+ - **Go**: `type Foo struct` and `func (f *Foo) Bar()` are separate top-level declarations. The parser extracts `GoStruct[]` and `GoFunc[]`; the surface builder matches functions to structs by receiver type to produce `ApiClass`.
132
+ - **Rust**: `pub struct Foo` and `impl Foo { pub fn bar() }` are separate items. The parser extracts `RustStruct[]` and `RustFunc[]`; the surface builder matches impl methods to structs.
133
+
134
+ ### Parser + Surface (classification)
135
+
136
+ When the AST co-locates methods inside classes (like Ruby), but the classification logic — deciding whether a parsed class becomes an `ApiClass`, `ApiInterface`, or `ApiEnum` — is complex enough to warrant its own file.
137
+
138
+ - **PHP**: The parser returns `PhpClass[]` with methods, properties, and constants already attached. The surface builder classifies each class: resource classes (extends configured model base class via `hints.modelBaseClasses`) → `ApiInterface`, const-only classes → `ApiEnum`, PHP interface declarations → `ApiInterface`, service/exception classes → `ApiClass`.
139
+ - **Python**: The parser returns `PythonClass[]`, `PythonTypeAlias[]`, and `__all__` exports. The surface builder classifies: `Protocol` subclasses → `ApiClass`, model base class subclasses (configured via `hints.modelBaseClasses`, e.g., `BaseModel` for Pydantic) → `ApiInterface`, `TypedDict` subclasses → `ApiInterface`, `Literal[...]` type aliases → `ApiEnum`, exception subclasses → `ApiClass`. Transitive base class resolution is needed (a class inheriting from another model class is also a model).
140
+
141
+ | Language | Files | Why |
142
+ | -------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
143
+ | Node | `node.ts` | TS compiler API resolves everything in one pass |
144
+ | Ruby | `ruby.ts` + `ruby-parser.ts` | AST co-locates methods in classes/modules — classification handled inline by separate extract functions |
145
+ | Go | `go.ts` + `go-parser.ts` + `go-surface.ts` | Structs and methods are separate declarations — surface builder joins by receiver type and qualifies duplicate names across packages |
146
+ | Rust | `rust.ts` + `rust-parser.ts` + `rust-surface.ts` | Structs and impl blocks are separate items — surface builder joins by impl target type |
147
+ | PHP | `php.ts` + `php-parser.ts` + `php-surface.ts` | Methods co-located in classes, but classification (resource vs enum vs service vs interface) requires heuristics |
148
+ | Python | `python.ts` + `python-parser.ts` + `python-surface.ts` | Methods co-located in classes, but classification (Protocol vs BaseModel vs TypedDict vs Literal vs exception) requires heuristics and transitive base resolution |
149
+
150
+ The `*-parser.ts` file handles tree-sitter AST traversal and returns language-specific intermediate types (`GoStruct`, `PhpClass`, `PythonClass`, etc.). The `*-surface.ts` file transforms those into `ApiSurface` types (`ApiClass`, `ApiInterface`, etc.) — either by structural joining or classification. The `*.ts` orchestrator file owns the `Extractor` constant, `LanguageHints`, file walking, and the `extract()` method.
151
+
152
+ All tree-sitter parsers use `safeParse()` from `src/utils/tree-sitter.ts` to handle files larger than 32KB (works around a buffer size limit in tree-sitter 0.21.x's native binding).
153
+
154
+ ## Language-Specific Strategies
155
+
156
+ ### Node (reference implementation)
157
+
158
+ - Uses the TypeScript compiler API (`typescript` package) to load and analyze the SDK
159
+ - Reads `tsconfig.json` to discover source files
160
+ - Resolves the entry point from `package.json` `main`/`types` fields
161
+ - Extracts classes, interfaces, type aliases, and enums from exported declarations
162
+ - File: `src/compat/extractors/node.ts`
163
+
164
+ ### Ruby
165
+
166
+ - Uses tree-sitter-ruby to parse `.rb` files under `lib/`
167
+ - Extracts classes (with `attr_accessor`/`attr_reader` properties and methods), service modules (modules with `class << self`), and enum-like modules (modules with string constants)
168
+ - Respects `private`/`protected`/`public` visibility markers
169
+ - Files: `src/compat/extractors/ruby.ts`, `ruby-parser.ts`
170
+
171
+ ### Go
172
+
173
+ - Uses tree-sitter-go to parse `.go` files (excluding `_test.go`, `vendor/`, `internal/`)
174
+ - Extracts structs (with JSON struct tags), functions/methods (matching receivers to structs), const blocks (enum patterns), and type aliases
175
+ - Package-qualifies duplicate names (e.g., `organizations.Client` vs `sso.Client`)
176
+ - Filters `context.Context` params (infrastructure, not API surface)
177
+ - Files: `src/compat/extractors/go.ts`, `go-parser.ts`, `go-surface.ts`
178
+
179
+ ### Rust
180
+
181
+ - Uses tree-sitter-rust to parse `.rs` files under `src/` (excluding `target/`)
182
+ - Extracts pub structs (with serde rename attributes), pub enums (with serde variant renames), impl block methods, pub traits, and type aliases
183
+ - Maps `Option<T>` to optional, unwraps `Result<T, E>` return types
184
+ - Matches impl blocks to struct definitions to distinguish `ApiClass` (has methods) from `ApiInterface` (data only)
185
+ - Files: `src/compat/extractors/rust.ts`, `rust-parser.ts`, `rust-surface.ts`
186
+
187
+ ### PHP
188
+
189
+ - Uses tree-sitter-php to parse `.php` files under `lib/` (fallback to `src/`), excluding `vendor/`, `tests/`
190
+ - Parser extracts classes and interfaces with methods, properties, constants, and `RESOURCE_ATTRIBUTES` arrays
191
+ - Surface builder classifies: resource classes (extends configured model base class with `RESOURCE_ATTRIBUTES`) → `ApiInterface`, const-only classes → `ApiEnum`, PHP `interface` declarations → `ApiInterface`, service/exception classes → `ApiClass`
192
+ - Reads both native type hints and PHPDoc `@param`/`@return` annotations for type information
193
+ - Files: `src/compat/extractors/php.ts`, `php-parser.ts`, `php-surface.ts`
194
+
195
+ ### Python
196
+
197
+ - Uses tree-sitter-python to parse `.py` files, finding the source root via `src/` or top-level package directories (looks for `__init__.py`)
198
+ - Parser extracts classes (with fields, methods, decorators, base classes), module-level type aliases, and `__all__` exports
199
+ - Surface builder classifies: `Protocol` subclasses → `ApiClass` (canonical service), model base class subclasses (configured via `hints.modelBaseClasses`, e.g., `BaseModel` for Pydantic) → `ApiInterface`, `TypedDict` subclasses → `ApiInterface`, `Literal[...]` type aliases → `ApiEnum`, exception subclasses → `ApiClass`. Uses transitive base resolution for model and exception detection.
200
+ - Skips private modules (`_*.py` except `__init__.py`), test files, and `__pycache__`
201
+ - Files: `src/compat/extractors/python.ts`, `python-parser.ts`, `python-surface.ts`
202
+
203
+ ## Building a New Extractor
204
+
205
+ 1. Create the extractor in `src/compat/extractors/{language}.ts` in oagen core
206
+ 2. Implement the `Extractor` interface — export a named constant (e.g., `export const rubyExtractor: Extractor = { ... }`)
207
+ 3. Provide `hints` as a `LanguageHints` object. Every extractor must have hints — the differ and overlay delegate all language-specific logic through them.
208
+ 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.
209
+ 5. Use `safeParse()` from `src/utils/tree-sitter.ts` instead of calling `parser.parse()` directly.
210
+ 6. Export from `src/index.ts`
211
+ 7. Register in the consumer project's `oagen.config.ts`
212
+ 8. Add tests in `test/compat/extractors/{language}.test.ts`
213
+ 9. Create a fixture SDK in `test/fixtures/sample-sdk-{language}/` with known classes, methods, and exports
214
+ 10. Verify deterministic output: extracting twice produces identical JSON
215
+
216
+ Use `/generate-extractor <language>` for a guided workflow.
217
+
218
+ ## Existing Extractors
219
+
220
+ | Language | Files |
221
+ | -------- | ---------------------------------------------------- |
222
+ | Node | `node.ts` |
223
+ | Ruby | `ruby.ts`, `ruby-parser.ts` |
224
+ | Go | `go.ts`, `go-parser.ts`, `go-surface.ts` |
225
+ | Rust | `rust.ts`, `rust-parser.ts`, `rust-surface.ts` |
226
+ | PHP | `php.ts`, `php-parser.ts`, `php-surface.ts` |
227
+ | Python | `python.ts`, `python-parser.ts`, `python-surface.ts` |