From ffbb993bca6f4f62a6351f9d52b9b38552ddbb9b Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Wed, 2 Sep 2026 17:23:30 -0500 Subject: [PATCH 01/93] feat(recheck): add the recheck engine package 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. --- .changeset/config.json | 2 +- .oxfmtrc.json | 1 + package-lock.json | 997 ++++++- packages/recheck/CHANGELOG.md | 122 + packages/recheck/CONTRIBUTING.md | 116 + packages/recheck/README.md | 1933 +++++++++++++ .../examples/appendices/google.appendix.yaml | 91 + .../inclusive-language.appendix.yaml | 61 + .../appendices/microsoft.appendix.yaml | 99 + .../appendices/plain-language.appendix.yaml | 88 + .../technical-english.appendix.yaml | 33 + packages/recheck/examples/google.yaml | 1525 ++++++++++ .../recheck/examples/inclusive-language.yaml | 304 ++ packages/recheck/examples/microsoft.yaml | 1542 +++++++++++ packages/recheck/examples/plain-language.yaml | 325 +++ .../recheck/examples/technical-english.yaml | 134 + packages/recheck/package.json | 78 + packages/recheck/presets/google/PROVENANCE.md | 1020 +++++++ packages/recheck/presets/google/sources.json | 192 ++ .../presets/inclusive-language/PROVENANCE.md | 175 ++ .../presets/inclusive-language/sources.json | 107 + .../recheck/presets/microsoft/PROVENANCE.md | 1556 +++++++++++ .../recheck/presets/microsoft/sources.json | 494 ++++ .../presets/plain-language/PROVENANCE.md | 370 +++ .../presets/plain-language/sources.json | 108 + .../presets/technical-english/PROVENANCE.md | 44 + .../recheck/scripts/generate-examples.mjs | 713 +++++ .../recheck/skills/recheck-config/SKILL.md | 66 + packages/recheck/skills/recheck-lint/SKILL.md | 50 + .../recheck/src/__tests__/lint-files.test.ts | 306 +++ .../src/__tests__/markdoc-config.test.ts | 153 ++ .../recheck/src/__tests__/public-api.test.ts | 255 ++ .../public-api-exports.typecheck.ts | 24 + .../config/__tests__/examples-drift.test.ts | 54 + .../config/__tests__/fixtures/google-clean.md | 97 + .../fixtures/google-violations-single-h1.md | 17 + .../__tests__/fixtures/google-violations.md | 208 ++ .../fixtures/inclusive-language-clean.md | 34 + .../fixtures/inclusive-language-violations.md | 34 + .../__tests__/fixtures/markdoc-clean.md | 22 + .../__tests__/fixtures/markdoc-violations.md | 98 + .../__tests__/fixtures/microsoft-clean.md | 225 ++ .../fixtures/microsoft-violations.md | 355 +++ .../fixtures/plain-language-clean.md | 43 + .../fixtures/plain-language-violations.md | 38 + .../fixtures/technical-english-clean.md | 8 + .../fixtures/technical-english-violations.md | 7 + .../__tests__/preset-composition.test.ts | 297 ++ .../preset-detection-only-acceptance.test.ts | 166 ++ .../preset-google-fix-wave-c.test.ts | 337 +++ .../config/__tests__/preset-google.test.ts | 292 ++ .../preset-inclusive-language.test.ts | 47 + .../config/__tests__/preset-markdoc.test.ts | 310 +++ .../config/__tests__/preset-microsoft.test.ts | 1257 +++++++++ .../__tests__/preset-plain-language.test.ts | 62 + .../preset-technical-english.test.ts | 51 + .../src/config/__tests__/presets.test.ts | 954 +++++++ .../__tests__/readme-prose-opt-ins.test.ts | 100 + .../spelling-peer-dependencies.test.ts | 390 +++ .../__tests__/validate-tags-file.test.ts | 137 + .../src/config/__tests__/validate.test.ts | 677 +++++ .../src/config/presets/api-descriptions.ts | 27 + packages/recheck/src/config/presets/google.ts | 1834 +++++++++++++ .../src/config/presets/inclusive-language.ts | 370 +++ packages/recheck/src/config/presets/index.ts | 156 ++ .../recheck/src/config/presets/markdoc.ts | 108 + .../src/config/presets/markdown-relaxed.ts | 101 + .../recheck/src/config/presets/markdown.ts | 139 + .../recheck/src/config/presets/microsoft.ts | 2444 +++++++++++++++++ .../recheck/src/config/presets/minimal.ts | 24 + .../src/config/presets/plain-language.ts | 392 +++ packages/recheck/src/config/presets/prose.ts | 144 + .../src/config/presets/technical-english.ts | 87 + packages/recheck/src/config/schema.ts | 197 ++ packages/recheck/src/config/validate.ts | 1562 +++++++++++ .../src/core/__tests__/auto-fix.test.ts | 244 ++ .../src/core/__tests__/baseline.test.ts | 193 ++ .../src/core/__tests__/case-preserve.test.ts | 48 + .../core/__tests__/crlf-line-endings.test.ts | 71 + .../src/core/__tests__/directives.test.ts | 90 + .../src/core/__tests__/embedded-mode.test.ts | 101 + .../core/__tests__/fix-idempotency.test.ts | 101 + .../src/core/__tests__/image-metadata.test.ts | 241 ++ .../src/core/__tests__/map-limit.test.ts | 42 + .../core/__tests__/markdoc-fix-safety.test.ts | 308 +++ .../src/core/__tests__/readability.test.ts | 55 + .../src/core/__tests__/rule-filters.test.ts | 131 + .../core/__tests__/runner-markdoc-ctx.test.ts | 140 + .../src/core/__tests__/runner-token.test.ts | 258 ++ .../recheck/src/core/__tests__/runner.test.ts | 434 +++ packages/recheck/src/core/auto-fix.ts | 158 ++ packages/recheck/src/core/baseline.ts | Bin 0 -> 7181 bytes packages/recheck/src/core/case-preserve.ts | 54 + packages/recheck/src/core/directives.ts | 92 + packages/recheck/src/core/files.ts | 287 ++ packages/recheck/src/core/inline-code.ts | 112 + packages/recheck/src/core/line-endings.ts | 62 + packages/recheck/src/core/markdoc-tags.ts | 163 ++ packages/recheck/src/core/prose-extract.ts | 97 + packages/recheck/src/core/readability.ts | 59 + packages/recheck/src/core/rule-filters.ts | 146 + packages/recheck/src/core/runner.ts | 514 ++++ packages/recheck/src/core/timing.ts | 38 + .../src/data/__tests__/proper-nouns.test.ts | 129 + .../recheck/src/data/markdoc-realm-schema.ts | 797 ++++++ packages/recheck/src/data/proper-nouns.ts | 46 + .../src/data/realm-front-matter-schema.ts | 74 + packages/recheck/src/index.ts | 208 ++ .../fixtures/committee-proposals.txt | 1 + .../metrics/__tests__/fixtures/expected.json | 53 + .../fixtures/wikipedia-non-euclidean.txt | 1 + .../src/metrics/__tests__/formulas.test.ts | 153 ++ .../src/metrics/__tests__/statistics.test.ts | 95 + .../__tests__/tools/derive-expected.mjs | 127 + packages/recheck/src/metrics/formulas.ts | 82 + packages/recheck/src/metrics/index.ts | 4 + packages/recheck/src/metrics/statistics.ts | 77 + .../src/parser/__tests__/markdoc-flag.test.ts | 156 ++ .../src/parser/__tests__/parser.test.ts | 141 + packages/recheck/src/parser/index.ts | 205 ++ .../markdoc/__tests__/adversarial.test.ts | 82 + .../parser/markdoc/__tests__/pairing.test.ts | 202 ++ .../parser/markdoc/__tests__/schema.test.ts | 82 + .../src/parser/markdoc/__tests__/span.test.ts | 1109 ++++++++ .../parser/markdoc/__tests__/tokenize.test.ts | 496 ++++ .../src/parser/markdoc/extract-statics.ts | 174 ++ .../recheck/src/parser/markdoc/pairing.ts | 164 ++ packages/recheck/src/parser/markdoc/schema.ts | 154 ++ packages/recheck/src/parser/markdoc/span.ts | 848 ++++++ .../recheck/src/parser/markdoc/structure.ts | 206 ++ packages/recheck/src/parser/markdoc/syntax.ts | 394 +++ packages/recheck/src/parser/types.ts | 34 + packages/recheck/src/reporter/fixes.ts | 51 + .../src/reporter/formats/github-actions.ts | 24 + packages/recheck/src/reporter/formats/json.ts | 35 + .../recheck/src/reporter/formats/sarif.ts | 80 + .../recheck/src/reporter/formats/table.ts | 66 + packages/recheck/src/reporter/index.ts | 54 + .../src/reporter/prioritize-problems.ts | 29 + packages/recheck/src/reporter/statistics.ts | 68 + packages/recheck/src/reporter/summary.ts | 66 + packages/recheck/src/rules/CONTRIBUTING.md | 249 ++ .../src/rules/__tests__/registry.test.ts | 22 + .../recheck/src/rules/__tests__/utils.test.ts | 126 + packages/recheck/src/rules/registry.ts | 88 + .../scope/__tests__/capitalization.test.ts | 992 +++++++ .../rules/scope/__tests__/conditional.test.ts | 382 +++ .../rules/scope/__tests__/consistency.test.ts | 629 +++++ .../src/rules/scope/__tests__/helpers.ts | 33 + .../src/rules/scope/__tests__/length.test.ts | 419 +++ .../scope/__tests__/max-image-size.test.ts | 467 ++++ .../src/rules/scope/__tests__/metric.test.ts | 808 ++++++ .../rules/scope/__tests__/occurrence.test.ts | 407 +++ .../src/rules/scope/__tests__/pattern.test.ts | 393 +++ .../rules/scope/__tests__/repetition.test.ts | 411 +++ .../__tests__/semantic-line-breaks.test.ts | 851 ++++++ .../rules/scope/__tests__/spelling.test.ts | 675 +++++ .../src/rules/scope/__tests__/swap.test.ts | 737 +++++ .../rules/scope/__tests__/title-case.test.ts | 351 +++ .../recheck/src/rules/scope/capitalization.ts | 199 ++ .../recheck/src/rules/scope/conditional.ts | 132 + .../recheck/src/rules/scope/consistency.ts | 218 ++ packages/recheck/src/rules/scope/length.ts | 92 + .../recheck/src/rules/scope/max-image-size.ts | 93 + packages/recheck/src/rules/scope/metric.ts | 78 + .../recheck/src/rules/scope/occurrence.ts | 61 + packages/recheck/src/rules/scope/pattern.ts | 84 + .../recheck/src/rules/scope/repetition.ts | 170 ++ .../src/rules/scope/semantic-line-breaks.ts | 275 ++ packages/recheck/src/rules/scope/spelling.ts | 278 ++ packages/recheck/src/rules/scope/swap.ts | 178 ++ .../recheck/src/rules/scope/title-case.ts | 409 +++ .../__tests__/blanks-around-fences.test.ts | 57 + .../__tests__/blanks-around-headings.test.ts | 48 + .../__tests__/blanks-around-lists.test.ts | 73 + .../__tests__/blanks-around-tables.test.ts | 66 + .../token/__tests__/code-block-style.test.ts | 39 + .../token/__tests__/code-fence-style.test.ts | 39 + .../__tests__/commands-show-output.test.ts | 47 + .../rules/token/__tests__/convergence.test.ts | 186 ++ .../__tests__/descriptive-link-text.test.ts | 65 + .../token/__tests__/emphasis-style.test.ts | 52 + .../__tests__/fenced-code-language.test.ts | 46 + .../token/__tests__/first-line-h1.test.ts | 77 + .../token/__tests__/front-matter.test.ts | 217 ++ .../src/rules/token/__tests__/harness.ts | 85 + .../token/__tests__/heading-increment.test.ts | 24 + .../__tests__/heading-start-left.test.ts | 28 + .../token/__tests__/heading-style.test.ts | 33 + .../src/rules/token/__tests__/helpers.test.ts | 325 +++ .../rules/token/__tests__/hr-style.test.ts | 30 + .../rules/token/__tests__/line-length.test.ts | 155 ++ .../token/__tests__/link-fragments.test.ts | 593 ++++ .../link-image-reference-definitions.test.ts | 54 + .../token/__tests__/link-image-style.test.ts | 88 + .../rules/token/__tests__/list-indent.test.ts | 41 + .../rules/token/__tests__/list-length.test.ts | 202 ++ .../token/__tests__/list-marker-space.test.ts | 50 + .../__tests__/markdoc-attributes.test.ts | 375 +++ .../token/__tests__/markdoc-pairing.test.ts | 166 ++ .../token/__tests__/markdoc-syntax.test.ts | 179 ++ .../__tests__/markdoc-unknown-tag.test.ts | 124 + .../rules/token/__tests__/messages.test.ts | 42 + .../rules/token/__tests__/no-alt-text.test.ts | 64 + .../token/__tests__/no-bare-urls.test.ts | 55 + .../__tests__/no-blanks-blockquote.test.ts | 47 + .../__tests__/no-duplicate-heading.test.ts | 63 + .../no-duplicate-link-destinations.test.ts | 68 + .../__tests__/no-emphasis-as-heading.test.ts | 72 + .../token/__tests__/no-empty-headings.test.ts | 56 + .../token/__tests__/no-empty-links.test.ts | 46 + .../token/__tests__/no-hard-tabs.test.ts | 62 + .../token/__tests__/no-inline-html.test.ts | 68 + .../__tests__/no-missing-space-atx.test.ts | 26 + .../no-missing-space-closed-atx.test.ts | 28 + .../__tests__/no-multiple-blanks.test.ts | 37 + .../__tests__/no-multiple-space-atx.test.ts | 26 + .../no-multiple-space-blockquote.test.ts | 40 + .../no-multiple-space-closed-atx.test.ts | 30 + .../token/__tests__/no-reversed-links.test.ts | 40 + .../token/__tests__/no-space-in-code.test.ts | 67 + .../__tests__/no-space-in-emphasis.test.ts | 101 + .../token/__tests__/no-space-in-links.test.ts | 51 + .../__tests__/no-trailing-punctuation.test.ts | 35 + .../__tests__/no-trailing-spaces.test.ts | 67 + .../rules/token/__tests__/ol-prefix.test.ts | 81 + .../token/__tests__/proper-names.test.ts | 60 + .../__tests__/reference-links-images.test.ts | 71 + .../token/__tests__/required-headings.test.ts | 56 + .../rules/token/__tests__/single-h1.test.ts | 57 + .../__tests__/single-trailing-newline.test.ts | 34 + .../token/__tests__/strong-style.test.ts | 50 + .../__tests__/table-column-count.test.ts | 46 + .../__tests__/table-column-style.test.ts | 130 + .../token/__tests__/table-pipe-style.test.ts | 55 + .../rules/token/__tests__/ul-indent.test.ts | 61 + .../rules/token/__tests__/ul-style.test.ts | 67 + .../src/rules/token/blanks-around-fences.ts | 55 + .../src/rules/token/blanks-around-headings.ts | 134 + .../src/rules/token/blanks-around-lists.ts | 78 + .../src/rules/token/blanks-around-tables.ts | 47 + .../src/rules/token/code-block-style.ts | 34 + .../src/rules/token/code-fence-style.ts | 39 + .../src/rules/token/commands-show-output.ts | 45 + .../src/rules/token/descriptive-link-text.ts | 63 + .../recheck/src/rules/token/emphasis-style.ts | 62 + .../src/rules/token/fenced-code-language.ts | 37 + .../recheck/src/rules/token/first-line-h1.ts | 117 + .../recheck/src/rules/token/front-matter.ts | 207 ++ .../src/rules/token/heading-increment.ts | 30 + .../src/rules/token/heading-start-left.ts | 36 + .../recheck/src/rules/token/heading-style.ts | 45 + packages/recheck/src/rules/token/helpers.ts | 868 ++++++ packages/recheck/src/rules/token/hr-style.ts | 31 + packages/recheck/src/rules/token/index.ts | 312 +++ .../recheck/src/rules/token/line-length.ts | 129 + .../recheck/src/rules/token/link-fragments.ts | 472 ++++ .../token/link-image-reference-definitions.ts | 64 + .../src/rules/token/link-image-style.ts | 148 + .../recheck/src/rules/token/list-indent.ts | 64 + .../recheck/src/rules/token/list-length.ts | 60 + .../src/rules/token/list-marker-space.ts | 59 + .../src/rules/token/markdoc-attributes.ts | 326 +++ .../src/rules/token/markdoc-pairing.ts | 108 + .../recheck/src/rules/token/markdoc-syntax.ts | 129 + .../src/rules/token/markdoc-unknown-tag.ts | 72 + packages/recheck/src/rules/token/messages.ts | 25 + .../recheck/src/rules/token/no-alt-text.ts | 54 + .../recheck/src/rules/token/no-bare-urls.ts | 96 + .../src/rules/token/no-blanks-blockquote.ts | 40 + .../src/rules/token/no-duplicate-heading.ts | 113 + .../token/no-duplicate-link-destinations.ts | 78 + .../src/rules/token/no-emphasis-as-heading.ts | 58 + .../src/rules/token/no-empty-headings.ts | 29 + .../recheck/src/rules/token/no-empty-links.ts | 85 + .../recheck/src/rules/token/no-hard-tabs.ts | 99 + .../recheck/src/rules/token/no-inline-html.ts | 54 + .../src/rules/token/no-missing-space-atx.ts | 41 + .../token/no-missing-space-closed-atx.ts | 47 + .../src/rules/token/no-multiple-blanks.ts | 38 + .../src/rules/token/no-multiple-space-atx.ts | 59 + .../token/no-multiple-space-blockquote.ts | 46 + .../token/no-multiple-space-closed-atx.ts | 24 + .../src/rules/token/no-reversed-links.ts | 53 + .../src/rules/token/no-space-in-code.ts | 83 + .../src/rules/token/no-space-in-emphasis.ts | 100 + .../src/rules/token/no-space-in-links.ts | 60 + .../rules/token/no-trailing-punctuation.ts | 44 + .../src/rules/token/no-trailing-spaces.ts | 106 + packages/recheck/src/rules/token/ol-prefix.ts | 78 + .../recheck/src/rules/token/proper-names.ts | 113 + .../src/rules/token/reference-links-images.ts | 46 + .../src/rules/token/required-headings.ts | 84 + packages/recheck/src/rules/token/single-h1.ts | 74 + .../rules/token/single-trailing-newline.ts | 28 + .../recheck/src/rules/token/strong-style.ts | 62 + .../src/rules/token/table-column-count.ts | 47 + .../src/rules/token/table-column-style.ts | 218 ++ .../src/rules/token/table-pipe-style.ts | 66 + packages/recheck/src/rules/token/ul-indent.ts | 77 + packages/recheck/src/rules/token/ul-style.ts | 102 + packages/recheck/src/rules/types.ts | 81 + packages/recheck/src/rules/utils.ts | 105 + .../src/scopes/__tests__/extractor.test.ts | 609 ++++ .../scopes/__tests__/markdoc-scope.test.ts | 348 +++ .../src/scopes/__tests__/selector.test.ts | 216 ++ .../src/scopes/__tests__/sentences.test.ts | 263 ++ packages/recheck/src/scopes/extractor.ts | 569 ++++ packages/recheck/src/scopes/selector.ts | 143 + packages/recheck/src/scopes/sentences.ts | 199 ++ packages/recheck/src/scopes/types.ts | 47 + packages/recheck/src/scopes/vocabulary.ts | 73 + .../assertion-config.typecheck.ts | 28 + packages/recheck/src/types/assertions.ts | 224 ++ packages/recheck/src/types/index.ts | 5 + packages/recheck/src/types/nspell.d.ts | 6 + packages/recheck/src/types/problems.ts | 22 + packages/recheck/src/types/reporting.ts | 36 + packages/recheck/src/types/rules.ts | 63 + packages/recheck/src/types/validation.ts | 5 + packages/recheck/src/utils/is-plain-object.ts | 4 + packages/recheck/tsconfig.json | 10 + tsconfig.build.json | 3 +- vitest.config.ts | 1 + 324 files changed, 63118 insertions(+), 62 deletions(-) create mode 100644 packages/recheck/CHANGELOG.md create mode 100644 packages/recheck/CONTRIBUTING.md create mode 100644 packages/recheck/README.md create mode 100644 packages/recheck/examples/appendices/google.appendix.yaml create mode 100644 packages/recheck/examples/appendices/inclusive-language.appendix.yaml create mode 100644 packages/recheck/examples/appendices/microsoft.appendix.yaml create mode 100644 packages/recheck/examples/appendices/plain-language.appendix.yaml create mode 100644 packages/recheck/examples/appendices/technical-english.appendix.yaml create mode 100644 packages/recheck/examples/google.yaml create mode 100644 packages/recheck/examples/inclusive-language.yaml create mode 100644 packages/recheck/examples/microsoft.yaml create mode 100644 packages/recheck/examples/plain-language.yaml create mode 100644 packages/recheck/examples/technical-english.yaml create mode 100644 packages/recheck/package.json create mode 100644 packages/recheck/presets/google/PROVENANCE.md create mode 100644 packages/recheck/presets/google/sources.json create mode 100644 packages/recheck/presets/inclusive-language/PROVENANCE.md create mode 100644 packages/recheck/presets/inclusive-language/sources.json create mode 100644 packages/recheck/presets/microsoft/PROVENANCE.md create mode 100644 packages/recheck/presets/microsoft/sources.json create mode 100644 packages/recheck/presets/plain-language/PROVENANCE.md create mode 100644 packages/recheck/presets/plain-language/sources.json create mode 100644 packages/recheck/presets/technical-english/PROVENANCE.md create mode 100644 packages/recheck/scripts/generate-examples.mjs create mode 100644 packages/recheck/skills/recheck-config/SKILL.md create mode 100644 packages/recheck/skills/recheck-lint/SKILL.md create mode 100644 packages/recheck/src/__tests__/lint-files.test.ts create mode 100644 packages/recheck/src/__tests__/markdoc-config.test.ts create mode 100644 packages/recheck/src/__tests__/public-api.test.ts create mode 100644 packages/recheck/src/__typecheck__/public-api-exports.typecheck.ts create mode 100644 packages/recheck/src/config/__tests__/examples-drift.test.ts create mode 100644 packages/recheck/src/config/__tests__/fixtures/google-clean.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/google-violations-single-h1.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/google-violations.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/inclusive-language-clean.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/inclusive-language-violations.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/markdoc-clean.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/markdoc-violations.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/microsoft-clean.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/microsoft-violations.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/plain-language-clean.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/plain-language-violations.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/technical-english-clean.md create mode 100644 packages/recheck/src/config/__tests__/fixtures/technical-english-violations.md create mode 100644 packages/recheck/src/config/__tests__/preset-composition.test.ts create mode 100644 packages/recheck/src/config/__tests__/preset-detection-only-acceptance.test.ts create mode 100644 packages/recheck/src/config/__tests__/preset-google-fix-wave-c.test.ts create mode 100644 packages/recheck/src/config/__tests__/preset-google.test.ts create mode 100644 packages/recheck/src/config/__tests__/preset-inclusive-language.test.ts create mode 100644 packages/recheck/src/config/__tests__/preset-markdoc.test.ts create mode 100644 packages/recheck/src/config/__tests__/preset-microsoft.test.ts create mode 100644 packages/recheck/src/config/__tests__/preset-plain-language.test.ts create mode 100644 packages/recheck/src/config/__tests__/preset-technical-english.test.ts create mode 100644 packages/recheck/src/config/__tests__/presets.test.ts create mode 100644 packages/recheck/src/config/__tests__/readme-prose-opt-ins.test.ts create mode 100644 packages/recheck/src/config/__tests__/spelling-peer-dependencies.test.ts create mode 100644 packages/recheck/src/config/__tests__/validate-tags-file.test.ts create mode 100644 packages/recheck/src/config/__tests__/validate.test.ts create mode 100644 packages/recheck/src/config/presets/api-descriptions.ts create mode 100644 packages/recheck/src/config/presets/google.ts create mode 100644 packages/recheck/src/config/presets/inclusive-language.ts create mode 100644 packages/recheck/src/config/presets/index.ts create mode 100644 packages/recheck/src/config/presets/markdoc.ts create mode 100644 packages/recheck/src/config/presets/markdown-relaxed.ts create mode 100644 packages/recheck/src/config/presets/markdown.ts create mode 100644 packages/recheck/src/config/presets/microsoft.ts create mode 100644 packages/recheck/src/config/presets/minimal.ts create mode 100644 packages/recheck/src/config/presets/plain-language.ts create mode 100644 packages/recheck/src/config/presets/prose.ts create mode 100644 packages/recheck/src/config/presets/technical-english.ts create mode 100644 packages/recheck/src/config/schema.ts create mode 100644 packages/recheck/src/config/validate.ts create mode 100644 packages/recheck/src/core/__tests__/auto-fix.test.ts create mode 100644 packages/recheck/src/core/__tests__/baseline.test.ts create mode 100644 packages/recheck/src/core/__tests__/case-preserve.test.ts create mode 100644 packages/recheck/src/core/__tests__/crlf-line-endings.test.ts create mode 100644 packages/recheck/src/core/__tests__/directives.test.ts create mode 100644 packages/recheck/src/core/__tests__/embedded-mode.test.ts create mode 100644 packages/recheck/src/core/__tests__/fix-idempotency.test.ts create mode 100644 packages/recheck/src/core/__tests__/image-metadata.test.ts create mode 100644 packages/recheck/src/core/__tests__/map-limit.test.ts create mode 100644 packages/recheck/src/core/__tests__/markdoc-fix-safety.test.ts create mode 100644 packages/recheck/src/core/__tests__/readability.test.ts create mode 100644 packages/recheck/src/core/__tests__/rule-filters.test.ts create mode 100644 packages/recheck/src/core/__tests__/runner-markdoc-ctx.test.ts create mode 100644 packages/recheck/src/core/__tests__/runner-token.test.ts create mode 100644 packages/recheck/src/core/__tests__/runner.test.ts create mode 100644 packages/recheck/src/core/auto-fix.ts create mode 100644 packages/recheck/src/core/baseline.ts create mode 100644 packages/recheck/src/core/case-preserve.ts create mode 100644 packages/recheck/src/core/directives.ts create mode 100644 packages/recheck/src/core/files.ts create mode 100644 packages/recheck/src/core/inline-code.ts create mode 100644 packages/recheck/src/core/line-endings.ts create mode 100644 packages/recheck/src/core/markdoc-tags.ts create mode 100644 packages/recheck/src/core/prose-extract.ts create mode 100644 packages/recheck/src/core/readability.ts create mode 100644 packages/recheck/src/core/rule-filters.ts create mode 100644 packages/recheck/src/core/runner.ts create mode 100644 packages/recheck/src/core/timing.ts create mode 100644 packages/recheck/src/data/__tests__/proper-nouns.test.ts create mode 100644 packages/recheck/src/data/markdoc-realm-schema.ts create mode 100644 packages/recheck/src/data/proper-nouns.ts create mode 100644 packages/recheck/src/data/realm-front-matter-schema.ts create mode 100644 packages/recheck/src/index.ts create mode 100644 packages/recheck/src/metrics/__tests__/fixtures/committee-proposals.txt create mode 100644 packages/recheck/src/metrics/__tests__/fixtures/expected.json create mode 100644 packages/recheck/src/metrics/__tests__/fixtures/wikipedia-non-euclidean.txt create mode 100644 packages/recheck/src/metrics/__tests__/formulas.test.ts create mode 100644 packages/recheck/src/metrics/__tests__/statistics.test.ts create mode 100644 packages/recheck/src/metrics/__tests__/tools/derive-expected.mjs create mode 100644 packages/recheck/src/metrics/formulas.ts create mode 100644 packages/recheck/src/metrics/index.ts create mode 100644 packages/recheck/src/metrics/statistics.ts create mode 100644 packages/recheck/src/parser/__tests__/markdoc-flag.test.ts create mode 100644 packages/recheck/src/parser/__tests__/parser.test.ts create mode 100644 packages/recheck/src/parser/index.ts create mode 100644 packages/recheck/src/parser/markdoc/__tests__/adversarial.test.ts create mode 100644 packages/recheck/src/parser/markdoc/__tests__/pairing.test.ts create mode 100644 packages/recheck/src/parser/markdoc/__tests__/schema.test.ts create mode 100644 packages/recheck/src/parser/markdoc/__tests__/span.test.ts create mode 100644 packages/recheck/src/parser/markdoc/__tests__/tokenize.test.ts create mode 100644 packages/recheck/src/parser/markdoc/extract-statics.ts create mode 100644 packages/recheck/src/parser/markdoc/pairing.ts create mode 100644 packages/recheck/src/parser/markdoc/schema.ts create mode 100644 packages/recheck/src/parser/markdoc/span.ts create mode 100644 packages/recheck/src/parser/markdoc/structure.ts create mode 100644 packages/recheck/src/parser/markdoc/syntax.ts create mode 100644 packages/recheck/src/parser/types.ts create mode 100644 packages/recheck/src/reporter/fixes.ts create mode 100644 packages/recheck/src/reporter/formats/github-actions.ts create mode 100644 packages/recheck/src/reporter/formats/json.ts create mode 100644 packages/recheck/src/reporter/formats/sarif.ts create mode 100644 packages/recheck/src/reporter/formats/table.ts create mode 100644 packages/recheck/src/reporter/index.ts create mode 100644 packages/recheck/src/reporter/prioritize-problems.ts create mode 100644 packages/recheck/src/reporter/statistics.ts create mode 100644 packages/recheck/src/reporter/summary.ts create mode 100644 packages/recheck/src/rules/CONTRIBUTING.md create mode 100644 packages/recheck/src/rules/__tests__/registry.test.ts create mode 100644 packages/recheck/src/rules/__tests__/utils.test.ts create mode 100644 packages/recheck/src/rules/registry.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/capitalization.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/conditional.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/consistency.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/helpers.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/length.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/max-image-size.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/metric.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/occurrence.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/pattern.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/repetition.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/semantic-line-breaks.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/spelling.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/swap.test.ts create mode 100644 packages/recheck/src/rules/scope/__tests__/title-case.test.ts create mode 100644 packages/recheck/src/rules/scope/capitalization.ts create mode 100644 packages/recheck/src/rules/scope/conditional.ts create mode 100644 packages/recheck/src/rules/scope/consistency.ts create mode 100644 packages/recheck/src/rules/scope/length.ts create mode 100644 packages/recheck/src/rules/scope/max-image-size.ts create mode 100644 packages/recheck/src/rules/scope/metric.ts create mode 100644 packages/recheck/src/rules/scope/occurrence.ts create mode 100644 packages/recheck/src/rules/scope/pattern.ts create mode 100644 packages/recheck/src/rules/scope/repetition.ts create mode 100644 packages/recheck/src/rules/scope/semantic-line-breaks.ts create mode 100644 packages/recheck/src/rules/scope/spelling.ts create mode 100644 packages/recheck/src/rules/scope/swap.ts create mode 100644 packages/recheck/src/rules/scope/title-case.ts create mode 100644 packages/recheck/src/rules/token/__tests__/blanks-around-fences.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/blanks-around-headings.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/blanks-around-lists.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/blanks-around-tables.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/code-block-style.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/code-fence-style.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/commands-show-output.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/convergence.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/descriptive-link-text.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/emphasis-style.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/fenced-code-language.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/first-line-h1.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/front-matter.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/harness.ts create mode 100644 packages/recheck/src/rules/token/__tests__/heading-increment.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/heading-start-left.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/heading-style.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/helpers.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/hr-style.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/line-length.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/link-fragments.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/link-image-reference-definitions.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/link-image-style.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/list-indent.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/list-length.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/list-marker-space.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/markdoc-attributes.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/markdoc-pairing.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/markdoc-syntax.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/markdoc-unknown-tag.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/messages.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-alt-text.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-bare-urls.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-blanks-blockquote.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-duplicate-heading.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-duplicate-link-destinations.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-emphasis-as-heading.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-empty-headings.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-empty-links.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-hard-tabs.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-inline-html.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-missing-space-atx.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-missing-space-closed-atx.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-multiple-blanks.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-multiple-space-atx.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-multiple-space-blockquote.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-multiple-space-closed-atx.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-reversed-links.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-space-in-code.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-space-in-emphasis.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-space-in-links.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-trailing-punctuation.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/no-trailing-spaces.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/ol-prefix.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/proper-names.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/reference-links-images.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/required-headings.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/single-h1.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/single-trailing-newline.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/strong-style.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/table-column-count.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/table-column-style.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/table-pipe-style.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/ul-indent.test.ts create mode 100644 packages/recheck/src/rules/token/__tests__/ul-style.test.ts create mode 100644 packages/recheck/src/rules/token/blanks-around-fences.ts create mode 100644 packages/recheck/src/rules/token/blanks-around-headings.ts create mode 100644 packages/recheck/src/rules/token/blanks-around-lists.ts create mode 100644 packages/recheck/src/rules/token/blanks-around-tables.ts create mode 100644 packages/recheck/src/rules/token/code-block-style.ts create mode 100644 packages/recheck/src/rules/token/code-fence-style.ts create mode 100644 packages/recheck/src/rules/token/commands-show-output.ts create mode 100644 packages/recheck/src/rules/token/descriptive-link-text.ts create mode 100644 packages/recheck/src/rules/token/emphasis-style.ts create mode 100644 packages/recheck/src/rules/token/fenced-code-language.ts create mode 100644 packages/recheck/src/rules/token/first-line-h1.ts create mode 100644 packages/recheck/src/rules/token/front-matter.ts create mode 100644 packages/recheck/src/rules/token/heading-increment.ts create mode 100644 packages/recheck/src/rules/token/heading-start-left.ts create mode 100644 packages/recheck/src/rules/token/heading-style.ts create mode 100644 packages/recheck/src/rules/token/helpers.ts create mode 100644 packages/recheck/src/rules/token/hr-style.ts create mode 100644 packages/recheck/src/rules/token/index.ts create mode 100644 packages/recheck/src/rules/token/line-length.ts create mode 100644 packages/recheck/src/rules/token/link-fragments.ts create mode 100644 packages/recheck/src/rules/token/link-image-reference-definitions.ts create mode 100644 packages/recheck/src/rules/token/link-image-style.ts create mode 100644 packages/recheck/src/rules/token/list-indent.ts create mode 100644 packages/recheck/src/rules/token/list-length.ts create mode 100644 packages/recheck/src/rules/token/list-marker-space.ts create mode 100644 packages/recheck/src/rules/token/markdoc-attributes.ts create mode 100644 packages/recheck/src/rules/token/markdoc-pairing.ts create mode 100644 packages/recheck/src/rules/token/markdoc-syntax.ts create mode 100644 packages/recheck/src/rules/token/markdoc-unknown-tag.ts create mode 100644 packages/recheck/src/rules/token/messages.ts create mode 100644 packages/recheck/src/rules/token/no-alt-text.ts create mode 100644 packages/recheck/src/rules/token/no-bare-urls.ts create mode 100644 packages/recheck/src/rules/token/no-blanks-blockquote.ts create mode 100644 packages/recheck/src/rules/token/no-duplicate-heading.ts create mode 100644 packages/recheck/src/rules/token/no-duplicate-link-destinations.ts create mode 100644 packages/recheck/src/rules/token/no-emphasis-as-heading.ts create mode 100644 packages/recheck/src/rules/token/no-empty-headings.ts create mode 100644 packages/recheck/src/rules/token/no-empty-links.ts create mode 100644 packages/recheck/src/rules/token/no-hard-tabs.ts create mode 100644 packages/recheck/src/rules/token/no-inline-html.ts create mode 100644 packages/recheck/src/rules/token/no-missing-space-atx.ts create mode 100644 packages/recheck/src/rules/token/no-missing-space-closed-atx.ts create mode 100644 packages/recheck/src/rules/token/no-multiple-blanks.ts create mode 100644 packages/recheck/src/rules/token/no-multiple-space-atx.ts create mode 100644 packages/recheck/src/rules/token/no-multiple-space-blockquote.ts create mode 100644 packages/recheck/src/rules/token/no-multiple-space-closed-atx.ts create mode 100644 packages/recheck/src/rules/token/no-reversed-links.ts create mode 100644 packages/recheck/src/rules/token/no-space-in-code.ts create mode 100644 packages/recheck/src/rules/token/no-space-in-emphasis.ts create mode 100644 packages/recheck/src/rules/token/no-space-in-links.ts create mode 100644 packages/recheck/src/rules/token/no-trailing-punctuation.ts create mode 100644 packages/recheck/src/rules/token/no-trailing-spaces.ts create mode 100644 packages/recheck/src/rules/token/ol-prefix.ts create mode 100644 packages/recheck/src/rules/token/proper-names.ts create mode 100644 packages/recheck/src/rules/token/reference-links-images.ts create mode 100644 packages/recheck/src/rules/token/required-headings.ts create mode 100644 packages/recheck/src/rules/token/single-h1.ts create mode 100644 packages/recheck/src/rules/token/single-trailing-newline.ts create mode 100644 packages/recheck/src/rules/token/strong-style.ts create mode 100644 packages/recheck/src/rules/token/table-column-count.ts create mode 100644 packages/recheck/src/rules/token/table-column-style.ts create mode 100644 packages/recheck/src/rules/token/table-pipe-style.ts create mode 100644 packages/recheck/src/rules/token/ul-indent.ts create mode 100644 packages/recheck/src/rules/token/ul-style.ts create mode 100644 packages/recheck/src/rules/types.ts create mode 100644 packages/recheck/src/rules/utils.ts create mode 100644 packages/recheck/src/scopes/__tests__/extractor.test.ts create mode 100644 packages/recheck/src/scopes/__tests__/markdoc-scope.test.ts create mode 100644 packages/recheck/src/scopes/__tests__/selector.test.ts create mode 100644 packages/recheck/src/scopes/__tests__/sentences.test.ts create mode 100644 packages/recheck/src/scopes/extractor.ts create mode 100644 packages/recheck/src/scopes/selector.ts create mode 100644 packages/recheck/src/scopes/sentences.ts create mode 100644 packages/recheck/src/scopes/types.ts create mode 100644 packages/recheck/src/scopes/vocabulary.ts create mode 100644 packages/recheck/src/types/__typecheck__/assertion-config.typecheck.ts create mode 100644 packages/recheck/src/types/assertions.ts create mode 100644 packages/recheck/src/types/index.ts create mode 100644 packages/recheck/src/types/nspell.d.ts create mode 100644 packages/recheck/src/types/problems.ts create mode 100644 packages/recheck/src/types/reporting.ts create mode 100644 packages/recheck/src/types/rules.ts create mode 100644 packages/recheck/src/types/validation.ts create mode 100644 packages/recheck/src/utils/is-plain-object.ts create mode 100644 packages/recheck/tsconfig.json diff --git a/.changeset/config.json b/.changeset/config.json index 8e494ba8fc..b90a77748a 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -2,7 +2,7 @@ "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", "changelog": "./format.js", "commit": false, - "fixed": [["@redocly/cli", "@redocly/openapi-core", "@redocly/respect-core"]], + "fixed": [["@redocly/cli", "@redocly/openapi-core", "@redocly/respect-core", "@redocly/recheck"]], "linked": [], "access": "public", "baseBranch": "main", diff --git a/.oxfmtrc.json b/.oxfmtrc.json index c3005c873d..633728c596 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -9,6 +9,7 @@ "output/", "packages/respect-core/src/modules/runtime-expressions/abnf-parser.js", "packages/core/src/rules/common/__tests__/fixtures/invalid-yaml.yaml", + "packages/recheck/src/config/__tests__/fixtures/", "tests/performance/api-definitions/", "tests/e2e/generate-client/examples/*/src/api/", "tests/e2e/generate-client/examples/*/generators/", diff --git a/package-lock.json b/package-lock.json index 83b424b7df..466bfa732c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1722,6 +1722,32 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/@markdoc/markdoc": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@markdoc/markdoc/-/markdoc-0.5.9.tgz", + "integrity": "sha512-req3Wpi1xXIs7C+n2nsKv7oGeC0xREkpd+D+JRGZ/pIMXEuQSlT+s/I760fqdmiysvkx10EC3MGlScokRRcaSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.7.0" + }, + "optionalDependencies": { + "@types/linkify-it": "^3.0.1", + "@types/markdown-it": "12.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", @@ -3052,6 +3078,10 @@ "resolved": "packages/core", "link": true }, + "node_modules/@redocly/recheck": { + "resolved": "packages/recheck", + "link": true + }, "node_modules/@redocly/respect-core": { "resolved": "packages/respect-core", "link": true @@ -3166,9 +3196,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3186,9 +3213,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3206,9 +3230,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3226,9 +3247,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3246,9 +3264,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3266,9 +3281,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3738,6 +3750,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -3773,6 +3794,13 @@ "integrity": "sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==", "dev": true }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-pointer": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/@types/json-pointer/-/json-pointer-1.0.34.tgz", @@ -3786,6 +3814,32 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, "node_modules/@types/mdast": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz", @@ -3795,6 +3849,20 @@ "@types/unist": "^2" } }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.20.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", @@ -3872,8 +3940,7 @@ "node_modules/@types/unist": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.7.tgz", - "integrity": "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==", - "dev": true + "integrity": "sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g==" }, "node_modules/@types/yargs": { "version": "17.0.32", @@ -5078,6 +5145,15 @@ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -5301,7 +5377,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5328,6 +5403,29 @@ "integrity": "sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==", "dev": true }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decode-named-character-reference/node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5350,7 +5448,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5376,6 +5473,30 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dictionary-en": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/dictionary-en/-/dictionary-en-4.0.0.tgz", + "integrity": "sha512-3NHnE1uq33ZE/CIwaZ6gqxa4BnglHnxeAcTM0GJ7cmtRGcvX9InMK/IqLtYcUMFCUpUNgybH+DzkqdgAo3F1zg==", + "dev": true, + "license": "(MIT AND BSD)", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -6253,6 +6374,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6343,6 +6477,14 @@ "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -6430,10 +6572,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "license": "MIT", "engines": { "node": ">=18" @@ -7090,7 +7231,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, "funding": [ { "type": "github", @@ -7362,6 +7502,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7539,9 +7695,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7563,9 +7716,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7587,9 +7737,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7611,9 +7758,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8512,6 +8656,158 @@ "parse-entities": "^2.0.0" } }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-directive/node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/micromark-extension-directive/node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/micromark-extension-directive/node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/micromark-extension-directive/node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/micromark-extension-directive/node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/micromark-extension-directive/node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/micromark-extension-directive/node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-gfm": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz", @@ -8543,6 +8839,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-gfm-strikethrough": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz", @@ -8592,33 +8908,403 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, - "engines": { - "node": ">=8.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/milliparsec": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/milliparsec/-/milliparsec-4.0.0.tgz", - "integrity": "sha512-/wk9d4Z6/9ZvoEH/6BI4TrTCgmkpZPuSRN/6fI9aUHOfXdNTuj/VhLS7d+NqG26bi6L9YmGXutVYvWC8zQ0qtA==", - "dev": true, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">=20" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/mime": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", - "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/milliparsec": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/milliparsec/-/milliparsec-4.0.0.tgz", + "integrity": "sha512-/wk9d4Z6/9ZvoEH/6BI4TrTCgmkpZPuSRN/6fI9aUHOfXdNTuj/VhLS7d+NqG26bi6L9YmGXutVYvWC8zQ0qtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/mime": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", "dev": true, "funding": [ "https://github.com/sponsors/broofa" @@ -8812,7 +9498,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msw": { @@ -9041,6 +9726,16 @@ "node": ">=18" } }, + "node_modules/nspell": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/nspell/-/nspell-2.1.5.tgz", + "integrity": "sha512-PSStyugKMiD9mHmqI/CR5xXrSIGejUXPlo88FBRq5Og1kO5QwQ5Ilu8D8O5I/SHpoS+mibpw6uKA8rd3vXd2Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^2.0.0" + } + }, "node_modules/oas-kit-common": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", @@ -12290,6 +12985,188 @@ "integrity": "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==", "license": "MIT" }, + "packages/recheck": { + "name": "@redocly/recheck", + "version": "2.51.0", + "license": "MIT", + "dependencies": { + "@redocly/ajv": "^8.18.3", + "ajv-formats": "^3.0.1", + "colorette": "^2.0.20", + "js-yaml": "^4.3.1", + "micromark": "^4.0.2", + "micromark-extension-directive": "^4.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "micromark-extension-gfm-autolink-literal": "^2.1.0", + "micromark-extension-gfm-footnote": "^2.1.0", + "micromark-extension-gfm-table": "^2.1.1", + "micromark-extension-math": "^3.1.0", + "micromark-util-symbol": "^2.0.0", + "picomatch": "^4.0.4", + "string-width": "^8.2.1" + }, + "devDependencies": { + "@markdoc/markdoc": "^0.5.2", + "@types/js-yaml": "^4.0.9", + "@types/picomatch": "^2.3.4", + "dictionary-en": "^4.0.0", + "micromark-util-types": "^2.0.2", + "nspell": "^2.1.5", + "typescript": "6.0.2" + }, + "engines": { + "node": ">=22.12.0 || >=20.19.0 <21.0.0", + "npm": ">=10" + }, + "peerDependencies": { + "dictionary-en": "^4.0.0", + "nspell": "^2.1.5" + }, + "peerDependenciesMeta": { + "dictionary-en": { + "optional": true + }, + "nspell": { + "optional": true + } + } + }, + "packages/recheck/node_modules/@types/picomatch": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.4.tgz", + "integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==", + "dev": true, + "license": "MIT" + }, + "packages/recheck/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "packages/recheck/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "packages/recheck/node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "packages/recheck/node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/recheck/node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/recheck/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "packages/recheck/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/recheck/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "packages/respect-core": { "name": "@redocly/respect-core", "version": "2.52.0", diff --git a/packages/recheck/CHANGELOG.md b/packages/recheck/CHANGELOG.md new file mode 100644 index 0000000000..11fccc6f8a --- /dev/null +++ b/packages/recheck/CHANGELOG.md @@ -0,0 +1,122 @@ +# @redocly/recheck + +## 0.12.0 + +### Minor Changes + +- f564df1f13f: Added an embedded mode for markdown that lives inside another document, such as an OpenAPI `description` field. + The engine's `embedded` option parses a leading `---` as content instead of front matter, and drops the rules embedded markdown cannot support, even when a config names them. + Added the `recheck/api-descriptions` preset: the markdown rules that make sense for description fields, curated from measurements over two large API definitions. + +## 0.11.1 + +### Patch Changes + +- ecf6b33ae2b: Fixed `link-fragments` reporting valid deep links to `accordion` Markdoc tags as broken. + The rule resolves accordion anchors from the `title` attribute, the same way the theme generates ids for `details` elements, in the same file and across files with `crossFile`. + +## 0.11.0 + +### Minor Changes + +- 074add45843: Added a built-in Realm front matter schema, selected with `schema: realm`, that validates the options a Realm page accepts without vendoring a copy that goes stale. + Added a `strict: true` option that turns a misspelled front matter option into a finding. + +## 0.10.0 + +### Minor Changes + +- 0c017e5da0b: Unified Recheck into one command, matching the interface planned for `redocly recheck`: linting runs by default, and the other actions moved to flags. + `recheck run ` becomes `recheck `, `recheck readability ` becomes `recheck --readability`, `recheck baseline ` becomes `recheck --generate-baseline`, `recheck validate` becomes `recheck --validate-config`, and `recheck markdoc-schema` becomes `recheck --generate-markdoc-schema`. + A removed subcommand fails with the exact replacement invocation. + +### Patch Changes + +- 04f17f80cb7: Fixed sentence detection in blockquotes: quote continuation markers no longer glue sentences together, and a blank line (quoted or not) now always ends a sentence, even without terminal punctuation. + A leading bold label (`**Label:** Description.`) no longer counts toward a sentence's word count in `length` rules such as `technical-english/sentence-length`. +- fa0297e8cf2: Fenced code, html blocks, and comments nested inside blockquotes or list items no longer count toward sentence-scoped rules: sentences now derive only from the prose around them. + The `run` command now reports an error when given more than one path instead of silently scanning only the first. +- b9771145bba: Fixed sentence splitting at: indented continuation lines, terminators inside closing emphasis or quotes, next sentences that open with emphasis markers, and Markdown hard breaks. + Sentences inside multi-line list items, blockquotes, and Markdoc tag bodies were joined into one span. + +## 0.9.0 + +### Minor Changes + +- 5bb15bcbb85: Added `recheck/technical-english`: a preset that helps writers follow the principles of ASD-STE100 Simplified Technical English — sentence length, paragraph length, and passive voice. + ASD-STE100 Simplified Technical English is a Copyright and a Trade Mark of ASD, Brussels, Belgium. + + The preset is an independent work, not endorsed by ASD or the STEMG, and reproduces no part of the standard. + +### Patch Changes + +- b1ae95e5f84: Updated the built-in Realm Markdoc schema with additional attributes for the `card` tag: `linkIcon`, `cta`, `iconColor`, `badge`, `badgeColor`, and `badgeIcon`. + +## 0.8.0 + +### Minor Changes + +- 1c5d579c05a: Added two agent skills to the npm package under `skills/`: + - `recheck-lint` tells an agent to run recheck on touched Markdown before committing or outputting it + - `recheck-config` guides writing and tuning `recheck.yaml` from measured counts + Copy them into `.claude/skills/` (or an equivalent) to make every agent a recheck user. +- f1bc475d0e7: Cross-file link validation was improved to correctly resolve links to `
` sections, prevent Markdoc tags in headings from changing anchors, and support an `ignoredTargets` option to skip renderer-generated routes by glob. + +## 0.7.0 + +### Minor Changes + +- 50a631cb634: Added the `front-matter` rule: it validates front matter against JSON Schema with `@redocly/ajv`, the validator Redocly CLI uses. + Map file patterns to inline schemas or schema files; findings point at the offending key's line, a file with no front matter validates as an empty object, and invalid YAML is reported at the block start. + +## 0.6.0 + +### Minor Changes + +- 17ffa8b9b99: Added `rootDir` to the `link-fragments` assertion: with `crossFile: true`, site-root absolute links (`/x/y`) resolve against it, with the same existence and fragment checks as relative links. + For a monorepo with several docs projects, `rootDir` also takes a map from source-directory prefix to that directory's site root, and the longest matching prefix wins. + Paths are relative to the working directory, and absolute links stay skipped when the option is not set. + +## 0.5.0 + +### Minor Changes + +- b8b29053f50: Added a baseline: `recheck baseline` records the error findings that exist today as one count per file per rule, and `recheck run` with a `baseline` config key then fails only on new findings, suppresses matched ones, and fails when the baseline goes stale so counts only step down. +- 39767b060b8: Added `crossFile: true` to the `link-fragments` assertion: relative link and image targets must exist on disk, and `file.md#anchor` fragments must exist in the target file. + Extensionless links resolve like the Realm router (`./page` tries `page.md`; a directory link reads its `index.md`), targets are cached by modification time, and the option is off by default. +- 89302ace3c3: Added `recheck readability`: per-file Flesch reading ease, Flesch-Kincaid grade, Automated Readability Index, words, and sentences, with medians, as a table or JSON. + It reads the same prose and formulas as the `metric` assertion, so reported scores and gated bounds can never disagree, and it replaces the always-report threshold workaround previously needed to get scores out of a lint run. +- 1d404e664ee: The `semantic-line-breaks` assertion checks list items. It reports bullet or numbered items with two sentences on one line and reflows them with `--fix` to align continuation lines under the list content. Nested pseudo-marker lines (e.g., `- 1. Text`, `- a. Text`) remain excluded. +- 73e4213006c: Added `accordion` and `accordion-group` Markdoc tags to the built-in `realm` schema. + +## 0.4.1 + +### Patch Changes + +- 66ea4ea7448: Fixed readability metric scores to match standard readability tools: headings are excluded from the score, and a block with no end punctuation, such as a list item or a table cell, now ends a sentence, so list-heavy pages no longer score far below zero. + +## 0.4.0 + +### Minor Changes + +- 34637eddbc0: Added a top-level `excludes` option so a shared ignore list is declared once instead of repeated on every rule, and marked which findings `--fix` can repair with a `[fixable]` tag in the table output and a `fixable` field in JSON output. + +### Patch Changes + +- 34637eddbc0: Fixed an issue where a period inside a link, as in `See [Step 1. Configure](#step-1) for details.`, was read as the end of a sentence and caused false `semantic-line-breaks` reports. + +## 0.3.0 + +### Minor Changes + +- 45993f8f036: Adds `--rule` and `--exclude-rule` to `recheck run`, so you can work through findings one rule at a time instead of facing the whole list at once. Both take the short rule name shown in the report or the full config key, and both can be repeated. A name that matches no rule in your config is an error rather than an empty run. + +### Patch Changes + +- ddd48125fc2: Fixed five false positives in `capitalization` rules, including incorrect lowercasing of the first word and improper capitalization after inline code. The rule now better distinguishes sentence boundaries, preserving correct casing for version tokens like `v2.0`, vendor extensions such as `x-codeSamples`, and technical terms including `curl`. + +## 0.2.0 + +### Minor Changes + +- 888cb06d233: Rebuilt on a new parser, with breaking changes since 0.1.0: config validation is now strict, so rule options that were previously ignored now fail validation. Adds markdownlint-equivalent rules with autofix, prose checks (word swaps, patterns, repetition, capitalization, readability, spelling), ready-made style presets, and opt-in Markdoc tag linting. diff --git a/packages/recheck/CONTRIBUTING.md b/packages/recheck/CONTRIBUTING.md new file mode 100644 index 0000000000..c2cdb01e88 --- /dev/null +++ b/packages/recheck/CONTRIBUTING.md @@ -0,0 +1,116 @@ +# Contributing to Recheck + +This document covers the build/test/release gotchas a contributor working on +Recheck itself runs into. If you're adding or porting a lint rule, see +[`src/rules/CONTRIBUTING.md`](src/rules/CONTRIBUTING.md) instead — that file +is specifically about authoring assertions (scope rules and token rules); this +one is about everything else. + +## The stale `tsconfig.tsbuildinfo` trap + +**If `pnpm build` looks like it did nothing** (output missing, stale, or not +reflecting a source change you just made) — `pnpm build` can silently no-op +and still report success. TypeScript's incremental build considers +`tsconfig.tsbuildinfo` (and `tsconfig.typecheck.tsbuildinfo`) authoritative for +"are the outputs up to date," so it can skip regenerating `dist/` even after +`dist/` itself has been deleted, if those cache files still exist and look +current. Delete **both** the `dist/` directory and the `*.tsbuildinfo` files +before rebuilding if output ever looks wrong — deleting `dist/` alone is not +sufficient: + +```bash +rm -rf dist tsconfig.tsbuildinfo tsconfig.typecheck.tsbuildinfo +pnpm build +``` + +This has caught contributors (and reviewers reading a stale `dist/`) more than +once. + +## `pnpm parity` requires `--corpus` + +Unlike `pnpm build`/`pnpm test`, `pnpm parity` has no default corpus — a bare +invocation exits with status 2 and a usage error: + +``` +Usage: node benchmarks/parity/run-parity.mjs --corpus [--profile default|rebilly] [--rules MD001,MD013] +``` + +Always pass `--corpus` explicitly, e.g.: + +```bash +pnpm parity --corpus monorepo-docs --profile default +``` + +`--profile` defaults to `default` (recheck/markdown preset vs. markdownlint's +own defaults) if omitted; pass `--profile rebilly` to diff against a real +`.markdownlint.yaml` instead. See `benchmarks/parity/run-parity.mjs` for the +list of known corpora — an unrecognized `--corpus` value fails fast with the +same kind of usage error, naming the corpora it does know about. + +## The Markdoc schema drift test needs a fresh theme build, and only works in this monorepo + +`src/data/markdoc-realm-schema.ts` (the built-in `MARKDOC_REALM_SCHEMA` that `markdoc: +true` / `{ schema: 'realm' }` resolves to) is generated, not hand-written. It comes from +the composed Markdoc configuration Realm renders with: `@markdoc/markdoc`'s built-in tags, +overridden by `packages/portal`'s built-in tags, overridden by `packages/theme`'s tag map. + +`src/data/__tests__/markdoc-realm-schema.test.ts` re-resolves that same composition at +test time and fails if it no longer matches the committed file. Editing a tag's schema in +theme or portal without regenerating therefore breaks the test, by design. + +Regenerate with: + +```bash +pnpm markdoc-schema:generate # writes src/data/markdoc-realm-schema.ts +pnpm markdoc-schema:generate --check # exits 1 if the file would change; writes nothing +``` + +Two things to get right: + +- **Build `packages/theme` first.** The generator resolves `@redocly/theme/markdoc/default` + to that package's built `lib/` output (via its `exports` map), not to `src/`. A stale + build — theme compiled a while back, its tags edited since — resolves successfully + against out-of-date definitions, so it produces a wrong schema with nothing obviously + broken about it. Run `nx run @redocly/theme:build` (or `pnpm --filter @redocly/theme run +build`) before regenerating if you've touched a theme tag definition. The generator's + `assertThemeBuildIsFresh` check refuses to run in both failure cases: a missing + `lib/markdoc/` throws immediately, and an mtime comparison of every `src/markdoc/` file + against every `lib/markdoc/` file catches a build that merely predates a source edit. +- **Use the package script, not a bare `tsx`/`node` invocation.** `packages/portal`'s tag + modules are TypeScript source with no built JS output, so the generator needs `tsx`'s + esbuild-based loader to resolve their imports — and the repo root carries a stale `tsx` + shim that `npx tsx` prefers over the real devDependency. `pnpm markdoc-schema:generate` + resolves the correct one; running the script directly with `npx tsx` or plain `node` + may not. + +**Both the generator and the drift test only work inside this monorepo.** They import +`@redocly/theme` as a package dependency and read `packages/portal`'s TypeScript source by +relative path, which only resolves while `packages/recheck` sits in the same pnpm +workspace as theme and portal. Moving recheck to another repository would break both +imports, and neither this document nor the current code picks a replacement — the likely +options are comparing against theme's published package or running a separate cross-repo +check. + +## `scripts/generate-examples.mjs` and `oxfmt` are coupled — don't skip the regenerate step + +The repo's pre-commit hook runs `oxfmt --write` over every staged file, YAML +included (see the root `package.json`'s `lint-staged` config, `**/*` -> +`oxfmt --write`). `oxfmt` has its own opinion on quote style that the example +generator's YAML serializer doesn't match by default, so +`scripts/generate-examples.mjs` runs `oxfmt` itself on its own output (inside +`renderExample()`, the one function both the CLI and the drift test call) — +this is what keeps a freshly generated file byte-identical to what the +pre-commit hook would otherwise rewrite it to. + +**Practical consequence**: if you change the generator's formatting (or +anything that affects `examples/*.yaml`/`examples/appendices/*.yaml`), you +must regenerate and re-commit the generated files in the same change, or +`examples-drift.test.ts` will fail for the next person — the committed files +and a fresh render need to match exactly, and only running `oxfmt` by hand +without regenerating (or vice versa) will not produce that match. + +```bash +pnpm examples:generate +``` + +Run this after touching `scripts/generate-examples.mjs`, before committing. diff --git a/packages/recheck/README.md b/packages/recheck/README.md new file mode 100644 index 0000000000..0f01ce831b --- /dev/null +++ b/packages/recheck/README.md @@ -0,0 +1,1933 @@ +# Recheck + +Recheck combines a **markdown linter** (structure/format — full markdownlint rule parity: 53 built-in rules with auto-fix) and a **prose linter** (style/voice — Vale-style scopes like `sentence`/`paragraph`/`heading` with `swap`/`pattern`/`repetition`/`consistency`/`capitalization` rules) in **one tool with one simple YAML config** — replacing a markdownlint + Vale combo with one line: + +```yaml +extends: [recheck/markdown, recheck/prose] +``` + +Recheck is also built to be **embedded by other tools**: it exposes a library-first API (`parseMarkdown`, `extractScopes`, `lintContent`, `lintFiles`, `runRules` — see [Library API](#library-api)) so tools like Redocly CLI's `lint` command can add markdown and prose linting too, including linting markdown strings embedded inside API descriptions. + +## Features + +✅ **Modern Scope-Based Architecture** + +- File-first processing: each file is parsed once into a [micromark](https://github.com/micromark/micromark) AST, then segmented into scopes +- Full scope vocabulary: `all`, `raw`, `summary` (alias: `default`), `sentence`, `paragraph`, `heading` (+ `heading.h1`-`h6`), `code`, `list-item`, `blockquote`, `table.header`, `table.cell`, `markdoc.tag`, `frontmatter`, `html`, `comment`, `alt`, `link` +- Selector syntax for precise targeting: `~` negates a term, `&` joins terms into a conjunction — e.g. `scope: ['~blockquote & ~heading']` +- Vale-compatible scope notation (e.g., `heading.h1`, `heading.h2`) +- Efficient rule indexing for fast processing at scale + +✅ **Flexible Configuration Format** + +- Modern `assertions`-based rule definitions +- `severity` levels: `off`, `info`, `warn`, `error` +- Array-based scope targeting for precise control +- Comprehensive JSON Schema validation + +✅ **Production-Ready Engine** + +- High-performance JavaScript engine optimized for large repositories +- Successfully processes 300+ files with 1,000+ issues efficiently +- Built-in rules for common content quality checks +- Safe auto-fix capabilities for appropriate rules + +✅ **Developer-Friendly CLI** + +- Table, JSON, SARIF, and GitHub Actions output formats for CI/CD integration +- Universal output-path option for file export +- Inline PR annotations with GitHub Actions format +- Severity filtering and detailed statistics +- Auto-fix with granular control +- Comprehensive error reporting + +## Installation + +```bash +# Navigate to the recheck package +cd packages/recheck + +# Install dependencies +pnpm install + +# Build the project +pnpm build +``` + +**Contributing to Recheck itself** (build-cache problems, `pnpm parity`'s required `--corpus` flag, the `generate-examples.mjs`/`oxfmt` coupling) is covered in [CONTRIBUTING.md](CONTRIBUTING.md), not here — this README is for people adopting Recheck as a linter. + +## Usage + +### Validate Configuration + +```bash +# Validate with explicit config file +node dist/cli.js --validate-config --config recheck.example.yaml + +# Auto-discover config file in current directory +node dist/cli.js --validate-config +``` + +### Run content linting + +```bash +# Run on current directory +node dist/cli.js . --config recheck.example.yaml + +# Run on specific file +node dist/cli.js README.md --config recheck.example.yaml + +# Filter by severity (only show errors) +node dist/cli.js . --severity error + +# Show all enabled rules (info and above) +node dist/cli.js . --severity info + +# Work one rule at a time. This helps you clear a large list of findings. +# Give the name that the report shows, or the full config key. Use the flag +# more than one time for more than one rule. A rule from a namespace other +# than `recheck/` keeps that namespace: use `google/passive-voice`. +node dist/cli.js . --rule semantic-line-breaks +node dist/cli.js . -r us-spelling -r recheck/oxford-comma + +# ...and its inverse, to silence a rule you have already triaged +node dist/cli.js . --exclude-rule semantic-line-breaks + +# Use --rule with --fix to clear one rule's findings across all documents +node dist/cli.js . --rule semantic-line-breaks --fix + +# A name that matches no rule in your config is an error, not an empty run: +# a misspelled filter that reported "no issues" would look the same as a +# clean document set. The error message lists the rules your config loaded. + +# Output formats (table is default) +node dist/cli.js . --output table # Human-readable table (default) +node dist/cli.js . --output json # Structured JSON for CI +node dist/cli.js . --output sarif # SARIF format for security tools +node dist/cli.js . --output github-actions # GitHub Actions annotations (inline PR comments) + +# Show detailed statistics +node dist/cli.js . --stats + +# Auto-fix safe issues (35 fixable rules total: swap + semantic-line-breaks +# natively, plus 33 of the 53 markdownlint-parity rules — see the rule table +# under "Markdownlint parity" below for the full per-rule breakdown) +node dist/cli.js . --fix + +# Combine auto-fix with statistics +node dist/cli.js . --fix --stats + +# Limit annotations for CI (applies to file output, default: 20) +node dist/cli.js . --annotations-limit 50 + +# Output to file (works with all formats) +node dist/cli.js . --output json --output-path report.json +node dist/cli.js . --output sarif --output-path recheck.sarif +node dist/cli.js . --output json --output-path limited.json --annotations-limit 50 + +# Emit run summary to a file (json or text) +node dist/cli.js . --summary json --summary-path recheck-summary.json + +# Scan only changed files (via file list or stdin) +# From file: +node dist/cli.js . --changed-only --changed-list changed.txt +# Or with stdin: +git diff --name-only origin/main... | node dist/cli.js . --changed-only +``` + +## Library API + +The CLI is a thin wrapper around a public library API, published from `packages/recheck`'s `dist/index.js`. +This is the intended integration point for embedding Recheck in another tool (a build step, an editor extension, or another CLI like Redocly CLI's `lint`) rather than shelling out: + +```ts +import { lintContent, lintFiles } from '@redocly/recheck'; + +// A config is a flat map of `recheck/` -> rule definition — the same +// shape a YAML config file resolves to once `extends` presets are expanded. +// Load from YAML (via `loadConfig`, which resolves `extends` for you) or +// build one programmatically, as here: +const config = { + 'recheck/no-hard-tabs': { + severity: 'error' as const, + message: 'Hard tabs', + assertions: { 'no-hard-tabs': {} }, + }, +}; + +// Lint an in-memory string — no file I/O. Useful for linting markdown that +// isn't on disk, e.g. a `description` field pulled out of an OpenAPI document. +const problems = await lintContent('# Title\n\nSome *text*.\n', config); + +// Lint files from disk, optionally writing auto-fixes back: +const { problems: fileProblems, fixedFiles } = await lintFiles(['README.md'], config, { + fix: true, +}); +``` + +Key exports: + +- **`parseMarkdown(content, options?)`** — parses a markdown string into a micromark-based token tree, once. + Every other API in this list builds on this tree rather than re-parsing. + `options.markdoc` is a boolean here: `true` also tokenizes `{% ... %}` Markdoc tag spans into `markdocTag` tokens, and `false` or omitted gives you the same tree as passing no options at all. + The [object form](#markdoc-aware-linting-markdoc-true) (`{ schema, extend }`) is a config-file concept only — it resolves down to this boolean before any file is parsed, and `ParseOptions.markdoc` does not accept it. +- **`extractScopes(tree, content)`** — segments a parsed token tree into Vale-style scopes (`sentence`, `paragraph`, `heading`, `list-item`, `blockquote`, `table.cell`, etc.) for prose/style rules to run against. +- **`lintContent(content, config, opts?)`** — lints a single in-memory markdown string against a config; no disk access. + Rules that need on-disk facts (e.g. `max-image-size`) require `opts.metadata` to be supplied by the caller. +- **`lintFiles(paths, config, opts?)`** — lints markdown files from disk; pass `{ fix: true }` to also write auto-fixes back, looping lint → fix → re-lint until the file converges. + Files that can't be read are skipped (with a console warning) and reported in the returned `skippedFiles` (`{ path, reason }[]`), so callers can detect incomplete coverage programmatically. + `opts.root` sets the lint root that image-metadata loading is confined to (default `process.cwd()`) — image refs resolving outside it are treated as missing without touching the disk. + `opts.maxProblems` caps the total problems collected: once a file's lint pushes the run to the cap, later files aren't linted at all and the returned `truncated` flag is set. +- **`runRules(files, rules)`** — the lower-level engine entry point for callers that already have a `NormalizedRule[]` (e.g. from `loadConfig`) and want to run against an explicit in-memory file list, bypassing `lintFiles`'s own config loading/validation. + Under `{ fix: true }` its `RunResult` separates the fixes that genuinely landed (`fixes`) from proposals dropped by overlap resolution (`skippedFixes`). +- **`applyFixesToContent(content, fixes)`** — applies `Fix` edits to a string, preserving the file's own line endings (CRLF files stay CRLF). + Returns `{ content, applied, skipped }`: every input fix is classified as genuinely applied or skipped (overlapping edits, out-of-range lines), so callers can report what actually changed rather than every proposal. +- **`computeTextStatistics(prose)`** — computes word/sentence/syllable/character/complex-word counts for a plain prose string (not markdown — extract prose from a scope first). + Sentence counting reuses `splitSentences` internally, so it agrees with the rest of the engine on sentence boundaries. + Tokenization is ASCII-only by design (accented or non-Latin letters don't count as word characters), so readability scores are meaningful for English prose. +- **`computeReadability(formula, stats)`** — scores a `TextStatistics` object with one of six standard readability formulas: `flesch-reading-ease`, `flesch-kincaid-grade`, `gunning-fog`, `smog`, `coleman-liau`, `automated-readability`. + Returns `0` (rather than `NaN`/`Infinity`) when `stats.words` or `stats.sentences` is `0`. +- **`TECHNICAL_PROPER_NOUNS`** — the [built-in technical proper-noun vocabulary](#built-in-technical-proper-noun-vocabulary) `capitalization`/`spelling` consume by default; re-exported so you can read it or build your own tooling around the same list. + +This is exactly the surface a host tool needs to add both markdown-structure linting and prose/style linting to content it already has in memory — for example, linting the markdown inside an OpenAPI `description` field without writing it to a temp file first. + +## Configuration Format + +Configuration uses a modern `assertions`-based format with `severity` levels and flexible scope targeting. + +A top-level `excludes` applies to every rule, so a path you never lint is stated once rather than repeated on each rule. +It is merged ahead of a rule's own `excludes`, which still apply: + +```yaml +excludes: + - '**/_partials/**' + - 'CHANGELOG.md' +``` + +```yaml +recheck/us-spelling: + scope: all # default + severity: error + message: 'Use the US spelling "%s" instead of British "%s".' + link: https://docs.microsoft.com/en-us/style-guide/word-choice/use-us-spelling-avoid-non-english-words + appliesTo: + - 'docs/**' # Only apply to documentation + assertions: + swap: + ignoreCase: true + wordBoundary: true + pairs: + color: colour + behavior: behaviour + organize: organise + exceptions: + files: [docs/style-guide.md] + lines: + - "British spellings such as 'color'" + +recheck/no-gerund-headings: + severity: error + scope: + - heading.h1 + - heading.h2 + - heading.h3 + message: 'Do not start headings with a gerund.' + excludes: + - '**/drafts/**' # Exclude draft documents + assertions: + pattern: + ignoreCase: true + tokens: + - '^\\w*ing.*' + +recheck/config-line-length: + severity: error + message: 'Config docs: keep lines under %s characters.' + appliesTo: + - 'docs/config/**' # Only apply to config documentation + assertions: + line-length: + lineLength: 100 + codeBlocks: false + +recheck/ul-style-dash: + severity: error + message: "Use '-' for unordered list bullets." + excludes: + - '**/examples/**' # Allow mixed styles in examples + assertions: + ul-style: + style: dash +``` + +## Baseline + +A baseline lets a team adopt recheck on a large document set with no cleanup project first: record the findings that exist today, then fail only on new ones. + +```bash +recheck --generate-baseline # writes recheck-baseline.yaml next to your config +``` + +Activate it with one config line: + +```yaml +baseline: ./recheck-baseline.yaml +``` + +The file stores one count per file per rule, errors only, sorted for stable diffs: + +```yaml +version: 1 +files: + docs/index.md: + recheck/semantic-line-breaks: 3 +``` + +With the baseline active, `recheck`: + +- **suppresses** findings whose (file, rule) count matches the baseline, and reports how many matched; +- **fails** when a count rises — the group's findings are printed with `(baseline 3, found 5)` context; +- **fails** when a count falls, because the baseline is stale — the message says to run `recheck --generate-baseline` and commit the result. + Counts only step down, so the file equals reality at every green commit. + +Warnings are never baselined; they do not affect exit codes. +Partial runs (`--rule`, `--changed-only`, a narrower path) compare only the files they scanned and the rules they ran, so they never false-alarm about what they did not see. +Line numbers are deliberately not stored: counts survive unrelated edits, and a baseline diff in review reads as "this PR pays down 4 findings." +A renamed file is a new path with no budget, so its pre-existing findings report as new until you regenerate — the baseline diff then shows the counts moving from the old path to the new one. + +## Readability + +`recheck --readability` reports scores per file: Flesch reading ease, Flesch-Kincaid grade, Automated Readability Index (ARI), words, and sentences, plus medians. +ARI is a grade level computed from exact character counts, with no syllable heuristic, which makes it steadier on technical vocabulary. +It is score-shaped, not rule-shaped: it never gates and always exits 0 when it ran. +To gate on a bound, use the `metric` assertion — both read the same prose and the same formulas, so they can never disagree. + +```bash +recheck docs --readability +recheck docs --readability --output json +recheck docs --readability --changed-only < changed.txt # score only listed files +``` + +The score reads flowing prose the way standard readability tools do: headings, code, and Markdoc tags are excluded, and every block ends a sentence. +A file with no prose reports `—` (null in JSON) rather than a fake zero. +In CI, run it twice — once on the PR head and once on the merge-base worktree — and join on file to show each changed page's score change. + +## Agent skills + +Agents write a growing share of markdown, and a skill makes each one a recheck user with no person in the loop. +Two skills ship in the npm package under `skills/`: + +- **`recheck-lint`** — run recheck on touched markdown before committing or outputting it, fix errors, and never suppress findings to pass. +- **`recheck-config`** — write and tune `recheck.yaml`: measure the corpus, set severities from counts, prefer fixes over exceptions, and adopt a baseline for large corpora. + +To use them with Claude Code, copy them into your project: + +```bash +cp -r node_modules/@redocly/recheck/skills/recheck-lint .claude/skills/ +cp -r node_modules/@redocly/recheck/skills/recheck-config .claude/skills/ +``` + +Each skill is one `SKILL.md` with a trigger description and instructions, so other agent runtimes can adapt them with a rename. + +## Exceptions + +Rules can be configured with exceptions to skip specific files or lines: + +### File Exceptions + +Skip entire files using glob patterns or exact matches: + +```yaml +recheck/us-spelling: + # ... other config + exceptions: + files: + - 'docs/style-guide.md' # Exact filename + - 'docs/api/*.md' # Glob pattern + - '**/CHANGELOG.md' # Recursive glob +``` + +**File matching supports:** + +- **Basename matching**: `style-guide.md` matches any file with that name +- **Relative path matching**: `docs/style-guide.md` matches the specific path +- **Glob patterns**: `docs/*.md` matches all markdown files in docs directory + +### Line Exceptions + +Skip specific lines using fragment matching: + +```yaml +recheck/no-trailing-spaces: + # ... other config + exceptions: + lines: + - 'British spellings such as' # Fragment match + - 'Code example:' # Beginning of line + - '// ignore-lint' # Comment-based exception +``` + +**Line matching behavior:** + +- **Fragment matching**: If the line contains the exception text anywhere, it's skipped +- **Case-sensitive**: `"Code Example"` does not match `"code example"` +- **Multiple patterns**: Any matching pattern will skip the line + +### Exception Examples + +````yaml +# Skip documentation style guides for spelling rules +recheck/us-spelling: + exceptions: + files: ['docs/style-guide.md', '**/*style*'] + lines: ["British spellings such as 'colour'"] + +# Skip auto-generated files and code blocks +recheck/no-trailing-spaces: + exceptions: + files: ['**/generated/**', 'CHANGELOG.md'] + lines: ['```', 'Code example:', '// formatter-ignore'] +```` + +## Inline Directives + +Beyond config-level `exceptions`, individual Markdown files can silence rules +inline with HTML comments — the same mechanism ESLint/Vale users expect. +A +directive names rules by their **short name** (`oxford-comma`) or **full +name** (`recheck/oxford-comma`) — both work. +A directive is inert inside a +fenced code block (it has to be real, parsed HTML, not just matching text). + +```markdown + + +Everything below this point is unchecked, for every rule. + + + +Checking resumes here. + + + +Only these two rules are off from here on. + + + +us-spelling is still off; oxford-comma is back on. + + + +This one line is exempt from oxford-comma; the rest of the file isn't. + + + +Nothing in this file is linted at all, no matter where this comment sits. +``` + +The five forms: + +| Directive | Effect | +| ------------------------------------ | --------------------------------------------------------------------------------------------------- | +| `` | Disables **all** rules from this line to the end of the file, or until a matching `recheck-enable`. | +| `` | Disables only the **listed** rules from this line on (same end conditions). | +| `` | Re-enables all rules (or, with rule names, only the listed ones) from this line on. | +| `` | Disables all rules (or, with rule names, only the listed ones) for exactly the next line. | +| `` | Disables every rule for the whole file, regardless of where the comment appears. | + +Rule naming: list one or more rules space-separated, by short name +(`oxford-comma`) or full name (`recheck/oxford-comma`) — both work on every +form that accepts names; omitting names targets every rule. +Naming a rule +that isn't configured produces a warning (`recheck-directive`, severity +`warn`) pointing at the directive's line — useful for catching a typo in +the disabled rule name — but disables nothing. + +## Rule Types and Assertions + +### Assertion Types + +Rules are defined using `assertions` that specify their behavior: + +#### Swap Assertions (`swap`) + +Text replacement with configurable options. +**Fixable**: each match is replaced with its pair's value, with the matched text's own casing applied to the replacement -- +an all-lowercase match inserts the replacement as configured, a Capitalized match capitalizes just the replacement's first word, +and an ALL-CAPS match (2+ letters) uppercases the whole replacement; +any other (mixed-case) casing is left as configured, since it carries no reliable intent to infer. +This matters most with `ignoreCase: true`: without it, a sentence-initial `'Behaviour'` would be fixed to literal `'behavior'`, silently lowercasing the start of the sentence -- with it, it fixes to `'Behavior'`. +(With `keysAreRegex: true`, casing is inferred from the MATCHED text, not the regex key, so this applies uniformly to regex keys too.) +When two pairs' matches overlap in the source (a compound key together with the shorter keys it contains), the longest match wins and is reported and fixed as one span. + +```yaml +assertions: + swap: + ignoreCase: true + wordBoundary: true + pairs: + color: colour + behavior: behaviour +``` + +| Option | Type | Required | Description | +| -------------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pairs` | `object` | Yes | Find → replace entries: each key is searched for in the segment's content and reported/fixed with its value. Keys must be non-empty strings; values must be strings. | +| `ignoreCase` | `boolean` | No | Matches keys case-insensitively. Default `false`. | +| `wordBoundary` | `boolean` | No | Wraps each key in `\b...\b` so only whole words match. Default `false`. | +| `keysAreRegex` | `boolean` | No | Keys are literal text by default; set `true` to treat each key as a regex (for example, `favou?rite` matches both spellings). An invalid regex key is ignored and matches nothing; the rule's other pairs still apply. Default `false`. | +| `includeCode` | `boolean` | No | Matches inside inline code spans (`` `like this` ``) are skipped by default, so a pair like `master: primary` doesn't fire inside `` `git checkout master` ``. Set `true` to scan inline code too. Default `false`. | + +A missing or empty `pairs`, an empty-string key, a non-string replacement value, or an unknown option key under `swap` is a validation error. + +The rule's `message` gets two positional `%s` substitutions, in this order: **1st = the replacement, 2nd = the matched text** — with the pair `utilize: use`, the message `'Use "%s" instead of "%s".'` renders as `'Use "use" instead of "utilize".'` + +#### Pattern Assertions (`pattern`) + +Regex-based pattern matching: + +```yaml +assertions: + pattern: + ignoreCase: true + tokens: + - '^\\w*ing.*' +``` + +| Option | Type | Required | Description | +| ------------- | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `tokens` | `string[]` | Yes | Regex patterns matched against each segment's content. An invalid regex is caught and silently produces zero problems rather than crashing the run. | +| `ignoreCase` | `boolean` | No | Matches every token case-insensitively. Default `false`. | +| `includeCode` | `boolean` | No | Matches inside inline code spans (`` `like this` ``) are skipped by default, so a token like `master` doesn't fire inside `` `git checkout master` ``. Set `true` to scan inline code too. Default `false`. | + +#### Occurrence Assertions (`occurrence`) + +Vale-parity `occurrence` check: counts regex matches within each scoped segment and flags the segment when the count falls outside `[min, max]`. +`min: 1` with no `max` acts as an existence check — it flags a segment where the pattern is missing entirely. + +```yaml +assertions: + occurrence: + pattern: '[.!?]' + max: 3 +``` + +| Option | Type | Required | Description | +| ------------ | --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `pattern` | `string` | Yes | Regex matched against each segment's content (whole segment, not per-line). | +| `min` | `number` | At least one of `min`/`max` | Minimum allowed match count; fewer matches is a violation. `min: 1` with no `max` reads as "the pattern must be present". | +| `max` | `number` | At least one of `min`/`max` | Maximum allowed match count; more matches is a violation. | +| `ignoreCase` | `boolean` | No | Matches `pattern` case-insensitively. Default `false`. | + +Omitting both `min` and `max` is a validation error — an occurrence assertion with no bound can never report anything. +An unknown option key under `occurrence` is likewise a validation error. + +The rule's `message` gets two positional `%s` substitutions, in this order: **1st = the actual match count, 2nd = the bound that was violated** (`min` or `max`, whichever applied), e.g. `'Too many sentences (%s found, max %s).'` → `'Too many sentences (4 found, max 3).'`. +Not fixable (detection-only): a count-based violation has no single match position to anchor an edit to. + +#### Repetition Assertions (`repetition`) + +Vale-parity `repetition` check: flags an adjacent repeated word — two tokens matching `pattern`, separated only by whitespace (which may include a single hard-wrap newline), so `'the theory'` is not flagged (different words) but `'the the'` and a hard-wrapped `'the\nthe rest'` are. +**Fixable**: collapses the pair back to one occurrence, keeping the FIRST token's casing/text (so `'The the'` fixes to `'The'`, not `'the'`). + +```yaml +assertions: + repetition: + ignoreCase: true # default +``` + +| Option | Type | Required | Description | +| ------------ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pattern` | `string` | No | Regex used to tokenize each segment's content. Default `\w+`. | +| `ignoreCase` | `boolean` | No | Compares adjacent tokens case-insensitively. Default **`true`** — unlike every other assertion's case-sensitive default, since `'The the'` is the overwhelmingly common typo this check exists to catch. Set `false` to require an exact-case repeat. | + +An unknown option key under `repetition` is a validation error, as is a non-string `pattern` or a non-boolean `ignoreCase`; both options are optional, so an empty `repetition: {}` is valid. + +The rule's `message` gets one positional `%s` substitution: the repeated word itself, e.g. `'Repeated word "%s".'` → `'Repeated word "the".'`. +Fix idempotency holds under repeated `--fix` passes: `'the the the'` converges to `'the'`. + +#### Consistency Assertions (`consistency`) + +Vale-parity `consistency` check: each `either` entry declares one alternative group — the key and the value are the two variants (both matched as literals with word boundaries, like `swap` keys). +Whichever variant appears **first in the file (by source order)** wins file-wide; every later occurrence of the other variant is flagged. +**Fixable**: each later occurrence is replaced with the winning variant **literally as written in `either`** — unlike `swap`, the losing match's own casing is not preserved here (with `ignoreCase: true`, a later `'Behaviour'` in a `behavior`-first document fixes to `'behavior'`). + +```yaml +assertions: + consistency: + either: + behavior: behaviour + color: colour +``` + +| Option | Type | Required | Description | +| ------------ | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `either` | `object` | Yes | Map of variant pairs; key and value are the two alternatives of one group. Each pair gets its own independent first-seen winner. Must be non-empty. | +| `ignoreCase` | `boolean` | No | Matches variants case-insensitively (so `'Behaviour'` counts as an occurrence of `behaviour`). Default `false`. | + +Omitting `either`, leaving it empty, or giving it non-string or empty-string keys or values is a validation error — a consistency assertion with no variant pairs can never report anything, and an empty-string key would otherwise reach the scan loop as a zero-width regex that never terminates. +An unknown option key under `consistency` is likewise a validation error. + +Matches from overlapping scopes (e.g. `scope: [paragraph, sentence]`, where every sentence segment sits inside its paragraph segment) are deduplicated by source position before the winner is decided, so each occurrence is counted — and fixed — exactly once. + +The rule's `message` gets two positional `%s` substitutions, in this order: **1st = the offending (later) match, 2nd = the first-seen winner**, e.g. `'Inconsistent spelling: "%s" conflicts with first-seen "%s".'` → `'Inconsistent spelling: "behaviour" conflicts with first-seen "behavior".'`. + +#### Conditional Assertions (`conditional`) + +Vale-parity `conditional` check: if `first` (a regex pattern) matches anywhere within the rule's scoped segments, `second` (a regex pattern) must exist **somewhere in the whole file** — checked against the full raw file content, not just the rule's own scope, so a `second` match sitting inside a code block still satisfies a rule scoped to `paragraph`. +When `second` is absent file-wide, every `first` match becomes its own problem, at its exact source position. +**Detection-only** (not fixable) — there is no single well-defined edit that would "introduce" `second`. + +```yaml +assertions: + conditional: + first: '\bTODO\b' + second: '\bDONE\b' +``` + +| Option | Type | Required | Description | +| ------------ | --------- | -------- | --------------------------------------------------------------------------------------------- | +| `first` | `string` | Yes | Regex; if it matches anywhere in the rule's scoped segments, `second` is required. Non-empty. | +| `second` | `string` | Yes | Regex; must match somewhere in the whole file content once `first` has matched. Non-empty. | +| `ignoreCase` | `boolean` | No | Matches both `first` and `second` case-insensitively. Default `false`. | + +Unlike `swap`/`consistency`'s escaped-literal variants, `first` and `second` are raw user regex patterns (like `pattern`'s `tokens`). +Missing, empty, or non-string `first`/`second` is a validation error, as is an unknown option key or a non-boolean `ignoreCase` — but `first`/`second` are **not** validated as compilable regexes at config-load time; an invalid regex in either one silently produces zero problems at runtime instead (same convention as `pattern`). + +Matches from overlapping scopes (e.g. `scope: [paragraph, sentence]`) are deduplicated by source position, so each occurrence of `first` is reported exactly once. + +The rule's `message` gets two positional `%s` substitutions, in this order: **1st = the offending `first` match, 2nd = the `second` pattern that was never introduced**, e.g. `'"%s" appears but "%s" was never introduced.'` → `'"TODO" appears but "DONE" was never introduced.'`. + +#### Capitalization Assertions (`capitalization`) + +Vale-parity `capitalization` check: flags (and — for four of its `match` values — fixes) a scoped segment whose text doesn't already match the required casing. +`match` is one of `$title`, `$sentence`, `$lower`, `$upper`, or else a **custom regex** the whole segment text must satisfy. + +```yaml +assertions: + capitalization: + match: $title + style: chicago # optional, default 'ap' — only affects $title + exceptions: [GitHub, iPhone] +``` + +| Option | Type | Required | Description | +| ------------------- | ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `match` | `string` | Yes | `$title`, `$sentence`, `$lower`, `$upper`, or a regex the whole segment text must satisfy. Non-empty. | +| `style` | `'ap' \| 'chicago'` | No | Stopword list `$title` uses (see below). Default `'ap'`. Accepted alongside any `match`, but only has an effect on `$title` — a documented no-op elsewhere, not a validation error. | +| `exceptions` | `string[]` | No | Words (or phrases — see below) kept in their EXACT as-written casing from this list, everywhere they appear — including the first/last word — overriding every other rule. Unioned with the [built-in technical proper-noun vocabulary](#built-in-technical-proper-noun-vocabulary) unless `builtinVocabulary: false`. | +| `builtinVocabulary` | `boolean` | No | Default `true`. Whether [`TECHNICAL_PROPER_NOUNS`](#built-in-technical-proper-noun-vocabulary) is unioned into `exceptions`. Set `false` for a closed vocabulary of only this rule's own `exceptions`. | + +An `exceptions` entry containing whitespace or a dot (e.g. `Node.js`, `VS Code`) is matched as a whole PHRASE against the segment text — case-insensitively but otherwise literally, longest-match-first when phrases overlap — and preserved verbatim, instead of being looked up per word. + +Unknown option keys, a missing/empty `match`, an invalid `style`, a non-string-array `exceptions`, or a non-boolean `builtinVocabulary` are all validation errors. + +**`$title`** — AP or Chicago title case, implemented in `rules/scope/title-case.ts`'s `apTitleCase`/`chicagoTitleCase`: + +- The first and last word are **always** capitalized, regardless of any stopword list. +- A hyphenated compound (e.g. `well-known`) runs **each hyphen part** through the same stopword test a standalone word gets for the active style — `well-known` → `Well-Known`, but `editor-in-chief` → `Editor-in-Chief` (`in` is a stopword in both styles). + The compound's first part always capitalizes when the compound opens the title, and its last part always capitalizes when the compound closes the title — e.g. `the new state-of-the-art` → `The New State-of-the-Art` (changed from the original simplification by product decision during execution, 2026-07-27). +- A word already in ALL-CAPS (2+ letters, e.g. an acronym like `API`) is left exactly as written. +- **AP** (default) lowercases articles (`a`, `an`, `the`), coordinating conjunctions (`and`, `but`, `or`, `nor`, `for`, `so`, `yet`), and prepositions of **3 letters or fewer** (`at`, `by`, `in`, `of`, `off`, `on`, `out`, `to`, `up`, `via`). +- **Chicago** lowercases the same articles/conjunctions, plus **every** preposition regardless of length (the short ones above, plus `about`, `above`, `across`, `after`, `against`, `along`, `among`, `around`, `before`, `behind`, `below`, `between`, `during`, `through`, `toward`, `under`, `until`, `with`, `within`, `without`) — + e.g. Chicago lowercases `'...walking through the park'` → `'...walking through the Park'`, where AP capitalizes `Through`. + +**`$sentence`** — only the first word is capitalized; every other word is lowercased unless it's an `exceptions` entry (as-written) or already ALL-CAPS (left alone). + +**Word position counts a phrase exception as one word.** +A _phrase_ exception (one containing whitespace or a dot, like `Node.js` or `VS Code` — see the phrase-matching note above) is a single atomic token in the word sequence the `$`-styles case: it's emitted in its exact as-written form, and it **occupies a position**, so it never changes which word counts as first or last. +With `exceptions: [VS Code]`, the already-correctly-cased heading `## VS Code actions for teams` produces no finding under `$sentence` (`actions` is the second word, not the first), and `## a guide to Node.js` becomes `## A Guide to Node.js` under `$title`/AP (`Node.js` is the last word, so `to` is a mid-title stopword and stays lowercase). +Single-word exceptions (e.g. `GitHub`) behave as they always have — resolved by lookup rather than position. + +This used to be a bug, tracked as [Redocly/redocly#25610](https://github.com/Redocly/redocly/issues/25610) and fixed since: +phrase exceptions were previously _masked out_ of the text before word position was computed, which made a leading phrase promote the next word to sentence-initial under `$sentence` +(`## VS Code actions for teams` was flagged, and under `fix: true` rewritten to `## VS Code Actions for teams`) +and made a trailing phrase promote the preceding word to last-word position under `$title` (`a guide to Node.js` → `A Guide To Node.js`). +If you had worked around it by rephrasing headings or by swapping in a custom regex `match`, neither is needed any more. +See `rules/scope/title-case.ts`'s `recaseWords` for the tokenization that replaced the masking. + +**`$lower`** / **`$upper`** — the whole segment must be all-lowercase / all-uppercase respectively; no exceptions/ALL-CAPS carve-out (unconditional, matching Vale's own `$lower`/`$upper`). + +**Custom regex** — the whole segment text must satisfy the pattern. +**Detection-only**: unlike the four `$`-styles, a failing regex is flagged but never auto-fixed, even though the rule itself is registered fixable. +Like `pattern`'s `tokens`, an invalid regex is caught and silently produces zero problems rather than crashing the run. + +**Inline code is frozen.** +A backtick-delimited span in the segment text (e.g. a heading like ``'the `configFile` option'``) is treated like an exception: its content is never flagged or rewritten by any of the four `$`-styles, even if it would otherwise land on the first/last word. + +**Fixable** for `$title`/`$sentence`/`$lower`/`$upper` only, one segment-wide edit per flagged segment. +A **multi-line** segment (e.g. a soft-wrapped paragraph) is skipped entirely under these four styles — neither a problem nor a fix — since a `Fix` can only rewrite a single line; a custom regex `match` has no such restriction and still checks (and reports) multi-line segments, since it never produces a fix regardless of segment span. + +The rule's `message` gets two positional `%s` substitutions, in this order: **1st = the segment's own text (first line only), 2nd = the `match` value itself** (e.g. `'$title'`, or the literal regex source for custom-regex mode), e.g. `'"%s" should use %s capitalization.'` → `'"the great escape" should use $title capitalization.'`. + +#### Metric Assertions (`metric`) + +Scores the document's prose with one of six published readability formulas and flags the file once when the score falls outside `[min, max]`. + +```yaml +assertions: + metric: + formula: flesch-reading-ease + min: 30 +``` + +| Option | Type | Required | Description | +| --------- | -------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| `formula` | `string` | Yes | One of `flesch-reading-ease`, `flesch-kincaid-grade`, `gunning-fog`, `smog`, `coleman-liau`, `automated-readability`. | +| `min` | `number` | At least one of `min`/`max` | Minimum acceptable score; a lower score is a violation. | +| `max` | `number` | At least one of `min`/`max` | Maximum acceptable score; a higher score is a violation. | + +Omitting both `min` and `max`, an unrecognized `formula`, or an unknown option key are all validation errors — a metric assertion with no bound can never report anything, and an unrecognized formula would otherwise reach the scoring engine's own exhaustive-switch failure at lint time instead of at config validation. + +**Always summary-scoped.** +Unlike every other assertion above, `metric` does not honor a configurable `scope:` — readability is a property of the WHOLE document's prose, not something a selector could sensibly narrow (a readability score isn't meaningful for one paragraph in isolation the way an `occurrence` count is). +Config validation forces every `metric` rule to `scope: summary`. +Omit `scope` on a `metric` rule (or write `scope: summary` explicitly); configuring any other scope prints a warning (`metric is always summary-scoped; ignoring configured scope ...`) and applies `summary` behavior anyway. +Text from overlapping segments (e.g. a list nested inside a blockquote) is deduplicated by source position, same as `consistency`/`conditional` above. + +**What the score reads.** +The metric scores flowing prose the way standard readability tools do: `paragraph`, `list-item`, `blockquote`, `table.cell`, and `table.header` text counts; **headings are excluded**, and `code`, `frontmatter`, `html`, `comment`, `alt`, and `link` content is never counted. +Every block that does not end in terminal punctuation ends a sentence — an unpunctuated list item is one sentence, not a fragment fused into its neighbors. +Without that rule, a run of bullets scored as one enormous "sentence" and pushed Flesch reading ease far below zero; with it, scores line up with other readability tools within syllable-heuristic differences. + +**Non-prose stripping.** +Before scoring, each segment's text also has Markdoc tag-marker spans (`{% tag attr="x" %}`, `{% /tag %}`, and the `{%- ... -%}` trim variant) and backtick-delimited inline code spans stripped out — neither is readable prose, and both otherwise skew word/syllable counts. +Prose between two block-tag markers still counts (only the marker spans themselves are removed); a paragraph consisting only of tag markers contributes nothing. +Multi-backtick delimiters (` `like this` `) are handled conservatively as a simple open-run/close-run pair match, not a full CommonMark-correct implementation. + +**Detection-only** (not fixable) — there is no single edit that would "fix" a readability score. +Reports at most **one** problem per file, always at `line: 1, column: 1` (there is no single source position a whole-document score belongs to) — never divided by zero: a file with no prose at all (empty, or only code/frontmatter) is never flagged, regardless of `min`/`max`. + +The rule's `message` is substituted against up to **four** values, in this order: **1st = the formula name, 2nd = the computed score, 3rd = `min` (or `-∞` if unset), 4th = `max` (or `∞` if unset)** — e.g. the internal fallback `'Readability (%s) is %s; expected between %s and %s.'` → `'Readability (flesch-reading-ease) is 42.1; expected between 60 and ∞.'`. +The `message` validation cap is per-assertion: a `metric` rule's `message` may use up to **4** `%s` placeholders (one per value above), while every other assertion stays capped at 2. +Fewer placeholders than values is fine — substitution is positional, so a 2-slot message receives the leading values (formula name, then score). + + + +#### Spelling Assertions (`spelling`) + +Vale-parity `spelling` check (detection-only): tokenizes each scoped segment's text into words and flags any word an [nspell](https://github.com/wooorm/nspell)/Hunspell speller doesn't recognize, with up to three suggested corrections. + +```yaml +assertions: + spelling: + vocab: [Redocly, Reunite] + ignore: ['\bAcme\w*'] +``` + +| Option | Type | Required | Description | +| ------------------- | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `dictionary` | `string` | No | Base path (WITHOUT the `.aff`/`.dic` extension) to a custom Hunspell dictionary pair, e.g. `dictionary: dict/custom` reads `dict/custom.aff` and `dict/custom.dic`. Resolved relative to `process.cwd()` (where the CLI is invoked from) unless absolute. Omit to use the bundled default English dictionary. | +| `vocab` | `string[]` | No | Extra known-good words, matched case-insensitively; never flagged even when the speller itself doesn't recognize them (product names, jargon, etc.). Unioned with the [built-in technical proper-noun vocabulary](#built-in-technical-proper-noun-vocabulary) unless `builtinVocabulary: false`. | +| `ignore` | `string[]` | No | Regex patterns; a token matching ANY of them is never flagged, e.g. `['\bAcme\w*']` to allow every inflection of a brand name. An invalid pattern is silently ignored, same convention as `pattern`'s `tokens`. | +| `builtinVocabulary` | `boolean` | No | Default `true`. Whether [`TECHNICAL_PROPER_NOUNS`](#built-in-technical-proper-noun-vocabulary) is unioned into the accepted-word set alongside `vocab`. A multi-token entry (`Node.js`, `VS Code`) is split into its individual words, each accepted separately — correct for a per-word spell check, unlike `capitalization`'s whole-phrase matching. Set `false` for a closed vocabulary of only this rule's own `vocab`. | + +All options are optional — an empty `spelling: {}` is valid (default dictionary, no extra vocabulary, no ignore patterns, built-in vocabulary on). +Unknown option keys, a non-string/empty-string `dictionary`, a `vocab`/`ignore` entry that isn't a non-empty string, or a non-boolean `builtinVocabulary` are all validation errors. + +**Optional peer dependencies — install to enable.** +`nspell` and its default dictionary (`dictionary-en`) are **optional peer dependencies**: installing `@redocly/recheck` itself pulls in **neither**. +Enable `spelling` with: + +```bash +npm i nspell dictionary-en +``` + +...or, if every `spelling` rule in your config sets its own `dictionary` path, you only need the speller itself (the bundled dictionary is never touched): + +```bash +npm i nspell +``` + +If a config enables `spelling` without the required peer(s) installed, `recheck --validate-config` fails with an actionable error naming the exact command above — never a bare `Cannot find module 'nspell'` surfacing for the first time at lint time. + +**Dictionaries load lazily.** +Neither `nspell` nor `dictionary-en` is imported unless some rule in your config actually has a `spelling` assertion — a config without one never touches either package, at either `validate` or lint time. +The loaded speller (including the ~500KB parsed dictionary) is cached per dictionary source for the process's lifetime, so every file/rule sharing the same `dictionary` (or the shared default) reuses one instance rather than reloading it per call. + +**Word tokenization.** +Words are matched with `/\p{L}+(?:['’]\p{L}+)?/gu` — Unicode letter runs, with an optional apostrophe-joined suffix so contractions (`don't`, `it's`) tokenize as one word. +A token is skipped (never checked) when it's in `vocab` (case-insensitively), matches any `ignore` pattern, is ALL-CAPS (2+ letters, e.g. an acronym) — matching the same ALL-CAPS carve-out `$title`/`$sentence` capitalization use — or is digit-adjacent (see below). +Because `\p{L}` can never match a digit, a token touching one is never captured WHOLE by the tokenizer in the first place: a digit-adjacent identifier like `config2` still splits into a letter-only fragment (`config`) as its own regex match. +Rather than checking that fragment like any other word, a digit-adjacency guard looks at the character immediately before and after each match and skips it when either neighbor is a digit — so common digit-bearing identifiers (`sha256` → `sha`, `utf8` → `utf`, `oauth2` → `oauth`, `es6` → `es`, `log4j` → both `log` and `j`, `2fast` → `fast`) are no longer flagged as false-positive misspellings. +This mitigates, but doesn't eliminate, every false positive from the tokenizer's inability to capture digits at all — a token entirely surrounded by non-digit characters is still checked normally, so a genuine misspelling elsewhere in the same sentence is still flagged. + +**Code is never spell-checked, by construction of scope segmentation — not something this assertion special-cases.** +A fenced or indented code block is its own `scope: 'code'` segment, entirely distinct from `paragraph`/`heading`/etc.; scoping `spelling` to prose (the common case, e.g. `scope: paragraph` or an array of prose scopes) means `ctx.segments` never contains one. +A backtick-delimited **inline** code span, though, remains embedded as raw text inside a prose segment's own content (verified directly against the extractor) — those spans are masked out before tokenizing, the same length-preserving technique `capitalization`'s backtick-span freezing uses, so positions of any remaining flagged word stay exact. +Scoping `spelling` to `all`/`raw` (or leaving `scope` at its default) checks the whole raw file, literal code included — same default-scope behavior every other native assertion (`swap`, `pattern`, ...) has. + +**Detection-only** — no `fix`. +The rule's `message` gets two positional `%s` substitutions, in this order: **1st = the unrecognized word, 2nd = a suggestion suffix** — either `''` (zero suggestions) or `' — did you mean: a, b, c?'` (one to three, comma-joined) — e.g. the internal fallback `'Unknown word "%s"%s'` → `'Unknown word "wrold" — did you mean: wold, world?'`. + +#### Built-in technical proper-noun vocabulary + +`capitalization` and `spelling` both ship a built-in list of common technical/product proper nouns — `TECHNICAL_PROPER_NOUNS`, exported from `@redocly/recheck`'s public API (`import { TECHNICAL_PROPER_NOUNS } from '@redocly/recheck'`) so you can read or extend it yourself. +It exists so a config that turns on sentence-case headings or spelling doesn't immediately need to hand-list the same 15+ mixed-case technology names every project already has to deal with (`OpenAPI`, `npm`, `Node.js`, `VS Code`, ...). + +**On by default**, per rule: + +- `capitalization` unions it into `exceptions` (so a listed name keeps its as-written casing under every `$`-style, including `$sentence`). +- `spelling` unions it into `vocab` (so those words are never reported as misspellings), splitting any multi-token entry into its individual words first — a per-word spell check has no way to accept a whole phrase atomically the way `capitalization`'s phrase matching does. +- Either union is opted out of independently with that rule's own `builtinVocabulary: false`, restoring strict pre-built-in behavior (a closed vocabulary of only what you list yourself). +- Your own `exceptions`/`vocab` on the same rule **compose** with the built-ins rather than replacing them — unlike a preset-shipped list on the same rule key, which a same-key override _would_ replace entirely (see [`extends` presets](#extends-presets) above). + This is exactly how [`recheck/prose`](#extends-presets)'s `capitalization` rule gets its protection for common technical nouns without shipping any `exceptions` of its own. + +**Multi-token entries work.** +An entry containing a dot or whitespace (`Node.js`, `VS Code`, `Visual Studio Code`, `GitHub Actions`, `Google Cloud`, `Azure DevOps`) is matched by `capitalization` as a whole phrase against the segment text (longest-match-first, case-insensitive but otherwise literal) and preserved verbatim — not looked up per word, which is what a single-token entry like `GitHub` still gets. + +**Inclusion bar** (why an entry is — or isn't — in the list, and the bar to clear before proposing one): an entry qualifies if it's an unambiguous technology, product, or company name whose exception listing wouldn't _weaken_ capitalization/spelling checks — concretely, its lowercase form must not be a legitimate English word in its own right. +That covers ordinary Title-Case brand names (`Android`, `Kubernetes`, `Redocly`) just as much as entries with an internal capital (`OpenAPI`, `GraphQL`), a dot (`Node.js`), or forced lowercase (`npm`) — `$sentence` lowercases every non-first word regardless of how "ordinary" its casing looks, so plain Title-Case names need protection too. +Excluded, deliberately: + +- **Pure ALL-CAPS acronyms** (`JWT`, `YAML`) — already handled structurally by the ALL-CAPS carve-out both `capitalization` and `spelling` apply, so listing them adds maintenance for no behavior change. + Note this is narrower than "looks like an acronym": `OAuth` and `AsyncAPI` are mixed-case, not pure ALL-CAPS, and are in the list. +- **Terms with legitimate lowercase prose usage** — generic English (`cloud`, `apps`), + words that are ALSO ordinary English words even though they're Redocly product names too (`Realm`, `Replay`, `Respect` — listing them would force-capitalize ordinary usage like "we respect your privacy"; `Node` — the common technical noun, superseded by the `Node.js` phrase entry for the platform specifically), + and — caught by a later audit, not the original pass — ordinary brand-shaped words with a real dictionary meaning (`Chrome`, `Markdown`, `Postman`, `Prettier`, `Safari`, `Swagger`, `Windows`; see `src/data/proper-nouns.ts`'s header for each one's disqualifying lowercase usage). + A few real dictionary words (`Android`, `Docker`, `TypeScript`) were judged rare enough in ordinary lowercase usage to keep anyway — a documented, deliberate risk-acceptance, not an oversight. + List your own such names in your rule's own `exceptions`/`vocab`, which compose with this list as described above. + +Two automated tests in `src/data/__tests__/proper-nouns.test.ts` enforce this: +one checks every entry's shape against the bar above — no pure ALL-CAPS, and, mechanically, no single-token entry whose lowercase form the REAL spelling dictionary (`dictionary-en`/`nspell`, the same pair `spelling` loads at runtime) accepts as a legitimate English word, unless it's named in an explicit accepted-risk allowlist — +plus alphabetization and no duplicates. +A round-trip guard separately drives every entry through the real `capitalization` and `spelling` rules and fails the suite if any entry can't actually be protected — the list can't silently regress into decoration. + +#### Length Assertions (`length`) + +Recheck-original, detection-only check: measures each scoped segment's size — in characters, words, or sentences — and flags a segment whose measurement falls outside `[min, max]`. +Unlike `metric` (always whole-document), `length` honors whatever `scope` the rule configures — e.g. `scope: alt` to cap image alt text, or `scope: sentence` to cap sentence length in words. +[`recheck/google`](#extends-presets) ships this for the guide's stated "fewer than 26 words per sentence" limit (`google/sentence-length`); Microsoft's 150-character alt-text cap is the other published example of this shape. + +```yaml +assertions: + length: + unit: characters + max: 150 +``` + +| Option | Type | Required | Description | +| ------ | ---------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `unit` | `'characters' \| 'words' \| 'sentences'` | Yes | What `min`/`max` count: raw character length, whitespace-delimited words (the same tokenizer `metric` uses for its own word counts — see `metrics/statistics.ts`'s `tokenizeWords`), or sentences via the shared `splitSentences` sentence-boundary logic (`scopes/sentences.ts`). | +| `min` | `number` | At least one of `min`/`max` | Minimum allowed size; a smaller segment is a violation. | +| `max` | `number` | At least one of `min`/`max` | Maximum allowed size; a larger segment is a violation. | + +Omitting both `min` and `max`, a missing/unrecognized `unit`, or an unknown option key are all validation errors — same reasoning as `occurrence`/`metric` above. +An inverted range (`min` > `max`) is also an error. + +**Detection-only** (not fixable) — there is no single edit that would resize a segment to fit. +Reports at most one problem per flagged segment, at the segment's own `startLine`/`startColumn`. + +The rule's `message` gets three positional `%s` substitutions, in this order: **1st = the segment's measured size, 2nd = the unit name, 3rd = the bound that was violated** (`min` or `max`, whichever applied), e.g. the internal fallback `'Segment is %s %s; at most %s allowed'` → `'Segment is 151 characters; at most 150 allowed'`. +The message validation cap for `length` is **3** placeholders (one per value above), same reasoning as `metric`'s 4-cap. + +#### Built-in Prose Assertions + +Beyond `swap`, `pattern`, `occurrence`, `repetition`, `consistency`, `conditional`, `capitalization`, `metric`, `spelling`, and `length` above, Recheck ships a small set of native prose/format checks: + +- `semantic-line-breaks` - Semantic line break validation ✅ **Fixable** +- `max-image-size` - Oversized image detection + +Three of the assertions above (`repetition`, `consistency`, `capitalization`) are bundled, pre-configured, in the [`recheck/prose`](#extends-presets) preset, +and `capitalization`/`length` are also used by [`recheck/google`](#extends-presets) (sentence-case headings and list items, and a sentence-length cap); +the remaining four (`occurrence`, `conditional`, `metric`, `spelling`) are documented [opt-ins](#opt-in-prose-assertions) with copy-paste snippets, not shipped in any preset by default. + +### Recheck-original structural rules + +Seven rules have no markdownlint counterpart, so they sit outside the 53-rule parity set +(and outside the parity comparison). +All seven are **detection-only** (`fix: false`). +The +canonical list is `RECHECK_ORIGINAL_TOKEN_RULE_NAMES` in `src/rules/token/index.ts`. + +The table below covers five of them. +The other two — `markdoc-unknown-tag` and +`markdoc-attributes` — need a tag schema to check anything, so they are documented with +the [`recheck/markdoc`](#extends-presets) preset instead. + +| Rule | Flags | Why | +| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `no-empty-headings` | A heading whose text content is empty (a bare `#`, or markup that renders to nothing such as `## `) | An empty heading still lands in the document outline and in screen-reader heading navigation. Inline code counts as content, so `` # `config.yaml` `` is fine. | +| `no-duplicate-link-destinations` | The second and later links to one destination when the link **text** differs from the first occurrence's | Screen-reader users listing a page's links hear one target described inconsistently; the texts also drift apart over time. Repeating the _same_ text for the same destination is ordinary prose and is not flagged. Resolves reference links through their definition. | +| `list-length` | A list (ordered or unordered) with fewer than `min` items (default 2) or more than `max` items (no default — unbounded unless set) | A single-item list usually reads better as a plain sentence, and a very long list asks readers to hold too many parallel items in mind. Every list is evaluated independently, including nested sublists — a short sublist is flagged even when its parent list is long enough. | +| `markdoc-syntax` | A grammar-level Markdoc tag error — a malformed span, an unquoted "bareword" attribute/primary value, or a close tag carrying attributes | These are invalid under real Markdoc's own grammar regardless of any tag schema, so the rule fires on custom/unknown tags and under `schema: false` alike. See the [`recheck/markdoc`](#extends-presets) preset bullet below for the full behavior and a config example. | +| `markdoc-pairing` | An unclosed, orphaned, or interleaved (crossed) Markdoc tag pair, or a schema-declared self-closing tag written with a close it must not have | Same grammar-level scope as `markdoc-syntax` — see the [`recheck/markdoc`](#extends-presets) preset bullet below. | + +The first three rules are **opt-in — not shipped in any preset**; configure them +individually as shown below. + +`markdoc-syntax` and `markdoc-pairing` work the other way around: they ship only inside +the [`recheck/markdoc`](#extends-presets) preset, and both need `markdoc: true` (or the +object form) to ever see a Markdoc tag token. +Naming either rule key on its own, without +the flag, validates but can never report anything — and you get no warning about it, +because the stale-config warning fires on `extends` containing `"recheck/markdoc"` +(`warnStaleMarkdocPreset` in `config/validate.ts`), not on individual rule keys. +The +preset bullet below covers both rules' full behavior with the flag on, plus a config +example. + +```yaml +recheck/empty-headings: + severity: error + message: 'Headings should have text content.' + assertions: + no-empty-headings: {} + +recheck/link-text-consistency: + severity: warn + message: 'Link destination "%s" is already linked by different text.' + assertions: + no-duplicate-link-destinations: {} + +recheck/list-length: + severity: warn + message: 'List has %s item(s).' + assertions: + list-length: { min: 2, max: 10 } +``` + +For markdown structure/format rules (headings, lists, links, tables, whitespace, and 49 more), see [Markdownlint parity](#markdownlint-parity) below — `no-trailing-spaces`, `no-hard-tabs`, `line-length`, `ul-style` (bullet style), `no-duplicate-heading`, and `link-fragments` are all part of that 53-rule set, not this native list. +(`max-line-length`, `bullet-style`, `no-duplicate-headings`, and `no-broken-fragment-links` were pre-parity native ids for those same rules; they were removed rather than kept as aliases — see [Migrate from markdownlint](#migrate-from-markdownlint).) + +### Enhanced Scope Support + +The `scope` field supports a string, an array (OR'd together), and a `~negation` / `&`-conjunction +selector syntax: + +```yaml +scope: all # Apply to all content (default) +scope: raw # Apply to raw file content, bypassing scope segmentation +scope: summary # Apply to the document's prose: paragraph, heading, list-item, blockquote, and table-cell text (alias: default) +scope: sentence # Apply to sentences only +scope: paragraph # Apply to paragraphs only +scope: heading # Apply to all headings +scope: code # Apply to code blocks only +scope: list-item # Apply to list item text +scope: blockquote # Apply to blockquote text +scope: table.header # Apply to table header cells +scope: table.cell # Apply to table body cells +scope: markdoc.tag # Apply to Markdoc tag spans (`{% ... %}`), requires markdoc: true +scope: frontmatter # Apply to YAML frontmatter +scope: html # Apply to raw HTML blocks +scope: comment # Apply to HTML comments +scope: alt # Apply to image alt text +scope: link # Apply to link text +scope: # Apply to specific heading levels + - heading.h1 + - heading.h2 + - heading.h3 +scope: # Selector syntax: '~' negates, '&' conjoins + - "~blockquote & ~heading" +``` + +### Markdoc-aware linting (`markdoc: true`) + +Opt-in — off by default, since Liquid/Jinja templates use the same `{% %}` delimiters and would otherwise get mistokenized as Markdoc: + +```yaml +markdoc: true # shorthand for `{ schema: 'realm' }` +``` + +Writing _about_ Markdoc syntax rather than using it (docs like this one, a tutorial, a changelog entry)? +Wrap the literal `{% ... %}` in a code span — `` `{% partial /%}` `` — instead of leaving it bare in prose. +Code spans never tokenize as Markdoc tags whether the flag is on or off, so that's the escape hatch. + +#### Object form: choosing or extending the tag schema + +`markdoc: true` is shorthand for the common case. +The object form adds two things the +boolean can't express: turning the schema-aware checks off while keeping tag tokenization, +and layering a project's own custom tags over the built-in schema. + +```yaml +markdoc: + schema: realm # required -- 'realm' (the built-in schema below) or `false`; there is no default if this key is omitted + extend: # optional: your own tags, merged over the base schema + tags: + myCustomTag: + selfClosing: true + attributes: + level: + type: string + enum: [info, warning, danger] + required: true +``` + +- **`schema: realm`** — the same built-in schema `markdoc: true` uses: `@markdoc/markdoc`'s + own built-in tags composed with `@redocly/theme`'s tag definitions. + It's generated from a + theme build rather than hand-written, and a test fails if it drifts out of sync (see + [CONTRIBUTING.md](CONTRIBUTING.md) for the regeneration command). + This is what most + projects want, and what the four [`recheck/markdoc`](#extends-presets) rules validate + against by default. +- **`schema: false`** — tokenization and tag **pairing** still run, so `markdoc.tag` scope, + prose-scope exclusion, fix protection, and `markdoc-syntax`/`markdoc-pairing`'s + grammar-level checks all still work. + Only the two schema-dependent rules + (`markdoc-unknown-tag`, `markdoc-attributes`) go inert, since there's no schema left for + "unknown tag" or "missing required attribute" to mean anything against. + Use this if you + write Markdoc tags but don't have (or don't want) a schema to validate them against. +- **`extend.tags`** — merges your own tag definitions over the base schema. + On a name + collision the merge is a **whole-tag replace**, matching how Markdoc's own config + composition works, not a per-attribute deep merge. + Declare your project's custom tags + here (for example, a docs site's own `@theme/markdoc/schema.ts` overrides) so + `markdoc-unknown-tag` and `markdoc-attributes` validate against your real tag surface + instead of flagging every custom tag as unknown. + Under `schema: false` there is no base + to merge over, so `extend` does nothing. +- **`extend.tagsFile`** — the same tag-definition surface as `extend.tags`, but sourced from + a separate YAML file instead of written inline into `recheck.yaml`. + This is the shape + [`recheck --generate-markdoc-schema`](#generate-a-tagsfile-recheck---generate-markdoc-schema) below generates, + so a project with tags defined in TypeScript (a `@theme/markdoc/schema.ts` module, say) + never hand-transcribes them into YAML. + + ```yaml + markdoc: + schema: realm + extend: + tagsFile: ./recheck-markdoc-tags.yaml + ``` + + - **Resolution**: the path is resolved relative to the directory containing the + `recheck.yaml`/`recheck.yml` that names it — never the process's current working + directory — so `tagsFile: ./tags.yaml` always reads the file next to that config, + wherever `recheck` is invoked from. + - **Precedence**: tags merge in the order built-in schema → `tagsFile` → inline + `extend.tags`, each layer a whole-tag replace on a name collision (same rule as + `extend.tags` above). + `tags` and `tagsFile` can both be set on the same `extend` block; + `extend` with neither key is rejected by config validation as a likely no-op. + - **Errors are fatal to the whole run, not a silent markdoc downgrade.** + A `tagsFile` that + doesn't exist, isn't valid YAML, isn't a YAML map, or contains a tag entry with an + invalid shape all fail `recheck`/`recheck --validate-config` outright + (`Configuration validation failed!`, the same failure every other structurally-invalid + config produces) — markdoc checking is never quietly switched off while the rest of the + config keeps running. + +Turning `markdoc` on (either form) changes how every prose rule sees a Markdoc tag, not just `markdoc.tag` (above): + +- **Prose scopes exclude the tag itself.** + `paragraph`, `heading`, `list-item`, `blockquote`, and `table.header`/`table.cell` all blank a tag's own `{% ... %}` span out of their content before any rule runs — a `swap`/`pattern`/`capitalization` match can't fire on the tag's syntax, and a `length`/`metric` count doesn't include it. + The blanking is position-preserving (same-width spaces, never a deletion), so real text on either side of a tag keeps its exact line and column. +- **A segment with no prose left isn't emitted at all.** + A heading or table cell whose entire text IS a tag (`# {% #anchor %}`) produces no `heading.h1`/`table.cell` segment — there's nothing for a heading or cell rule to check, so none fires on it. +- **`--fix` never rewrites a Markdoc tag's bytes.** + Every proposed fix is checked against the document's tag spans before it's applied: + one that doesn't touch a tag goes through untouched, one that fully covers a tag with a same-length replacement gets the tag spliced back in, + and anything that would change a tag's length or split it in half is withheld instead — a withheld fix is reported (`skippedFixes` in the [Library API](#library-api)), not silently swallowed. +- **Two CommonMark constructs Markdoc doesn't have stop being recognized.** + Markdoc's own tokenizer disables indented code blocks and setext headings (the `Title\n===\n` underline form) unconditionally, which is how Realm renders, so `markdoc: true` disables them too — and only while the flag is on: + - A 4+-space-indented block that would otherwise be an indented code block parses as ordinary content instead: a paragraph, list, or fence, whichever the un-indented text would have produced. + This shows up most with a block-positioned tag followed immediately by more indented lines, and with genuinely indented example text. + Realm renders both as prose, so matching that is the intent. + - A text line immediately followed by a `---`/`===` line, with no blank line between, no longer forms a heading. + This also fixes a common false positive: a tag on its own line (`{% table %}`, say) directly followed by a `---` line is ordinary Markdoc table-row syntax, but without the flag it reads as a setext heading whose text is the tag itself. + With the flag on, the tag is its own token and can't merge into a paragraph that `---` would complete. + - Practical effect: on documents using either construct, expect `heading-style`, `blanks-around-headings`, `capitalization`, and `code-block-style` findings to shift when you first turn the flag on. + They are moving to match how Markdoc actually renders, not regressing. + +### Generate a tagsFile: `recheck --generate-markdoc-schema` + +Projects that define their own Markdoc tags in TypeScript — a `@theme/markdoc/schema.ts` +module exporting a `tags` map, the shape both `docs/realm` and `docs/intranet` use in this +monorepo — can generate an `extend.tagsFile` YAML file from it instead of hand-transcribing +each tag's schema: + +```bash +recheck --generate-markdoc-schema --from path/to/schema.ts --out recheck-markdoc-tags.yaml +``` + +- **`--from `** (repeatable) — a project schema module to extract tags from, resolved + relative to the current working directory. + The module must export `tags` (named or on a + `default` object) mapping tag name to a Markdoc tag config; only the statically-checkable + facets (`selfClosing`, and each attribute's `type`/`required`/`default`/`enum`) are + extracted — anything richer (a custom attribute class, a `validate()` function) is written + out as `dynamic: true`, the same reduction the built-in `realm` schema goes through. + Pass + `--from` more than once to merge several modules; an identical tag definition repeated + across modules is fine, but two modules disagreeing about the same tag's shape fails the + command rather than letting flag order silently pick one. +- **`--out `** — where to write the generated YAML, resolved relative to the current + working directory. + The file opens with a generated-file header naming its source module(s) + and the exact command to regenerate it. +- **`--check`** — verifies the output file matches what a fresh generation would produce, + without writing it: exits `0` and prints ` is up to date.` when it matches, exits `1` + and prints a one-line diagnosis (file missing, or stale) otherwise. + This is what a CI drift + check should call — see this repo's own wiring below. + +**TypeScript sources need a loader.** +`recheck --generate-markdoc-schema` dynamic-`import()`s each +`--from` module directly; running the command under plain `node` against a `.ts` module +fails with an actionable one-line error naming the fix, rather than a raw stack trace: + +```text +could not import "path/to/schema.ts" — TypeScript sources need a loader, e.g.: pnpm exec tsx +node_modules/.bin/recheck --generate-markdoc-schema … (Cannot find module '' imported from '') +``` + +(wrapped above for line length; the real message is one line. +The parenthetical is Node's +own error and its shape varies: for a schema whose extensionless internal imports plain +`node` cannot resolve — the common case — the "imported from" path is your schema file +itself; for a `--from` path that doesn't exist at all it is recheck's own command module.) + +Run it through `tsx` instead (directly, or via a package script that already wraps it, like +this repo's `recheck:markdoc-tags` below) — a plain `.js` schema module needs no loader and +works under either. + +**Experimental, pending a canonical manifest.** +This command is an interim bridge, not a +long-term source of truth: [issue #25666](https://github.com/Redocly/redocly/issues/25666) +tracks Realm itself emitting one canonical, statics-only Markdoc tag/schema manifest, which +would let this generator (and its drift check) retire in favor of reading that manifest +directly. +Until then, `recheck --generate-markdoc-schema` is the supported way to keep a project's +`tagsFile` in sync with its real tag schema modules. + +#### Worked example: this repo's own setup + +This monorepo's root `recheck.yaml` uses `extend.tagsFile` to pull in the custom tags from +both `docs/realm` and `docs/intranet`'s own `@theme/markdoc/schema.ts` modules: + +```yaml +markdoc: + schema: realm + extend: + tagsFile: ./recheck-markdoc-tags.yaml +``` + +The committed `recheck-markdoc-tags.yaml` is generated, not hand-written — its header names +the exact regenerate command: + +```yaml +# Generated file — do not hand-edit. +# Source module(s): ../../docs/realm/@theme/markdoc/schema.ts, ../../docs/intranet/@theme/markdoc/schema.ts +# Regenerate: recheck --generate-markdoc-schema --from ../../docs/realm/@theme/markdoc/schema.ts --from ../../docs/intranet/@theme/markdoc/schema.ts --out ../../recheck-markdoc-tags.yaml +``` + +The root `package.json` wraps that same invocation in one script, run from +`packages/recheck` via `tsx` (the schema modules are TypeScript source, see above): + +```bash +pnpm run recheck:markdoc-tags # regenerate recheck-markdoc-tags.yaml +pnpm run recheck:markdoc-tags --check # verify it's current; exits 1 on drift +``` + +**Do not add `--` before `--check`.** +`pnpm run recheck:markdoc-tags -- --check` looks +equivalent but isn't: the script itself already ends in `pnpm --filter @redocly/recheck exec +tsx dist/cli.js --generate-markdoc-schema …`, and pnpm's own `--` forwarding through that nested `exec` +makes yargs read `--check` as a positional argument instead of the `--check` flag — the +command then silently regenerates the file and always exits `0`, defeating the whole point +of a drift check. +Always call it as `pnpm run recheck:markdoc-tags --check`, with no extra +`--`. + +CI runs exactly that check on every PR, as its own step in +`.github/workflows/recheck.yml` (after the package is built): + +```yaml +- name: Markdoc tags file is current + run: pnpm run recheck:markdoc-tags --check +``` + +If it fails, regenerate locally with `pnpm run recheck:markdoc-tags` and commit the result. + +### Rule Severity Levels + +Rules can be configured with different severity levels: + +- **`off`**: Disable the rule completely +- **`info`**: Informational messages (exit code 0) +- **`warn`**: Warning messages (exit code 0) +- **`error`**: Error messages (exit code 1) + +### Auto-Fix Safety + +Fixability is declared by each assertion, not by config — a rule can be automatically corrected if and only if its assertion implements a `fix()`. +The `autoFixable` config key was removed — rules declare fixability; use `fix: false` to opt out. +A config that still sets `autoFixable` now fails validation with an unknown-property error. +To opt a rule out of auto-fixing, set `fix: false` on it instead. + +The `enabled` config key was likewise removed — it was schema-legal but never actually consulted by the engine (use `severity: off` to disable a rule). +A config that still sets `enabled` now fails validation with an unknown-property error. + +- ✅ **Fixable native assertions**: `swap`, `semantic-line-breaks`, `repetition`, `consistency`, `capitalization` (except its custom-regex `match` mode, which is always detection-only) +- ❌ **Not fixable native assertions**: `pattern`, `max-image-size`, `occurrence`, `conditional`, `metric`, `spelling` +- Of the 53 markdownlint-parity rules, 33 are fixable — see the [rule table](#markdownlint-parity) for the full per-rule breakdown (includes `no-trailing-spaces`, `no-hard-tabs`, `ul-style`, and more). + +## Markdownlint parity + +Recheck ports all 53 of [markdownlint](https://github.com/DavidAnson/markdownlint)'s built-in rules (MD001-MD060, minus retired ids) as native `assertions`, verified against upstream by a differential parity harness (see [Parity with markdownlint](#parity-with-markdownlint) below). +Enable the full set with one line: + +```yaml +extends: [recheck/markdown] +``` + +Each rule is available as its own `recheck/` assertion id, so you can also enable a subset directly: + +```yaml +recheck/heading-increment: + severity: error + message: 'Heading levels should only increment by one level at a time.' + assertions: + heading-increment: {} +``` + +| Rule name | MD id | Fixable | +| ---------------------------------- | ----- | ------- | +| `heading-increment` | MD001 | No | +| `heading-style` | MD003 | No | +| `ul-style` | MD004 | Yes | +| `list-indent` | MD005 | Yes | +| `ul-indent` | MD007 | Yes | +| `no-trailing-spaces` | MD009 | Yes | +| `no-hard-tabs` | MD010 | Yes | +| `no-reversed-links` | MD011 | Yes | +| `no-multiple-blanks` | MD012 | Yes | +| `line-length` | MD013 | No | +| `commands-show-output` | MD014 | Yes | +| `no-missing-space-atx` | MD018 | Yes | +| `no-multiple-space-atx` | MD019 | Yes | +| `no-missing-space-closed-atx` | MD020 | Yes | +| `no-multiple-space-closed-atx` | MD021 | Yes | +| `blanks-around-headings` | MD022 | Yes | +| `heading-start-left` | MD023 | Yes | +| `no-duplicate-heading` | MD024 | No | +| `single-h1` | MD025 | No | +| `no-trailing-punctuation` | MD026 | Yes | +| `no-multiple-space-blockquote` | MD027 | Yes | +| `no-blanks-blockquote` | MD028 | No | +| `ol-prefix` | MD029 | Yes | +| `list-marker-space` | MD030 | Yes | +| `blanks-around-fences` | MD031 | Yes | +| `blanks-around-lists` | MD032 | Yes | +| `no-inline-html` | MD033 | No | +| `no-bare-urls` | MD034 | Yes | +| `hr-style` | MD035 | No | +| `no-emphasis-as-heading` | MD036 | No | +| `no-space-in-emphasis` | MD037 | Yes | +| `no-space-in-code` | MD038 | Yes | +| `no-space-in-links` | MD039 | Yes | +| `fenced-code-language` | MD040 | No | +| `first-line-h1` | MD041 | No | +| `no-empty-links` | MD042 | No | +| `required-headings` | MD043 | No | +| `proper-names` | MD044 | Yes | +| `no-alt-text` | MD045 | No | +| `code-block-style` | MD046 | No | +| `single-trailing-newline` | MD047 | Yes | +| `code-fence-style` | MD048 | No | +| `emphasis-style` | MD049 | Yes | +| `strong-style` | MD050 | Yes | +| `link-fragments` | MD051 | Yes | +| `reference-links-images` | MD052 | No | +| `link-image-reference-definitions` | MD053 | Yes | +| `link-image-style` | MD054 | Yes | +| `table-pipe-style` | MD055 | No | +| `table-column-count` | MD056 | No | +| `blanks-around-tables` | MD058 | Yes | +| `descriptive-link-text` | MD059 | No | +| `table-column-style` | MD060 | Yes | + +_(53 rules, 33 fixable. +Generated from the built rule registry — `dist/rules/token/index.js`'s `allTokenRules`, cross-referenced against `benchmarks/parity/rule-map.mjs` for MD ids.)_ + +### `extends` presets + +Recheck ships nine built-in presets, referenced by id under `extends:`. +Presets are applied in listed order, then your own rule keys are merged on top — **your config always wins**: a rule key you define overrides the same key from a preset, and per-assertion options you set override just that assertion's preset options (other preset options for the same rule are preserved). + +There is one exception. +A rule may attach a milder severity to some of its own reports, and your config can't escalate those: `recheck/markdoc-attributes` reports unknown attributes at `warn` no matter what severity you give the rule (see the `recheck/markdoc` bullet below). +`severity: 'off'` still works as expected — it disables the rule entirely, so no reports of any severity. + +- **`recheck/markdown`** — the full 53-rule set from the table above, all at `severity: error` with upstream-faithful default options. + Equivalent to markdownlint's `{ default: true }`. +- **`recheck/markdown-relaxed`** — mirrors markdownlint's own `style/relaxed.json`: the same 53 rules, with `no-trailing-spaces`, `no-hard-tabs`, `no-multiple-blanks`, `no-multiple-space-blockquote`, `no-blanks-blockquote`, `line-length`, `ul-indent`, `no-inline-html`, `no-bare-urls`, `fenced-code-language`, and `first-line-h1` turned off. +- **`recheck/minimal`** — a small, high-signal set: `no-trailing-spaces`, `no-hard-tabs`, `single-trailing-newline`, `no-reversed-links`, `no-empty-links`. +- **`recheck/prose`** — Recheck's Vale-parity starter set, all at `severity: warn`: `repetition` (default options), + `consistency` (one US spelling enforced file-wide for `behavior`/`color`/`license`/`organize` vs. their British spellings, matched with `ignoreCase: true` so a capitalized, sentence-initial variant like `Colour` still counts), + and `capitalization` (`$sentence`, `scope: heading` only, `fix: false`, no preset-level `exceptions` — see below). + All three are scoped to prose segments — `repetition` and `consistency` to `summary` (the document's prose: paragraph, heading, list-item, blockquote, and table-cell text), `capitalization` to headings — so the preset never flags (and `--fix` never rewrites) code samples or frontmatter. + `extends: [recheck/markdown, recheck/prose]` is the one-liner that replaces a markdownlint + Vale combo. + See [Opt-in prose assertions](#opt-in-prose-assertions) below for three more prose assertions that exist but are deliberately **not** in this preset. +- **`recheck/markdoc`** — four Recheck-original rules that check Markdoc tag syntax itself (`{% tag attr="value" %}`) rather than prose or markdownlint parity. + All four are `fix: false`. + - `recheck/markdoc-syntax` (`error`) — malformed spans, unquoted "bareword" values, and close tags carrying attributes. + - `recheck/markdoc-pairing` (`error`) — unclosed, orphaned, or crossed tag pairs, and a self-closing tag written without a slash or given a close tag it shouldn't have. + - `recheck/markdoc-unknown-tag` (`warn`, because custom tags are common) — a tag name the schema doesn't declare. + - `recheck/markdoc-attributes` (mixed) — a missing required attribute, an enum or type violation, and a duplicate attribute report at `error`; an unknown attribute name, whether named or a stray positional value, always reports at `warn`. + That `warn` is set per report by the rule itself, so it wins over the rule's configured severity: setting the rule to `severity: error` does not escalate those reports. + Only `severity: 'off'` removes them, by disabling the rule. + + **These rules only fire when Markdoc tokenization is also on.** + Set `markdoc: true` (or the object form, see below) alongside `extends: [recheck/markdoc]`. + Extending the preset without the flag validates, but prints a console warning that the four rules can never report. + The flag stays an explicit opt-in because Liquid and Jinja templates use the same `{% %}` delimiters for unrelated syntax, so Recheck never assumes it. + +- **`recheck/google`** — Google's developer documentation style guide (https://developers.google.com/style), CC BY 4.0, synced 2026-07-29. + 99 rules covering heading/list/table/link structure, sentence-case headings, sentence length, voice and contractions, plain language, product naming, compound word forms, and inclusive/precise-language terminology — + all derived from the _live_ guide (see `packages/recheck/presets/google/PROVENANCE.md` for the rule -> source page -> quote -> verdict table, including everything considered and NOT shipped, and why). + `extends: [recheck/google]` is a one-line adoption of Google's style; combine with `recheck/markdown` for full structural linting too. + Rule ids are namespaced `google/` (not `recheck/`) so they never collide with the markdownlint-parity or other style-guide presets. + Structural/mechanical rules (heading hierarchy, list mechanics, alt-text presence, sentence length) are `severity: error`; every word-choice, terminology, and punctuation-convention rule is `severity: warn`. + See `packages/recheck/presets/google/sources.json` for the fetched-page hashes. + **Adopting this preset has a real, measured performance cost — roughly 2.7× the standard `recheck/markdown`-only profile's lint time on a docs-sized document set** — see [Performance](#performance) below (Phase 4) before turning it on in CI. +- **`recheck/microsoft`** — the Microsoft Writing Style Guide (https://learn.microsoft.com/en-us/style-guide/welcome/), CC BY 4.0 (via the guide's backing GitHub repository's LICENSE file — no `learn.microsoft.com` page states the licence itself, see `packages/recheck/presets/microsoft/PROVENANCE.md`), synced 2026-07-30. + 93 rules covering heading/list/table/alt-text structure, the guide's own numeric thresholds (paragraph length, list length, comma density, alt-text length), its signature "use contractions" rule, US spelling, bias-free and people-first terminology, and a large A-Z terminology word list — + all derived from the _live_ guide and checked against four independent verification passes (~490 rules/entries across ~340 page fetches), with every Tier-1 pair anchored or demoted to detection-only wherever it was found capable of rewriting correct prose. + Rule ids are namespaced `microsoft/`. + Structural rules and the A-Z word list's three unconditional tiers are `severity: error`; voice, punctuation-convention, and UI-terminology rules are `severity: warn`. + Audience-conditional and UI-conditional entries (Microsoft's own "Tier 4") are never enforced, and developer-audience carve-outs relevant to API documentation (`header`, `context menu`, `disk`, `directory`) are excluded rather than misfiring on Redocly's own docs — see `packages/recheck/presets/microsoft/PROVENANCE.md` for the full table, every excluded candidate, and why. + Unlike `recheck/google` (which allows `click`), this preset bans all input-specific UI verbs (`click`, `press`, `hit`) in favor of `select` — the sharpest divergence between the two guides. + See `packages/recheck/presets/microsoft/sources.json` for the fetched-page hashes. +- **`recheck/inclusive-language`** — composable, guide-agnostic: the _intersection_ of `recheck/google` and `recheck/microsoft`'s inclusive/bias-free/ableist/accessibility content — + terminology both flagship guides independently state should be avoided (`slave`, `master/slave`, `blacklist`/`whitelist`, `DMZ`, `grayed-out`, `he/she`, `normal person`/`healthy person`, `suffering from`/`victim of`, `differently abled`, `crippled`, `nuke`). + All `warn` severity, all detection-only. + Needed no new web fetch — every term was already confirmed against a live page by five existing verification reports; see `packages/recheck/presets/inclusive-language/PROVENANCE.md` for the report → row → term table and every single-guide term left out on purpose. + Layer it onto either flagship or onto `recheck/prose`: `extends: [recheck/google, recheck/inclusive-language]`. + **Because it's built as an intersection, every one of its 11 rules is already shipped by at least one flagship's own preset** + (measured: 7 of 11 duplicate a `google/*` finding on the same span when stacked onto `recheck/google` alone, 6 of 11 duplicate a `microsoft/*` finding when stacked onto `recheck/microsoft` alone — see `packages/recheck/presets/inclusive-language/PROVENANCE.md`'s "Duplicate-finding audit"). + Its full, zero-duplicate value is realized standalone, with `recheck/prose`, or on a project using neither flagship; stacked onto exactly one flagship it still fills that flagship's own gaps, but expect a majority of its findings to be reported twice. +- **`recheck/plain-language`** — composable, derived from the _live_ US federal plain-language guidance (`digital.gov/guides/plain-language`; public domain, no attribution constraint). + Smaller than a first read of the old `plainlanguage.gov` site would suggest: + that site is now dead and redirects to a much thinner overview, so there's no sentence-length or readability-`metric` rule (`metric` stays a documented [opt-in](#opt-in-prose-assertions), unchanged) — + only paragraph length (the one family with real, quotable numbers), filler/wordy phrases, complex-word substitutes, redundant pairs, double negatives, and jargon-to-plain examples. + **`shall` is never flagged** — it's a defined RFC 2119 normative keyword used throughout specs and API docs, exactly what Recheck lints; `implement` and `command` carry the identical technical-sense collision and are excluded the same way. + All `warn`/`error` (paragraph-length ceiling only) severity, all detection-only. + `in order to` and `utilize`/`utilization` are deliberately NOT shipped despite being live, verbatim guide content — both flagships already ship the identical pair, so keeping them here would only ever produce a duplicate finding, never new coverage (measured: this cut duplicate findings on the same fixture from 6 to 3 against `recheck/google`, and from 5 to 3 against `recheck/microsoft`). + The 3 that remain are an accepted paragraph-length overlap with `recheck/microsoft` (two independently-sourced numbers, not the same fact restated) and a coincidental substring collision with `use-contractions`, not content duplication. + See `packages/recheck/presets/plain-language/PROVENANCE.md` for every rule's source quote, every family considered and left out, and the full duplicate-finding audit. + +- **`recheck/technical-english`** — composable, an original rule set that helps writers follow the principles of ASD-STE100 Simplified Technical English: sentence length (max 25 words, the descriptive-text bound; tighten to 20 for procedures), paragraph length (max 6 sentences), and a passive-voice heuristic at `info`. + ASD-STE100 Simplified Technical English is a Copyright and a Trade Mark of ASD, Brussels, Belgium. + This preset is an independent work that ASD and the STEMG do not review, approve, certify, or endorse, and it reproduces no part of the standard — not its text and not its dictionary (compose with `recheck/plain-language` for word-choice checking). + See `packages/recheck/presets/technical-english/PROVENANCE.md` for the STEMG correspondence and every deliberate omission. + + **All five of the presets above — `recheck/google`, `recheck/microsoft`, + `recheck/inclusive-language`, `recheck/plain-language`, and `recheck/technical-english` — are + detection-only by design, not by omission: no rule in any of them + auto-fixes, ever.** + This is enforced structurally (`fix: false` on every rule, set once by a + loop at the end of each preset's builder function) and guarded by a test + that reads the live preset object and fails if a future rule change ever + makes one fixable again — see `preset-google.test.ts`'s and + `preset-microsoft.test.ts`'s "is detection-only" describe blocks, and + `preset-composition.test.ts`'s list-driven version covering all four. + `recheck/google` and `recheck/microsoft` once had fixable rules. + Every + attempt to define a safe subset of them found the fixes corrupting + genuinely correct prose, in every category previously believed safe: + spelling (Hemingway's correctly spelled _A Moveable Feast_ → "A Movable + Feast"), hyphenation ("read only the introduction" → "read-only the + introduction"), and at least one outright inversion of meaning ("No SQL is + used here" → "NoSQL is used here"). + A rule's _category_ does not predict + fix safety at this scale: a style guide states intent while + `swap`/`consistency`/`pattern` match tokens, and narrowing which + categories count as "safe" doesn't close that gap. + Detection is + unaffected — every rule still runs and reports, and you apply the fix + yourself with the judgment style guidance has always required. + This is the + same reason Vale, the tool these presets replace, never shipped this class + of bug. + See the "Detection-only" sections of + `packages/recheck/presets/google/PROVENANCE.md` and + `packages/recheck/presets/microsoft/PROVENANCE.md` for the full history. + + The heading rule uses **sentence case** _(changed from AP title case by product decision 2026-07-29: Redocly's own guide, Google, and Microsoft all mandate sentence case)_. + Two details make that default safe out of the box: + - **The [built-in technical proper-noun vocabulary](#built-in-technical-proper-noun-vocabulary)** — `TECHNICAL_PROPER_NOUNS` — is unioned into `exceptions` by `capitalization` itself (default `builtinVocabulary: true`), so this preset doesn't ship its own copy: `$sentence` still won't flag `OpenAPI`, `GitHub`, `macOS`, and the rest of that list out of the box. + It's a common-vocabulary floor, not a full brand list — extend it with your own product/company names via this rule's own `exceptions`, which **compose** with the built-ins rather than replacing them (unlike a preset-shipped list, which a same-key override would have replaced entirely). + - **`fix: false`** — a sentence-case auto-fix would lowercase any proper noun the built-ins and your own `exceptions` don't cover, silently damaging content. + Set `fix: true` on your own `recheck/capitalization` key (or drop the key) once your exceptions list covers your vocabulary. + +```yaml +extends: + - recheck/markdown-relaxed + +# Your own overrides win over the preset: +recheck/line-length: + severity: warn + assertions: + line-length: + lineLength: 120 +``` + +Multiple presets can be listed; later presets in the list override earlier ones for the same rule key, before your own top-level rule keys are merged in last. + +### Tune a preset + +Adopting a whole style-guide preset doesn't mean accepting every rule at its shipped severity. +Because your own config's rule keys always win over a preset's (see above), you can turn individual rules off, downgrade them, or silence single occurrences — all verified against a live build, not just read from source: + +```yaml +extends: [recheck/markdown, recheck/microsoft] + +microsoft/az-navigation: + severity: off # turn a rule off entirely + +microsoft/heading-sentence-case: + severity: warn # downgrade an error to a warning (this rule ships at error) +``` + +Or silence one occurrence instead of the whole rule, with an inline directive (works on any rule, from any preset — see [Inline Directives](#inline-directives) above): + +```markdown + + +Click the hot link to continue. + + + +...several occurrences here are all silenced... + + + + +``` + +**The sharp edge:** merging a user override on top of a preset rule happens per _assertion id_, not per option inside it. +Setting a partial override on a bundled `swap` or `pattern` rule doesn't just change the one option you named — it **replaces that assertion object entirely**, silently discarding everything else it carried. +For example: + +```yaml +microsoft/spelling-hyphenation: + assertions: + swap: + ignoreCase: false +``` + +drops the preset's whole `pairs` map along with it, and the config then fails validation outright (verified against this exact rule on a live build): + +```text +Rule "microsoft/spelling-hyphenation": swap requires a "pairs" object mapping find -> replace strings +``` + +So today, to reject just one term out of a bundled `swap`/`pattern` rule, your options are: turn the whole rule off, restate its entire `pairs`/`tokens` yourself, or inline-disable each occurrence as shown above. +Two assertion types already have a real per-term escape hatch that doesn't hit this edge: `capitalization`'s `exceptions` (an array of allowed terms that **composes** with the built-in technical-proper-noun vocabulary and anything else you add, rather than replacing it) and `spelling`'s `ignore`. +A per-term opt-out for `swap`/`pattern` is a known follow-up, not shipped yet. + +### Example configs + +`packages/recheck/examples/{google,microsoft,inclusive-language,plain-language,technical-english}.yaml` are ready-to-copy configs for the five style presets, +generated by `pnpm examples:generate` (`packages/recheck/scripts/generate-examples.mjs`) so they can never drift from the preset they document — +a test (`src/config/__tests__/examples-drift.test.ts`) byte-compares each on-disk file against a fresh render and fails, naming the file, if either the preset or the file's own hand-maintained appendix (`examples/appendices/.appendix.yaml`) changes without regenerating. + +Each file has four parts, in this order: + +1. **An attribution header** — source, license, and sync date, as YAML comments (mirrors that preset's `PROVENANCE.md`). +2. **`# What to paste`** — the actual adoption cost: a two-to-four-line `extends` block. + This is the only part most readers need; everything below it is supporting material, not something to copy. +3. **`# How to tune it`** — override patterns verified to work today (turn a rule off, downgrade its severity, inline-disable one occurrence with an HTML comment), + plus a documented sharp edge: overriding one option on a bundled `swap`/`pattern` rule's `assertions` **replaces that assertion entirely**, silently discarding options like a `pairs` map you didn't restate (merging is per _assertion id_, not per option) — + restate the whole map, turn the rule off, or use an inline directive instead. + `capitalization`'s `exceptions` and `spelling`'s `ignore` are the two assertion types that already have a real per-term escape hatch; an equivalent for `swap`/`pattern` is a known follow-up, not shipped yet. +4. **`# Full expansion (reference)`** — the preset's entire resolved rule set (alphabetized), so a reader can see exactly what they're adopting without running the tool. + Every value here is identical to what the `extends` block above already resolves to, so copying this section too is redundant, not broken — it's for reading, not pasting. + +A hand-maintained appendix is appended verbatim after part 4: NOISY candidates the guide states but the preset doesn't enforce (shown as the rule they'd be if shipped, commented out, with a one-line false-positive note each) and a checklist of guide content that needs a human, not a linter (NOT-ENFORCEABLE — active voice, missing-Oxford-comma detection, and similar). + +### Opt-in prose assertions + +`recheck/prose` (above) intentionally ships only `repetition`, `consistency`, and `capitalization` — a small, broadly-applicable default. +Three more Vale-parity/native assertions exist (see [Assertion Types](#assertion-types) above for full per-option tables) but are **not shipped in any preset**, because their thresholds, patterns, or dictionaries are inherently project-specific rather than having one right-for-everyone default: `conditional`, `metric`, `spelling`. +(`length` and `occurrence` used to be entries here; neither is an opt-in any more — [`recheck/google`](#extends-presets) ships `length` directly for the guide's sentence-length limit, and [`recheck/microsoft`](#extends-presets) ships `occurrence` directly for the guide's comma-density rule, so neither one's default bounds are "no one right answer" any more.) +Add any of the three by copying its rule below into your own config, alongside `extends: [recheck/prose]`: + +```yaml +extends: [recheck/markdown, recheck/prose] + +# conditional: if "TBD" appears, a tracking-issue link must exist somewhere in the file. +recheck/tbd-needs-tracking-link: + severity: warn + message: '"%s" appears but "%s" was never introduced.' + assertions: + conditional: + first: '\bTBD\b' + second: 'https://github\.com/\S+/issues/\d+' + +# metric: flag prose below a Flesch reading-ease floor (higher score = easier to read). +# The message's four slots are positional: formula, score, min, max (see "Metric Assertions"). +recheck/readability-floor: + severity: warn + message: 'Readability (%s) is %s; expected between %s and %s.' + assertions: + metric: + formula: flesch-reading-ease + min: 30 + +# spelling: requires the optional `nspell`/`dictionary-en` peers -- see +# "Spelling Assertions" above for the install command. +recheck/us-spelling-check: + severity: warn + message: 'Unknown word "%s"%s' + assertions: + spelling: + vocab: [Redocly, Reunite] +``` + +Each snippet's `severity`, `message`, `scope`, and `exceptions` are yours to adjust — see [Rule Types and Assertions](#rule-types-and-assertions) for every option each assertion accepts, and [Inline Directives](#inline-directives) to silence any one of them on a specific line or file with an HTML comment instead of turning it off entirely. + +### Migrate from markdownlint + +A markdownlint config maps onto Recheck almost 1:1 — `extends` a preset, then override individual rules by their Recheck name (same short name markdownlint uses, e.g. `line-length` for MD013) under `assertions`: + +```yaml +extends: + - recheck/markdown + +recheck/line-length: + severity: warn + message: 'Keep lines under %s characters.' + assertions: + line-length: + lineLength: 120 + codeBlocks: false + +recheck/ul-style: + severity: off +``` + +**Renamed legacy assertion ids — old ids are no longer accepted.** +A handful of ids from Recheck's pre-parity native rules were converged onto their markdownlint-parity replacements. +These four were removed outright, not kept as aliases: using the old id in a config now fails validation with an `unknown assertion type ""` error, and the config must be updated to the new id: + +| Old id (removed) | Use instead | +| -------------------------- | ------------------------------ | +| `max-line-length` | `line-length` (MD013) | +| `bullet-style` | `ul-style` (MD004) | +| `no-duplicate-headings` | `no-duplicate-heading` (MD024) | +| `no-broken-fragment-links` | `link-fragments` (MD051) | + +Two other ids are **upstream markdownlint's own alternate rule names**, not a Recheck deprecation — these remain permanent, warning-free aliases and require no config change: + +| Upstream synonym | Canonical id | +| -------------------- | ----------------------- | +| `first-line-heading` | `first-line-h1` (MD041) | +| `single-title` | `single-h1` (MD025) | + +**Two intentional behavior changes** vs. plain markdownlint defaults, both on rules that predate the parity port and kept their exact ids: + +- **`no-trailing-spaces` (MD009)** now exempts lines with _exactly 2_ trailing spaces by default (a markdown hard line break), instead of flagging all trailing whitespace. + Set `strict: true` to restore the old flag-everything behavior (matches markdownlint's default). +- **`no-hard-tabs` (MD010)**'s `spacesPerTab` option now defaults to `1` (matching markdownlint's own upstream default) — Recheck's earlier, pre-parity native rule had defaulted this to `2`. + If you were relying on that old default, set `spacesPerTab: 2` explicitly. + +Token-rule options aren't individually schema-validated, so an option name a rule doesn't recognize is silently ignored — it has no effect, and produces no warning or error. + +### Cross-file link validation + +`link-fragments` accepts `crossFile: true` to validate links across files, replacing external link checkers such as `mlc` for repo-internal links: + +- A relative link or image target must exist on disk (`[x](./missing.md)` flags). +- A `file.md#anchor` fragment must exist in the target file's headings and anchors. +- Extensionless links resolve the way the Realm router does: `./page` tries `page.md`, and a directory link reads its `index.md`. +- Site-root absolute paths (`/x/y`) resolve against the `rootDir` option when set, and are skipped without it. +- `ignoredTargets` skips destinations by glob (`['/gateways/**']`) — for routes a renderer generates from data, with no file on disk. +- Links to `
` sections resolve: a `
` without an explicit `id` gets one derived from its `` text, the same way the theme generates them in the browser. +- Markdoc tags in headings (`## Payments {% badge /%}`) do not change the heading's anchor. + In a monorepo with several docs projects, give `rootDir` a map from source-directory prefix to that directory's site root; the longest matching prefix wins, and files under no prefix keep the skip. + Paths are relative to the working directory. +- External URLs and `mailto:` are skipped. + +Each target file is read once per run and cached by modification time. +The option is off by default; in-document fragment checking is unchanged. + +### Known differences from markdownlint + +- **Inline HTML-comment disable directives are not supported**, for example: + + ```html + + ``` + + (dash HTML-escaped above so this very README doesn't trip markdownlint's own directive scanner — markdownlint recognizes these directives even inside fenced code, so the literal syntax can't appear here unescaped). + Markdownlint's HTML-comment-based per-line/per-region rule toggles (`markdownlint-disable`, `markdownlint-disable-next-line`, `markdownlint-enable`, etc.) are a distinct engine feature, not a rule port, and Recheck doesn't parse them today. + Use config-level `exceptions` (file/line patterns) or `excludes`/`appliesTo` to achieve the same effect. + Native support is a possible future addition; no decision has been made yet. + +### Parity with markdownlint + +The 53 ported rules are checked against upstream markdownlint by a differential harness (`pnpm parity`, `benchmarks/parity/run-parity.mjs`): the harness lints the same real-world document set with both Recheck (via a config translated from markdownlint's option surface) and markdownlint itself, then set-diffs the findings. +As of this writing it reports **zero unexplained differences** across: + +- `mdn-content` (MDN Web Docs, ~14.5k files) +- `electron` (Electron's docs + repo markdown) +- `monorepo-docs` (this monorepo's own `docs/` tree) + +on both the `default` profile (full `recheck/markdown` preset vs. markdownlint `{ default: true }`) and a `rebilly` profile (a real third-party `.markdownlint.yaml` translated to Recheck config). +A small, explicitly documented allowlist (`benchmarks/parity/allowlist.json`) covers the one known permanent engine-surface gap — inline `markdownlint-disable` directives (see [Known differences](#known-differences-from-markdownlint) above) — scoped to the exact rules it can affect (MD010, MD011, MD033, MD059). +Everything else matches exactly. + +**`pnpm parity` requires `--corpus`** — running it bare exits `2` with a usage error (`Usage: node benchmarks/parity/run-parity.mjs --corpus [--profile default|rebilly] [--rules MD001,MD013]`) rather than running against a default document set. +Always pass a document-set name, e.g. `pnpm parity --corpus monorepo-docs --profile default`. + +### Front matter validation + +The `front-matter` token rule validates front matter against JSON Schema, using `@redocly/ajv` — the same validator Redocly CLI uses. +Map file patterns to schemas; the first matching mapping wins, and a file that matches no mapping is not checked. + +```yaml +recheck/front-matter: + severity: error + message: 'Front matter: %s' + assertions: + front-matter: + schemas: + - files: ['.changeset/**'] + schema: + type: object + patternProperties: + '^@redocly/': { enum: [major, minor, patch] } + additionalProperties: false + - files: ['docs/**'] + schemaFile: schemas/docs-front-matter.yaml +``` + +- `schema` is an inline JSON Schema object, or the name of a built-in schema (see below); `schemaFile` loads one from a YAML or JSON file, relative to the working directory. +- A file with no front matter validates as an empty object, so the schema's `required` list decides whether front matter is mandatory. +- Findings point at the line of the offending top-level key; front matter that is not valid YAML is one finding at the block start. + +#### Built-in schema: Realm page front matter + +`schema: realm` validates the front matter options a Realm page accepts, so a project gets the check without vendoring a copy that goes stale: + +```yaml +- files: ['docs/**'] + schema: realm + strict: true +``` + +It covers the front-matter-only options (`excludeFromSearch`, `sidebar`, `slug`, `template`, `navigation`, `keywords`), the options that override `redocly.yaml` (`banner`, `breadcrumbs`, `codeSnippet`, `colorMode`, `feedback`, `footer`, `markdown`, `metadata`, `navbar`, `navigation`, `rbac`, `redirects`, `search`, `seo`, `versionPicker`), and `title`/`description`. + +The schema checks the **type** of each known key, not the inner shape of the option objects. +`seo`, `markdown`, and their siblings are whole configuration blocks that Realm evolves independently, so encoding their structure here would drift and start rejecting valid pages. +Type checking still catches what actually goes wrong: a misspelled key (with `strict`), and a value of the wrong kind (`excludeFromSearch: "true"`). + +`strict: true` adds `additionalProperties: false`, which turns a misspelled option into a finding. +It is off by default because pages carry project data that Markdoc templates read back through `$frontmatter.` — Redocly's own docs use `products` and `plans` this way on 268 pages. +To keep `strict` and allow your own keys, copy the built-in as a starting point and add them. + +## GitHub Actions Integration + +Recheck provides seamless GitHub Actions integration for automated content quality checking on pull requests. + +### Output Format: `github-actions` + +Use the `--output github-actions` format to generate inline file annotations that appear directly on pull request files: + +```bash +# Basic GitHub Actions output +node dist/cli.js docs --output github-actions + +# With annotation limits (recommended for PR workflows) +node dist/cli.js docs --output github-actions --annotations-limit 20 +``` + +### Annotation Output + +The GitHub Actions format produces annotations that GitHub automatically displays as inline comments: + +```text +::error title=recheck/no-trailing-spaces,file=docs/guide.md,line=42,col=15,endColumn=18::Trailing spaces +::warning title=recheck/ul-style,file=docs/api.md,line=23,col=1,endColumn=2::Unordered list style +``` + +### GitHub Actions Limits + +GitHub Actions has strict limits on annotations: + +- **10 error** and **10 warning** annotations per step +- **50 total** annotations per job + +**Recommendation**: Use `--annotations-limit 20` (the default) to stay well within these limits while prioritizing the most critical issues. + +### Workflow Example + +```yaml +- name: Run recheck with inline annotations + run: | + node packages/recheck/dist/cli.js docs \ + --config recheck.yaml \ + --output github-actions \ + --changed-only < changed-files.txt +``` + +This creates both inline file annotations AND summary comments when combined with the PR comment workflow. + +## Performance + +Recheck's file-first, parse-once architecture is benchmarked directly against `markdownlint`'s own library API (`benchmarks/run-markdownlint.mjs`) on the same corpora, using `pnpm bench` (see `benchmarks/bench.mjs`): + +- **Phase 1** (10 native scope rules vs. markdownlint's default rules, `monorepo-docs` document set, 953 files): recheck **0.91×** markdownlint's median time (3219ms vs. 3521ms) — see `benchmarks/results/phase1-ast-core.json` / `baseline-markdownlint.json`. +- **Phase 2** (all 53 markdownlint-parity rules via `extends: [recheck/markdown]` vs. markdownlint's `{ default: true }`, equivalent rule sets): + - `monorepo-docs` (954 files): recheck 4468ms vs. markdownlint 3755ms — **1.19×**. + - `mdn-content` (14,515 files, a real-world OSS document set): recheck 41839ms vs. markdownlint 34548ms — **1.21×**. + +Both Phase 2 numbers sit comfortably inside the ±40% parity gate (recheck's median must fall within `[0.6×, 1.4×]` of markdownlint's on an equivalent rule set) enforced by: + +```bash +pnpm bench --subject benchmarks/run-recheck-mdl-preset.mjs --corpus monorepo-docs --record