Skip to content

fix(differ): report a list envelope field added, removed or retyped - #459

Open
gjtorikian wants to merge 1 commit into
issue/428-inline-union-variant-addedfrom
issue/439-envelope-field-added-removed
Open

gjtorikian wants to merge 1 commit into
issue/428-inline-union-variant-addedfrom
issue/439-envelope-field-added-removed

Conversation

@gjtorikian

Copy link
Copy Markdown
Collaborator

Closes #439

What

oagen diff compared a list envelope's envelopeFields only for their extensions. In the issue's documents, total_count added to { data, has_more } was no change, and so were its removal and its retype to string (exit 0 each time). The envelope's fields are now compared as a model's fields are. The added field is an additive field-added (exit 1). The removed or retyped one is a breaking field-removed / field-type-changed (exit 2). Each sits under the operation-modified change's envelopeFieldChanges.

How

diffOperations (src/differ/operations.ts) read the envelope through diffEnvelopeFieldExtensions. That function skipped a field only one side declared and compared nothing but extensions. It is replaced by a call to diffFields (src/differ/models.ts, now exported and typed to read anything that carries fields) on the two envelopeFields lists, with direction response, since the SDK only ever reads the envelope. An absent list reads as no field, and the comparison still runs only while both specs paginate. So the entries and classifications are exactly a response-only model's:

  • added: additive, a required one included (details: 'required on a response-only model')
  • removed or retyped: breaking (a format-only change, or an inline union that only gained variants, stays additive)
  • requiredness, access flags, deprecated and extensions: as on any model's field

hasBreaking now also checks envelopeFieldChanges for a breaking entry.

Since diffFields also reports a deprecated flip, collectBehaviorChanges (src/differ/diff.ts) mirrors an envelope field-deprecated-changed entry as a deprecated-changed entry with a new envelope-field node (DeprecatedNode, src/differ/types.ts), the node the extension mirror already uses.

Docs: docs/cli.md gains a List envelopes paragraph. Its Deprecation and Extensions paragraphs and its operation-modified table row, the Deprecated section of docs/architecture/ir-types.md, and the envelopeFieldChanges JSDoc now state the same rule. The sentence "No other property of an envelope field is compared" is gone.

Tests

  • test/differ/pagination-changes.test.ts, diffOperations on an IR listPets:
    • a field added (additive) and removed (breaking)
    • a required field added (additive, required on a response-only model)
    • a field retyped (breaking), and a format-only change (additive)
    • requiredness both ways, readOnly and deprecated, as on a model's field
    • an absent list read as no field; the key left off when no envelope field changed
    • nothing compared while only one side paginates
  • Same file, end to end: the issue's three documents through parseSpec + diffSpecs. Added gives additive (modified: 1, breaking: 0, so exit 1). Removed and retyped give breaking (breaking: 1, so exit 2). Identical documents give no change.
  • test/differ/deprecated.test.ts: a has_more deprecation flip is a field-deprecated-changed entry in envelopeFieldChanges and a deprecated-changed behavior change on an envelope-field node, both ways.
  • Seven of the eight new tests fail on the base because nothing is reported. The "only while both specs paginate" guard passes on both.
  • No existing expectation changed.

Stack

Based on issue/428-inline-union-variant-added (#458). Merge that first; GitHub retargets this PR when the base branch is deleted.

Decision / Assumptions

  • A model's rule, read as response-only. The issue asks for "the same edit to a model's field", so this reuses diffFields rather than a narrower added/removed/retyped check. The envelope is only ever read, so a required field added is additive, as on any response-only model, and its details reuse that wording. field-required-changed stays direction-blind, as diffFields keeps it for every model (optional → required breaking, required → optional additive). I didn't give envelope fields a reader-side requiredness rule of their own like response headers have.
  • Deprecation mirrored. Reusing diffFields brings field-deprecated-changed entries, and the docs say every flip is mirrored into behaviorChanges. So DeprecatedNode gains envelope-field, following the ExtensionsNode precedent. The alternatives were dropping deprecation entries for envelope fields (unlike a model's field) or reporting them unmirrored (against the documented invariant).
  • An absent envelopeFields reads as none. A list response that moves between an unwrapped envelope and a kept wrapper model ($ref) also lists its companions as removed or added. That operation is already breaking through responseChanged / paginationDataPathChanged, so no classification moves.
  • No !. No change that was reported before is reclassified: the issue's pairs went from no change (exit 0) to additive or breaking, and the IR is untouched.
  • Overlaps, as on a model's field. A companion model renamed is a field-type-changed here beside model-removed / model-added. A removed or retyped stop field is an entry here beside paginationEndOfIterationChanged.

@greptile-apps

greptile-apps Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge.

Summary

The PR compares list envelope fields through diffFields and reports changes under envelopeFieldChanges.

  • Paginated operation diffs now report changes to envelope fields.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Both operations paginate] --> B[Compare envelope fields with diffFields]
  B --> C[Attach envelopeFieldChanges]
  C --> D[Classify operation and count summary]
  C --> E[Mirror deprecation and extension changes]
  D --> F[CLI exit code]
Loading

Reviews (1) · Last reviewed commit: "fix(differ): report a list envelope fiel..." · Reviewed by Greptile

@MichaelDeBoey MichaelDeBoey left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks, this fixes #439!

The issue's script now prints the field-added entry for total_count under envelopeFieldChanges. oagen diff exits 1 for the addition and 2 for the removal and for the retype to string, and identical documents still exit 0. I checked OAS 3.1 and 3.0 (identical results), with the envelope inline, behind a components/responses $ref and built with allOf. All of these follow a response-only model's field:

  • a required field added: additive, required on a response-only model
  • requiredness flipped either way
  • a format-only change: additive
  • a field made nullable: breaking
  • an object const retyped from list to page: breaking
  • an inline union that gains a variant: additive
  • readOnly gained: breaking
  • deprecated and x-* on has_more: additive, and mirrored as an envelope-field node in behaviorChanges

A page_info companion object added, removed or turned into a string is reported here beside model-added/model-removed. So is a $ref companion pointed at another component. A field added inside page_info, inside a nested page_info.cursors, or inside a $ref'd PageInfo component is still reported only under model-modified. An envelope kept as a $ref component model still has no envelopeFields, and its fields are reported under model-modified as the new List envelopes paragraph says. The docs/cli.md, ir-types.md and JSDoc changes match what the head does.

@gjtorikian
gjtorikian force-pushed the issue/428-inline-union-variant-added branch from 96c21c3 to a96b666 Compare October 28, 2026 17:28
`PaginationMeta.envelopeFields` is every property of an unwrapped list
envelope besides the data array, what an emitter types the envelope
from. `diffOperations` compared those fields only for their
`extensions` (`diffEnvelopeFieldExtensions`), skipping a field one side
lacked, so `total_count` added to `{ data, has_more }`, removed from it
or retyped to `string` was no change and `oagen diff` exited 0.

The envelope's fields are now compared as a model's fields are, by
`diffFields` (src/differ/models.ts, now exported and reading anything
that carries `fields`), judged as a response-only model since the SDK
only ever reads the envelope: a field added is additive, required or
not; one removed or retyped is breaking; format, requiredness, access
flags, `deprecated` and extensions are classified as on any model's
field, each an entry in `envelopeFieldChanges`. The operation is
breaking when an entry is. A deprecation flip on an envelope field is
mirrored into `behaviorChanges` as an `envelope-field` node, as its
extension change already was. `docs/cli.md` gains a **List envelopes**
paragraph; the Deprecation and Extensions paragraphs, the Deprecated
section of `docs/architecture/ir-types.md` and the
`envelopeFieldChanges` JSDoc say the same.

Closes #439
@gjtorikian
gjtorikian force-pushed the issue/439-envelope-field-added-removed branch from ffb3326 to 918d9b9 Compare October 28, 2026 17:29
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.

2 participants