Skip to content

feat(ipa): add IPA-132 OperationResponse schema rules - #1437

Merged
julius-jogela merged 12 commits into
mainfrom
feat-ipa-132-operation-response-schema
Aug 20, 2026
Merged

julius-jogela merged 12 commits into
mainfrom
feat-ipa-132-operation-response-schema

Conversation

@julius-jogela

@julius-jogela julius-jogela commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Proposed changes

This PR adds the IPA-132 rules validating how long-running operation status is communicated through the OperationResponse schema. The schema checks validate the OperationResponse reference schema by check type, with the field lists declared in the ruleset's functionOptions.

Jira ticket: CLOUDP-429837

Rules added

All at warn severity, anchored on $.components.schemas.OperationResponse (shared OperationResponseSchema alias):

Rule Checks
xgen-IPA-132-operation-endpoints-must-return-operation-response Every 2xx response of a GET on an Operations endpoint must carry a JSON schema; the single Operation endpoint (.../operations/{operationId}) must reference the OperationResponse schema; the Operations collection endpoint must reference a paginated wrapper defining the results array directly (inline and allOf-composed wrappers rejected, consistent with the IPA-110 pagination rules). Responses defined as $refs to components.responses are followed. Exceptions at the response level
xgen-IPA-132-operation-response-must-define-required-fields The schema is flat (no composition) and defines operationId, status, operationType, createdAt, updatedAt, expiresAt, each listed as required
xgen-IPA-132-operation-response-must-define-optional-fields customMethod, error, resultHref, retryAfterSeconds defined and not listed as required — each is present only under a runtime condition that cannot be validated statically
xgen-IPA-132-operation-response-must-use-standard-enum-values status enum exactly PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED; operationType enum exactly CREATE, UPDATE, DELETE, CUSTOM; error.retryStrategy enum exactly IMMEDIATE, BACKOFF, NONE
xgen-IPA-132-operation-response-must-define-nested-object-structure The error object must define code, message and retryable
xgen-IPA-132-operations-should-expose-status-fields The schema should define statusMessage, progress (with completed, total, unit) and estimatedCompletionTime. Kept separate because it enforces should-level guidance and severity is per-rule

Each check-type rule skips fields whose presence another rule owns, so a missing field is reported exactly once. Each rule's description lists the IPA-132 guidelines it covers.

There is one and only one OperationResponse, provided as scaffolding, so the schema must define its properties directly: allOf/oneOf/anyOf composition is a violation reported by the required-fields rule, and the remaining schema rules skip composed schemas.

Testing

  • Per rule: valid, invalid (exact error count, message and path), violation-with-exception (suppressed) and adoption-with-exception (flagged as unnecessary) cases, plus unit tests for the composition and enum helpers.
  • Full suite: 124 suites / 786 tests pass. gen-ipa-docs, prettier and eslint clean.
  • spectral lint openapi/.raw/v2.yaml with the full ruleset: 0 xgen-IPA-132-* findings — the spec defines no OperationResponse schema yet.

Checklist

  • I have signed the MongoDB CLA
  • I have added tests that prove my fix is effective or that my feature works

},
},
// A paginated wrapper composed with allOf, which Spectral does not flatten
PaginatedAllOfOperationResponse: {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we allow allOf? Could you clarify that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Spectral resolves $refs but does not flatten allOf, so without merging the allOf branches a composed-but-compliant wrapper would be a false violation — per OAS semantics an instance must satisfy every subschema, so a results array defined in any branch is genuinely part of the schema. Root-level allOf composition is a common idiom in our spec (107 of the 1820 schemas in openapi/.raw/v2.yaml are composed this way), so the first real paginated Operations wrapper may well extend a shared pagination base like this. This test pins that case.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

My question is more about whether we expect allOf usage in the Operations endpoints

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update on the allOf case after a deeper review pass: I've kept allOf support for the OperationResponse schema itself (now merged conjunctively — re-declared properties combine keywords and enums intersect), but dropped allOf acceptance for the paginated wrapper. The wrapper shape I blessed here would fail xgen-IPA-110-collections-response-define-results-array at error severity on /…/{id}/operations, since schemaIsPaginated doesn't traverse allOf — so IPA-132 now requires the results array directly on the wrapper, consistent with IPA-110. Teaching the IPA-110 pagination predicate allOf would change an existing error-level rule and belongs in its own PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Final state after our Slack discussion: composition is not expected anywhere for the Operation status contract, so as of 7cda85d the rules enforce that directly — OperationResponse must be a flat schema (allOf/oneOf/anyOf is a violation), and the paginated wrapper must define results directly per IPA-110. The allOf merge machinery is removed.

type: 'object',
required: ['operationId', 'operationType', 'createdAt', 'updatedAt'],
properties: {
operationId: { type: 'string' },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looking at the Technical Design document, I see that there are other required fields, in other words, core metadata. Shouldn't they be involved here as well?

@julius-jogela julius-jogela Aug 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deliberate split, each rule maps 1:1 onto a guideline ID in the IPA repo, and IPA-132-operation-response-must-include-core-metadata scopes core metadata to operationId, operationType, createdAt and updatedAt (plus customMethod declared but not required). The other required fields in the design doc are owned by their own guidelines:

  • status → xgen-IPA-132-operation-status-must-use-the-standard-status-enum (this PR)
  • resultHref, retryAfterSeconds, error → conditionally required at runtime, so statically they can only be checked as declared-and-not-required; covered by xgen-IPA-132-successful-lros-must-reference-the-result-resource, xgen-IPA-132-in-progress-operations-must-expose-retry-after-seconds and xgen-IPA-132-failed-lros-must-report-a-structured-error in the next PR.
  • expiresAt → owned by IPA-132-operations-must-expire-after-a-finite-retention-period, which is review-level in the IPA repo because retention and the 404-after-expiry are backend behavior

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I recall a discussion where we said that since there are several guidelines covering required fields, the plan was to cover all of them in one validation rule, so we don't need a separate validation rule for each required-field guideline. Isn't that still the case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Restructured in 907d1c6 following our Slack discussion. The schema checks are now split by check type instead of by guideline, with the field lists in functionOptions: one rule for all the always-required fields (operationId, status, operationType, createdAt, updatedAt), one for the optional/conditional ones, one for enum values and one for nested structure.

Two things I kept on purpose: operations-should-expose-status-fields stays its own rule since severity is per-rule and that one is a should, and expiresAt still isn't linted because the expiry guideline is review-level. If we decide it should be required, it's a guideline change in the IPA repo first and then a one-line functionOptions addition here.

Comment thread tools/spectral/ipa/rulesets/IPA-132.yaml Outdated
Base automatically changed from feat-ipa-132-operations-resource-rules to main August 18, 2026 08:25
Add the three rules validating how long-running operation status is
communicated through the OperationResponse schema:

- xgen-IPA-132-operation-endpoints-must-return-operation-response: the
  single Operation endpoint must reference the OperationResponse schema,
  and the Operations collection endpoint must return a paginated
  response whose results reference it.
- xgen-IPA-132-operation-status-must-use-the-standard-status-enum: the
  schema must report progress through a status field using exactly the
  enum PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED.
- xgen-IPA-132-operation-response-must-include-core-metadata:
  operationId, operationType, createdAt and updatedAt must be defined
  and required, operationType must use exactly the enum CREATE, UPDATE,
  DELETE, CUSTOM, and customMethod must be defined but not required.

The schema name, both enums and the exact-enum-match helper live in
utils/longRunningOperations.js for reuse by the remaining IPA-132 rules.
Per review of the IPA-132 guideline examples, the status field must be
listed as required: an OperationResponse that can legally omit status
defeats polling. The status enum rule now checks required membership in
addition to the exact enum, matching the updated guideline example.
Per review, the test fixtures follow the same pattern across the IPA-132
test files: the operations and schemas under test are defined once at
the top of the file and reused across the test cases.
Address findings from a deeper review of the OperationResponse rules:

- The status enum and core metadata rules read properties and required
  lists through allOf sub-schemas, which Spectral does not flatten, so
  an allOf-composed OperationResponse no longer produces false
  violations.
- The Operations collection endpoint must reference a paginated wrapper
  schema: inline wrappers are rejected, consistent with
  xgen-IPA-110-collections-use-paginated-prefix, which rejects them at
  error severity. The results property of the wrapper may live in one of
  its allOf sub-schemas.
- The single Operation endpoint no longer accepts an array schema whose
  items reference OperationResponse.
- The endpoint rule checks its cheap path and media type gates before
  walking the document.
@julius-jogela
julius-jogela force-pushed the feat-ipa-132-operation-response-schema branch from a42e887 to d70c98d Compare August 18, 2026 10:15
…pe rules

Replace the per-guideline schema rules with four declarative rules driven by
functionOptions field lists, validating the OperationResponse reference schema
by check type: required fields, conditionally-present optional fields, standard
enum values and nested object structure. The should-level status fields rule
stays separate because severity is per-rule.
- Follow responses defined as refs to components.responses instead of
  silently skipping them, and flag 2xx responses without a JSON schema
- Reject allOf-composed paginated wrappers, consistent with the IPA-110
  pagination rules, and require the results array directly on the wrapper
- Merge allOf properties conjunctively: keyword maps combine instead of
  overwriting and enums apply intersected
- Anchor property violations at their actual document location inside
  allOf members instead of a location that only exists after merging
- Iterate response codes from the responses object so missing content is
  visible, moving rule exceptions to the response level
- Drive the should-level status fields rule from functionOptions like its
  siblings, and fix the a/an grammar in its messages
- Drop the unused enum constants: the ruleset functionOptions are the
  single source of truth for enum values
Operation records must expire, so the record must always carry its
expiration date. The retention behavior itself and the 404 after expiry
remain review-level, only the field declaration is statically checkable.
Comment thread tools/spectral/ipa/rulesets/IPA-132.yaml Outdated
Comment thread tools/spectral/ipa/rulesets/IPA-132.yaml Outdated
Comment thread tools/spectral/ipa/rulesets/IPA-132.yaml Outdated
Drop the internal guideline-ID mappings from the rendered descriptions,
clarify how allOf/oneOf/anyOf composition relates to the OperationResponse
schema, and trim the enum rule's field-presence justification.
There is one and only one OperationResponse, provided as scaffolding, so
the schema must define its properties directly: allOf, oneOf and anyOf
composition is a violation reported by the required-fields rule, and the
remaining schema rules skip composed schemas. Removes the allOf merge
machinery this design decision makes unnecessary.
const OPERATIONS_COLLECTION_ERROR_MESSAGE = `The Operations collection endpoint must return a paginated response whose results reference the ${OPERATION_RESPONSE_SCHEMA_NAME} schema.`;
const MISSING_SCHEMA_ERROR_MESSAGE = 'The Operations endpoint must define a JSON response schema.';

const COMPONENT_RESPONSES_REF_PREFIX = '#/components/responses/';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In our spec components/responses is reserved for error responses. Every 2xx response defines content inline or references the schema via components/schemas. Since this rule only evaluates 2xx codes, resolveResponseRef and the contentBasePath branch are unreachable. Could we drop them and read response.content directly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right — removed. resolveResponseRef and contentBasePath are gone and the rule reads response.content directly. Rule description, README, and tests updated accordingly.

…nt rule

In this spec components.responses is reserved for error responses: every
2xx response defines its content inline. The endpoint rule only evaluates
2xx codes, so resolveResponseRef and the contentBasePath branch could
never run. Read response.content directly instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js';
import { usesComposition } from './utils/longRunningOperations.js';

const COMPOSITION_ERROR_MESSAGE =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IPA132OperationResponseMustDefineRequiredFields owns two unrelated checks: "don't use allOf/oneOf/anyOf" and "define these six fields." Because they share a rule name, they share an exception as well, and that opens a gap.

If you ran a schema composed via allOf and missing operationId entirely through this validation, plus one exception:

"x-xgen-IPA-exception": {
  "xgen-IPA-132-operation-response-must-define-required-fields": "we compose from a base schema"
}

Result: 0 warnings. The exception suppresses the composition error.

Suggested fix: extract it into its own rule, xgen-IPA-132-operation-response-must-not-use-composition, on the same OperationResponseSchema alias. That matches the naming already used here, gives composition its own exception key and its own adoption metric, and leaves each rule named for what it checks.

Please update the other rules accordingly if needed

@julius-jogela julius-jogela Aug 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, extracted as suggested. Composition rejection now lives in xgen-IPA-132-operation-response-must-not-use-composition with its own exception key and adoption metric. Your bypass scenario — an allOf schema with a required-fields exception — is now a test case and produces the composition violation.

The required-fields rule owned two unrelated checks sharing one exception
key, so an exception granted for composition also silenced the six
required-field checks. Composition rejection now lives in
xgen-IPA-132-operation-response-must-not-use-composition with its own
exception key and adoption metric, and every other OperationResponse
schema rule skips composed schemas relying on it.

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

@yelizhenden-mdb yelizhenden-mdb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, thanks for addressing all the feedback

@julius-jogela
julius-jogela merged commit 6820ca5 into main Aug 20, 2026
9 checks passed
@julius-jogela
julius-jogela deleted the feat-ipa-132-operation-response-schema branch August 20, 2026 12:22
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.

2 participants