Skip to content

feat(compat): detect type and enum renames as soft-risk, not breaking#49

Merged
gjtorikian merged 1 commit intomainfrom
feat/differ-detect-renames
May 2, 2026
Merged

feat(compat): detect type and enum renames as soft-risk, not breaking#49
gjtorikian merged 1 commit intomainfrom
feat/differ-detect-renames

Conversation

@gjtorikian
Copy link
Copy Markdown
Collaborator

Why

The compat differ flags every spec change that vanishes a type/enum symbol as breaking — even when the value-level public-API surface is preserved (e.g. an upstream spec promotes ApiKeyOrganizationApiKey + UserApiKey; the wire shape returned by individual endpoints is identical, but ApiKey no longer exists as a symbol). Result: CI gates default to red on spec changes that would not actually break consumer code, and downstream SDK PRs require manual approval or workarounds.

workos/openapi-spec#17 is the motivating example — 33 reported breaks, none of which alter wire format or runtime behavior. They split into two categories:

Category PR #17 example Items
Type rename (old type's field set is preserved by a newly-named type) ApiKeyWithValueOrganizationApiKeyWithValue ~25
Enum canonical-flip (old enum's wire-value set is preserved by a newly-named enum, dotnet-only because other languages emit type aliases) ApplicationsOrderApiKeysOrder, VaultByokKey…CompletedDataKeyProvider…DeletedDataKeyProvider ~6

In both cases consumer field accesses, method calls, and runtime serialization still work. Only explicit type annotations on the old name need to migrate. That's exactly the existing soft-risk severity — "may affect callers depending on usage" — not breaking.

What

Three new post-passes in diffSnapshots, all pure downgrades (never upgrade severity):

  • detectTypeRenames — for every symbol_removed whose baseline owner held ≥ 1 field, look for a newly-added candidate type whose field set is a non-strict superset of the removed type's fields. On match, downgrade to soft-risk and record the rename. Pairing is alphabetical-first when several candidates fit, so the result is deterministic.

  • detectEnumRenames — same shape but for enums, with strict equality on wire-value sets (narrowing or shifting an enum is a real wire break and must stay flagged). Catches the dotnet-only case where dedup canonical-flips disappear the old enum.

  • cascadeRenameDowngrades — runs after both detectors. Walks every remaining change and:

    • downgrades symbol_removed whose owner is in either rename map (the field/method still exists on the new owner under a different fqName)
    • downgrades return_type_changed/field_type_changed whose old → new pair matches a recorded rename (the type swap is the rename itself, not a meaningful signature break)

Each downgraded change gets a remediation string pointing at the parent rename so the report stays useful for review.

Why soft-risk, not additive

Explicit type annotations on the old name still need to migrate — var k *workos.ApiKeyWithValue = ... does fail to compile when the type symbol is gone. That's not "additive." Soft-risk is the right level: visible in the report, configurable as a fail threshold for strict consumers (failOn: 'soft-risk'), but not a breaking-grade gate by default.

Concrete impact on PR #17

After this lands and a fresh diff is run:

  • Breaking (33)Breaking (0)
  • Soft-risk gains the same rows with remediation hints attached
  • CI default-passes; reviewers still see the changes

Tests

11 new unit tests in test/compat/rename-detection.test.ts (mirroring forked-schemas.test.ts conventions):

  • happy path: type rename downgrade fires for ApiKeyWithValue→OrganizationApiKeyWithValue
  • cascade: owned-field removals downgrade
  • cascade: return_type_changed downgrades when old→new pair matches
  • cascade: works for Iterator<Foo> style generic return wrappers
  • negative: doesn't fire when candidate drops a baseline field (real reshape)
  • negative: doesn't fire when candidate type already existed in baseline (swap, not rename)
  • determinism: alphabetical-first pairing when multiple candidates match
  • enum happy path: VaultByokKey…KeyProvider rename downgrade
  • enum cascade: enum_member removals downgrade with Owned by renamed symbol hint
  • enum negative: doesn't fire when value sets differ
  • enum negative: doesn't fire on strict-superset (real value-set addition)

Test plan

  • npm test — 1377/1377 (was 1366, +11 new)
  • npm run lint clean
  • npx tsc --noEmit clean
  • npm run build succeeds

Followups (out of scope here)

  • Once this lands and ships, close oagen-emitters#74 (stable canonical selection) — the differ-side fix obsoletes the emitter-side workaround for the dedup case.
  • The corresponding workos/workos PR (#58186) for spec-side consolidation also becomes optional. The two enumName/registerZodComponent changes there for PaginationOrder / VaultByokKeyProvider are still nice-to-have for spec aesthetics, but no longer needed to keep CI green.

🤖 Generated with Claude Code

When a baseline symbol disappears and a structurally-equivalent symbol
takes its place in the candidate, the public-API surface is preserved at
the value level — only typed references and (in some languages) enum
class names actually need to migrate. The current differ flags these as
`symbol_removed` (breaking) anyway, which produces false-positive CI
gates for spec changes that are functionally additive.

Two new post-passes in `diffSnapshots`, both pure downgrades (never
upgrade severity, so they cannot make existing reports worse):

`detectTypeRenames` — for every `symbol_removed` whose baseline owner
held ≥ 1 field, look for a newly-added candidate type whose field set is
a non-strict superset of the removed type's fields. On match, downgrade
to soft-risk and record the rename. Pairing is alphabetical-first when
several candidates fit, so the result is deterministic.

`detectEnumRenames` — same shape but for enums, with strict equality on
wire-value sets (narrowing or shifting an enum is a real wire break and
must stay flagged). Catches the dotnet-only "old canonical enum
disappeared because dedup heuristic flipped to a new shorter name"
case from workos/openapi-spec#17.

`cascadeRenameDowngrades` — runs after both detectors. Walks every
remaining `symbol_removed` whose owner is in either rename map and
downgrades it (the field/method still exists on the new owner under a
different fqName). Also downgrades `return_type_changed` and
`field_type_changed` whose old → new pair matches a recorded rename
(the type swap is the rename itself, not a meaningful signature break).

Soft-risk, not additive, because explicit type annotations on the old
name still need to migrate — `var k *workos.ApiKeyWithValue = ...` does
fail to compile when the type symbol is gone. Soft-risk is the existing
"may affect callers depending on usage" category and is the right level.

Concrete impact on workos/openapi-spec#17:
- ApiKey/ApiKeyWithValue/APIKeyWithValueOwner family (~25 of 33 items):
  baseline types had clean superset matches in OrganizationApi*; all
  downgrade to soft-risk with cascade across method return types and
  owned fields.
- ApplicationsOrder, VaultByokKeyVerificationCompletedDataKeyProvider
  (~6 of 33): identical wire-value enums in candidate; downgrade to
  soft-risk with cascade across enum members.
- Net: Breaking 33 → Breaking 0; Soft-risk gains the same rows with
  remediation hints attached.

Adds 11 unit tests in test/compat/rename-detection.test.ts mirroring the
forked-schemas.test.ts conventions: covers happy-path downgrade for
both types and enums, cascade to children/return types/iterator
generics, deterministic alphabetical pairing, plus negative cases (real
field reshape, candidate already in baseline, enum value-set mismatch,
strict-superset enum addition).

Existing tests untouched and still pass (1366 → 1377 with 11 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant