@redocly/recheck 0.9.0 → 0.10.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.
- package/README.md +153 -93
- package/dist/cli.js +110 -99
- package/dist/cli.js.map +1 -1
- package/dist/commands/baseline.d.ts +1 -1
- package/dist/commands/baseline.js +1 -1
- package/dist/commands/markdoc-schema.js +6 -6
- package/dist/commands/markdoc-schema.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +1 -1
- package/dist/commands/run.js.map +1 -1
- package/dist/core/baseline.js +0 -0
- package/dist/core/baseline.js.map +1 -1
- package/dist/parser/markdoc/extract-statics.d.ts +1 -1
- package/dist/parser/markdoc/extract-statics.js +2 -2
- package/dist/parser/markdoc/extract-statics.js.map +1 -1
- package/dist/rules/scope/length.d.ts.map +1 -1
- package/dist/rules/scope/length.js +5 -1
- package/dist/rules/scope/length.js.map +1 -1
- package/dist/rules/scope/metric.js +1 -1
- package/dist/rules/scope/metric.js.map +1 -1
- package/dist/scopes/extractor.d.ts.map +1 -1
- package/dist/scopes/extractor.js +89 -28
- package/dist/scopes/extractor.js.map +1 -1
- package/dist/scopes/sentences.d.ts.map +1 -1
- package/dist/scopes/sentences.js +85 -26
- package/dist/scopes/sentences.js.map +1 -1
- package/package.json +12 -3
- package/skills/recheck-config/SKILL.md +4 -4
- 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
|
|
71
|
+
node dist/cli.js . --config recheck.example.yaml
|
|
72
72
|
|
|
73
73
|
# Run on specific file
|
|
74
|
-
node dist/cli.js
|
|
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
|
|
77
|
+
node dist/cli.js . --severity error
|
|
78
78
|
|
|
79
79
|
# Show all enabled rules (info and above)
|
|
80
|
-
node dist/cli.js
|
|
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
|
|
87
|
-
node dist/cli.js
|
|
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
|
|
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
|
|
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
|
|
101
|
-
node dist/cli.js
|
|
102
|
-
node dist/cli.js
|
|
103
|
-
node dist/cli.js
|
|
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
|
|
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
|
|
111
|
+
node dist/cli.js . --fix
|
|
112
112
|
|
|
113
113
|
# Combine auto-fix with statistics
|
|
114
|
-
node dist/cli.js
|
|
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
|
|
117
|
+
node dist/cli.js . --annotations-limit 50
|
|
118
118
|
|
|
119
119
|
# Output to file (works with all formats)
|
|
120
|
-
node dist/cli.js
|
|
121
|
-
node dist/cli.js
|
|
122
|
-
node dist/cli.js
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
303
|
-
recheck
|
|
304
|
-
recheck
|
|
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 --
|
|
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.
|
|
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.
|
|
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.
|
|
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`) —
|
|
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.**
|
|
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:
|
|
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.
|
|
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.**
|
|
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.**
|
|
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.**
|
|
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.**
|
|
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.**
|
|
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.**
|
|
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.**
|
|
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.**
|
|
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.**
|
|
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`),
|
|
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:
|
|
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,
|
|
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.**
|
|
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
|
|
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.**
|
|
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.**
|
|
957
|
-
|
|
958
|
-
-
|
|
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.**
|
|
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.**
|
|
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`.**
|
|
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),
|
|
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.**
|
|
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.
|
|
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.
|
|
1211
|
-
|
|
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 —
|
|
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]`.
|
|
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:
|
|
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
|
|
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),
|
|
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.)
|
|
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.**
|
|
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 |
|
|
@@ -1519,17 +1576,17 @@ Use the `--output github-actions` format to generate inline file annotations tha
|
|
|
1519
1576
|
|
|
1520
1577
|
```bash
|
|
1521
1578
|
# Basic GitHub Actions output
|
|
1522
|
-
node dist/cli.js
|
|
1579
|
+
node dist/cli.js docs --output github-actions
|
|
1523
1580
|
|
|
1524
1581
|
# With annotation limits (recommended for PR workflows)
|
|
1525
|
-
node dist/cli.js
|
|
1582
|
+
node dist/cli.js docs --output github-actions --annotations-limit 20
|
|
1526
1583
|
```
|
|
1527
1584
|
|
|
1528
1585
|
### Annotation Output
|
|
1529
1586
|
|
|
1530
1587
|
The GitHub Actions format produces annotations that GitHub automatically displays as inline comments:
|
|
1531
1588
|
|
|
1532
|
-
```
|
|
1589
|
+
```text
|
|
1533
1590
|
::error title=recheck/no-trailing-spaces,file=docs/guide.md,line=42,col=15,endColumn=18::Trailing spaces
|
|
1534
1591
|
::warning title=recheck/ul-style,file=docs/api.md,line=23,col=1,endColumn=2::Unordered list style
|
|
1535
1592
|
```
|
|
@@ -1547,7 +1604,7 @@ GitHub Actions has strict limits on annotations:
|
|
|
1547
1604
|
```yaml
|
|
1548
1605
|
- name: Run recheck with inline annotations
|
|
1549
1606
|
run: |
|
|
1550
|
-
node packages/recheck/dist/cli.js
|
|
1607
|
+
node packages/recheck/dist/cli.js docs \
|
|
1551
1608
|
--config recheck.yaml \
|
|
1552
1609
|
--output github-actions \
|
|
1553
1610
|
--changed-only < changed-files.txt
|
|
@@ -1574,11 +1631,13 @@ pnpm bench --subject benchmarks/run-markdownlint.mjs --corpus monorepo-docs --re
|
|
|
1574
1631
|
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
1632
|
|
|
1576
1633
|
- **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),
|
|
1634
|
+
- 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),
|
|
1635
|
+
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
1636
|
- 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
1637
|
Comfortably under the "investigate if >2x standard" threshold; no pathological per-rule cost found.
|
|
1580
1638
|
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;
|
|
1639
|
+
- 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;
|
|
1640
|
+
single 3-run session, not a statistically rigorous multi-session average — treat the deltas as directional, not precise.
|
|
1582
1641
|
`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
1642
|
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
1643
|
|
|
@@ -1618,7 +1677,8 @@ Recheck trades a bit of the Phase 1 constant-factor lead for full rule-count par
|
|
|
1618
1677
|
- `string-width` - measures the display width of strings containing wide/ambiguous-width or ANSI-styled characters, for CLI table output alignment
|
|
1619
1678
|
|
|
1620
1679
|
### 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).
|
|
1680
|
+
- `nspell` + `dictionary-en` - Hunspell-compatible spell checker (and its bundled English dictionary) backing the `spelling` assertion (see [Spelling Assertions](#spelling-assertions-spelling) above).
|
|
1681
|
+
**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
1682
|
Run `npm i nspell dictionary-en` to enable it (or just `npm i nspell` if every `spelling` rule supplies its own `dictionary` path).
|
|
1623
1683
|
|
|
1624
1684
|
### Development Dependencies
|
|
@@ -1631,7 +1691,7 @@ Recheck trades a bit of the Phase 1 constant-factor lead for full rule-count par
|
|
|
1631
1691
|
## Example Output
|
|
1632
1692
|
|
|
1633
1693
|
### Standard Run
|
|
1634
|
-
```
|
|
1694
|
+
```text
|
|
1635
1695
|
🏃 Running recheck on: docs/
|
|
1636
1696
|
✅ Configuration loaded successfully!
|
|
1637
1697
|
Config file: recheck.yaml
|