Handle $ARGUMENTS. check, or no argument, runs the gate bound. deep runs --length=4 and compares its failures with Findings. lower runs --lower after a fix removes a known finding. typed <keys> runs --typed=<keys> to show one sequence's results. update brings the verification in line with changed code, as Coverage record says.
The property. Typed key by key into a paragraph between two others, every shorthand the kits declare gives its declared result whole: the substituted symbol, or the content with exactly its marks and no delimiter left. Inside a word, a mark shorthand stays literal, except subscript and superscript. No typed sequence changes the paragraphs above or below. When a keystroke fires a rule, one undo gives back the document from before that keystroke.
The harm. A user who types ==yellow==, ***both***, H~2~O or !>= from the docs gets symbols or stray delimiters instead of what the kit promises, and closing an inline-code backtick pulls the next paragraph into the current one. Each case is one string a test can check, but rules that share keys collide in sequences nobody wrote down, as five Beta bugs on the Autoformat demo showed.
What it checks
apps/www/scripts/autoformat-input-rules.ts types into real editors built from BasicMarksKit then AutoformatKit, the order EditorKit uses, with editor.update.text.insert once per key, as apps/www/src/__tests__/package-integration/autoformat/current-kit.spec.tsx does. It checks the TypeScript code itself, exhaustively up to a bound, not a model of it. No verifier runs on input rules, so the script enumerates the inputs. Each run starts from the paragraphs x, the caret paragraph and y, with the caret at the end of the caret paragraph.
- It reads the declarations with the Babel parser, because the repository's TypeScript has no compiler API. From
autoformat.tsxit takes everycreateTextSubstitutionInputRuleconstant in aninputRuleslist, with each pattern'sformatandmatch. Frombasic-marks.tsxit takes every<Mark>Rules.markdown()call in aninputRuleslist, with itsvariantorvalue. A value that is not a string literal, aninputRulesvalue that is not a list literal, a rule of another shape or a file with noinputRuleslist exits 2. Each worker also counts the text input rules an editor of the two kits runs, throughgetPlateRuntime. The script exits 2 when that count differs from the number of rules it read, because a rule that reaches the editor another way would go untyped. - A mark rule's syntax comes from the script's
EXPECTED_MARK_SYNTAXtable, not from the factories inBaseMarkPlugins.ts, so a factory whose delimiters drift from the table fails. The table copies the Inline Mark Autoformat rules ofdocs/editor-behavior/markdown-editing-spec.mdwhere they name a syntax:**,*,`,~~,==,~,^and***. The spec names only the opening runs__*,__**and___***ofEDIT-PROFILE-AUTOFMT-MARK-003, so each combo's closing run mirrors its opening run,___***a***___forboldItalicUnderline. The spec names no syntax for the_variants of bold and italic, the≡highlight variant or underline, so their rows hold CommonMark's__and_and what their factories type,≡a≡and__a__. A factory or variant the table lacks exits 2. - A declared character that no key types, such as
≤in≤=or the≡highlight variant, is typed as the source of the rule that writes it,<=and==, as a user would reach it. - Declared shorthands. Each substitution source is typed at a paragraph start, after the word
wand after a boldw, and must give the context followed by its symbol; a smart quote is typed arounda. Each mark rule is typed aroundaand around one character for each substitution rule whose source that character completes with the rule's opening or closing delimiter, such as~0~forsubscriptNumbersRuleand~=~forequalityRule. It must give the content with the rule's marks, plus bold in the bold context. Typed right afterw, a mark rule must leave no leaf with its marks, asEDIT-PROFILE-AUTOFMT-MARK-004says, except subscript and superscript, which must apply there, asEDIT-PROFILE-AUTOFMT-MARK-002'sH~2~Osays. These are theautoformat-declaredfindings, owned by the kit file that declares the shorthand. The check judges the caret paragraph on its own. When plate#5226 joinsyinto it, the script drops thatybefore comparing, because the blocks rule already reports the join. - Sequences. Every sequence of up to three keys over
aand the characters of the mark syntaxes and of the substitution sources that hold no letter or digit is typed at a paragraph start. That is 15 characters and 3,615 sequences. A run whose result holds other than three paragraphs, or changesxory, is anautoformat-blocksfinding, owned bycreateInputRules.ts. - Undo, on every run. The last keystroke fired a rule when the caret paragraph is no longer its old text plus the key, or its old characters changed marks, or the other paragraphs changed. Then one
editor.api.history.undo()must give back the children from before that keystroke. Each shorter sequence is a run of its own, so every keystroke of every sequence is checked. These are theautoformat-undofindings, owned byInputRulesPlugin.ts. - It sorts the findings and compares them with
apps/www/scripts/autoformat-input-rules-baseline.jsonthroughtooling/scripts/finding-baseline.mjs, the shrink-only baseline the decision-order and lint checks use. A finding's identity names the rule or sequence, the typed keys and the context, never the result.
Run it
Run every command from the repository root.
pnpm check:autoformat-ruleschecks the gate bound, three keys. It makes 3,910 runs and 352 declared checks in about 6 s on an 18-core M5 Max at a load average near 7, slower while other sessions load the machine. It prints one JSON summary line, then each finding the baseline does not list.pnpm check autoformat-rulesruns the same step through the check runner, andpnpm check --affectedruns it for a change to the script or its baseline,apps/www/src/registry/components/editor/,packages/platejs/src/,packages/plitejs/src/ortooling/scripts/finding-baseline.mjs.--length=4types every sequence of up to four keys: 54,535 runs in about 80 s on the same machine with 8 workers at a load average near 7. A bound other than the gate's skips the baseline and exits 1 on any finding, because the known findings repeat there in more sequences. Compare its counts with Findings. Run it after a change to how rules match or apply.--typed='<keys>'runs one typed sequence in each context the gate types it in, or at a start, after a word, after a bold word and inside a word when the gate has no such run, and prints every finding without reading the baseline.--jobs=<n>sets the number of worker processes. It defaults to the machine's cores, at most 8, because each worker holds its own editor runtime. Any other argument exits 2, and so does--initor--lowerwith--lengthor--typed.- After a fix removes a known finding, the gate fails until you run
pnpm check:autoformat-rules --lower, which drops the entries the tree no longer holds. The baseline never grows.--lowerrefuses a new entry, andfinding-baseline.mjsrefuses an entry its copy at the base commit lacks.--initseeds only a rule the baseline lacks.
Read a failure
A finding prints its rule and identity, then what each paragraph holds. A leaf is its text in quotes, followed by its marks, and | separates paragraphs:
autoformat-declared comparisonsRule "!>=" at start
expected "x" | "≱" | "y"
got "x" | "≯=" | "y"
autoformat-blocks "`a`" at start
"x" | "`a" | "y" then "`" gives "x" | "a"⟨code⟩ "y"
autoformat-undo "->" at start
"x" | "-" | "y" then ">" gives "x" | "→" | "y"
one undo gives "x" | "" | "y" (applied)
The first is !> firing as ≯ before !>= can complete. The second is the closing backtick joining y into the caret paragraph. The third is a planted history bug from What a pass proves, where one undo also takes back the - typed before the rule.
Tell a real bug from a modeling error before you change code:
- Real bug. The typed keys are what a user types for a shorthand the kit declares, or a sequence of real keys, and the result differs from the spec's or the declaration's. Fix the rule, the kit's rule order or the runtime, then run
--lower. When two declarations share their keys, as__a__does forBoldRules _andUnderlineRules, the spec's first matching rule wins, so the later one never applies. Fix that by removing or changing a declaration, never by adding a baseline entry. - Modeling error.
EXPECTED_MARK_SYNTAXnames a syntax the spec does not promise, the content the script picks makes the expected result ambiguous, or the difference is one no user sees, such as an empty leaf. The script compares leaves with empty text dropped and equal marks merged. Fix the table or the script with the spec rule it follows. - Intended change. Changing what a shorthand gives changes its declaration in the kit, or the spec and
EXPECTED_MARK_SYNTAXtogether, in the same change. Never weaken the property or add a baseline entry to get green.
Findings
The gate finds 160: 74 autoformat-declared, 86 autoformat-blocks and no autoformat-undo. --length=4 finds the same 74 autoformat-declared, no autoformat-undo and 1,976 autoformat-blocks, each a one-character delimiter closing around one or two keys. Each class is one cause:
==a==gives≡a≡, so neither highlight variant ever applies, becauseequalityRuleturns==into≡first and its insert fires no rule (plate#5201, t_167ab658). Spec ruleEDIT-PROFILE-AUTOFMT-MARK-005says highlight wins. 24 entries.!>=,!<=and!~=give≯=,≮=and!≈, because the shorter!>,!<and~=fire on the key before (plate#5202). 9 entries.<=gives⇐, never≤, becausearrowsRuledeclares<=and runs beforecomparisonsRule, andarrowsRule's own≤=, typed<==, gives⇐=(t_dceb960e). 6 entries.__a__gives bold, never underline, becauseBoldRules _andUnderlineRulesboth match__and bold comes first. 3 entries.~0~and^0^give₀~and⁰^, because the digit substitution fires inside the span (plate#5204).~+~,~=~and^o^fail the same way, 20 entries in all, and~~0~~,~~+~~and~~=~~give a subscript and a stray~instead of strikethrough, 9 more.- Inside a word,
w~a~,w~!~andw^a^stay literal, againstEDIT-PROFILE-AUTOFMT-MARK-002'sH~2~O. 3 entries. - A one-character delimiter,
`,*,_,~or^, closing around content of any length joinsyinto the caret paragraph (plate#5226; PR plate#5237 changes the next-block merge). That gives all 86autoformat-blocksentries. The marks themselves apply, so*a*,_a_,`a`,~a~,~!~and^a^pass the declared check.
No run needs more than one undo. The undo check covers every keystroke of every sequence at a paragraph start up to the bound, and the last keystroke of each declared shorthand in its other contexts.
What a pass proves
The gate catches a shorthand that stops applying whole, a typed sequence that changes another paragraph, and a rule step that undo no longer takes back on its own. At base 95a313df66 with this check copied in, bugs planted in a detached proof worktree gave:
- Keeping a substitution's end text, by turning
if (match.end)increateTextSubstitutionInputRule's apply intoif (!match.end), gives 293autoformat-declared, 127autoformat-blocksand 456autoformat-undofindings against 86, 86 and 0. One isarrowsRule "->" at start, which gives-→. - Merging every text batch in
shouldMergeBatchofpackages/plitejs/src/history/history-merge-policy.tsgives 456autoformat-undofindings and leaves the other counts unchanged. One is the->failure in Read a failure. - Writing one pattern's
matchasString('->')exits 2, namingautoformat.tsx:7. - Adding a plugin to
AutoformatKitwhose rules sit inObject.fromEntries([['inputRules', [arrowsRule]]]), which the reader cannot see, exits 2, because the editor runs 29 text input rules and the lists declare 28. - After each plant was dropped, the gate matched the baseline again.
The current code fails autoformat-blocks and autoformat-declared on its own, as Findings lists, so those properties have failed on real bugs too.
Coverage record
Synced at the commit that adds this check, test(www): check every short typed sequence against the autoformat kits. The verification describes these files as they stand there:
| File | What the check exercises |
|---|---|
apps/www/src/registry/components/editor/autoformat.tsx |
the substitution rules it reads and types |
apps/www/src/registry/components/editor/basic-marks.tsx |
which mark rules the kit declares, read through EXPECTED_MARK_SYNTAX |
packages/platejs/src/features/basic-nodes/lib/BaseMarkPlugins.ts |
the <Mark>Rules.markdown() factories, judged against EXPECTED_MARK_SYNTAX |
packages/platejs/src/lib/plugins/input-rules/createInputRules.ts |
createMarkInputRule and createTextSubstitutionInputRule, their match and apply |
packages/platejs/src/lib/plugins/input-rules/InputRulesPlugin.ts |
the insertText rule transaction, one per accepted rule |
packages/platejs/src/internal/plugin/resolvePlugins.ts |
the order rules run in |
packages/platejs/src/internal/plugin/compilePlateModel.ts |
getPlateRuntime, which gives the count of text input rules the editor runs |
packages/plitejs/src/history/history-merge-policy.ts |
whether a rule's step merges with the typing before it |
docs/editor-behavior/markdown-editing-spec.md |
the Inline Mark Autoformat rules that EXPECTED_MARK_SYNTAX copies where they name a syntax; not run |
To see whether the verification needs another look, find the sync commit with git log --oneline -1 --grep 'check every short typed sequence against the autoformat kits', then run git log --oneline <that commit>..HEAD -- with the table's files and apps/www/scripts/autoformat-input-rules.ts. For each commit it lists, read the diff and confirm three things. The script still builds its editors from the kits the way EditorKit orders them. EXPECTED_MARK_SYNTAX still matches the spec's mark rules where they name a syntax. A new rule shape in either kit file still reaches the script, or exits 2. Move the sync commit in this file forward, naming its subject, only after that check and a run of pnpm check:autoformat-rules.
What it does not cover
- The other kits of
EditorKitand their input rules: block shorthands such as#, lists, quotes, code fences, the horizontal rule and link automd. The demo pages use the fullEditorKit. - The browser:
beforeinput, composition and IME, mobile keyboards, paste and the caret the DOM shows. The script calls the model'sinsertTextonce per key. - Where the caret lands after a rule and which marks carry to the next key, except as later keys in a sequence show them.
- Redo, undo past the last keystroke and the undo-on-delete of
EDIT-PROFILE-AUTOFMT-TEXT-004. - Content longer than one character, text after the caret, marks other than bold around the caret, and substitutions or smart quotes inside a word.
- Sequences of more than three keys at the gate, and keys outside the 15-character alphabet, so
(tm),1/2and©are typed only as declared shorthands. A mark rule gets one content per colliding rule and side, such as~0~without~1~to~9~. - A known finding whose result changes. An identity names the keys and the context, not the result, so a known failure that gets worse stays green.
- Which of two rules on the same keys should win. The check reports the declaration that never applies; the spec decides.
/pstack:maintain-formal-verification-skill brings this verification back in line after changes that did not recheck it.