Skip to content

feat(cli): add the redocly recheck command - #3082

Merged
adamaltman merged 21 commits into
aa/recheck-relocate-enginefrom
aa/recheck-command
Sep 24, 2026
Merged

adamaltman merged 21 commits into
aa/recheck-relocate-enginefrom
aa/recheck-command

Conversation

@adamaltman

@adamaltman adamaltman commented Sep 3, 2026 •

Copy link
Copy Markdown
Member

What/Why/How?

This PR adds the redocly recheck command.
It lints Markdown prose and structure with the recheck engine from packages/recheck.
Configuration lives in the recheck block of redocly.yaml.
You name presets in the root extends, for example extends: [recommended, recheck/markdown].

Core.
Core sets recheck/* entries in the root extends aside as ResolvedConfig.recheckExtends.
The API preset resolver skips them, so redocly lint ignores them.
Core takes no dependency on @redocly/recheck.

Engine.
The engine Logger gained an output channel for report payloads.
The table, JSON, SARIF, GitHub Actions, readability, and --stats payloads write to it.
Progress, warnings, and errors stay on log, warn, and error.
The readability action lost its quiet flag, because the channel split makes it unnecessary.
Library callers of runReadability now receive progress lines through log in JSON mode too.

CLI.
packages/cli bundles the engine.
The CLI bundles the spell-check packages, nspell and dictionary-en, and the build copies the dictionary data files next to their chunk.
The handler maps the engine's channels onto the CLI logger: progress to stderr, payloads to stdout.
So --format json, sarif, and github-actions keep stdout machine-readable.

Command.
redocly recheck [paths..] lints by default.
Action flags select one other action: --readability, --generate-baseline, or --generate-markdoc-schema (with --from, --out, --check).
Runs pick up a .redocly.recheck-baseline.yaml next to redocly.yaml by presence; baseline in the recheck block sets another path.
--fix applies to linting only.

Output flags: --format (table default), --output-path, --stats, --max-problems, --summary, --summary-path.
--output-path applies to json and sarif; with other formats the command warns and writes the report to stdout.
Selection flags: --severity (warn or error), --tags, --rule, --skip-rule.
To scope a run to changed files, pass their paths.

With no redocly.yaml, the command uses recheck/markdown and says so on stderr.
With a redocly.yaml that has no recheck/* preset and no recheck block, it checks nothing and says so, the same as lint.
The command skips an API description path with a stderr notice.
That path arrives in a later PR.

Shared-code changes.
commandWrapper keeps its original behavior; the handler throws AbortFlowError on a failed run, the same path lint uses.
Core reserves the plugin ids recheck, redocly, redoc, realm, and reunite; a plugin that declares one of them fails to load.
handleLintConfig gained a type guard, because the recheck --format values widened the shared CommandArgv union.
handleLintConfig also skips config linting for --format sarif, as it does for json, junit, and checkstyle, so SARIF stdout stays a single document.

Config schema.
Core pins @redocly/config 0.56.0, which carries the recheck root key.
check-config accepts the block, and the config lint no longer warns on it.
A check-config e2e fixture covers a valid block.

This PR stacks on #3080, which relocates the engine.
Its base changes to main after #3080 merges.

Reference

  • Redocly/redocly#26970: the Phase 2 design.
    It chose approach A: presets in the root extends, rules in a recheck block, no engine dependency in core.
  • Redocly/redocly#26711: the recheck-to-CLI tracker.
    Phase 1 shipped the standalone engine; Phase 2 moves it into the CLI.
  • feat(cli): add the recheck engine and the redocly recheck command #3080: PR-A, the engine relocation this PR builds on.

Testing

Unit tests cover the recheck/ namespace in core, the reserved plugin ids, and selectAction.
End-to-end tests live in tests/e2e/recheck/ with twelve fixtures and snapshots.
They cover lint, JSON and SARIF stdout, readability, and baseline generation.
They also cover the Markdoc schema, the fallback notice, the no-config notice, config errors, conflicting flags, and the output-path warning.
The test normalizes timing lines before it compares snapshots.

Gates: npm run compile, npm run typecheck, npm run lint, npm run format:check, npm run unit, and the recheck e2e suite.
In npm run unit, the three pre-existing oas3 timeouts are the only failures, the same as on main.
Manual: node packages/cli/lib/index.js recheck --help, and a run against an invalid recheck block to confirm --generate-markdoc-schema no longer depends on it.

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
New bundled dependency and config surface area, but API lint paths are isolated; main risk is CLI output/stream behavior and config parsing edge cases in CI.

Overview
Adds redocly recheck, a CLI command that lints Markdown prose and structure via @redocly/recheck, wired through a new recheck block in redocly.yaml and recheck/* presets (e.g. recheck/markdown) on the root extends.

Configuration and core: OpenAPI lint no longer resolves recheck/* extends; those names are stored as recheckExtends for the recheck command only. check-config accepts the recheck block; reserved plugin ids (recheck, redocly, etc.) are rejected at load time.

Engine and CLI: The recheck Logger gains an output channel so reports (table, JSON, SARIF, GitHub Actions, readability tables) stay on stdout while progress uses log/warn/error. The CLI bundles the engine (including spell-check dictionary-en data in the build), registers yargs options for lint, --fix, --readability, --generate-baseline, and --generate-markdoc-schema, and auto-picks .redocly.recheck-baseline.yaml when present. handleLintConfig skips config lint for sarif and tightens format typing where recheck widened the argv union.

Docs, changeset, unit tests, and tests/e2e/recheck/ fixtures cover the new command and config behavior.

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

@adamaltman
adamaltman requested review from a team as code owners September 3, 2026 04:20
@changeset-bot

changeset-bot Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 22c7c57

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

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

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 3, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 82.64% (🎯 79%) 19556 / 23663
🔵 Statements 82.26% (🎯 78%) 21134 / 25689
🔵 Functions 84.81% (🎯 82%) 3730 / 4398
🔵 Branches 74.77% (🎯 71%) 14203 / 18995
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/types.ts 100% 100% 100% 100%
packages/cli/src/commands/lint.ts 94.54% 73.46% 100% 94.54% 77-79, 160, 193
packages/cli/src/commands/recheck/index.ts 0% 0% 0% 0% 19-138
packages/cli/src/commands/recheck/select-action.ts 100% 100% 100% 100%
packages/core/src/config/bundle-extends.ts 91.66% 83.33% 100% 91.66% 35
packages/core/src/config/config-resolvers.ts 79.44% 66.16% 94.11% 80.22% 78, 106-109, 208, 217, 273, 293-294, 318, 331, 342, 345-349, 359-368, 392-394, 410, 413, 416, 419, 432-434, 438, 444, 447, 450-453, 456-459, 462-465, 479-481, 488, 491, 494, 497, 500, 503, 528-530, 535-541
packages/core/src/config/utils.ts 97.67% 81.81% 100% 98.82% 45, 97-99
packages/recheck/src/actions/baseline.ts 0% 0% 0% 0% 23-65
packages/recheck/src/actions/logger.ts 100% 100% 88.88% 100%
packages/recheck/src/actions/readability.ts 72.91% 34.48% 80% 74.41% 23, 42-46, 56-57, 75-80
packages/recheck/src/config/resolve.ts 100% 100% 100% 100%
packages/recheck/src/core/baseline.ts 95% 91.83% 100% 97.22% 37, 39, 43, 105
packages/recheck/src/reporter/statistics.ts 34.37% 21.05% 33.33% 36.66% 27-28, 39-61
packages/recheck/src/reporter/formats/github-actions.ts 0% 0% 0% 0% 8-22
packages/recheck/src/reporter/formats/json.ts 100% 83.33% 100% 100%
packages/recheck/src/reporter/formats/sarif.ts 92.85% 75% 100% 92.3% 79
packages/recheck/src/reporter/formats/table.ts 90.32% 80% 100% 91.66% 19, 53, 57
Generated in workflow #11973 for commit 22c7c57 by the Vitest Coverage Report Action

@RomanHotsiy RomanHotsiy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-read the docs plz

Comment thread docs/@v2/commands/recheck.md Outdated
Comment thread docs/@v2/commands/recheck.md Outdated
Comment thread docs/@v2/commands/recheck.md Outdated
@adamaltman

Copy link
Copy Markdown
Member Author

Re-read both docs pages and rewrote them in 2c3fe6d: plain introductions, no release notes, examples in the same shape as the lint page, and the not-yet-active apiDescriptions option removed from the reference.

@adamaltman
adamaltman force-pushed the aa/recheck-command branch 2 times, most recently from 42a5366 to 03cf1af Compare September 3, 2026 15:46
@adamaltman

Copy link
Copy Markdown
Member Author

Rebased onto the current #3080 head and added the @redocly/config 0.56.0 pin in 03cf1af.
check-config now accepts the recheck block, the skipped config-lint test is on, a check-config e2e fixture covers a valid block, and the two snapshots lost the transient warning line.
Nothing is deferred any more; the PR body reflects the final state.

@tatomyr tatomyr 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.

Had a quick look. My main concern is that the recheck interface/behaviour diverges from lint for no obvious reason. Please consider aligning them more closely.

Comment thread packages/cli/src/commands/recheck/cli-logger.ts Outdated
Comment thread packages/cli/src/commands/recheck/args.ts Outdated
Comment thread packages/cli/src/index.ts Outdated
Comment thread packages/cli/src/index.ts
Comment thread packages/cli/src/index.ts Outdated
Comment thread tsconfig.json
Comment thread packages/cli/src/commands/recheck/__tests__/cli-logger.test.ts Outdated
Comment thread packages/core/src/config/__tests__/recheck-namespace.test.ts Outdated
Comment thread packages/core/src/config/utils.ts Outdated
Comment thread packages/cli/src/commands/lint.ts Outdated
Comment thread packages/recheck/src/actions/logger.ts
Comment thread packages/cli/src/wrapper.ts Outdated
Comment thread packages/core/src/config/utils.ts
Comment thread packages/recheck/skills/recheck-config/SKILL.md Outdated
Comment thread packages/recheck/skills/recheck-lint/SKILL.md Outdated
Comment thread packages/core/src/config/config-resolvers.ts Outdated
@@ -0,0 +1,47 @@
import type { VerifyConfigOptions } from '../../types.js';

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.

Could you please refactor the structure a bit?
Let's move types inside the separate types.ts file and selectAction function inside the separate file named select-action.ts and then you can rename test file to select-action.test.ts.

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.

Done in 7e1138f.
Types live in types.ts, selectAction in select-action.ts, and the test is select-action.test.ts.

}
}

function lintOptions(argv: RecheckArgv): LintOptions {

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.

I'd prefer toLintOptions.

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.

Renamed to toLintOptions in 7e1138f.

Comment thread packages/cli/src/index.ts
},
}),
(argv) => {
commandWrapper(handleRecheck)(argv);

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.

Could you please add lazy import, because now we have performance degradation in check-config command: AlbinaBlazhko17#7 (comment).

const { handleRecheck } = await import('./commands/recheck/index.js');
      commandWrapper(handleRecheck)(argv);

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.

Done in 7e1138f.
The recheck handler loads through await import, the same way introspect-mcp does.

Comment thread packages/cli/src/commands/recheck/index.ts
Comment thread packages/cli/scripts/build.mjs Outdated
format: 'esm',
target: 'node20.19',
// The engine imports these spell-check packages dynamically; a user installs them on demand.
external: ['nspell', 'dictionary-en'],

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.

@adamaltman could you please explain why you decided to keep those two packages as a peer dependency?
I tested the case when we use those packages inside the deps and it has no performance impact: https://github.com/AlbinaBlazhko17/redocly-cli/pull/7/changes#diff-0ba29bdf689d78abba7ff7ecf6f2d5520d8ba5f3e0d3a3c842be64e265533847.

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.

Agreed, and bundled in 32add50.
They were externals so the engine's optional spell-check stayed on demand, but the CLI manifest carries no runtime dependencies, so a user could never get them.
Bundling needed one addition: dictionary-en reads its data files relative to its own module URL, so the build copies index.aff and index.dic next to the chunk esbuild picks.
The bundle grows by about 0.6 MB and the spell-check chunk stays lazy.

Comment thread packages/core/src/config/utils.ts
Comment thread packages/recheck/src/actions/logger.ts
Comment thread docs/@v2/commands/recheck.md Outdated

```yaml
recheck:
baseline: ./.recheck-baseline.yaml

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 add it manually? The lint ignore file is picked up merely by virtue of being in the project. I suggest we keep the same approach for baseline.

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.

Done in 6d6bc97.
A .redocly.recheck-baseline.yaml next to redocly.yaml is picked up by presence.
The baseline key stays as an override for another path.

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.

Ideally there should be no way to override the ignore file, otherwise it creates divergence from the current approach in the lint command.

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.

Noted.
Dropping the baseline key means discovery only, the same as the ignore file, and it removes a key that @redocly/config 0.56 already publishes.
It joins the config-shape decision, so one change covers both.

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.

Adam agreed; the baseline key goes and only discovery stays, on #3080.

Comment thread docs/@v2/commands/recheck.md Outdated
import { AbortFlowError } from '../../../utils/error.js';
import { handleRecheck } from '../index.js';

function fakeConfig(

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.

These tests don't work well as unit tests, since it's hard to tell what they're actually testing. Converting them to e2e tests would make the use cases clearer.

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.

Agreed.
4777213 replaces the handler unit tests with e2e fixtures under tests/e2e/recheck/, one directory per use case with a snapshot.
The stacked API-description PR gets the same treatment.

Comment thread packages/recheck/README.md
Comment thread packages/core/src/config/config-resolvers.ts
@adamaltman

Copy link
Copy Markdown
Member Author

The red e2e check is two respect tests that call the live Cafe API (cafe.redocly.com).
They fail the same way on a build of main at 93c155f, so the cause is upstream: the request now gets a 400 with a new required field, and the accept header lost application/problem+json.
This branch changes nothing under respect.

…ck block

The prose engine composes recheck/* presets itself, so the API preset
resolver skips them.
Report payloads go through output. Progress and diagnostics stay on log,
warn, and error.
The breakdown prints only inside the table report, so it goes with the
report payload.
The engine's optional spell-check peers stay external to the bundle, and
an adapter maps the engine Logger onto the CLI logger.
One command with action flags: lint by default, --readability,
--generate-baseline, and --generate-markdoc-schema. Presets come from the
root extends and rules from the recheck block.

commandWrapper now reads process.exitCode after a handler returns instead
of always reporting success, since recheck signals failure that way
instead of throwing. lint.ts only forwards argv.format to the shared
config-lint formatter when it is a real OutputFormat, since recheck's
own report formats (table, sarif) are not.
Move the --generate-markdoc-schema branch before config resolution so it
never depends on a valid recheck block. Add handler tests for readability,
baseline, markdoc-schema, and the JSON report format.
Add sarif to the config-lint early-return list so recheck no longer
leaks a stray report onto stdout. Warn when --output-path is set for
a format other than json or sarif, since the report goes to stdout in
that case. Treat a null recheck block the same as a missing one, and
clean up temp directories in the handler test suite.

Update the recheck docs and engine README: they described a command
that had not shipped yet, misplaced the output-path and --fix
behavior, and put the recheck command under the wrong docs heading
and sidebar entry. Correct the changeset wording to match.
Remove release notes from the command page. Describe presets and the
recheck block in one place. Drop the apiDescriptions option from the
reference until the API-description path ships.
The config schema now carries the recheck root key, so check-config
accepts the block and the config lint stops warning on it.
Rename --exclude-rule to --skip-rule and --annotations-limit to
--max-problems, drop --changed-only and --changed-list, limit --severity
to warn and error, and check nothing when redocly.yaml has no recheck
configuration. Build the engine logger inline and leave the bundled
config unmutated.
The CLI publish manifest carries no dependencies, so the rewrite of
@redocly/recheck had no effect.
…he baseline

Recheck now throws AbortFlowError on failure, so the wrapper's
existing exit path handles it, and wrapper.ts reverts to its
original behavior.
Plugin ids recheck, redocly, redoc, realm, and reunite are now
reserved and fail to load.
A skill doc no longer mentions the unsupported redocly.yml name.
The default baseline file is now hidden, at .recheck-baseline.yaml.
Two template literals held a raw NUL byte as the group separator, so
git treated the file as binary. The \0 escape produces the same string.
The doc comment no longer names the default baseline file.
Comment thread packages/cli/src/commands/recheck/index.ts
paths?: string[];
format: RecheckFormat;
'output-path'?: string;
severity?: 'warn' | 'error';

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.

What do we need severity on this level for? This is against the current lint behaviour.

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.

--severity error runs the error rules only; the docs CI gate uses it to keep warnings out of the gate step.
lint has no such flag, and the exit code already fails on errors alone, so dropping it is on the table.
Adam decides with the other interface questions.

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.

Adam agreed; --severity goes, on #3080.

… aa/recheck-command

# Conflicts:
#	package-lock.json
#	packages/cli/package.json
#	packages/cli/src/types.ts
#	scripts/local-pack.sh
@adamaltman
adamaltman merged commit 22c7c57 into aa/recheck-relocate-engine Sep 24, 2026
41 of 42 checks passed
@adamaltman
adamaltman deleted the aa/recheck-command branch September 24, 2026 16:19
@adamaltman

Copy link
Copy Markdown
Member Author

Folded into #3080 at 22c7c57; review continues there.

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.

5 participants