@workos/oagen 0.7.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 +1123 -436
- 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 +362 -41
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-BYadQQV7.mjs → diff-BaH9qa8k.mjs} +11 -2
- package/dist/{diff-BYadQQV7.mjs.map → diff-BaH9qa8k.mjs.map} +1 -1
- package/dist/differ-CTl98yoS.d.mts +116 -0
- package/dist/differ-CTl98yoS.d.mts.map +1 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +50 -10
- 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 +49 -9
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +10 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +49 -9
- package/dist/smoke.mjs.map +1 -1
- package/dist/tree-sitter-C53w_0WE.mjs.map +1 -1
- package/dist/{types-DJMbCtIO.d.mts → types-mTtkgy_h.d.mts} +180 -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/differ-CG5ZErKl.mjs.map +0 -1
- package/dist/types-DJMbCtIO.d.mts.map +0 -1
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Compatibility Policy
|
|
2
|
+
|
|
3
|
+
Human-authored compatibility policy lives in the `compat` section of `oagen.config.ts`. This is the single place for fail thresholds, language overrides, and intentional break approvals.
|
|
4
|
+
|
|
5
|
+
## Config Schema
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
export default {
|
|
9
|
+
compat: {
|
|
10
|
+
failOn: "breaking",
|
|
11
|
+
reportPath: "compat-report.json",
|
|
12
|
+
explain: true,
|
|
13
|
+
baselinePath: "sdk-surface.json",
|
|
14
|
+
languagePolicy: {
|
|
15
|
+
php: {
|
|
16
|
+
// optional sparse overrides
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
allow: [
|
|
20
|
+
// intentional break approvals
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Fields
|
|
27
|
+
|
|
28
|
+
| Field | Type | Default | Description |
|
|
29
|
+
| ---------------- | ------------------------------------------------ | ------------ | ------------------------------------------ |
|
|
30
|
+
| `failOn` | `'none' \| 'breaking' \| 'soft-risk'` | `'breaking'` | Severity threshold for `verify` failure |
|
|
31
|
+
| `reportPath` | `string` | — | Path to write machine-readable JSON report |
|
|
32
|
+
| `explain` | `boolean` | `false` | Include provenance explanations in output |
|
|
33
|
+
| `baselinePath` | `string` | — | Path to baseline compatibility snapshot |
|
|
34
|
+
| `languagePolicy` | `Record<LanguageId, Partial<CompatPolicyHints>>` | — | Per-language policy overrides |
|
|
35
|
+
| `allow` | `CompatApproval[]` | — | Intentional break approvals |
|
|
36
|
+
|
|
37
|
+
## Language Policy
|
|
38
|
+
|
|
39
|
+
Language policy determines which aspects of the public API are breaking. Built-in defaults capture language semantics:
|
|
40
|
+
|
|
41
|
+
| Language | Caller Uses Param Names | Constructor Order | Param Names Public | Overloads Public | Arity Public |
|
|
42
|
+
| -------- | ----------------------- | ----------------- | ------------------ | ---------------- | ------------ |
|
|
43
|
+
| PHP | yes | yes | yes | no | no |
|
|
44
|
+
| Python | yes | yes | yes | no | no |
|
|
45
|
+
| Ruby | yes | yes | yes | no | no |
|
|
46
|
+
| Go | no | yes | no | no | yes |
|
|
47
|
+
| Kotlin | yes | no | yes | yes | no |
|
|
48
|
+
| .NET | yes | no | yes | yes | no |
|
|
49
|
+
| Elixir | yes | no | yes | no | yes |
|
|
50
|
+
| Rust | no | yes | no | no | yes |
|
|
51
|
+
| Node | no | no | no | no | no |
|
|
52
|
+
|
|
53
|
+
### Overriding Language Defaults
|
|
54
|
+
|
|
55
|
+
Use `languagePolicy` only when a specific SDK intentionally diverges from language norms:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
compat: {
|
|
59
|
+
languagePolicy: {
|
|
60
|
+
node: {
|
|
61
|
+
// This Node SDK uses positional args, not options objects
|
|
62
|
+
methodParameterNamesArePublicApi: true,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Approvals
|
|
69
|
+
|
|
70
|
+
Approvals are concept-first: one approval covers one conceptual change across affected languages.
|
|
71
|
+
|
|
72
|
+
### Schema
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
interface CompatApproval {
|
|
76
|
+
symbol: string; // Fully-qualified symbol
|
|
77
|
+
category: string; // Change category (e.g., 'parameter_renamed')
|
|
78
|
+
appliesTo?: string[]; // Language IDs, or omit for all
|
|
79
|
+
match?: {
|
|
80
|
+
// Optional narrowing
|
|
81
|
+
parameter?: string;
|
|
82
|
+
member?: string;
|
|
83
|
+
oldName?: string;
|
|
84
|
+
newName?: string;
|
|
85
|
+
};
|
|
86
|
+
allowedReleaseLevel?: "major" | "minor" | "patch";
|
|
87
|
+
reason: string; // Required explanation
|
|
88
|
+
issue?: string; // Issue tracker reference
|
|
89
|
+
expiresAfterVersion?: string;
|
|
90
|
+
approved?: boolean; // Whether this approval is active (default: true)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### The `approved` Field
|
|
95
|
+
|
|
96
|
+
Approvals default to active. Set `approved: false` to deactivate an approval without removing it from the config — useful for temporarily disabling an approval or keeping a record of past approvals.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
{
|
|
100
|
+
symbol: 'Authorization.check',
|
|
101
|
+
category: 'parameter_renamed',
|
|
102
|
+
reason: 'Intentional rename for v3',
|
|
103
|
+
issue: 'SDK-1234',
|
|
104
|
+
approved: false, // deactivated — this break is no longer approved
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
When `approved` is `false`, the matching engine skips the approval entirely. When `approved` is `true` or omitted, the approval is active.
|
|
109
|
+
|
|
110
|
+
### Example
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
compat: {
|
|
114
|
+
allow: [
|
|
115
|
+
{
|
|
116
|
+
symbol: 'WorkOS\\Service\\UserManagement::createUser',
|
|
117
|
+
category: 'parameter_removed',
|
|
118
|
+
match: { parameter: 'passwordHashType' },
|
|
119
|
+
appliesTo: ['php', 'python', 'kotlin', 'dotnet'],
|
|
120
|
+
allowedReleaseLevel: 'major',
|
|
121
|
+
reason: 'Intentional wrapper-object migration',
|
|
122
|
+
issue: 'SDK-1234',
|
|
123
|
+
expiresAfterVersion: '6.0.0',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Matching Rules
|
|
130
|
+
|
|
131
|
+
Approvals must be narrow:
|
|
132
|
+
|
|
133
|
+
**Good:**
|
|
134
|
+
|
|
135
|
+
- One symbol, one category, one conceptual change
|
|
136
|
+
- Optionally one parameter/member
|
|
137
|
+
- Optionally a bounded set of affected languages
|
|
138
|
+
|
|
139
|
+
**Bad (rejected by validation):**
|
|
140
|
+
|
|
141
|
+
- Wildcard symbols (`*`, `Authorization.*`)
|
|
142
|
+
- Empty reason
|
|
143
|
+
- Missing symbol or category
|
|
144
|
+
|
|
145
|
+
### Anti-patterns
|
|
146
|
+
|
|
147
|
+
| Pattern | Problem |
|
|
148
|
+
| ---------------------------------- | ---------------------------------- |
|
|
149
|
+
| Approve all breaks in one language | Too broad — masks real regressions |
|
|
150
|
+
| Approve all parameter changes | Hides future unintentional breaks |
|
|
151
|
+
| No reason field | No audit trail |
|
|
152
|
+
| No issue reference | Can't trace back to decisions |
|
|
153
|
+
|
|
154
|
+
## Severity Determination
|
|
155
|
+
|
|
156
|
+
The default severity for a category comes from the classification engine. Language policy can modify this:
|
|
157
|
+
|
|
158
|
+
1. A `parameter_renamed` change is `breaking` by default
|
|
159
|
+
2. If the language policy says `methodParameterNamesArePublicApi: false` (e.g., Go), the classifier can downgrade to `soft-risk`
|
|
160
|
+
3. Approvals can further suppress changes from the failure threshold
|
|
161
|
+
|
|
162
|
+
## Fail Threshold
|
|
163
|
+
|
|
164
|
+
The `failOn` level determines which unapproved changes cause `oagen verify` to fail:
|
|
165
|
+
|
|
166
|
+
| Level | Fails on |
|
|
167
|
+
| ----------- | ---------------------------------------- |
|
|
168
|
+
| `none` | Never fails for compat |
|
|
169
|
+
| `breaking` | Unapproved breaking changes |
|
|
170
|
+
| `soft-risk` | Unapproved breaking OR soft-risk changes |
|