Skip to content

refactor(ipa): CLOUDP-453872 named rule conditions and defineRule - #1529

Open
DatMitro wants to merge 2 commits into
CLOUDP-453871-ipa-snapshot-scriptsfrom
CLOUDP-453872-ipa-conditions-define-rule
Open

DatMitro wants to merge 2 commits into
CLOUDP-453871-ipa-snapshot-scriptsfrom
CLOUDP-453872-ipa-conditions-define-rule

Conversation

@DatMitro

@DatMitro DatMitro commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Proposed changes

Formalizes how IPA rules are written: where a rule applies is declared as named, tested conditions (for example "is a singleton", "is not read-only") that are shared across rules, and defineRule (a plain JavaScript function that returns the same export default function Spectral calls, with the YAML unchanged) does the collection and error handling the same way for every rule.

This targets regressions and visibility. A shared definition changes in one place, and the snapshot compare in #1528 shows exactly which findings move (here, none). Every skipped component becomes attributable to a named condition, which follow-ups use to report why a rule did not apply and to flag redundant exceptions. The README "Applies to" sections and a conditions glossary are generated from the same conditions, replacing hand-written YAML text that had drifted from the code.

This PR migrates IPA-104 and IPA-113 (18 rules) as the first step of CLOUDP-453870. No change to results.

Why. Every rule repeats the same scaffolding, and the part that matters, where the rule applies, is buried in it:

  • early returns that record nothing, so a skipped component cannot be seen or counted;
  • a checkViolationsAndReturnErrors helper per rule with its own try/catch, each deciding on its own what an internal error does;
  • a call to evaluateAndCollectAdoptionStatus to record the adoption, violation or exception;
  • the same idea (e.g. where a Get method belongs) written slightly differently per rule;
  • docs that drift from the code: "Applies to" is hand-written in each rule's YAML description, separately from the code that decides it, so nothing keeps the two in step (at least 17 rules across the ruleset disagree with their implementation; two of the migrated ones are corrected here).

Across the ruleset, 114 rule files call evaluateAndCollectAdoptionStatus and 87 have their own try/catch.

Structure

  • Condition (utils/conditions/): a named, reusable yes/no question about a component, e.g. resource.isSingleton, with a plain-English definition and match/no-match examples that run as tests. Combine with allOf, anyOf, not.
  • RuleContext (utils/ruleContext.js): what conditions and checks read for one call (path, resourcePathItems, node, options), computed on first use and cached so a condition and the check share the work.
  • defineRule({ appliesTo, check }) (utils/defineRule.js) builds the Spectral function and standardizes:
    • early stops: appliesTo is evaluated in order and the component is skipped at the first condition that fails. Before, an early return was invisible: nothing recorded that a guard fired, or which one. Now every skip is attributable to a named condition at a single place, so the tracing follow-up (CLOUDP-453873) can record which condition skipped which component for each rule;
    • collection: the check returns errors, and defineRule records adoption, violation or exception, reading the exception from exceptionTarget (the input by default);
    • internal errors: onInternalError ('violation', 'return', 'throw') makes each rule's existing try/catch behavior an explicit option instead of a copied block. beforeCheck runs outside the try, for the operation ID rules whose unreadable verb override must keep failing the lint run.
  • Generated docs: the README "Applies to" sections and a CONDITIONS.md glossary are generated from the conditions by gen-ipa-docs, so the existing docs check covers them. This is the rule metadata change: the hand-written "Applies only to..." bullets (19) and 7 other implementation-detail lines are removed from the IPA-104 and IPA-113 YAML descriptions, and the README shows the generated section instead.

Impact on the 18 migrated rules: 741 to 321 lines; 12 check helpers, 14 try/catch blocks, 18 collection calls, 23 early returns and 18 ruleName/oas/path[1] preambles are gone. The shared code is new (about 350 lines for defineRule, the context and the registry, and about 500 for the conditions with their examples), so this PR's total grows; the saving shows as more rules move over.

IPA-113 singleton-should-have-update-method (imports and error message left out):

Before

export default (input, opts, { path, documentInventory, rule }) => {
  const ruleName = rule.name;
  const oas = documentInventory.resolved;
  const resourcePath = path[1];
  const resourcePathItems = getResourcePathItems(resourcePath, oas.paths);

  if (!(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePathItems))) {
    return;
  }

  if (isReadOnlyResource(resourcePathItems)) {
    return;
  }

  const errors = checkViolationsAndReturnErrors(input, path, ruleName);
  return evaluateAndCollectAdoptionStatus(errors, ruleName, input, path);
};

function checkViolationsAndReturnErrors(input, path, ruleName) {
  try {
    if (!(hasPutMethod(input) || hasPatchMethod(input))) {
      return [{ path, message: ERROR_MESSAGE }];
    }
    return [];
  } catch (e) {
    return handleInternalError(ruleName, path, e);
  }
}

After

export default defineRule({
  appliesTo: [isSingleton, not(isReadOnly)],
  check: ({ input, path }) => (hasPutMethod(input) || hasPatchMethod(input) ? [] : [{ path, message: ERROR_MESSAGE }]),
});

Two README descriptions that disagreed with the code are corrected: reset-method-not-on-readonly-singleton never checked for a singleton, and both reset rules evaluate the base resource. ipa-snapshot-compare is identical on openapi/.raw/v2.yaml and openapi/v2.json.

Jira ticket: CLOUDP-453872

Checklist

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

Changes to Spectral

  • I have read the README file for Spectral Updates

Further comments

Stacked on #1528 (CLOUDP-453871): the diff against main includes that PR's commit until it merges. The other rule families move to defineRule in follow-ups (CLOUDP-453873 and later).

@DatMitro
DatMitro force-pushed the CLOUDP-453872-ipa-conditions-define-rule branch from 1d47137 to a2adf86 Compare October 7, 2026 16:49
@DatMitro
DatMitro marked this pull request as ready for review October 8, 2026 08:27
@DatMitro
DatMitro requested a review from a team as a code owner October 8, 2026 08:27
@DatMitro
DatMitro requested a review from wtrocki October 8, 2026 08:27
@DatMitro
DatMitro force-pushed the CLOUDP-453872-ipa-conditions-define-rule branch from a2adf86 to 1c804dd Compare October 8, 2026 10:32
@DatMitro
DatMitro force-pushed the CLOUDP-453871-ipa-snapshot-scripts branch from f08bd17 to ba120df Compare October 8, 2026 12:23
… IPA-113

Rules now declare the conditions under which they apply (appliesTo) instead of early returns.
Conditions are named, defined once with a plain-English definition and examples, and the
divergent variants (path.isLegacyGetTarget, resource.isReadOnly's exception clause)
carry a divergence note.

- utils/conditions/: registry (defineCondition, allOf/anyOf/not, evaluate) and path, resource,
  operation, response conditions wrapping the existing helpers
- utils/ruleContext.js: per-call context with lazy, cached resourcePathItems/node/operation. Values a condition
  and the check both need (e.g. the base Get response of a custom method) are exposed as cached accessors next to
  the condition, so both read one definition
- utils/operationIdRules.js: validOperationIdRule / operationIdLengthRule factories (the conditions they take live in
  utils/conditions/operationIdRules.js); the IPA-104
  operation ID rules are built from them with the conditions that decide which operations are a Get
- utils/defineRule.js: builds the Spectral function; onInternalError preserves each rule's
  existing try/catch behavior
- resourceEvaluation.js: isReadOnlyResource split into anyPathHasException and
  getResponseIsAllReadOnly (same result)

No behavior change: ipa-snapshot-compare is identical on openapi/.raw/v2.yaml and openapi/v2.json.
- generateRulesetReadme.js renders an "Applies to" section for rules built with defineRule, from
  their given and appliesTo conditions, and writes rulesets/CONDITIONS.md: each named condition's
  definition, examples, variant/divergence notes and the rules using it.
- Conditions get a short label for these docs.
- Hand-written applicability bullets removed from the 18 IPA-104/113 descriptions, since they are
  now generated and can't drift. Two descriptions corrected: reset-method-not-on-readonly-singleton
  does not check for a singleton, and both reset rules evaluate the base resource.
- CONTRIBUTING.md: new section on conditions, defineRule and the report scripts.

The code-health docs check covers CONDITIONS.md, as it is written by gen-ipa-docs.
No behavior change: snapshots identical on both specs.
@DatMitro
DatMitro force-pushed the CLOUDP-453872-ipa-conditions-define-rule branch from 1c804dd to c9b0edf Compare October 8, 2026 12:25
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.

1 participant