Skip to content

feat(cli): lint API descriptions with redocly recheck - #3093

Open
adamaltman wants to merge 16 commits into
aa/recheck-commandfrom
aa/recheck-api-descriptions
Open

adamaltman wants to merge 16 commits into
aa/recheck-commandfrom
aa/recheck-api-descriptions

Conversation

@adamaltman

@adamaltman adamaltman commented Sep 8, 2026

Copy link
Copy Markdown
Member

What/Why/How?

This PR makes redocly recheck lint the description fields of API descriptions.
Each finding reports the file, line, and column of the description in the source.
Pages and descriptions share one report and one exit code.

Discovery.
With no paths, the command lints the Markdown files under the project root and every API in apis.
With paths, a Markdown file or directory lints as pages, and a file that detectSpec recognizes lints its descriptions.
An API description that does not parse is an error and fails the run, as in lint.

Extraction.
The CLI walks each API document with the public openapi-core walker.
A visitor collects every string description with its pointer and owning source, external $ref files included.
summary stays out.

Engine.
The engine gained one input kind, an embedded input: file, pointer, content, and a position mapper.
runLint lints embedded inputs in embedded mode, so document-shape rules do not run on them.
The apiDescriptions.rules block adjusts rules for descriptions only; a name that is not in effect is a config error.

Each description finding carries its pointer and reports at the mapped source position.
--fix skips descriptions and says once how many fixable findings it skipped.
The count-based baseline covers description findings by file and rule.

Positions (Decision D).
A literal block scalar maps exactly: indicator line plus content line, block indentation plus column.
A folded block anchors to its first content line.
Single-line plain and quoted scalars without escapes map the column exactly; multi-line or escaped scalars anchor to the value start.
JSON follows the quoted rule.

Suppression (Decision E).
Inline <!-- recheck-disable --> directives work inside a description.
.redocly.lint-ignore.yaml suppresses a description finding by file, rule, and pointer; the command reports how many findings it suppressed.
The rule key can be the full name or the short name.
The baseline generator applies the same suppression, so the two never disagree.

Selection.
Two APIs that share a $ref file report a description once.
Descriptions in remote $ref files are skipped, and the command says how many.
An explicit API path with no descriptions still emits the report, so --format json always prints a document.

--readability covers Markdown only and says so once when it skips API descriptions.

Known limitation.
Take a rule that is off for descriptions but on for pages.
In a run that covers pages and descriptions together, it can mark a stale baseline entry for an API file.
A description-only run does not.
--generate-baseline clears it, and the message says so.

This PR stacks on #3082, which adds the command.
Its base changes to main after #3082 merges.

Reference

  • Redocly/redocly#26970: the Phase 2 design.
    Decision D sets the position mapping; Decision E sets suppression through directives, the ignore file, and the count baseline.
  • Redocly/redocly#26711: the recheck-to-CLI tracker.
  • feat(cli): add the redocly recheck command #3082: PR-B, the command this PR extends.

Testing

Unit tests cover the engine's embedded inputs: remapping, pointer, overrides, ignore predicate, fix skip, and baseline.
They also cover description extraction with $ref files and dedup, and every position-mapping rule with a fixture per scalar style.

End-to-end fixtures under tests/e2e/recheck/ cover the API-description cases.
They exercise an explicit API path, apis discovery, an unreadable API, a YAML page, and a shared $ref file.
They also cover an API with no descriptions, the ignore file by full and short name, the readability notice, and the baseline guard.
An OpenAPI file with a finding in a literal block, a $ref'd description that the ignore file suppresses, and a JSON description with --format json.
The Rebilly smoke runs redocly recheck over the Rebilly description with the default recheck/markdown preset: 6,463 descriptions and 608 findings in about 21 seconds.

Gates: npm run compile, npm run typecheck, npm run lint, npm run format:check, npm run unit, the recheck e2e suite, and npm run smoke:rebilly.
In npm run unit, the three pre-existing oas3 timeouts are the only failures, the same as on main.

Screenshots (optional)

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

Note

Medium Risk
Adds a large CLI/recheck integration path (API walking, position mapping, baselines) and a breaking ResolvedRecheckConfig field; behavior is well-tested but touches default recheck runs when apis is configured.

Overview
redocly recheck now lints description fields in OpenAPI/API specs alongside Markdown, with findings reported at the real file line/column (including JSON pointers on each issue).

The CLI walks API documents (local $ref files included), turns each description into an embedded input for @redocly/recheck, and maps lint positions back into YAML/JSON (literal/folded blocks, quoted scalars, etc.). Default runs with no paths also pick up local APIs from apis; unreadable specs fail the run; remote $ref descriptions are skipped with a count.

The recheck engine adds lintEmbeddedInputs, merges description findings into the same report/baseline/change-only flow, and resolves recheck.apiDescriptions.rules into a separate descriptionRules set (page rules unchanged). --fix does not rewrite API files; suppress via .redocly.lint-ignore.yaml by file, rule, and pointer.

Breaking for library users: ResolvedRecheckConfig now requires descriptionRules (replacing raw apiDescriptionRules on the resolved config).

Reviewed by Cursor Bugbot for commit 5821708. Bugbot is set up for automated code reviews on this repo. Configure here.

@adamaltman
adamaltman requested review from a team as code owners September 8, 2026 01:28
@changeset-bot

changeset-bot Bot commented Sep 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5821708

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

This PR includes changesets to release 5 packages
Name Type
@redocly/cli Minor
@redocly/recheck 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

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 82.23% (🎯 78%) 19620 / 23859
🔵 Statements 81.9% (🎯 78%) 21214 / 25902
🔵 Functions 84.49% (🎯 81%) 3734 / 4419
🔵 Branches 74.58% (🎯 71%) 14305 / 19180
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/commands/recheck/descriptions.ts 92% 75% 100% 100% 37, 54
packages/cli/src/commands/recheck/index.ts 8.26% 2.46% 5.26% 9% 35-80, 112-291
packages/cli/src/commands/recheck/positions.ts 100% 94.11% 100% 100%
packages/recheck/src/index.ts 100% 93.75% 100% 100%
packages/recheck/src/actions/baseline.ts 86.48% 60% 100% 85.71% 49-56
packages/recheck/src/actions/embedded.ts 100% 100% 100% 100%
packages/recheck/src/actions/lint.ts 85.1% 86.45% 68.75% 85.71% 73-78, 118-124, 143, 211-217, 241-243, 285-289, 294-299, 374-375
packages/recheck/src/config/resolve.ts 93.33% 96.07% 100% 92.72% 94-95, 99-100
packages/recheck/src/core/rule-filters.ts 94.64% 83.33% 100% 100% 57, 93, 107
packages/recheck/src/reporter/formats/table.ts 90.32% 83.33% 100% 92% 19, 58, 62
Generated in workflow #11869 for commit 5821708 by the Vitest Coverage Report Action

@cursor cursor Bot 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.

Stale Bugbot comment from a previous run.

Comment thread packages/recheck/src/actions/lint.ts Outdated
Comment thread packages/recheck/src/actions/lint.ts
Comment thread packages/recheck/src/reporter/formats/table.ts
@adamaltman
adamaltman force-pushed the aa/recheck-api-descriptions branch 2 times, most recently from 8ad2891 to 5d54e7b Compare September 8, 2026 20:42

@cursor cursor Bot 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.

Stale Bugbot comment from a previous run.

Comment thread packages/cli/src/commands/recheck/index.ts
Comment thread docs/@v2/commands/recheck.md Outdated

The `recheck` command lints Markdown files for prose and structure problems.
It checks headings, sentences, links, images, and Markdoc tags against a set of rules.
It also lints the `description` fields of API descriptions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
It also lints the `description` fields of API descriptions.
`recheck` also lints the `description` fields of API descriptions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Applied in 0d4da07, thank you.

Comment thread .changeset/recheck-api-descriptions.md Outdated
Comment on lines +6 to +7
`redocly recheck` now lints the `description` fields of API descriptions.
Findings report the source line and column, `.redocly.lint-ignore.yaml` suppresses them by file, rule, and pointer, and the `apiDescriptions.rules` block adjusts rules for descriptions only.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
`redocly recheck` now lints the `description` fields of API descriptions.
Findings report the source line and column, `.redocly.lint-ignore.yaml` suppresses them by file, rule, and pointer, and the `apiDescriptions.rules` block adjusts rules for descriptions only.
Improved `redocly recheck` to lint the `description` fields of API descriptions.
You can suppress the findings by file, rule, and pointer, or adjust rules for descriptions only.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Applied in 0d4da07, thank you.

@adamaltman
adamaltman force-pushed the aa/recheck-api-descriptions branch from 0d4da07 to f04433a Compare September 11, 2026 11:23

@cursor cursor Bot 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.

Stale Bugbot comment from a previous run.

Comment thread packages/recheck/src/config/resolve.ts
Comment thread packages/recheck/src/actions/lint.ts
@adamaltman
adamaltman force-pushed the aa/recheck-api-descriptions branch from f00701f to 804065c Compare September 18, 2026 05:15

@cursor cursor Bot 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.

Stale Bugbot comment from a previous run.

Comment thread packages/cli/src/commands/recheck/positions.ts Outdated
Descriptions extracted from API documents lint in embedded mode with the
apiDescriptions overrides, remap to their source position, carry their
pointer, and join the same report and baseline as page findings.
An off rule for descriptions plus a matching --rule name threw and
stopped the whole run. Strip off names first, and reject an unknown
severity string in an apiDescriptions override.
Every string description, external $ref files included, comes back with
its pointer and owning source; summary stays out.
Literal blocks map exactly; folded, multi-line, and escaped scalars anchor
to the value start.
The check now stops at the closing quote, so a trailing comment cannot
turn an exact mapping into an anchor. A test covers the explicit
indentation indicator on a literal block.
API paths and the apis block feed descriptions into the same run as
Markdown pages. The ignore file suppresses by file, rule, and pointer.
… API descriptions

The baseline takes the same ignore predicate as the lint run, so a
suppressed description finding never lands in the baseline and no run
reports it stale. The ignore file accepts the short rule name that the
report prints.

Descriptions that two APIs share through a $ref now report once, and
two aliases with the same root walk once.

--changed-only filters API descriptions with the same changed set as
Markdown pages, and an empty result emits the empty report. An API with
no descriptions writes the empty report as well, so --format json always
has JSON on stdout.

The baseline gate counts embedded files as scanned and adds the
description rules that ran to the executed rules. The table report keeps
description findings out of the fixable count, and a folded scalar
anchors to its first content line.
Classify a requested path into api, unreadable-api, or not-api instead
of collapsing a parse failure into a Markdown page. Route unreadable
API paths into collectEmbeddedInputs, log the failure as an error, and
force exit code 1 when the lint or baseline action otherwise reports
zero. Reword a comment that named the retired --exclude-rule flag.
Print "nothing to check" for an empty target list instead of a bare
colon, and skip the redundant "No markdown files found" line for it.
Note in the docs that a description lints only local APIs and that a
parse failure fails the run.
…rker consistent

Filter description rules by severity and tags only, then apply name
selection with matchesRuleName, so a mixed run no longer aborts when
apiDescriptions.rules lowers a rule below the requested severity.

Skip descriptions reached through a remote $ref when collecting
embedded inputs, since their absoluteRef is a URL that baselineKeyMapper
and the changed-file filter cannot turn into a stable path, and log how
many were skipped.

Stop marking a table finding [fixable] when it carries a pointer, since
--fix never rewrites inside an API description.
With --generate-baseline, a run that could not read an API description
still wrote the baseline before it failed, so a partial baseline could
replace a good one. The command now reports the count, writes nothing,
and fails.
…nned

Reject a rule-object apiDescriptions override with an unknown severity,
matching the check already applied to string overrides.
Count a parsed API file with no local descriptions as scanned, so a
leftover baseline entry for it goes stale instead of staying silent.
The handler scenarios now run the real CLI against fixture directories, so
the assertions read the same output a user sees. The command test file keeps
only the pure-function tests and mirrors index.ts by name.
Bound the block scalar content search to lines inside the value's
end location. An empty or whitespace-only description no longer
picks up a sibling key as its content line. When no content line
exists, the mapper anchors to the indicator line and column instead.
@adamaltman
adamaltman force-pushed the aa/recheck-api-descriptions branch from 804065c to 5821708 Compare September 18, 2026 12:58

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5821708. Configure here.

);
// An API description that failed to parse fails the gate even when the
// lint or baseline action otherwise found nothing to report.
return failureCount > 0 && exitCode === 0 ? 1 : exitCode;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unreadable APIs trigger false stale baselines

Medium Severity

--generate-baseline stops when an API file does not parse, but a normal lint run still applies the baseline gate. A default recheck walk uses . as scanRoots, and that root matches every baseline file. An unreadable API is not added to apiFiles, so leftover counts for it are reported as stale errors. Those extra errors fail the gate and tell people to regenerate a baseline that this run also refuses to write.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5821708. Configure here.

if (absoluteRef !== undefined && !isAbsoluteUrl(absoluteRef)) files.add(absoluteRef);
}

return { descriptions: collected, files: [...files] };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Broken local refs skip description linting

Medium Severity

resolveDocument records missing or unreadable local $ref targets as errors and does not throw. collectDescriptions never inspects those errors, so those files are omitted from files and their descriptions are never linted. Remote $refs are counted and announced, and a root that does not parse fails the run. A broken local $ref does neither, so the command can exit 0 with incomplete coverage.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5821708. Configure here.

const overrides = applyDescriptionOverrides(validation.rules, apiDescriptions?.rules);
if (overrides.errors.length > 0) {
return { success: false, errors: overrides.errors };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Invalid description overrides are silently ignored

Low Severity

apiDescriptions is stripped before engine schema validation, then accepted if it is any plain object. A typo such as rule instead of rules, a non-object apiDescriptions value, or extra keys produces no config error. Unknown rule names inside rules are rejected, so the same block is strict in one place and silent in the others. Overrides then never apply and descriptions keep the page rule set.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5821708. Configure here.

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