Skip to content

Feat/diff command - #2940

Draft
vadyvas wants to merge 19 commits into
mainfrom
feat/diff-command
Draft

Feat/diff command#2940
vadyvas wants to merge 19 commits into
mainfrom
feat/diff-command

Conversation

@vadyvas

@vadyvas vadyvas commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What/Why/How?

Adds a new experimental redocly diff <base> <revision> command that compares two API descriptions and reports what was added, removed, and changed.

  • Structural diff for every supported spec (OpenAPI 2/3.0/3.1/3.2, AsyncAPI, Arazzo, Overlay, OpenRPC) by reusing the existing openapi-core type trees — no per-type hand-written traversal.
  • Breaking-change classification (breaking / warning / non-breaking) for OpenAPI 3.x via a polarity-aware, lint-style rule registry (worst verdict wins).
  • Pipeline: each side is bundled and collected (via walkDocument) into a flat stable-pointer map → two-pass compare into a change list → classify. List items with a natural identity (e.g. parameters by in+name) are matched by identity, so reordering is not reported as a change. Shared components are diffed once; whether a component change is breaking is derived from where it's used (request/response), via a usage index.
  • Output formats: stylish (default), json (versioned schema), markdown (PR comments), html (self-contained report).
  • CI gate: --fail-on breaking|warning|none (default breaking) sets the exit code.

Usage:

redocly diff v1/openapi.yaml v2/openapi.yaml

redocly diff https://example.com/openapi.yaml ./openapi.yaml --format=json

redocly diff main-openapi.yaml pr-openapi.yaml --fail-on=warning

redocly diff v1.yaml v2.yaml --format=html -o diff-report.html

Reference

Testing

Screenshots (optional)

image

Check yourself

  • This PR follows the contributing guide
  • All new/updated code is covered by tests
  • Core code changed? - Tested with other Redocly products (internal contributions only)
  • New package installed? - Tested in different environments (browser/node)
  • Documentation update has been considered

Security

  • The security impact of the change has been considered
  • Code follows company security practices and guidelines

@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7d66075

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@redocly/cli Minor
@redocly/openapi-core Minor
@redocly/respect-core Minor
@redocly/client-generator Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

};

function escapeCell(value: string): string {
return value.replace(/\|/g, '\\|').replace(/\n/g, ' ');
@vadyvas

vadyvas commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@RomanHotsiy
short overview of how the diff command works and what it looks like. interested in your feedback on the design

flowchart LR
    A["base + revision"] --> C["collect ×2<br/>(walkDocument → flat maps)"] --> D["compare<br/>(set diff)"] --> E["classify<br/>(polarity + rules)"] --> F["report<br/>(stylish/json/md/html)"]
    C -. "$ref edges" .-> U[UsageIndex] -. polarity .-> E
Loading

Inputs for now are files or urls - resolved through the existing BaseResolver; comparing against git revisions isn't supported

  1. Collect — each side (base and revision) is bundled and walked with the existing walkDocument + type trees into a flat Map<stablePointer, NodeEntry>. Pointers are stable: list items are keyed by identity (e.g. params by in+name), not array index, so reordering isn't a change; $ref are kept as attributes, so a shared schema is diffed once at its component path.
  2. Compare — dumb two-pass set-diff over the union of keys: only in base → removed, only in revision → added, in both → shallow property diff. A removed/added subtree collapses into one change.
  3. Classify — verdicts are binary: breaking / non-breaking. The key concept is polarity: the same change flips meaning by direction (a property becoming required breaks requests but is safe in responses). Polarity is derived from the pointer; for shared components it comes from the UsageIndex, where the component is referenced (request/response), transitively.
    Rules are tiny lint-style {id, description, visit()} objects in a registry (granular, since the rule set will likely grow)
  4. Reportstylish, json, markdown, html.
redocly diff base.yaml revision.yaml 
redocly diff base.yaml revision.yaml --fail-on=breaking --format=json
GET /pets
  ✖ breaking      changed  parameters/{query:limit} · required
      Parameter became required. (parameter-became-required)

3 breaking, 1 non-breaking.

@vadyvas vadyvas self-assigned this Aug 7, 2026
vadyvas and others added 19 commits August 7, 2026 18:30
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compare two API descriptions and report added, removed, and changed
parts. Structural diff works for every supported spec type via the
existing openapi-core type trees; breaking-change classification
(breaking / warning / non-breaking) applies to OpenAPI 3.x.

The diff engine lives entirely in the CLI package and consumes only the
public @redocly/openapi-core API (walkDocument, type trees, bundle) —
packages/core is untouched. Pipeline: collect each side into a flat
stable-pointer map, two-pass compare into a change list, then classify
with a polarity-aware lint-style rule registry (worst verdict wins).

Supports stylish, json, markdown, and html output and a --fail-on CI
gate. Marked [experimental]; 14 starter rules documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… verdicts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ons, and path-param matching

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e case

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adapt to the new collectSpecData signature (it now takes the document,
not its parsed value) and regenerate the e2e snapshots, which still held
the original flat output: they predate the two-level compat model, the
per-operation grouping, the location and verdict lines, and the
fail-on summary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lint formatters in core already cover the formats CI tools expect, so
map breaking changes onto lint problems and hand them to formatProblems
instead of writing six more serializers. This adds codeframe, checkstyle,
codeclimate, summary, github-actions, and junit to the diff command; with
github-actions, every breaking change becomes an inline pull request
annotation.

A lint problem always carries a severity, so these formats describe
breaking changes only — the full change list stays in the json format.
They print to stdout, and --output now reports that clearly instead of
writing nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One test per rule, each with its own minimal base/revision pair, asserting
the rule id the command should attribute the change to. Thirteen of them
fail today and describe the intended contract: request body required and
removed, string and numeric constraint tightening, additionalProperties,
oneOf narrowing, format, the three security cases, response headers, and
parameter serialization.

Two of the failures are false positives rather than gaps: widening a
request type is reported as breaking, and 3.0 `nullable: true` compared
against 3.1 `type: [.., 'null']` reports a change although both describe
the same schema.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Polarity was inferred from the pointer text, which mistook a schema property
named `responses` for the context of the same name and had to give up entirely
under `callbacks` and `webhooks`. It now walks the ancestors the walker recorded
and reads their node types, so the direction below a callback or a webhook is
flipped rather than skipped, and a property can no longer pose as a context.
That also uncovered a real defect: a usage edge named the `$ref` path, which is
not a node and so could never be looked up; it now names the node holding the
reference.

The new rules cover request bodies becoming required or disappearing, numeric
and string constraints, `format`, `additionalProperties`, `oneOf`/`allOf`
membership, response headers, parameter serialization, and security schemes and
requirements. They share one vocabulary: a constraint moves `tighter` or
`looser`, and the engine's polarity decides which of the two breaks.

Two false positives are gone with them. A type is now compared as the set of
values it accepts, so widening a request type is no longer breaking, and 3.0's
`nullable: true` folds into 3.1's `type: [..., 'null']` so the two spellings
compare as equal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The per-rule fixtures now sit beside breaking-changes and follow the same
shape — base.yaml, revision.yaml and a stylish snapshot — so each rule's report
is reviewable as the output a reader actually sees. Every test still names the
rule id it exercises, so a regenerated snapshot cannot quietly stop covering it.

Reviewing the snapshots turned up a stray label: a change on the document root
rendered with an empty name before the property.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vadyvas
vadyvas force-pushed the feat/diff-command branch from fe59519 to 7d66075 Compare August 7, 2026 16:47
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Performance Benchmark (Lower is Faster)

CLI Version Bundle Lint Check Config
cli-latest ▓ 1.00x ± 0 ▓ 1.01x ± 0 ▓ 1.00x (Fastest)
cli-next ▓ 1.00x (Fastest) ▓ 1.00x (Fastest) ▓ 1.00x ± 0.01

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 77.57% (🎯 77%) 12093 / 15589
🔵 Statements 77.65% (🎯 77%) 12981 / 16717
🔵 Functions 81.83% (🎯 81%) 2464 / 3011
🔵 Branches 71.15% (🎯 70%) 8948 / 12576
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/commands/diff/fail-on.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/index.ts 0% 0% 0% 0% 46-107
packages/cli/src/commands/diff/engine/align-paths.ts 100% 95.83% 100% 100%
packages/cli/src/commands/diff/engine/collect.ts 97.56% 91.17% 100% 100% 39
packages/cli/src/commands/diff/engine/compare.ts 100% 97.36% 100% 100%
packages/cli/src/commands/diff/engine/index.ts 95.45% 66.66% 80% 95.23% 73
packages/cli/src/commands/diff/engine/locate.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/engine/node-identity.ts 100% 83.33% 100% 100%
packages/cli/src/commands/diff/engine/predicates.ts 89.28% 81.69% 100% 89.74% 57, 63, 103, 115-119
packages/cli/src/commands/diff/engine/types.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/engine/classify/chain.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/engine/classify/index.ts 100% 90% 100% 100%
packages/cli/src/commands/diff/engine/classify/oas3.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/engine/classify/oas3_1.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/engine/classify/polarity.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/engine/classify/usage.ts 100% 90.9% 100% 100%
packages/cli/src/commands/diff/engine/classify/rules/operation-rules.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/engine/classify/rules/parameter-rules.ts 87.5% 87.5% 100% 89.47% 49-52
packages/cli/src/commands/diff/engine/classify/rules/ref-rules.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/engine/classify/rules/request-body-rules.ts 20% 0% 0% 25% 8-12, 20-21
packages/cli/src/commands/diff/engine/classify/rules/response-rules.ts 66.66% 30% 66.66% 77.77% 16, 27-32
packages/cli/src/commands/diff/engine/classify/rules/schema-rules.ts 68.18% 57.31% 91.66% 66.66% 22-24, 61, 74, 87, 100, 129-138, 149-158, 167-174, 183-192, 207-228
packages/cli/src/commands/diff/engine/classify/rules/security-rules.ts 25% 0% 0% 33.33% 13-14, 31-41, 49-50
packages/cli/src/commands/diff/serializers/change-side.ts 100% 66.66% 100% 100%
packages/cli/src/commands/diff/serializers/html.ts 100% 50% 100% 100%
packages/cli/src/commands/diff/serializers/json.ts 100% 100% 100% 100%
packages/cli/src/commands/diff/serializers/markdown.ts 100% 50% 100% 100%
packages/cli/src/commands/diff/serializers/problems.ts 100% 76.92% 100% 100%
packages/cli/src/commands/diff/serializers/stylish.ts 95.55% 78.94% 100% 100% 56, 62
packages/core/src/format/format.ts 59.37% 47.54% 67.56% 58.57% 25, 69-70, 115, 119-145, 163, 167, 175-189, 226-227, 236-342, 347, 360-366, 384, 421-427, 480-492, 506-507, 536
Generated in workflow #11245 for commit 7d66075 by the Vitest Coverage Report Action

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.

3 participants