Repository navigation
feat(cli): add the recheck engine and the redocly recheck command - #3080
Conversation
🦋 Changeset detectedLatest commit: e5dbdb5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
Performance Benchmark (Lower is Faster)
|
|
The latest commit resolves the five review findings:
|
|
Applied both README suggestions in 377925a. Thank you. |
|
Ported Redocly/redocly#26904 in aa8e41c: the |
The engine moves from the Redocly monorepo without its standalone CLI layer: no bin, no yargs, no recheck.yaml discovery. It joins the fixed version group.
…erride The engine tests never type-checked in their old repository. RecheckConfig now declares the top-level keys next to the rule entries, and the package joins the existing cognitive-complexity override instead of per-site disables.
Rule names always carry a slash, so a template-literal index signature keeps engine keys and rule entries apart at compile time.
The block nests rules under `rules` and takes presets from the root extends. Engine warnings flow through a warn callback.
Each action takes a resolved config and a Logger. The reporters return their output through the same Logger, so the engine prints nothing on its own.
Scan link ranges in sentences.ts with a linear walk instead of a regex. Strip summary tags with a scanner instead of a one-pass replace. Escape only bare brackets and parens in link-image-style fixes. Build baselines on prototype-free maps. Confine image metadata to the root each file was discovered under.
Split the release sentence in two and state where the docs live.
Removed or merged: - 21 per-rule "passes a document with no X" tests (identical bare-paragraph input), replaced by one table over allTokenRules in token/__tests__/index.test.ts - 14 helper unit tests duplicated by rule-level tests (filterByPredicate 3, getDescendantsByType 3, getParentOfType 3, addRangeToSet 3, getHeadingText 2) - registry.test.ts (2 tests: a hard-coded rule count and an originals list that mirror the source; unregistered rules fail every rule test and presets.test.ts) - 1 no-duplicate-heading "scales to thousands" test that cannot detect a slowdown - 1 no-hard-tabs default-codeBlocks test subsumed by the codeBlocks:false test - utils.test.ts: 3 file-exception variants and the temp-dir setup; kept one shouldProcessFile case, the matcher stays covered by fileMatchesAnyPattern - convergence.test.ts: 4 "removed id" rows to 1 unknown-id test, 2 unknown-key tests to 1 - unused tokenRuleUnitHarness test helper
Removed categories: - 5 detection-only "fix wave" / acceptance files (about 100 sentence-level --fix cases) and the per-preset namespace, detection-only, floor and fixture tests. They became 4 table-driven contracts over the five style presets plus markdoc: namespace, detection-only (data and a real --fix run over the violation fixtures), every shipped rule fires, clean prose clean. - copied preset inventories: the 53-name markdown list, 21 copied default messages, 6 markdown-relaxed per-key tests (now one exact contract), the minimal list and message copies, the prose data copies, the markdoc shape and message tests, and the test-only PROSE_PRESET_ASSERTIONS, MARKDOC_PRESET_RULE_NAMES, MARKDOC_VIOLATION_CLASSES and empty MARKDOWN/MINIMAL_PRESET_MESSAGES exports plus the unused `messages` parameter of registerPresetRules. - the exact-source-grep test of markdoc report call sites, the registry "mutation proof" self-test and the vacuity probe. - duplicates: composition and stacking tests merged into one table, the consistency engine guard replays (owned by consistency.test), two README snippet checks subsumed by the resolving test, the all/raw lintContent rejections (owned by validate and selector tests), the non-vacuity markdoc probe, 5 validate "accepts" cases covered by the vocabulary test, 2 runLint path-pattern replays (owned by utils.test), the empty-directory twin, and the resolve shared-preset test. - repaired: the registry-immutability test could not detect a missing structuredClone; it now checks every preset. - the two per-pair coverage harnesses share one helper.
Removed or merged: - core config recheck.test.ts: 3 tests (orderRecheckPresets x2 duplicated by the createConfig preset-order tests; "block is not an object" x2 duplicated by utils.test mergeExtends, which reaches both guards) - cli print.test.ts: 1 test (printBaselineRun happy path, covered by the generate-baseline e2e snapshot plus the handler baseline test); 7 printMarkdocSchemaRun tests merged into one table; the applied-fixes test no longer re-asserts the reportFixes output by stderr index - cli formatters: 1 table test (fixable marks and counts, covered by the markdown-findings e2e snapshot); a "removed N character(s)" row moved into the reportFixes test, its owner - cli select-action.test.ts: 2 tests (duplicate markdoc-schema assertion; two-flags rejection, covered by the conflicting-flags e2e) - e2e: markdown-clean case and snapshot directory (covered by no-config and markdown-findings) Production: prioritizeProblems drops its unused limit parameter and dead branch; buildSarif is no longer exported (no caller outside sarif.ts).
Audited the scope-rule, scopes, parser (including Markdoc) and metrics tests. Every edit was checked with a mutation battery of about 280 production mutations, run before and after the edit. No mutation that failed a test before the edit passes after it. Removed or merged in the tests (area test count 1011 -> 962, rows of tables counted as tests): - Consolidated into table-driven contracts: capitalization per-style fix/clean/code-span cases (about 50 tests), title-case AP/Chicago cases (about 37), semantic-line-breaks detect-vs-fix pairs (about 45), spelling word/vocab/code-span/digit cases (about 20), swap table-cell and case-preserving cases, span.test.ts whitespace, "=" spacing, shortcut, "@" variable and bare-function cases, statistics syllables (4 -> 1), stripNonProse (8 -> 1), max-image-size reference images (3 -> 1). - Validation blocks of 10 rule test files replaced by expectValidOptions/expectInvalidOptions tables (shared helper in scope/__tests__/helpers.ts, also holds the deduplicated buildScopedContext that was copied into 7 files). - Deleted duplicates (about 40): title-case length-preserving loop and 3 Chicago-only copies; consistency execute-only overlap test, observed casing, can't/cannot and extra contraction rows; repetition lone-token idempotence; pattern/swap control tests; spelling multi-backtick control, config2/42 test and fallback duplicate; sentences reference link duplicate; selector "every vocabulary term compiles" block and call-shape test; extractor "has a fixture" test; parser flag-omitted vs flag-false tests, helper self-test, flag-on duplicates of the tokenize tests; tokenize flag-off duplicates, annotation kinds and the 33-line tag; pairing bare-function duplicate; max-image-size within-limit test. - Repaired 3 tests that could not fail (they passed under the mutation they were meant to catch): max-image-size external URL and non-existent file fixtures, spelling leading-digit fixture. Production (net -4 lines): removed the stripNonProse re-export in rules/scope/metric.ts (only a test used it; the test now imports the owner), un-exported formatSuggestionSuffix (spelling.ts) and SUMMARY_BLOCK_SOURCES (extractor.ts), whose only outside callers were tests. Kept on purpose: tokenize timing tests (the absolute ceilings guard inputs the ratio tests never reach), the sentences linear-time test (about 3 ms against a 2000 ms ceiling), title-case isAllCapsWord and control-character rows, schema and formula suites.
- The 'baseline file not found' message no longer tells users to remove a `baseline` key from the recheck block; that key is now a config error. - PROVENANCE.md files point to the preset tests that still exist.
|
@Redocly/dark-side I pushed a few changes to this PR and removed 12K lines of code. Comment s by agent: I pushed 17 commits to this branch: a test audit, a comment cleanup, a config resolution cleanup, and a few small fixes. Together they remove about 12,600 lines (+6,423 / −19,003), almost all of it tests and comments. Checks on the final commit: typecheck, lint (0 errors), format, compile, all unit tests (5,881 pass, 2 skipped) and all e2e tests (416 pass). Test auditI used the Commits:
Comment cleanupCommit Small fixesCommits Follow-up cleanupsFour more commits, one per item from the first review.
Config resolution cleanupCommits
Not done
|
…onfig The command reads the baseline from `.redocly.recheck-baseline.yaml` next to `redocly.yaml`, and `resolveRecheckConfig` rejects a `baseline` key in the block. Nothing read the `baseline` value that `validate` accepted and returned, so drop it from the schema, `validate`, the engine config type and the tests that only covered it.
Only `isRecheckPreset` (used by the CLI) and `mergeRecheckRules` (used by the engine) are used outside core. The other recheck helpers stay inside core and out of the public API.
- Move the list of Recheck-only token rules into the preset test that uses it. - Drop the `tokenRules` array and `clearTokenRulesForTests` from the registry. Only the name map is used to resolve rules, and registering a rule again under the same name replaces it, so the two runner tests no longer need to clear the registry.
The core `lintConfig` test already checks that `check-config` accepts the `recheck` block next to a `recheck/*` preset and rejects `extends` inside it, so the e2e case checked the same thing a second time.
tatomyr
left a comment
There was a problem hiding this comment.
The changes mostly look good. My main concern is changing the config resolution behaviour for Recheck compared to the existing approach. Also, the proposed approach won't let to run Recheck in the VS Code extension/Reunite easily.
|
|
||
| formatProblems(problems, { | ||
| format: argv.format, | ||
| format: argv.format === 'table' ? undefined : argv.format, |
There was a problem hiding this comment.
What is this for? Do you introduce the table format for the lint command? Same for the sarif type above.
There was a problem hiding this comment.
No, lint doesn't get a new format. handleLintConfig runs for every command: the wrapper lints redocly.yaml with that command's --format.
recheck defaults to --format=table, which the config problem formatter doesn't have, so nothing would print; undefined falls back to codeframe.
sarif is one JSON document on stdout, so it skips the separate config report, like json, junit and checkstyle.
I added a comment above the line in ae12d0a.
Core accepts ready plugin objects in loadConfig and createConfig. The recheck command passes the presets of @redocly/recheck as a plugin, so recheck/* entries resolve and merge like any other preset, in extends order. Without the plugin, core skips recheck/* entries. Config.recheckExtends and the special case in bundleExtends are gone.
Presets resolve in core, so validate, lintContent, lintFiles and resolveRecheckConfig take the final rules and no extends key. The Markdoc warning now fires for any enabled markdoc-* rule while markdoc parsing is off. Core no longer exports mergeRecheckRules.
Core passes preset rule objects through by reference, and validation writes defaults into the rules it checks, so validate now works on a copy. This covers lintContent and lintFiles as well as resolveRecheckConfig.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e5dbdb5. Configure here.
| break; | ||
| case 'json': | ||
| outputJsonFormat(prioritized, fileCount, baseline); | ||
| break; |
There was a problem hiding this comment.
JSON report totalIssues reflects capped count, not actual
Medium Severity
When --max-problems is set, generateReport passes only the capped prioritized slice to outputJsonFormat. Inside that formatter, totalIssues: problems.length then reflects the capped count rather than the true total. The table format correctly receives all problems for counting and only the capped set for display, but JSON and SARIF consumers see a totalIssues that understates the real problem count, with no way to know problems were hidden.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e5dbdb5. Configure here.
|
You can ignore linkchecker failures. The 'dead links' it reports all are from this repository and are actually live. Looks like a GitHub availability issue. |
…ptions Brings in the squash-merged engine (#3080), whose preset resolution now runs through the normal extends path. The tests this branch added move from the removed `extends` input to the `presetBlock` helper, and the $ref test loads the presets plugin. # Conflicts: # packages/cli/src/commands/recheck/__tests__/index.test.ts # packages/recheck/src/actions/__tests__/lint.test.ts # packages/recheck/src/config/__tests__/resolve.test.ts # packages/recheck/src/config/resolve.ts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


What/Why/How?
Recheck Phase 2 moves the prose-linting engine from the Redocly monorepo into this repository and adds the
redocly recheckcommand (design: Redocly/redocly#26970).This PR carries both; #3082 held the command and folded into this branch.
Engine package.
packages/recheckjoins the fixed version group.What moved: parser, scopes, rules, runner, auto-fix, directives, config validation, and presets.
Also moved: metrics, reporters, examples, the example generator, and two agent skills.
What did not move: the standalone
cli.ts, the yargs command files, andrecheck.yamldiscovery.The markdownlint parity harness and a drift test that needs the Realm theme source also stayed behind.
The command files became plain actions:
runLint,generateBaseline,runReadability, andgenerateMarkdocSchema.Each takes a resolved config and returns a typed result; the engine prints nothing.
resolveRecheckConfigtakes the orderedrecheck/*names and the merged block, and returns normalized rules.The engine depends on
@redocly/openapi-corefor the rule merge and composes the presets named inextendsitself.Preset entries carry no message;
validatefills a token rule's default at validation.Core.
Core imports nothing from
@redocly/recheck, so it stays lean for browser contexts.config/recheck.tsowns therecheck/prefix check, the reserved plugin id, the rule merge, and the block merge.bundleExtendssetsrecheck/*entries aside inextendsorder, across shared files and apis, intoConfig.recheckExtends.mergeExtendsmerges therecheckblocks it sees, andConfig.recheckholds the result, typed withRecheckConfigfrom@redocly/config.lintreads no rules from either; a custom plugin cannot use the idrecheck.Core pins
@redocly/config0.57, which carries therecheckroot key, socheck-configaccepts the block.Command.
redocly recheck [paths..]lints by default.Action flags select one other action:
--readability,--generate-baseline, or--generate-markdoc-schema(with--from,--output,--check).Runs pick up a
.redocly.recheck-baseline.yamlnext toredocly.yamlby presence; abaselinekey in the block is a config error.--fixapplies to linting only.Output flags:
--format(tabledefault),--stats,--max-problems,--summary,--summary-path.The report goes to stdout and the logs to stderr, so
> filegives a clean JSON or SARIF file.--max-problemsapplies to every format; the table counts every problem and prints the capped rows.Selection flags:
--tags,--rule,--skip-rule; there is no--severity, the same aslint.To scope a run to changed files, pass their paths.
With no
redocly.yaml, the command usesrecheck/markdownand says so on stderr.With a
redocly.yamlthat has norecheck/*preset and norecheckblock, it checks nothing and says so, the same aslint.The command reads the root config; it warns when
apis.<name>carries recheck settings.Config errors print the path where the user wrote the key, such as
recheck.rules.<key>, and a message without the key.The handler loads lazily and throws
AbortFlowErroron a failed run, the same pathlintuses;commandWrapperkeeps its behavior.Printing.
The CLI owns every printed line and every report format.
packages/cli/src/commands/recheck/print.tsturns each result into the lines the engine printed before, through the core logger: progress on stderr, payloads on stdout.The report formats live in
packages/cli/src/commands/recheck/formatters/.The engine's
Loggeris gone, and the e2e snapshots did not change through the rework.One accepted edge: when writing a fixed file fails, the run reports the error without the auto-fix header.
CLI build.
packages/clibundles the engine and the spell-check packages,nspellanddictionary-en; the build copies the dictionary data files next to their chunk.scripts/local-pack.shpacks the engine.Review outcome.
Roman and tatomyr settled the config integration on #3082: tatomyr's plan, the nested block, and core imports the presets from the engine.
Commits 21d0f29 to e3ec9e5 implement it.
The round of 2026-09-26 (AlbinaBlazhko17, vadyvas) made the preset data pure.
On 2026-09-27 Roman reversed the dependency direction: core imports nothing from the engine, and the engine depends on core, as respect-core does.
The round of 2026-09-28 (AlbinaBlazhko17) dropped
--output-path, renamed--out, aligned--max-problemsand the GitHub Actions escaping withlint, and cleaned the error paths.Two asks live elsewhere: Redocly/redocly#28359 drops
baselinefrom the config schema, and Redocly/redocly#28358 tracks one shared schema.I answered the config-shape threads on #3082.
On 2026-10-01 Roman audited the tests (one file per module, exact assertions, test-only exports removed) and trimmed core's recheck exports to
isRecheckPresetandmergeRecheckRules.Follow-ups.
apis.<name>.recheckmerges in core, but the@redocly/configschema rejects it; claim it there or stop merging it.A program that embeds the engine without core now loads core too.
Core's main entry loads in about 96 milliseconds, and the engine alone in about 183 milliseconds.
A core subpath export of
config/recheck.js, like./lib/bundle-oas, would remove that cost; it adds public surface, so it waits for a maintainer's go.A
rulesvalue of the wrong type underextendskeeps the preset rules; config lint warns about it.Reference
Testing
Unit tests cover the engine, the recheck config merge and preset ordering in core, and
selectAction.End-to-end tests live in
tests/e2e/recheck/with ten 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, and conflicting flags.
Gates:
npm run compile,npm run typecheck,npm run lint,npm run format:check,npm run unit, and the recheck and check-config e2e suites.Screenshots (optional)
Check yourself
Security
Note
Medium Risk
Adds a large new package and CLI surface plus config-resolution changes (plugins injection, reserved id, extends merging), though API lint paths stay separate and failures are mostly scoped to the recheck command.
Overview
This PR lands the Recheck markdown/prose linting engine in
packages/recheckand wires it into Redocly CLI asredocly recheck, with coordinated minor releases for CLI, core, and the new package.Engine and command. The engine exposes actions (
runLint,runReadability,generateBaseline,generateMarkdocSchema) that return structured results; the CLI owns reporting (table/JSON/SARIF/GitHub Actions, summaries, auto-fix output). The command supports lint (default),--readability,--generate-baseline, and--generate-markdoc-schema, plus rule selection, baselines besideredocly.yaml, and skipping API description paths for now. Without config it falls back torecheck/markdown; withredocly.yamlbut no recheck preset/block it no-ops with a notice.Config in openapi-core.
loadConfig/createConfigaccept optional readyplugins(used to inject@redocly/recheck/presets). Rootrecheckblock merges on resolvedConfig.recheck;recheck/*inextendsresolves like other presets when the recheck plugin is present and is ignored otherwise.lintdoes not consume recheck rules; plugin idrecheckis reserved.commandWrappercan pass plugins so only the recheck command loads presets.Repo hygiene. Changesets, docs (
recheckcommand andrecheckconfig reference), CONTRIBUTING for the engine, formatter/linter ignores, CLI build copies dictionary-en data for bundled spell-check, and broad unit/e2e coverage.Reviewed by Cursor Bugbot for commit e5dbdb5. Bugbot is set up for automated code reviews on this repo. Configure here.