Skip to content

RFC: TypeSpec Breaking Change Detection Tool - Design Overview#4580

Draft
markcowl wants to merge 1 commit into
Azure:mainfrom
markcowl:rfc/breaking-changes-overview
Draft

RFC: TypeSpec Breaking Change Detection Tool - Design Overview#4580
markcowl wants to merge 1 commit into
Azure:mainfrom
markcowl:rfc/breaking-changes-overview

Conversation

@markcowl

@markcowl markcowl commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Design overview RFC for @azure-tools/typespec-breaking-change, a TypeSpec-native API diff and breaking change detection tool for Azure ARM APIs.

This document covers:

  • Architecture (two-layer: diff engine + policy engine)
  • Comparison phases (Phase A: same-version regression, Phase B: cross-version breaking changes)
  • Rule summary tables (request/response/model breaking changes)
  • Suppression mechanism (two decorators: @approvedBreakingChange, @approvedUnversionedChange)
  • CI integration and output format

Supporting Documents

Additional design detail documents (taxonomy, classification, rules, OAD correlation, validation strategy) are available on the
fc/breaking-changes\ branch
for reference.

Design overview for @azure-tools/typespec-breaking-change, a TypeSpec-native
API diff and breaking change detection tool for Azure ARM APIs.

Covers architecture, comparison phases, rule summary tables, suppression
mechanism, CI integration, and output format.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-sdk

Copy link
Copy Markdown
Collaborator

No changes needing a change description found.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

⚡ Benchmark Results

⚠️ 20 metric(s) regressed above the +5% threshold:

Metric Baseline Current Change
total 🔴 563.6ms 🔴 751.8ms +33.4% 🔴
loader 🟢 158.7ms 🟡 213.4ms +34.5% 🔴
resolver 🟢 17.3ms 🟢 18.6ms +7.4% 🔴
checker 🟢 180.6ms 🟡 206.4ms +14.2% 🔴
linter 🟢 126.6ms 🟡 238.6ms +88.4% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/byos 🟢 5.6ms 🟡 10.7ms +91.7% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/no-header-explode 🟡 18.2ms 🔴 31.5ms +72.8% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/no-query-explode 🟡 18.3ms 🔴 32.5ms +77.7% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/no-response-body 🔴 21.9ms 🔴 44.7ms +104.0% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/no-route-parameter-name-mismatch 🟢 4.6ms 🟡 10.3ms +123.8% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/response-schema-problem 🔴 21.7ms 🔴 47.2ms +117.5% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/rpc-operation-request-body 🟢 0.3ms 🟢 1.8ms +511.5% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/use-standard-names 🟢 5.0ms 🟡 12.1ms +142.9% 🔴
 ↳ linter/@azure-tools/typespec-azure-resource-manager/lro-location-header 🟡 11.8ms 🟡 14.5ms +22.5% 🔴
 ↳ linter/@azure-tools/typespec-azure-resource-manager/no-response-body 🟡 19.0ms 🔴 23.0ms +21.5% 🔴
emit 🔴 5.65s 🔴 6.02s +6.5% 🔴
 ↳ emit/@azure-tools/typespec-python 🔴 4.08s 🔴 4.50s +10.4% 🔴
 ↳ emit/@typespec/http-client-js 🔴 1.12s 🔴 1.19s +6.5% 🔴
 ↳ emit/@typespec/openapi3 🟢 145.9ms 🟢 154.2ms +5.7% 🔴
 ↳ emit/@typespec/openapi3/compute 🟢 127.2ms 🟢 136.3ms +7.2% 🔴
Full details – comparing 2edbfd1 vs baseline 36fc2ca
Metric Baseline Current Change
total 🔴 563.6ms 🔴 751.8ms +33.4% 🔴
loader 🟢 158.7ms 🟡 213.4ms +34.5% 🔴
resolver 🟢 17.3ms 🟢 18.6ms +7.4% 🔴
checker 🟢 180.6ms 🟡 206.4ms +14.2% 🔴
validation 🟢 41.4ms 🟢 42.1ms +1.7%
 ↳ validation/@azure-tools/typespec-azure-core 🟢 6.2ms 🟢 6.8ms +10.3%
 ↳ validation/@typespec/http 🟢 5.2ms 🟢 5.7ms +8.8%
 ↳ validation/@typespec/rest 🟢 0.5ms 🟢 0.5ms +10.8%
 ↳ validation/@typespec/versioning 🔴 28.1ms 🔴 27.1ms -3.4%
 ↳ validation/compiler 🟢 1.4ms 🟢 1.4ms +1.4%
linter 🟢 126.6ms 🟡 238.6ms +88.4% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/auth-required 🟢 0.0ms 🟢 0.0ms +8.9%
 ↳ linter/@azure-tools/typespec-azure-core/bad-record-type 🟢 0.2ms 🟢 0.4ms +102.1%
 ↳ linter/@azure-tools/typespec-azure-core/byos 🟢 5.6ms 🟡 10.7ms +91.7% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/casing-style 🟢 0.6ms 🟢 0.8ms +34.3%
 ↳ linter/@azure-tools/typespec-azure-core/composition-over-inheritance 🟢 0.1ms 🟢 0.1ms -0.3%
 ↳ linter/@azure-tools/typespec-azure-core/documentation-required 🟢 0.8ms 🟢 1.3ms +60.9%
 ↳ linter/@azure-tools/typespec-azure-core/friendly-name 🟢 0.6ms 🟢 1.0ms +79.7%
 ↳ linter/@azure-tools/typespec-azure-core/key-visibility-required 🟢 0.2ms 🟢 0.4ms +178.1%
 ↳ linter/@azure-tools/typespec-azure-core/known-encoding 🟢 0.3ms 🟢 0.3ms +1.3%
 ↳ linter/@azure-tools/typespec-azure-core/long-running-polling-operation-required 🟢 0.3ms 🟢 0.3ms +22.8%
 ↳ linter/@azure-tools/typespec-azure-core/no-case-mismatch 🟢 0.2ms 🟢 0.3ms +11.4%
 ↳ linter/@azure-tools/typespec-azure-core/no-closed-literal-union 🟢 0.2ms 🟢 0.3ms +6.8%
 ↳ linter/@azure-tools/typespec-azure-core/no-enum 🟢 0.0ms 🟢 0.0ms +8.7%
 ↳ linter/@azure-tools/typespec-azure-core/no-error-status-codes 🟢 0.1ms 🟢 0.1ms +15.4%
 ↳ linter/@azure-tools/typespec-azure-core/no-explicit-routes-resource-ops 🟢 0.1ms 🟢 0.1ms +10.9%
 ↳ linter/@azure-tools/typespec-azure-core/no-format 🟢 0.5ms 🟢 0.8ms +55.8%
 ↳ linter/@azure-tools/typespec-azure-core/no-generic-numeric 🟢 0.4ms 🟢 0.5ms +29.1%
 ↳ linter/@azure-tools/typespec-azure-core/no-header-explode 🟡 18.2ms 🔴 31.5ms +72.8% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/no-legacy-usage 🟢 1.1ms 🟢 1.3ms +19.2%
 ↳ linter/@azure-tools/typespec-azure-core/no-multiple-discriminator 🟢 0.1ms 🟢 0.1ms +27.0%
 ↳ linter/@azure-tools/typespec-azure-core/no-nullable 🟢 0.2ms 🟢 0.4ms +80.6%
 ↳ linter/@azure-tools/typespec-azure-core/no-offsetdatetime 🟢 1.1ms 🟢 1.6ms +43.4%
 ↳ linter/@azure-tools/typespec-azure-core/no-openapi 🟢 2.0ms 🟢 2.6ms +34.6%
 ↳ linter/@azure-tools/typespec-azure-core/no-private-usage 🟢 1.8ms 🟢 2.3ms +27.9%
 ↳ linter/@azure-tools/typespec-azure-core/no-query-explode 🟡 18.3ms 🔴 32.5ms +77.7% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/no-response-body 🔴 21.9ms 🔴 44.7ms +104.0% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/no-rest-library-interfaces 🟢 0.0ms 🟢 0.0ms -1.1%
 ↳ linter/@azure-tools/typespec-azure-core/no-route-parameter-name-mismatch 🟢 4.6ms 🟡 10.3ms +123.8% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/no-rpc-path-params 🟢 0.2ms 🟢 0.2ms +31.4%
 ↳ linter/@azure-tools/typespec-azure-core/no-string-discriminator 🟢 0.0ms 🟢 0.0ms +10.2%
 ↳ linter/@azure-tools/typespec-azure-core/no-unknown 🟢 0.2ms 🟢 0.3ms +82.6%
 ↳ linter/@azure-tools/typespec-azure-core/no-unnamed-union 🟢 0.3ms 🟢 0.4ms +13.7%
 ↳ linter/@azure-tools/typespec-azure-core/operation-missing-api-version 🟢 0.2ms 🟢 0.2ms +2.2%
 ↳ linter/@azure-tools/typespec-azure-core/request-body-problem 🟢 0.3ms 🟢 0.3ms +5.5%
 ↳ linter/@azure-tools/typespec-azure-core/require-versioned 🟢 0.0ms 🟢 0.0ms +2.4%
 ↳ linter/@azure-tools/typespec-azure-core/response-schema-problem 🔴 21.7ms 🔴 47.2ms +117.5% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/rpc-operation-request-body 🟢 0.3ms 🟢 1.8ms +511.5% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/spread-discriminated-model 🟢 0.3ms 🟢 0.3ms +5.6%
 ↳ linter/@azure-tools/typespec-azure-core/use-standard-names 🟢 5.0ms 🟡 12.1ms +142.9% 🔴
 ↳ linter/@azure-tools/typespec-azure-core/use-standard-operations 🟢 0.1ms 🟢 0.1ms +22.2%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-common-types-version 🟢 3.7ms 🟢 4.3ms +15.5%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-custom-resource-no-key 🟢 0.1ms 🟢 0.1ms +18.5%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage 🟢 0.1ms 🟢 0.1ms +2.4%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-delete-operation-response-codes 🟢 4.7ms 🟢 5.4ms +14.8%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-no-path-casing-conflicts 🟢 3.8ms 🟢 4.5ms +19.0%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-no-record 🟢 0.3ms 🟢 0.4ms +7.0%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-post-operation-response-codes 🟢 0.4ms 🟢 0.5ms +23.7%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes 🟢 0.0ms 🟢 0.0ms -6.9%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-action-no-segment 🟢 0.2ms 🟢 0.2ms +16.0%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-duplicate-property 🟢 0.1ms 🟢 0.1ms +23.2%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-interface-requires-decorator 🟢 0.0ms 🟢 0.0ms +5.1%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-action-verb 🟢 0.1ms 🟢 0.1ms -0.7%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-envelope-property 🟢 0.1ms 🟢 0.1ms +9.5%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-version-format 🟢 0.0ms 🟢 0.0ms +14.0%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-key-invalid-chars 🟢 0.2ms 🟢 0.2ms +10.5%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-name-pattern 🟢 0.0ms 🟢 0.0ms -14.7%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-operation 🟢 0.1ms 🟢 0.2ms +8.1%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-operation-response 🟢 4.4ms 🟢 4.7ms +7.0%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-patch 🟢 0.3ms 🟢 0.3ms +17.6%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-path-segment-invalid-chars 🟢 0.2ms 🟢 0.2ms +7.2%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/arm-resource-provisioning-state 🟢 0.1ms 🟢 0.1ms +4.4%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/beyond-nesting-levels 🟢 0.1ms 🟢 0.1ms +15.4%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/empty-updateable-properties 🟢 0.1ms 🟢 0.2ms +20.5%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/improper-subscription-list-operation 🟢 0.0ms 🟢 0.0ms +7.3%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/lro-location-header 🟡 11.8ms 🟡 14.5ms +22.5% 🔴
 ↳ linter/@azure-tools/typespec-azure-resource-manager/missing-operations-endpoint 🟢 0.0ms 🟢 0.0ms -0.8%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers 🟢 0.3ms 🟢 0.3ms +9.9%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/no-empty-model 🟢 0.1ms 🟢 0.1ms +11.0%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/no-override-props 🟢 0.1ms 🟢 0.1ms +8.4%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/no-resource-delete-operation 🟢 0.2ms 🟢 0.2ms +23.8%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/no-response-body 🟡 19.0ms 🔴 23.0ms +21.5% 🔴
 ↳ linter/@azure-tools/typespec-azure-resource-manager/patch-envelope 🟢 0.1ms 🟢 0.1ms +3.2%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/resource-name 🟢 0.1ms 🟢 0.2ms +20.2%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/secret-prop 🟢 2.0ms 🟢 2.3ms +16.2%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/unsupported-type 🟢 0.3ms 🟢 0.4ms +17.9%
 ↳ linter/@azure-tools/typespec-azure-resource-manager/version-progression 🟢 0.0ms 🟢 0.0ms -3.9%
 ↳ linter/@azure-tools/typespec-client-generator-core/property-name-conflict 🟢 1.0ms 🟢 1.4ms +41.6%
 ↳ linter/@azure-tools/typespec-client-generator-core/require-client-suffix 🟢 0.2ms 🟢 0.2ms -4.3%
emit 🔴 5.65s 🔴 6.02s +6.5% 🔴
 ↳ emit/@azure-tools/typespec-autorest 🟢 191.5ms 🟢 175.6ms -8.3% 🟢
 ↳ emit/@azure-tools/typespec-python 🔴 4.08s 🔴 4.50s +10.4% 🔴
 ↳ emit/@typespec/http-client-js 🔴 1.12s 🔴 1.19s +6.5% 🔴
 ↳ emit/@typespec/openapi3 🟢 145.9ms 🟢 154.2ms +5.7% 🔴
 ↳ emit/@typespec/openapi3/compute 🟢 127.2ms 🟢 136.3ms +7.2% 🔴
 ↳ emit/@typespec/openapi3/write 🟢 19.3ms 🟢 18.0ms -6.7% 🟢

Averaged across 3 specs (azure-arm-resource-manager, azure-core-dataplane, azure-full).
Threshold: changes > ±5% are highlighted.
🟢 Fast · 🟡 Moderate (stages >200ms, rules >10ms) · 🔴 Slow (stages >400ms, rules >20ms)

@azure-sdk

Copy link
Copy Markdown
Collaborator

You can try these changes here

🛝 Playground 🌐 Website

@mikekistler mikekistler left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks awesome!

I left a few comments on things to think about / discuss, but overall this seems well thought out.

The tool is explicitly not trying to solve every compatibility problem around a service. The following are out of scope:

- SDK-facing concerns such as generated client names or client-shaping behavior.
- `operationId`, because it is not part of the HTTP wire contract and is primarily an SDK/client concern.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schema names probably also fall into this category.


After Phase A, the tool evaluates versions that are new on the head branch.

For every new version on head, the tool compares that new version against the **previous stable** version.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be done for any new or changed version on head. Yes, the changes will be flagged in Phase A, but we need to know if any are actually breaking vs the previous stable to know if we should permit them to be suppressed.

| `RequestParameterLocationChanged` | Moving between header/query/path/body | Error |
| `RequestParameterDefaultChanged` | Changing a parameter default value | Error |
| `RequestPropertyDefaultChanged` | Changing a property default value | Error |
| `RequestContentTypeRemoved` | Removing a request content type | Error |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about changes to readOnly -- are these flagged anywhere?


#### Removed property

When a property is removed, the approval is placed on the model that still survives:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this could still go on the property, since it is retained but just has a @removed attribute on it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this is answered below.

| `ApiVersionRemoved` | Removing a stable api-version | Error |
| `AuthSchemeRemoved` | Removing an authentication scheme | Error |
| `AuthSchemeAdded` (required) | Adding a required authentication scheme | Error |
| `OAuthScopeAdded` | Narrowing OAuth scopes | Error |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about changes to the version of ARM common types? These get flagged quite often by openapi-diff.


The CI semantics are intentionally conservative:

- **new approvals** require reviewer sign-off

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this "sign-off" an approval on the PR? If so, I'm concerned there may be confusion about what approvals mean. An approval by a breaking change reviewer only means "the new/changed suppressions are ok", but is not approving the API as a whole.


Approvals can outlive the changes they were meant to justify.
A property may be restored, a constraint may be relaxed again, or a declaration may be refactored so the old approval no longer matches any finding.
When that happens, the approval becomes stale.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like @approvedVersionedChange will always be stale immediately after merge, right?

└── suppressions / CI output
```

For the complete taxonomy of detectable diffs, see `diff-taxonomy.md`. For classification policies, see `breaking-change-classification.md`. For detailed rule definitions and type transition tables, see `typespec-breaking-change-rules.md`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems I did not see the three markdowns: diff-taxonomy.md, breaking-change-classification.md and typespec-breaking-change-rules.md, do I miss anything?

For versions that already existed before the change, the same rule semantics still apply when the tool detects a structural regression.

Rules are evaluated against canonical HTTP metadata.
That means the tool cares about the observable contract on the wire: operation identity, parameters, payload shapes, status codes, headers, content types, authentication, and encoded value sets.

@haolingdong-msft haolingdong-msft Jun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm my understanding: change a fixed enum(enum) to extensible enum(union) will not be detected as a breaking change, though type changed from enum to union in TypeSpec, right?

- **Request narrowing** is always breaking (Error) — the server accepts fewer inputs.
- **Response widening** is always breaking (Error) — the server returns more outputs.
- **Request widening** is not breaking (Ignore) — the server accepts more inputs.
- **Response narrowing** is not breaking (Ignore) — the server returns fewer outputs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the response narrowing mean remove a few properties from the response model? May I know why do we ignore those? (treated as non-breaking)

@haolingdong-msft

Copy link
Copy Markdown
Member

Thanks @markcowl for the PR! The design looks awesome. I really love the idea of creating canonicalization on TypeSpec versioning.


- Stable versions cannot be removed.
- Versions must be monotonically increasing.
- Preview replacement is allowed.

@mikeharder mikeharder Jun 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add "preview and stable must use distinct dates". specs shouldn't have both "2026-01-01" and "2026-01-01-preview" with the same date.

this might be covered by "must be monotonically increasing".


The design uses two suppression decorators:

- `@approvedBreakingChange` suppresses Phase B breaking changes. New or modified instances add the `BreakingChangeReviewRequired` label.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if that is actually the best, could we maybe not instead rely on the suppression mechanism replacing lint diff? Require a suppression for breaking changes but we highlight those newly added/scan for those codes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants