@redocly/recheck 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +174 -94
  2. package/dist/cli.js +110 -99
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/baseline.d.ts +1 -1
  5. package/dist/commands/baseline.js +1 -1
  6. package/dist/commands/markdoc-schema.js +6 -6
  7. package/dist/commands/markdoc-schema.js.map +1 -1
  8. package/dist/commands/run.d.ts.map +1 -1
  9. package/dist/commands/run.js +1 -1
  10. package/dist/commands/run.js.map +1 -1
  11. package/dist/core/baseline.js +0 -0
  12. package/dist/core/baseline.js.map +1 -1
  13. package/dist/data/realm-front-matter-schema.d.ts +28 -0
  14. package/dist/data/realm-front-matter-schema.d.ts.map +1 -0
  15. package/dist/data/realm-front-matter-schema.js +73 -0
  16. package/dist/data/realm-front-matter-schema.js.map +1 -0
  17. package/dist/parser/markdoc/extract-statics.d.ts +1 -1
  18. package/dist/parser/markdoc/extract-statics.js +2 -2
  19. package/dist/parser/markdoc/extract-statics.js.map +1 -1
  20. package/dist/rules/scope/length.d.ts.map +1 -1
  21. package/dist/rules/scope/length.js +5 -1
  22. package/dist/rules/scope/length.js.map +1 -1
  23. package/dist/rules/scope/metric.js +1 -1
  24. package/dist/rules/scope/metric.js.map +1 -1
  25. package/dist/rules/token/front-matter.d.ts.map +1 -1
  26. package/dist/rules/token/front-matter.js +40 -3
  27. package/dist/rules/token/front-matter.js.map +1 -1
  28. package/dist/scopes/extractor.d.ts.map +1 -1
  29. package/dist/scopes/extractor.js +89 -28
  30. package/dist/scopes/extractor.js.map +1 -1
  31. package/dist/scopes/sentences.d.ts.map +1 -1
  32. package/dist/scopes/sentences.js +85 -26
  33. package/dist/scopes/sentences.js.map +1 -1
  34. package/package.json +12 -3
  35. package/skills/recheck-config/SKILL.md +4 -4
  36. package/skills/recheck-lint/SKILL.md +2 -2
package/README.md CHANGED
@@ -58,77 +58,77 @@ pnpm build
58
58
 
59
59
  ```bash
60
60
  # Validate with explicit config file
61
- node dist/cli.js validate --config recheck.example.yaml
61
+ node dist/cli.js --validate-config --config recheck.example.yaml
62
62
 
63
63
  # Auto-discover config file in current directory
64
- node dist/cli.js validate
64
+ node dist/cli.js --validate-config
65
65
  ```
66
66
 
67
67
  ### Run content linting
68
68
 
69
69
  ```bash
70
70
  # Run on current directory
71
- node dist/cli.js run . --config recheck.example.yaml
71
+ node dist/cli.js . --config recheck.example.yaml
72
72
 
73
73
  # Run on specific file
74
- node dist/cli.js run README.md --config recheck.example.yaml
74
+ node dist/cli.js README.md --config recheck.example.yaml
75
75
 
76
76
  # Filter by severity (only show errors)
77
- node dist/cli.js run . --severity error
77
+ node dist/cli.js . --severity error
78
78
 
79
79
  # Show all enabled rules (info and above)
80
- node dist/cli.js run . --severity info
80
+ node dist/cli.js . --severity info
81
81
 
82
82
  # Work one rule at a time. This helps you clear a large list of findings.
83
83
  # Give the name that the report shows, or the full config key. Use the flag
84
84
  # more than one time for more than one rule. A rule from a namespace other
85
85
  # than `recheck/` keeps that namespace: use `google/passive-voice`.
86
- node dist/cli.js run . --rule semantic-line-breaks
87
- node dist/cli.js run . -r us-spelling -r recheck/oxford-comma
86
+ node dist/cli.js . --rule semantic-line-breaks
87
+ node dist/cli.js . -r us-spelling -r recheck/oxford-comma
88
88
 
89
89
  # ...and its inverse, to silence a rule you have already triaged
90
- node dist/cli.js run . --exclude-rule semantic-line-breaks
90
+ node dist/cli.js . --exclude-rule semantic-line-breaks
91
91
 
92
92
  # Use --rule with --fix to clear one rule's findings across all documents
93
- node dist/cli.js run . --rule semantic-line-breaks --fix
93
+ node dist/cli.js . --rule semantic-line-breaks --fix
94
94
 
95
95
  # A name that matches no rule in your config is an error, not an empty run:
96
96
  # a misspelled filter that reported "no issues" would look the same as a
97
97
  # clean document set. The error message lists the rules your config loaded.
98
98
 
99
99
  # Output formats (table is default)
100
- node dist/cli.js run . --output table # Human-readable table (default)
101
- node dist/cli.js run . --output json # Structured JSON for CI
102
- node dist/cli.js run . --output sarif # SARIF format for security tools
103
- node dist/cli.js run . --output github-actions # GitHub Actions annotations (inline PR comments)
100
+ node dist/cli.js . --output table # Human-readable table (default)
101
+ node dist/cli.js . --output json # Structured JSON for CI
102
+ node dist/cli.js . --output sarif # SARIF format for security tools
103
+ node dist/cli.js . --output github-actions # GitHub Actions annotations (inline PR comments)
104
104
 
105
105
  # Show detailed statistics
106
- node dist/cli.js run . --stats
106
+ node dist/cli.js . --stats
107
107
 
108
108
  # Auto-fix safe issues (35 fixable rules total: swap + semantic-line-breaks
109
109
  # natively, plus 33 of the 53 markdownlint-parity rules — see the rule table
110
110
  # under "Markdownlint parity" below for the full per-rule breakdown)
111
- node dist/cli.js run . --fix
111
+ node dist/cli.js . --fix
112
112
 
113
113
  # Combine auto-fix with statistics
114
- node dist/cli.js run . --fix --stats
114
+ node dist/cli.js . --fix --stats
115
115
 
116
116
  # Limit annotations for CI (applies to file output, default: 20)
117
- node dist/cli.js run . --annotations-limit 50
117
+ node dist/cli.js . --annotations-limit 50
118
118
 
119
119
  # Output to file (works with all formats)
120
- node dist/cli.js run . --output json --output-path report.json
121
- node dist/cli.js run . --output sarif --output-path recheck.sarif
122
- node dist/cli.js run . --output json --output-path limited.json --annotations-limit 50
120
+ node dist/cli.js . --output json --output-path report.json
121
+ node dist/cli.js . --output sarif --output-path recheck.sarif
122
+ node dist/cli.js . --output json --output-path limited.json --annotations-limit 50
123
123
 
124
124
  # Emit run summary to a file (json or text)
125
- node dist/cli.js run . --summary json --summary-path recheck-summary.json
125
+ node dist/cli.js . --summary json --summary-path recheck-summary.json
126
126
 
127
127
  # Scan only changed files (via file list or stdin)
128
128
  # From file:
129
- node dist/cli.js run . --changed-only --changed-list changed.txt
129
+ node dist/cli.js . --changed-only --changed-list changed.txt
130
130
  # Or with stdin:
131
- git diff --name-only origin/main... | node dist/cli.js run . --changed-only
131
+ git diff --name-only origin/main... | node dist/cli.js . --changed-only
132
132
  ```
133
133
 
134
134
  ## Library API
@@ -261,7 +261,7 @@ recheck/ul-style-dash:
261
261
  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.
262
262
 
263
263
  ```bash
264
- recheck baseline # writes recheck-baseline.yaml next to your config
264
+ recheck --generate-baseline # writes recheck-baseline.yaml next to your config
265
265
  ```
266
266
 
267
267
  Activate it with one config line:
@@ -279,11 +279,11 @@ files:
279
279
  recheck/semantic-line-breaks: 3
280
280
  ```
281
281
 
282
- With the baseline active, `recheck run`:
282
+ With the baseline active, `recheck`:
283
283
 
284
284
  - **suppresses** findings whose (file, rule) count matches the baseline, and reports how many matched;
285
285
  - **fails** when a count rises — the group's findings are printed with `(baseline 3, found 5)` context;
286
- - **fails** when a count falls, because the baseline is stale — the message says to run `recheck baseline` and commit the result.
286
+ - **fails** when a count falls, because the baseline is stale — the message says to run `recheck --generate-baseline` and commit the result.
287
287
  Counts only step down, so the file equals reality at every green commit.
288
288
 
289
289
  Warnings are never baselined; they do not affect exit codes.
@@ -293,15 +293,15 @@ A renamed file is a new path with no budget, so its pre-existing findings report
293
293
 
294
294
  ## Readability
295
295
 
296
- `recheck readability` reports scores per file: Flesch reading ease, Flesch-Kincaid grade, Automated Readability Index (ARI), words, and sentences, plus medians.
296
+ `recheck --readability` reports scores per file: Flesch reading ease, Flesch-Kincaid grade, Automated Readability Index (ARI), words, and sentences, plus medians.
297
297
  ARI is a grade level computed from exact character counts, with no syllable heuristic, which makes it steadier on technical vocabulary.
298
298
  It is score-shaped, not rule-shaped: it never gates and always exits 0 when it ran.
299
299
  To gate on a bound, use the `metric` assertion — both read the same prose and the same formulas, so they can never disagree.
300
300
 
301
301
  ```bash
302
- recheck readability docs
303
- recheck readability docs --output json
304
- recheck readability docs --changed-only < changed.txt # score only listed files
302
+ recheck docs --readability
303
+ recheck docs --readability --output json
304
+ recheck docs --readability --changed-only < changed.txt # score only listed files
305
305
  ```
306
306
 
307
307
  The score reads flowing prose the way standard readability tools do: headings, code, and Markdoc tags are excluded, and every block ends a sentence.
@@ -414,7 +414,7 @@ Nothing in this file is linted at all, no matter where this comment sits.
414
414
  The five forms:
415
415
 
416
416
  | Directive | Effect |
417
- |---|---|
417
+ | --- | --- |
418
418
  | `<!-- recheck-disable -->` | Disables **all** rules from this line to the end of the file, or until a matching `recheck-enable`. |
419
419
  | `<!-- recheck-disable rule… -->` | Disables only the **listed** rules from this line on (same end conditions). |
420
420
  | `<!-- recheck-enable -->` | Re-enables all rules (or, with rule names, only the listed ones) from this line on. |
@@ -437,7 +437,10 @@ Rules are defined using `assertions` that specify their behavior:
437
437
 
438
438
  #### Swap Assertions (`swap`)
439
439
  Text replacement with configurable options.
440
- **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.
440
+ **Fixable**: each match is replaced with its pair's value, with the matched text's own casing applied to the replacement --
441
+ an all-lowercase match inserts the replacement as configured, a Capitalized match capitalizes just the replacement's first word,
442
+ and an ALL-CAPS match (2+ letters) uppercases the whole replacement;
443
+ any other (mixed-case) casing is left as configured, since it carries no reliable intent to infer.
441
444
  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'`.
442
445
  (With `keysAreRegex: true`, casing is inferred from the MATCHED text, not the regex key, so this applies uniformly to regex keys too.)
443
446
  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.
@@ -505,7 +508,8 @@ The rule's `message` gets two positional `%s` substitutions, in this order: **1s
505
508
  Not fixable (detection-only): a count-based violation has no single match position to anchor an edit to.
506
509
 
507
510
  #### Repetition Assertions (`repetition`)
508
- 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'`).
511
+ 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.
512
+ **Fixable**: collapses the pair back to one occurrence, keeping the FIRST token's casing/text (so `'The the'` fixes to `'The'`, not `'the'`).
509
513
 
510
514
  ```yaml
511
515
  assertions:
@@ -525,7 +529,8 @@ Fix idempotency holds under repeated `--fix` passes: `'the the the'` converges t
525
529
 
526
530
  #### Consistency Assertions (`consistency`)
527
531
  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).
528
- 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'`).
532
+ Whichever variant appears **first in the file (by source order)** wins file-wide; every later occurrence of the other variant is flagged.
533
+ **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'`).
529
534
 
530
535
  ```yaml
531
536
  assertions:
@@ -549,7 +554,8 @@ The rule's `message` gets two positional `%s` substitutions, in this order: **1s
549
554
 
550
555
  #### Conditional Assertions (`conditional`)
551
556
  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`.
552
- 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`.
557
+ When `second` is absent file-wide, every `first` match becomes its own problem, at its exact source position.
558
+ **Detection-only** (not fixable) — there is no single well-defined edit that would "introduce" `second`.
553
559
 
554
560
  ```yaml
555
561
  assertions:
@@ -600,24 +606,31 @@ Unknown option keys, a missing/empty `match`, an invalid `style`, a non-string-a
600
606
  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).
601
607
  - A word already in ALL-CAPS (2+ letters, e.g. an acronym like `API`) is left exactly as written.
602
608
  - **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`).
603
- - **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`.
609
+ - **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`) —
610
+ e.g. Chicago lowercases `'...walking through the park'` → `'...walking through the Park'`, where AP capitalizes `Through`.
604
611
 
605
612
  **`$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).
606
613
 
607
- **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.
614
+ **Word position counts a phrase exception as one word.**
615
+ 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.
608
616
  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).
609
617
  Single-word exceptions (e.g. `GitHub`) behave as they always have — resolved by lookup rather than position.
610
618
 
611
- 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`).
619
+ This used to be a bug, tracked as [Redocly/redocly#25610](https://github.com/Redocly/redocly/issues/25610) and fixed since:
620
+ 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`
621
+ (`## VS Code actions for teams` was flagged, and under `fix: true` rewritten to `## VS Code Actions for teams`)
622
+ 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`).
612
623
  If you had worked around it by rephrasing headings or by swapping in a custom regex `match`, neither is needed any more.
613
624
  See `rules/scope/title-case.ts`'s `recaseWords` for the tokenization that replaced the masking.
614
625
 
615
626
  **`$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`).
616
627
 
617
- **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.
628
+ **Custom regex** — the whole segment text must satisfy the pattern.
629
+ **Detection-only**: unlike the four `$`-styles, a failing regex is flagged but never auto-fixed, even though the rule itself is registered fixable.
618
630
  Like `pattern`'s `tokens`, an invalid regex is caught and silently produces zero problems rather than crashing the run.
619
631
 
620
- **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.
632
+ **Inline code is frozen.**
633
+ 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.
621
634
 
622
635
  **Fixable** for `$title`/`$sentence`/`$lower`/`$upper` only, one segment-wide edit per flagged segment.
623
636
  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.
@@ -642,16 +655,19 @@ assertions:
642
655
 
643
656
  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.
644
657
 
645
- **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).
658
+ **Always summary-scoped.**
659
+ 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).
646
660
  Config validation forces every `metric` rule to `scope: summary`.
647
661
  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.
648
662
  Text from overlapping segments (e.g. a list nested inside a blockquote) is deduplicated by source position, same as `consistency`/`conditional` above.
649
663
 
650
- **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.
664
+ **What the score reads.**
665
+ 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.
651
666
  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.
652
667
  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.
653
668
 
654
- **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.
669
+ **Non-prose stripping.**
670
+ 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.
655
671
  Prose between two block-tag markers still counts (only the marker spans themselves are removed); a paragraph consisting only of tag markers contributes nothing.
656
672
  Multi-backtick delimiters (`` ``like this`` ``) are handled conservatively as a simple open-run/close-run pair match, not a full CommonMark-correct implementation.
657
673
 
@@ -683,7 +699,8 @@ assertions:
683
699
  All options are optional — an empty `spelling: {}` is valid (default dictionary, no extra vocabulary, no ignore patterns, built-in vocabulary on).
684
700
  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.
685
701
 
686
- **Optional peer dependencies — install to enable.** `nspell` and its default dictionary (`dictionary-en`) are **optional peer dependencies**: installing `@redocly/recheck` itself pulls in **neither**.
702
+ **Optional peer dependencies — install to enable.**
703
+ `nspell` and its default dictionary (`dictionary-en`) are **optional peer dependencies**: installing `@redocly/recheck` itself pulls in **neither**.
687
704
  Enable `spelling` with:
688
705
 
689
706
  ```bash
@@ -696,18 +713,21 @@ npm i nspell dictionary-en
696
713
  npm i nspell
697
714
  ```
698
715
 
699
- If a config enables `spelling` without the required peer(s) installed, `recheck validate` 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.
716
+ 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.
700
717
 
701
- **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.
718
+ **Dictionaries load lazily.**
719
+ 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.
702
720
  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.
703
721
 
704
- **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.
722
+ **Word tokenization.**
723
+ 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.
705
724
  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).
706
725
  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.
707
726
  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.
708
727
  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.
709
728
 
710
- **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.
729
+ **Code is never spell-checked, by construction of scope segmentation — not something this assertion special-cases.**
730
+ 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.
711
731
  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.
712
732
  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.
713
733
 
@@ -726,18 +746,23 @@ It exists so a config that turns on sentence-case headings or spelling doesn't i
726
746
  - 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).
727
747
  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.
728
748
 
729
- **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.
749
+ **Multi-token entries work.**
750
+ 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.
730
751
 
731
752
  **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.
732
753
  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.
733
754
  Excluded, deliberately:
734
755
  - **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.
735
756
  Note this is narrower than "looks like an acronym": `OAuth` and `AsyncAPI` are mixed-case, not pure ALL-CAPS, and are in the list.
736
- - **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).
757
+ - **Terms with legitimate lowercase prose usage** — generic English (`cloud`, `apps`),
758
+ 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),
759
+ 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).
737
760
  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.
738
761
  List your own such names in your rule's own `exceptions`/`vocab`, which compose with this list as described above.
739
762
 
740
- 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.
763
+ Two automated tests in `src/data/__tests__/proper-nouns.test.ts` enforce this:
764
+ 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 —
765
+ plus alphabetization and no duplicates.
741
766
  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.
742
767
 
743
768
  #### Length Assertions (`length`)
@@ -772,7 +797,9 @@ Beyond `swap`, `pattern`, `occurrence`, `repetition`, `consistency`, `conditiona
772
797
  - `semantic-line-breaks` - Semantic line break validation ✅ **Fixable**
773
798
  - `max-image-size` - Oversized image detection
774
799
 
775
- 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.
800
+ Three of the assertions above (`repetition`, `consistency`, `capitalization`) are bundled, pre-configured, in the [`recheck/prose`](#extends-presets) preset,
801
+ and `capitalization`/`length` are also used by [`recheck/google`](#extends-presets) (sentence-case headings and list items, and a sentence-length cap);
802
+ 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.
776
803
 
777
804
  ### Recheck-original structural rules
778
805
 
@@ -788,7 +815,7 @@ The other two — `markdoc-unknown-tag` and
788
815
  the [`recheck/markdoc`](#extends-presets) preset instead.
789
816
 
790
817
  | Rule | Flags | Why |
791
- |---|---|---|
818
+ | --- | --- | --- |
792
819
  | `no-empty-headings` | A heading whose text content is empty (a bare `#`, or markup that renders to nothing such as `## <span></span>`) | 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. |
793
820
  | `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. |
794
821
  | `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. |
@@ -924,7 +951,7 @@ markdoc:
924
951
  - **`extend.tagsFile`** — the same tag-definition surface as `extend.tags`, but sourced from
925
952
  a separate YAML file instead of written inline into `recheck.yaml`.
926
953
  This is the shape
927
- [`recheck markdoc-schema`](#generate-a-tagsfile-recheck-markdoc-schema) below generates,
954
+ [`recheck --generate-markdoc-schema`](#generate-a-tagsfile-recheck---generate-markdoc-schema) below generates,
928
955
  so a project with tags defined in TypeScript (a `@theme/markdoc/schema.ts` module, say)
929
956
  never hand-transcribes them into YAML.
930
957
  ```yaml
@@ -942,20 +969,27 @@ markdoc:
942
969
  `extend.tags` above).
943
970
  `tags` and `tagsFile` can both be set on the same `extend` block;
944
971
  `extend` with neither key is rejected by config validation as a likely no-op.
945
- - **Errors are fatal to the whole run, not a silent markdoc downgrade.** A `tagsFile` that
972
+ - **Errors are fatal to the whole run, not a silent markdoc downgrade.**
973
+ A `tagsFile` that
946
974
  doesn't exist, isn't valid YAML, isn't a YAML map, or contains a tag entry with an
947
- invalid shape all fail `recheck run`/`recheck validate` outright
975
+ invalid shape all fail `recheck`/`recheck --validate-config` outright
948
976
  (`Configuration validation failed!`, the same failure every other structurally-invalid
949
977
  config produces) — markdoc checking is never quietly switched off while the rest of the
950
978
  config keeps running.
951
979
 
952
980
  Turning `markdoc` on (either form) changes how every prose rule sees a Markdoc tag, not just `markdoc.tag` (above):
953
981
 
954
- - **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.
982
+ - **Prose scopes exclude the tag itself.**
983
+ `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.
955
984
  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.
956
- - **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.
957
- - **`--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.
958
- - **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:
985
+ - **A segment with no prose left isn't emitted at all.**
986
+ 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.
987
+ - **`--fix` never rewrites a Markdoc tag's bytes.**
988
+ Every proposed fix is checked against the document's tag spans before it's applied:
989
+ 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,
990
+ 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.
991
+ - **Two CommonMark constructs Markdoc doesn't have stop being recognized.**
992
+ 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:
959
993
  - 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.
960
994
  This shows up most with a block-positioned tag followed immediately by more indented lines, and with genuinely indented example text.
961
995
  Realm renders both as prose, so matching that is the intent.
@@ -965,7 +999,7 @@ Turning `markdoc` on (either form) changes how every prose rule sees a Markdoc t
965
999
  - 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.
966
1000
  They are moving to match how Markdoc actually renders, not regressing.
967
1001
 
968
- ### Generate a tagsFile: `recheck markdoc-schema`
1002
+ ### Generate a tagsFile: `recheck --generate-markdoc-schema`
969
1003
 
970
1004
  Projects that define their own Markdoc tags in TypeScript — a `@theme/markdoc/schema.ts`
971
1005
  module exporting a `tags` map, the shape both `docs/realm` and `docs/intranet` use in this
@@ -973,7 +1007,7 @@ monorepo — can generate an `extend.tagsFile` YAML file from it instead of hand
973
1007
  each tag's schema:
974
1008
 
975
1009
  ```bash
976
- recheck markdoc-schema --from path/to/schema.ts --out recheck-markdoc-tags.yaml
1010
+ recheck --generate-markdoc-schema --from path/to/schema.ts --out recheck-markdoc-tags.yaml
977
1011
  ```
978
1012
 
979
1013
  - **`--from <path>`** (repeatable) — a project schema module to extract tags from, resolved
@@ -997,13 +1031,14 @@ recheck markdoc-schema --from path/to/schema.ts --out recheck-markdoc-tags.yaml
997
1031
  This is what a CI drift
998
1032
  check should call — see this repo's own wiring below.
999
1033
 
1000
- **TypeScript sources need a loader.** `recheck markdoc-schema` dynamic-`import()`s each
1034
+ **TypeScript sources need a loader.**
1035
+ `recheck --generate-markdoc-schema` dynamic-`import()`s each
1001
1036
  `--from` module directly; running the command under plain `node` against a `.ts` module
1002
1037
  fails with an actionable one-line error naming the fix, rather than a raw stack trace:
1003
1038
 
1004
- ```
1039
+ ```text
1005
1040
  could not import "path/to/schema.ts" — TypeScript sources need a loader, e.g.: pnpm exec tsx
1006
- node_modules/.bin/recheck markdoc-schema … (Cannot find module '<a module your schema
1041
+ node_modules/.bin/recheck --generate-markdoc-schema … (Cannot find module '<a module your schema
1007
1042
  imports>' imported from '<path to your schema.ts>')
1008
1043
  ```
1009
1044
 
@@ -1017,12 +1052,13 @@ Run it through `tsx` instead (directly, or via a package script that already wra
1017
1052
  this repo's `recheck:markdoc-tags` below) — a plain `.js` schema module needs no loader and
1018
1053
  works under either.
1019
1054
 
1020
- **Experimental, pending a canonical manifest.** This command is an interim bridge, not a
1055
+ **Experimental, pending a canonical manifest.**
1056
+ This command is an interim bridge, not a
1021
1057
  long-term source of truth: [issue #25666](https://github.com/Redocly/redocly/issues/25666)
1022
1058
  tracks Realm itself emitting one canonical, statics-only Markdoc tag/schema manifest, which
1023
1059
  would let this generator (and its drift check) retire in favor of reading that manifest
1024
1060
  directly.
1025
- Until then, `recheck markdoc-schema` is the supported way to keep a project's
1061
+ Until then, `recheck --generate-markdoc-schema` is the supported way to keep a project's
1026
1062
  `tagsFile` in sync with its real tag schema modules.
1027
1063
 
1028
1064
  #### Worked example: this repo's own setup
@@ -1040,10 +1076,10 @@ markdoc:
1040
1076
  The committed `recheck-markdoc-tags.yaml` is generated, not hand-written — its header names
1041
1077
  the exact regenerate command:
1042
1078
 
1043
- ```
1079
+ ```yaml
1044
1080
  # Generated file — do not hand-edit.
1045
1081
  # Source module(s): ../../docs/realm/@theme/markdoc/schema.ts, ../../docs/intranet/@theme/markdoc/schema.ts
1046
- # Regenerate: recheck markdoc-schema --from ../../docs/realm/@theme/markdoc/schema.ts --from ../../docs/intranet/@theme/markdoc/schema.ts --out ../../recheck-markdoc-tags.yaml
1082
+ # Regenerate: recheck --generate-markdoc-schema --from ../../docs/realm/@theme/markdoc/schema.ts --from ../../docs/intranet/@theme/markdoc/schema.ts --out ../../recheck-markdoc-tags.yaml
1047
1083
  ```
1048
1084
 
1049
1085
  The root `package.json` wraps that same invocation in one script, run from
@@ -1054,9 +1090,10 @@ pnpm run recheck:markdoc-tags # regenerate recheck-markdoc-tags.yaml
1054
1090
  pnpm run recheck:markdoc-tags --check # verify it's current; exits 1 on drift
1055
1091
  ```
1056
1092
 
1057
- **Do not add `--` before `--check`.** `pnpm run recheck:markdoc-tags -- --check` looks
1093
+ **Do not add `--` before `--check`.**
1094
+ `pnpm run recheck:markdoc-tags -- --check` looks
1058
1095
  equivalent but isn't: the script itself already ends in `pnpm --filter @redocly/recheck exec
1059
- tsx dist/cli.js markdoc-schema …`, and pnpm's own `--` forwarding through that nested `exec`
1096
+ tsx dist/cli.js --generate-markdoc-schema …`, and pnpm's own `--` forwarding through that nested `exec`
1060
1097
  makes yargs read `--check` as a positional argument instead of the `--check` flag — the
1061
1098
  command then silently regenerates the file and always exits `0`, defeating the whole point
1062
1099
  of a drift check.
@@ -1187,7 +1224,9 @@ A rule may attach a milder severity to some of its own reports, and your config
1187
1224
  Equivalent to markdownlint's `{ default: true }`.
1188
1225
  - **`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.
1189
1226
  - **`recheck/minimal`** — a small, high-signal set: `no-trailing-spaces`, `no-hard-tabs`, `single-trailing-newline`, `no-reversed-links`, `no-empty-links`.
1190
- - **`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).
1227
+ - **`recheck/prose`** — Recheck's Vale-parity starter set, all at `severity: warn`: `repetition` (default options),
1228
+ `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),
1229
+ and `capitalization` (`$sentence`, `scope: heading` only, `fix: false`, no preset-level `exceptions` — see below).
1191
1230
  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.
1192
1231
  `extends: [recheck/markdown, recheck/prose]` is the one-liner that replaces a markdownlint + Vale combo.
1193
1232
  See [Opt-in prose assertions](#opt-in-prose-assertions) below for three more prose assertions that exist but are deliberately **not** in this preset.
@@ -1200,27 +1239,39 @@ A rule may attach a milder severity to some of its own reports, and your config
1200
1239
  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.
1201
1240
  Only `severity: 'off'` removes them, by disabling the rule.
1202
1241
 
1203
- **These rules only fire when Markdoc tokenization is also on.** Set `markdoc: true` (or the object form, see below) alongside `extends: [recheck/markdoc]`.
1242
+ **These rules only fire when Markdoc tokenization is also on.**
1243
+ Set `markdoc: true` (or the object form, see below) alongside `extends: [recheck/markdoc]`.
1204
1244
  Extending the preset without the flag validates, but prints a console warning that the four rules can never report.
1205
1245
  The flag stays an explicit opt-in because Liquid and Jinja templates use the same `{% %}` delimiters for unrelated syntax, so Recheck never assumes it.
1206
- - **`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).
1246
+ - **`recheck/google`** — Google's developer documentation style guide (https://developers.google.com/style), CC BY 4.0, synced 2026-07-29.
1247
+ 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 —
1248
+ 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).
1207
1249
  `extends: [recheck/google]` is a one-line adoption of Google's style; combine with `recheck/markdown` for full structural linting too.
1208
1250
  Rule ids are namespaced `google/<rule>` (not `recheck/<rule>`) so they never collide with the markdownlint-parity or other style-guide presets.
1209
1251
  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`.
1210
- 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.
1211
- - **`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.
1252
+ See `packages/recheck/presets/google/sources.json` for the fetched-page hashes.
1253
+ **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.
1254
+ - **`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.
1255
+ 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 —
1256
+ 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.
1212
1257
  Rule ids are namespaced `microsoft/<rule>`.
1213
1258
  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`.
1214
1259
  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.
1215
1260
  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.
1216
1261
  See `packages/recheck/presets/microsoft/sources.json` for the fetched-page hashes.
1217
- - **`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`).
1262
+ - **`recheck/inclusive-language`** — composable, guide-agnostic: the *intersection* of `recheck/google` and `recheck/microsoft`'s inclusive/bias-free/ableist/accessibility content —
1263
+ 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`).
1218
1264
  All `warn` severity, all detection-only.
1219
1265
  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.
1220
- 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").
1266
+ Layer it onto either flagship or onto `recheck/prose`: `extends: [recheck/google, recheck/inclusive-language]`.
1267
+ **Because it's built as an intersection, every one of its 11 rules is already shipped by at least one flagship's own preset**
1268
+ (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").
1221
1269
  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.
1222
1270
  - **`recheck/plain-language`** — composable, derived from the *live* US federal plain-language guidance (`digital.gov/guides/plain-language`; public domain, no attribution constraint).
1223
- 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.
1271
+ Smaller than a first read of the old `plainlanguage.gov` site would suggest:
1272
+ 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) —
1273
+ 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.
1274
+ **`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.
1224
1275
  All `warn`/`error` (paragraph-length ceiling only) severity, all detection-only.
1225
1276
  `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`).
1226
1277
  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.
@@ -1326,7 +1377,7 @@ microsoft/spelling-hyphenation:
1326
1377
 
1327
1378
  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):
1328
1379
 
1329
- ```
1380
+ ```text
1330
1381
  Rule "microsoft/spelling-hyphenation": swap requires a "pairs" object mapping find -> replace strings
1331
1382
  ```
1332
1383
 
@@ -1336,14 +1387,18 @@ A per-term opt-out for `swap`/`pattern` is a known follow-up, not shipped yet.
1336
1387
 
1337
1388
  ### Example configs
1338
1389
 
1339
- `packages/recheck/examples/{google,microsoft,inclusive-language,plain-language}.yaml` are ready-to-copy configs for the four style-guide 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/<name>.appendix.yaml`) changes without regenerating.
1390
+ `packages/recheck/examples/{google,microsoft,inclusive-language,plain-language,technical-english}.yaml` are ready-to-copy configs for the five style presets,
1391
+ generated by `pnpm examples:generate` (`packages/recheck/scripts/generate-examples.mjs`) so they can never drift from the preset they document —
1392
+ 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/<name>.appendix.yaml`) changes without regenerating.
1340
1393
 
1341
1394
  Each file has four parts, in this order:
1342
1395
 
1343
1396
  1. **An attribution header** — source, license, and sync date, as YAML comments (mirrors that preset's `PROVENANCE.md`).
1344
1397
  2. **`# What to paste`** — the actual adoption cost: a two-to-four-line `extends` block.
1345
1398
  This is the only part most readers need; everything below it is supporting material, not something to copy.
1346
- 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.
1399
+ 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),
1400
+ 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) —
1401
+ restate the whole map, turn the rule off, or use an inline directive instead.
1347
1402
  `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.
1348
1403
  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.
1349
1404
  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.
@@ -1354,7 +1409,8 @@ A hand-maintained appendix is appended verbatim after part 4: NOISY candidates t
1354
1409
 
1355
1410
  `recheck/prose` (above) intentionally ships only `repetition`, `consistency`, and `capitalization` — a small, broadly-applicable default.
1356
1411
  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`.
1357
- (`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]`:
1412
+ (`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.)
1413
+ Add any of the three by copying its rule below into your own config, alongside `extends: [recheck/prose]`:
1358
1414
 
1359
1415
  ```yaml
1360
1416
  extends: [recheck/markdown, recheck/prose]
@@ -1410,7 +1466,8 @@ recheck/ul-style:
1410
1466
  severity: off
1411
1467
  ```
1412
1468
 
1413
- **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.
1469
+ **Renamed legacy assertion ids — old ids are no longer accepted.**
1470
+ A handful of ids from Recheck's pre-parity native rules were converged onto their markdownlint-parity replacements.
1414
1471
  These four were removed outright, not kept as aliases: using the old id in a config now fails validation with an `unknown assertion type "<old>"` error, and the config must be updated to the new id:
1415
1472
 
1416
1473
  | Old id (removed) | Use instead |
@@ -1505,10 +1562,30 @@ recheck/front-matter:
1505
1562
  schemaFile: schemas/docs-front-matter.yaml
1506
1563
  ```
1507
1564
 
1508
- - `schema` is an inline JSON Schema object; `schemaFile` loads one from a YAML or JSON file, relative to the working directory.
1565
+ - `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.
1509
1566
  - A file with no front matter validates as an empty object, so the schema's `required` list decides whether front matter is mandatory.
1510
1567
  - 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.
1511
1568
 
1569
+ #### Built-in schema: Realm page front matter
1570
+
1571
+ `schema: realm` validates the front matter options a Realm page accepts, so a project gets the check without vendoring a copy that goes stale:
1572
+
1573
+ ```yaml
1574
+ - files: ['docs/**']
1575
+ schema: realm
1576
+ strict: true
1577
+ ```
1578
+
1579
+ 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`.
1580
+
1581
+ The schema checks the **type** of each known key, not the inner shape of the option objects.
1582
+ `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.
1583
+ Type checking still catches what actually goes wrong: a misspelled key (with `strict`), and a value of the wrong kind (`excludeFromSearch: "true"`).
1584
+
1585
+ `strict: true` adds `additionalProperties: false`, which turns a misspelled option into a finding.
1586
+ It is off by default because pages carry project data that Markdoc templates read back through `$frontmatter.<key>` — Redocly's own docs use `products` and `plans` this way on 268 pages.
1587
+ To keep `strict` and allow your own keys, copy the built-in as a starting point and add them.
1588
+
1512
1589
  ## GitHub Actions Integration
1513
1590
 
1514
1591
  Recheck provides seamless GitHub Actions integration for automated content quality checking on pull requests.
@@ -1519,17 +1596,17 @@ Use the `--output github-actions` format to generate inline file annotations tha
1519
1596
 
1520
1597
  ```bash
1521
1598
  # Basic GitHub Actions output
1522
- node dist/cli.js run docs --output github-actions
1599
+ node dist/cli.js docs --output github-actions
1523
1600
 
1524
1601
  # With annotation limits (recommended for PR workflows)
1525
- node dist/cli.js run docs --output github-actions --annotations-limit 20
1602
+ node dist/cli.js docs --output github-actions --annotations-limit 20
1526
1603
  ```
1527
1604
 
1528
1605
  ### Annotation Output
1529
1606
 
1530
1607
  The GitHub Actions format produces annotations that GitHub automatically displays as inline comments:
1531
1608
 
1532
- ```
1609
+ ```text
1533
1610
  ::error title=recheck/no-trailing-spaces,file=docs/guide.md,line=42,col=15,endColumn=18::Trailing spaces
1534
1611
  ::warning title=recheck/ul-style,file=docs/api.md,line=23,col=1,endColumn=2::Unordered list style
1535
1612
  ```
@@ -1547,7 +1624,7 @@ GitHub Actions has strict limits on annotations:
1547
1624
  ```yaml
1548
1625
  - name: Run recheck with inline annotations
1549
1626
  run: |
1550
- node packages/recheck/dist/cli.js run docs \
1627
+ node packages/recheck/dist/cli.js docs \
1551
1628
  --config recheck.yaml \
1552
1629
  --output github-actions \
1553
1630
  --changed-only < changed-files.txt
@@ -1574,11 +1651,13 @@ pnpm bench --subject benchmarks/run-markdownlint.mjs --corpus monorepo-docs --re
1574
1651
  Recheck trades a bit of the Phase 1 constant-factor lead for full rule-count parity (53 rules vs. 10) — still within budget, and the shared AST-parse-once architecture means adding prose/style rules on top costs little extra, since markdown structure parsing is already paid for.
1575
1652
 
1576
1653
  - **Phase 3** (prose profile — the standard Phase 2 rule set vs. that same set plus the Vale-parity prose additions, `monorepo-docs` document set, the same set on both sides):
1577
- - Standard profile (`recheck-mdl-preset.yaml`, `extends: [recheck/markdown]`, 53 rules, `run-recheck-mdl-preset.mjs`, 965 files): **4072ms** median — **-8.9% vs the Phase 2 recording** (`phase2-parity-recheck`, 4468ms, 954 files), i.e. no regression from the Phase 3 rule-registry additions, since the standard profile doesn't exercise any of them (the small speedup is run-to-run variance plus the document-set size difference, not an optimization claim).
1654
+ - Standard profile (`recheck-mdl-preset.yaml`, `extends: [recheck/markdown]`, 53 rules, `run-recheck-mdl-preset.mjs`, 965 files): **4072ms** median — **-8.9% vs the Phase 2 recording** (`phase2-parity-recheck`, 4468ms, 954 files),
1655
+ i.e. no regression from the Phase 3 rule-registry additions, since the standard profile doesn't exercise any of them (the small speedup is run-to-run variance plus the document-set size difference, not an optimization claim).
1578
1656
  - Prose profile (`recheck-prose-bench.yaml`, `extends: [recheck/markdown, recheck/prose]` plus one opt-in `occurrence` rule and one opt-in `conditional` rule, 58 rules, `run-recheck-prose.mjs`, same 965 files): **4547ms** median — **+11.7%** vs. the standard profile above, for the five added prose/scope rules (`repetition`, `consistency`, `capitalization`, and the two opt-ins).
1579
1657
  Comfortably under the "investigate if >2x standard" threshold; no pathological per-rule cost found.
1580
1658
  There is no hard pass/fail gate for this profile (new profile, first recording) — these numbers establish its baseline.
1581
- - Measured 2026-07-27 (local time; the result JSONs record the UTC date `2026-07-28`, so the file dates and this measurement date differ by design, not by error) at commit `27a8b6feb10` (immediately prior to the commit that added this benchmark profile), on an Apple M2 Max / Darwin 24.6.0 / Node v23.7.0 machine; single 3-run session, not a statistically rigorous multi-session average — treat the deltas as directional, not precise.
1659
+ - Measured 2026-07-27 (local time; the result JSONs record the UTC date `2026-07-28`, so the file dates and this measurement date differ by design, not by error) at commit `27a8b6feb10` (immediately prior to the commit that added this benchmark profile), on an Apple M2 Max / Darwin 24.6.0 / Node v23.7.0 machine;
1660
+ single 3-run session, not a statistically rigorous multi-session average — treat the deltas as directional, not precise.
1582
1661
  `pnpm bench --subject <script> --corpus monorepo-docs --runs 3 --record <label>` (median of 3 timed runs after 1 warm-up); recorded to `benchmarks/results/phase3-prose-standard.json` / `phase3-prose-profile.json`.
1583
1662
  A `--corpus self` (2-file) smoke pair recorded to `phase3-prose-standard-self.json` / `phase3-prose-profile-self.json` proves the harness/subject-script mechanics end-to-end but isn't large enough to be a meaningful timing signal on its own.
1584
1663
 
@@ -1618,7 +1697,8 @@ Recheck trades a bit of the Phase 1 constant-factor lead for full rule-count par
1618
1697
  - `string-width` - measures the display width of strings containing wide/ambiguous-width or ANSI-styled characters, for CLI table output alignment
1619
1698
 
1620
1699
  ### Optional Peer Dependencies
1621
- - `nspell` + `dictionary-en` - Hunspell-compatible spell checker (and its bundled English dictionary) backing the `spelling` assertion (see [Spelling Assertions](#spelling-assertions-spelling) above). **Not installed by installing `@redocly/recheck`** — both are declared `optional: true` in `peerDependenciesMeta`, loaded lazily via dynamic `import()` only when a config actually enables `spelling`.
1700
+ - `nspell` + `dictionary-en` - Hunspell-compatible spell checker (and its bundled English dictionary) backing the `spelling` assertion (see [Spelling Assertions](#spelling-assertions-spelling) above).
1701
+ **Not installed by installing `@redocly/recheck`** — both are declared `optional: true` in `peerDependenciesMeta`, loaded lazily via dynamic `import()` only when a config actually enables `spelling`.
1622
1702
  Run `npm i nspell dictionary-en` to enable it (or just `npm i nspell` if every `spelling` rule supplies its own `dictionary` path).
1623
1703
 
1624
1704
  ### Development Dependencies
@@ -1631,7 +1711,7 @@ Recheck trades a bit of the Phase 1 constant-factor lead for full rule-count par
1631
1711
  ## Example Output
1632
1712
 
1633
1713
  ### Standard Run
1634
- ```
1714
+ ```text
1635
1715
  🏃 Running recheck on: docs/
1636
1716
  ✅ Configuration loaded successfully!
1637
1717
  Config file: recheck.yaml