@workos/oagen 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/cli/index.mjs +1339 -451
  2. package/dist/cli/index.mjs.map +1 -1
  3. package/dist/compat/index.d.mts +144 -57
  4. package/dist/compat/index.d.mts.map +1 -1
  5. package/dist/compat/index.mjs +365 -35
  6. package/dist/compat/index.mjs.map +1 -1
  7. package/dist/{diff-CltJMcwG.mjs → diff-BaH9qa8k.mjs} +100 -23
  8. package/dist/diff-BaH9qa8k.mjs.map +1 -0
  9. package/dist/differ-CTl98yoS.d.mts +116 -0
  10. package/dist/differ-CTl98yoS.d.mts.map +1 -0
  11. package/dist/index.d.mts +4 -2
  12. package/dist/index.d.mts.map +1 -1
  13. package/dist/index.mjs +177 -4
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/{differ-CG5ZErKl.mjs → overlay-TYxHOCow.mjs} +1112 -553
  16. package/dist/overlay-TYxHOCow.mjs.map +1 -0
  17. package/dist/scripts/smoke/baseline.mjs +176 -3
  18. package/dist/scripts/smoke/baseline.mjs.map +1 -1
  19. package/dist/smoke.d.mts +43 -0
  20. package/dist/smoke.d.mts.map +1 -1
  21. package/dist/smoke.mjs +176 -3
  22. package/dist/smoke.mjs.map +1 -1
  23. package/dist/tree-sitter-C53w_0WE.mjs.map +1 -1
  24. package/dist/{types-C7g5sLuX.d.mts → types-mTtkgy_h.d.mts} +213 -2
  25. package/dist/types-mTtkgy_h.d.mts.map +1 -0
  26. package/dist/verify/index.d.mts +4 -3
  27. package/dist/verify/index.d.mts.map +1 -1
  28. package/dist/verify/index.mjs +12 -10
  29. package/dist/verify/index.mjs.map +1 -1
  30. package/docs/architecture/emitter-contract.md +2 -2
  31. package/docs/architecture/extractor-contract.md +6 -5
  32. package/docs/architecture/pipeline.md +4 -3
  33. package/docs/architecture/workflows.md +5 -3
  34. package/docs/cli.md +148 -6
  35. package/docs/contributor/public-api.md +1 -1
  36. package/docs/core/compatibility-ir.md +200 -0
  37. package/docs/core/compatibility-policy.md +170 -0
  38. package/docs/core/compatibility.md +83 -0
  39. package/docs/core/index.md +5 -0
  40. package/docs/core/manifest-schema.md +76 -0
  41. package/docs/core/quickstart.md +9 -2
  42. package/docs/core/verify-compat.md +235 -0
  43. package/package.json +1 -1
  44. package/skills/generate-emitter/SKILL.md +12 -7
  45. package/skills/generate-emitter/references/project-scaffold.md +27 -6
  46. package/skills/generate-extractor/SKILL.md +14 -8
  47. package/skills/generate-sdk/SKILL.md +2 -2
  48. package/skills/generate-smoke-test/SKILL.md +9 -5
  49. package/skills/review-operations/SKILL.md +2 -2
  50. package/skills/verify-compat/SKILL.md +1 -1
  51. package/dist/diff-CltJMcwG.mjs.map +0 -1
  52. package/dist/differ-CG5ZErKl.mjs.map +0 -1
  53. package/dist/types-C7g5sLuX.d.mts.map +0 -1
@@ -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 |
@@ -0,0 +1,83 @@
1
+ # Compatibility Verification
2
+
3
+ oagen includes a cross-language compatibility engine that detects breaking changes, classifies their severity per-language, and enforces policy before generated SDKs ship.
4
+
5
+ ## Overview
6
+
7
+ The compatibility system answers: **"will this regeneration break existing callers?"**
8
+
9
+ It works by comparing two snapshots of an SDK's public API surface — a baseline (the live SDK) and a candidate (the newly generated output) — and classifying every difference.
10
+
11
+ ## Lifecycle
12
+
13
+ ```
14
+ compat-extract → .oagen-compat-snapshot.json → compat-diff → report → compat-summary
15
+ ```
16
+
17
+ 1. **Extract** — `oagen compat-extract` runs an extractor against the live SDK and writes a `CompatSnapshot` JSON file to disk (`.oagen-compat-snapshot.json`)
18
+ 2. **Diff** — `oagen compat-diff` compares a baseline snapshot (committed) against a candidate snapshot (freshly extracted from generated output), classifying each difference
19
+ 3. **Enforce** — `compat-diff` exits non-zero when unapproved changes meet or exceed the fail threshold (`--fail-on`)
20
+ 4. **Summary** — `oagen compat-summary` formats the JSON report as a markdown PR comment
21
+
22
+ The baseline snapshot is committed to the repository and updated on each release. The candidate snapshot is extracted in CI from the freshly generated SDK.
23
+
24
+ ### CLI Commands
25
+
26
+ | Command | Purpose |
27
+ | ---------------------- | ---------------------------------------------- |
28
+ | `oagen compat-extract` | Extract a snapshot from a live SDK → JSON file |
29
+ | `oagen compat-diff` | Diff two snapshot files → classified report |
30
+ | `oagen compat-summary` | Format a report → markdown PR comment |
31
+
32
+ See [CLI Reference](../cli.md) for full argument tables.
33
+
34
+ ## Relation to Smoke Tests
35
+
36
+ Smoke tests verify **behavioral** correctness: does the generated SDK call the right endpoints with the right parameters? Compatibility verification checks **structural** correctness: does the generated SDK preserve the public API shape that callers depend on?
37
+
38
+ Both run during `oagen verify`. Smoke tests catch bugs. Compat checks catch breaking changes.
39
+
40
+ ## Relation to Spec Diffing
41
+
42
+ `oagen diff` compares two OpenAPI specs to show what changed at the API level. Compat verification compares two SDK surfaces to show what changed at the SDK level. A spec change may or may not produce a breaking SDK change, depending on the language and how the emitter handles it.
43
+
44
+ ## Key Concepts
45
+
46
+ ### Classification
47
+
48
+ Every detected change gets a category and severity:
49
+
50
+ - **Breaking** — Will break existing callers (e.g., `parameter_removed`, `symbol_renamed`)
51
+ - **Soft-risk** — May affect callers depending on usage (e.g., `constructor_reordered_named_friendly`)
52
+ - **Additive** — Safe to ship (e.g., `symbol_added`, `parameter_added_optional_terminal`)
53
+
54
+ See [Compatibility IR](compatibility-ir.md) for the full category list.
55
+
56
+ ### Language Policy
57
+
58
+ The same conceptual change has different severity in different languages. A parameter rename is breaking in PHP (named arguments) but soft-risk in Go (positional only). Language policy is defined by `CompatPolicyHints` with built-in defaults for all 9 supported languages.
59
+
60
+ See [Compatibility Policy](compatibility-policy.md) for details.
61
+
62
+ ### Concept-First Approvals
63
+
64
+ When a breaking change is intentional, it can be approved in `oagen.config.ts`. Approvals are concept-first: one approval targets one symbol and one change category, optionally scoped to specific languages.
65
+
66
+ See [Compatibility Policy](compatibility-policy.md) for the approval schema.
67
+
68
+ ### Provenance
69
+
70
+ Each classified change includes a provenance bucket explaining where the drift came from:
71
+
72
+ - `spec_shape_change` — The spec changed
73
+ - `emitter_template_change` — The emitter template changed
74
+ - `normalization_change` — Naming normalization produced different output
75
+ - `unknown` — Source of drift not determined
76
+
77
+ ## Further Reading
78
+
79
+ - [CLI Reference](../cli.md) — `compat-extract`, `compat-diff`, `compat-summary` argument tables
80
+ - [Compatibility IR](compatibility-ir.md) — Types, categories, parameters, `extractSnapshot()`
81
+ - [Compatibility Policy](compatibility-policy.md) — Config, approvals, language defaults
82
+ - [Manifest Schema](manifest-schema.md) — `.oagen-manifest.json` format
83
+ - [Verify Compat](verify-compat.md) — CI workflows, reports, GitHub Actions example
@@ -8,5 +8,10 @@ These documents define the stable center of oagen as an OSS framework.
8
8
  - [Pipeline Architecture](../architecture/pipeline.md)
9
9
  - [IR Type System Reference](../architecture/ir-types.md)
10
10
  - [Emitter Contract](../architecture/emitter-contract.md)
11
+ - [Compatibility Verification](compatibility.md)
12
+ - [Compatibility IR](compatibility-ir.md)
13
+ - [Compatibility Policy](compatibility-policy.md)
14
+ - [Manifest Schema](manifest-schema.md)
15
+ - [Verify Compat](verify-compat.md)
11
16
 
12
17
  Read the quickstart first. The goal is to get from a spec to generated files without learning compatibility overlays, extractors, or smoke workflows.
@@ -0,0 +1,76 @@
1
+ # Manifest Schema
2
+
3
+ `.oagen-manifest.json` is a generated provenance artifact. It records what was generated, from which inputs, and when. It is machine-owned — humans should not edit it.
4
+
5
+ ## Purpose
6
+
7
+ 1. **Pruning** — On regeneration, oagen deletes files from the previous manifest that are no longer emitted, preventing stale-file accumulation
8
+ 2. **Provenance** — Records the spec, emitter, and config used for each generation run
9
+ 3. **Compat versioning** — Records the compat schema version for snapshot compatibility
10
+
11
+ ## Schema (v2)
12
+
13
+ ```json
14
+ {
15
+ "version": 2,
16
+ "language": "php",
17
+ "sdkName": "workos-php",
18
+ "generatedAt": "2026-04-18T22:02:14.077Z",
19
+ "specSha": "sha256:abc123...",
20
+ "specPath": "../openapi-spec/spec/open-api-spec.yaml",
21
+ "emitterSha": "def456",
22
+ "emitterVersion": "0.7.0",
23
+ "configSha": "sha256:789abc...",
24
+ "compatSchemaVersion": "1",
25
+ "files": ["lib/Resource/CreateUser.php", "lib/Service/UserManagement.php"]
26
+ }
27
+ ```
28
+
29
+ ## Field Semantics
30
+
31
+ | Field | Type | Required | Description |
32
+ | --------------------- | ---------- | -------- | -------------------------------------------------------------------- |
33
+ | `version` | `number` | yes | Manifest schema version (currently `2`) |
34
+ | `language` | `string` | yes | Emitter target language (e.g., `"php"`) |
35
+ | `sdkName` | `string` | no | Human-readable SDK identity (e.g., `"workos-php"`) |
36
+ | `generatedAt` | `string` | yes | ISO-8601 timestamp of the generation run |
37
+ | `specSha` | `string` | no | SHA-256 hash of the source OpenAPI spec |
38
+ | `specPath` | `string` | no | Path or reference to the source spec |
39
+ | `emitterSha` | `string` | no | Git SHA of the emitter code used |
40
+ | `emitterVersion` | `string` | no | Emitter version string |
41
+ | `configSha` | `string` | no | SHA-256 hash of the effective `oagen.config.ts` |
42
+ | `compatSchemaVersion` | `string` | no | Version of the compat snapshot schema |
43
+ | `files` | `string[]` | yes | Sorted list of generated file paths (relative to manifest directory) |
44
+
45
+ ## Generated Ownership
46
+
47
+ The manifest is generated by `oagen generate` and written to the output directory. It should be committed to the SDK repository for pruning to work across regenerations.
48
+
49
+ **Never put human-authored content in the manifest.** Approvals, allowlists, and policy belong in `oagen.config.ts`. The manifest is regenerated on every run — any manual edits will be overwritten.
50
+
51
+ ## Pruning Semantics
52
+
53
+ When `oagen generate` runs:
54
+
55
+ 1. Read the existing `.oagen-manifest.json` (if present)
56
+ 2. Compare the previous `files` list against the current emission set
57
+ 3. Delete files that were previously generated but are no longer emitted
58
+ 4. Only delete files whose contents start with the oagen generation header (when a header guard is available)
59
+ 5. Write a new manifest with the current file list
60
+
61
+ On first adoption (no previous manifest), pruning is skipped.
62
+
63
+ ## Backward Compatibility
64
+
65
+ The manifest reader accepts both v1 and v2 manifests. v1 manifests (which lack `sdkName`, `specSha`, etc.) are read successfully — the new fields are simply absent. A v1 manifest written by an older oagen version will work with the current pruning system.
66
+
67
+ If the manifest version is higher than supported, it is ignored with a warning.
68
+
69
+ ## What Must Not Go in the Manifest
70
+
71
+ - Approvals or allowlists (human-authored → `oagen.config.ts`)
72
+ - Release thresholds (human-authored → `oagen.config.ts`)
73
+ - Manual exception records
74
+ - Policy overrides
75
+
76
+ The rule: **never make the generator rewrite a file that humans edit for policy.**
@@ -35,11 +35,18 @@ npm install
35
35
 
36
36
  The scaffold gives you:
37
37
 
38
- - `oagen.config.ts`
39
- - `src/demo/index.ts`
38
+ - `oagen.config.ts` -- a minimal config for local development that imports the plugin bundle
39
+ - `src/plugin.ts` -- plugin bundle export registering the stub emitter
40
+ - `src/demo/index.ts` -- stub emitter implementing the `Emitter` interface
40
41
  - build/test tooling
41
42
  - `sdk:generate` and related package scripts
42
43
 
44
+ > **Note:** The scaffolded project uses the plugin model. The consumer project
45
+ > (e.g. a spec repo) imports the plugin bundle from `src/plugin.ts` and layers
46
+ > its own spec interpretation policy on top. See the
47
+ > [CLI Reference](../cli.md#configuration-oagenconfigts) for plugin
48
+ > composition examples.
49
+
43
50
  ## 4. Replace the Stub Emitter
44
51
 
45
52
  Edit `src/demo/index.ts` to return a few simple files:
@@ -0,0 +1,235 @@
1
+ # Verify Compat
2
+
3
+ `oagen verify` runs compatibility checks as part of its verification pipeline. This document covers the compat-specific CLI flags, report outputs, and CI integration.
4
+
5
+ ## CLI Flags
6
+
7
+ ### Existing Flags (Compat-Related)
8
+
9
+ | Flag | Description |
10
+ | ---------------------- | ------------------------------------------------------------------- |
11
+ | `--api-surface <path>` | Baseline API surface JSON — enables compat verification |
12
+ | `--spec <path>` | OpenAPI spec for spec-scoped comparison |
13
+ | `--scope <mode>` | `full` or `spec-only` (default: `spec-only` when `--spec` provided) |
14
+ | `--max-retries <n>` | Max overlay patch iterations (default: 3) |
15
+ | `--diagnostics` | Write `verify-diagnostics.json` |
16
+
17
+ ### New Flags (Classified Compat)
18
+
19
+ | Flag | Description |
20
+ | -------------------------- | ------------------------------------------------------------ |
21
+ | `--compat-report <path>` | Write machine-readable classified compat report to this path |
22
+ | `--compat-fail-on <level>` | Override fail threshold: `none`, `breaking`, or `soft-risk` |
23
+ | `--compat-baseline <path>` | Path to baseline compatibility snapshot |
24
+ | `--compat-explain` | Include provenance explanations in terminal output |
25
+
26
+ CLI flags take precedence over `oagen.config.ts` settings.
27
+
28
+ ## Report Outputs
29
+
30
+ ### Machine-Readable Report (`--compat-report`)
31
+
32
+ ```json
33
+ {
34
+ "schemaVersion": "1",
35
+ "language": "php",
36
+ "summary": {
37
+ "breaking": 3,
38
+ "softRisk": 2,
39
+ "additive": 11
40
+ },
41
+ "changes": [
42
+ {
43
+ "severity": "breaking",
44
+ "category": "parameter_renamed",
45
+ "symbol": "Authorization.check",
46
+ "conceptualChangeId": "chg_parameter_renamed_authorization.check_resourceid",
47
+ "provenance": "emitter_template_change",
48
+ "old": { "parameter": "resourceId" },
49
+ "new": { "parameter": "resourceTarget" }
50
+ }
51
+ ]
52
+ }
53
+ ```
54
+
55
+ ### Human Summary (`--compat-explain`)
56
+
57
+ ```
58
+ Compat report for php:
59
+ 3 breaking, 2 soft-risk, 11 additive
60
+
61
+ Breaking:
62
+ [parameter_renamed] Authorization.check — Parameter "resourceId" renamed to "resourceTarget"
63
+ provenance: emitter_template_change
64
+ [parameter_removed] UserManagement.createUser — Parameter "passwordHashType" removed
65
+ provenance: spec_shape_change
66
+
67
+ Soft-risk:
68
+ [constructor_reordered_named_friendly] CreateUser.constructor — reordered (named-friendly)
69
+ provenance: normalization_change
70
+
71
+ Additive: 11 new symbol(s)
72
+ ```
73
+
74
+ ### Diagnostics (`--diagnostics`)
75
+
76
+ The existing `verify-diagnostics.json` continues to include legacy compat data (preservation score, violations by category/severity). The new classified report is written separately via `--compat-report`.
77
+
78
+ ## Failure Thresholds
79
+
80
+ The verify command fails when unapproved changes meet or exceed the fail threshold:
81
+
82
+ | `--compat-fail-on` | Fails on |
83
+ | ------------------ | ---------------------------------------- |
84
+ | `none` | Never fails for compat changes |
85
+ | `breaking` | Unapproved breaking changes (default) |
86
+ | `soft-risk` | Unapproved breaking OR soft-risk changes |
87
+
88
+ Approved changes (matched by `compat.allow` in config) are excluded from the failure threshold.
89
+
90
+ ## CI Integration
91
+
92
+ There are two CI models: **integrated** (via `oagen verify`) and **file-based** (via standalone `compat-extract` / `compat-diff` / `compat-summary` commands). The file-based model is recommended for new setups.
93
+
94
+ ### File-Based CI (Recommended)
95
+
96
+ The file-based model uses `.oagen-compat-snapshot.json` as a committed baseline artifact. On each PR, the candidate SDK is extracted, diffed against the baseline, and a summary is posted as a PR comment.
97
+
98
+ ```yaml
99
+ name: Compat Check
100
+ on: [pull_request]
101
+
102
+ jobs:
103
+ compat:
104
+ runs-on: ubuntu-latest
105
+ strategy:
106
+ matrix:
107
+ lang: [node, php, python, ruby, go, kotlin, dotnet, elixir, rust]
108
+ steps:
109
+ - uses: actions/checkout@v4
110
+
111
+ - name: Generate candidate SDK
112
+ run: oagen generate --spec openapi.yml --lang ${{ matrix.lang }} --output ./generated
113
+
114
+ - name: Extract candidate snapshot
115
+ run: |
116
+ oagen compat-extract \
117
+ --sdk-path ./generated \
118
+ --lang ${{ matrix.lang }} \
119
+ --output /tmp \
120
+ --spec openapi.yml
121
+
122
+ - name: Diff against baseline
123
+ run: |
124
+ oagen compat-diff \
125
+ --baseline .oagen-compat-snapshot.json \
126
+ --candidate /tmp/.oagen-compat-snapshot.json \
127
+ --output compat-report-${{ matrix.lang }}.json \
128
+ --fail-on breaking
129
+
130
+ - uses: actions/upload-artifact@v4
131
+ if: always()
132
+ with:
133
+ name: compat-report-${{ matrix.lang }}
134
+ path: compat-report-${{ matrix.lang }}.json
135
+
136
+ summary:
137
+ needs: compat
138
+ if: always()
139
+ runs-on: ubuntu-latest
140
+ steps:
141
+ - uses: actions/checkout@v4
142
+ - uses: actions/download-artifact@v4
143
+ with:
144
+ pattern: compat-report-*
145
+ merge-multiple: true
146
+
147
+ - name: Post cross-language PR summary
148
+ run: |
149
+ oagen compat-summary \
150
+ --report compat-report-*.json \
151
+ | gh pr comment ${{ github.event.pull_request.number }} --body-file -
152
+ env:
153
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
154
+ ```
155
+
156
+ For a single-language project, simplify to one job without the matrix:
157
+
158
+ ```bash
159
+ oagen compat-extract --sdk-path ./generated --lang node --output /tmp --spec openapi.yml
160
+ oagen compat-diff --baseline .oagen-compat-snapshot.json --candidate /tmp/.oagen-compat-snapshot.json --output report.json --fail-on breaking
161
+ oagen compat-summary --report report.json | gh pr comment --body-file -
162
+ ```
163
+
164
+ #### Baseline Management
165
+
166
+ The baseline snapshot is committed to the repository and updated as part of the release process:
167
+
168
+ ```bash
169
+ # After releasing a new SDK version, update the baseline
170
+ oagen compat-extract --sdk-path . --lang node --output .
171
+ git add .oagen-compat-snapshot.json
172
+ git commit -m "chore: update compat baseline for v2.1.0"
173
+ ```
174
+
175
+ #### Report-Only Mode (Phase 4)
176
+
177
+ To run compat checks without blocking PRs, set `--fail-on none`:
178
+
179
+ ```bash
180
+ oagen compat-diff \
181
+ --baseline .oagen-compat-snapshot.json \
182
+ --candidate /tmp/candidate-snapshot.json \
183
+ --output compat-report.json \
184
+ --fail-on none
185
+ ```
186
+
187
+ The PR summary will still be posted, but the job will not fail. Switch to `--fail-on breaking` when ready for hard enforcement.
188
+
189
+ ### Integrated CI (via `oagen verify`)
190
+
191
+ The `oagen verify` command runs compat checks as part of its verification pipeline. This is suitable when you want smoke tests and compat checks in a single step.
192
+
193
+ ```yaml
194
+ - name: Verify SDK
195
+ run: |
196
+ oagen verify \
197
+ --lang php \
198
+ --output ./generated \
199
+ --api-surface sdk-php-surface.json \
200
+ --spec openapi.yaml \
201
+ --compat-report compat-report.json \
202
+ --compat-fail-on breaking \
203
+ --diagnostics
204
+ ```
205
+
206
+ ### Exit Codes
207
+
208
+ | Code | Meaning |
209
+ | ---- | ---------------------------------------- |
210
+ | 0 | All checks passed |
211
+ | 1 | Compat violations or smoke test findings |
212
+ | 2 | SDK compile errors |
213
+
214
+ ## How to Read and Act on Findings
215
+
216
+ ### Breaking Changes
217
+
218
+ 1. Check the `provenance` field — is this from a spec change or an emitter change?
219
+ 2. If spec-driven: this may need a major version release
220
+ 3. If emitter-driven: fix the emitter to preserve backward compatibility
221
+ 4. If intentional: add an approval to `oagen.config.ts`
222
+
223
+ ### Soft-Risk Changes
224
+
225
+ 1. Review whether callers are likely affected
226
+ 2. If safe: no action needed (or add approval for documentation)
227
+ 3. If risky: fix the emitter or bump the release level
228
+
229
+ ### Additive Changes
230
+
231
+ Safe to ship. No action needed.
232
+
233
+ ## Overlay Retry Loop
234
+
235
+ The existing overlay retry loop (`--max-retries`) continues to work with the legacy compat system. It patches `public-api` and `export-structure` violations by correcting overlay mappings and regenerating. The classified compat report runs after the overlay loop has converged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Framework for building OpenAPI SDK emitters",
5
5
  "author": "WorkOS",
6
6
  "license": "MIT",
@@ -11,7 +11,7 @@ Scaffold a complete language emitter for oagen that translates the intermediate
11
11
 
12
12
  oagen has a plugin architecture for code generation. Each target language is an **emitter** — a TypeScript module that implements the `Emitter` interface. An emitter receives parsed IR nodes (models, enums, services, etc.) and returns `GeneratedFile[]` — arrays of `{ path, content }` pairs. The engine orchestrator calls each emitter method, prepends a file header, and writes the results to disk.
13
13
 
14
- Emitters live in **external projects** (not inside the oagen core repo). They import all oagen types from `@workos/oagen` and register via `oagen.config.ts` in their project.
14
+ Emitters live in **external projects** (not inside the oagen core repo). They import all oagen types from `@workos/oagen` and are exported through the emitter project's plugin bundle (e.g., `workosEmittersPlugin`), which the consumer project's `oagen.config.ts` imports.
15
15
 
16
16
  ## Reference Docs
17
17
 
@@ -41,7 +41,7 @@ Before starting, read and understand these oagen core types (all imported from `
41
41
  - `planOperation`, `OperationPlan` — operation analysis helpers
42
42
  - `toPascalCase`, `toSnakeCase`, `toCamelCase`, `toKebabCase`, `toUpperSnakeCase` — naming utilities
43
43
 
44
- Also read the project's `oagen.config.ts` to understand how emitters are registered.
44
+ Also read the project's plugin bundle export (e.g., `src/plugin.ts`) to understand how emitters are registered.
45
45
 
46
46
  If an `sdk_path` argument is provided, you MUST thoroughly study that SDK before proceeding to Step 1. The existing SDK's actual code — not generic conventions — drives every design decision.
47
47
 
@@ -227,17 +227,22 @@ export const {language}Emitter: Emitter = {
227
227
 
228
228
  ## Step 7: Register Emitter
229
229
 
230
- Add the emitter to `oagen.config.ts` and re-export from `src/index.ts`:
230
+ Add the emitter to the plugin bundle export (e.g., `src/plugin.ts`) and re-export from `src/index.ts`:
231
231
 
232
232
  ```typescript
233
- // oagen.config.ts
234
- import { {language}Emitter } from './src/{language}/index.js';
235
- const config: OagenConfig = { emitters: [/* existing, */ {language}Emitter] };
233
+ // src/plugin.ts
234
+ import { {language}Emitter } from './{language}/index.js';
235
+ export const workosEmittersPlugin = {
236
+ emitters: [/* existing, */ {language}Emitter],
237
+ // ...
238
+ };
236
239
 
237
240
  // src/index.ts
238
241
  export { {language}Emitter } from './{language}/index.js';
239
242
  ```
240
243
 
244
+ The consumer project's `oagen.config.ts` imports the plugin bundle, so the new emitter is automatically available.
245
+
241
246
  ## Step 8: Create Tests
242
247
 
243
248
  Create test files under `test/{language}/` **in the emitter project**:
@@ -386,7 +391,7 @@ This skill produces, in the emitter project:
386
391
  - `src/{language}/*.ts` — emitter generator files implementing the `Emitter` interface
387
392
  - `test/{language}/*.ts` — unit tests for each generator
388
393
  - `docs/sdk-architecture/{language}.md` — SDK design document
389
- - Updated `oagen.config.ts` with the new emitter registered
394
+ - Updated plugin bundle (`src/plugin.ts`) and `src/index.ts` with the new emitter registered
390
395
 
391
396
  ## Backwards Compatibility
392
397