Check that every small Markdown source parses, serializes and parses again to the same document, and that a streamed parse that continues its previous result equals a fresh parse at every chunking, over the plugins of the Markdown test editor. Use before and after changing packages/platejs/src/markdown/lib or another file in its coverage record, when the markdown-round-trip step of pnpm check fails, or when fixing a Markdown or streaming bug a seed or a tester found.
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Handle $ARGUMENTS. check, or no argument, runs the gate bound. deep runs --inline=3 --part=round-trip and --blocks=3 and compares their failures with Findings. lower runs --lower after a fix removes a known finding. case <identity> runs --case=<identity> to print one case's full detail. update brings the verification in line with changed code, as Coverage record says.
The property. Two rules hold for every source up to the bound. A source that editor.api.markdown.parse accepts serializes, and the serialized Markdown parses to the same document. A streamed parse with parseSlice that continues a previous preview of the same source gives the result a fresh parseSlice of that prefix gives, at every chunking. docs/vision/plate.md says streaming continues the prior result with previous, and that the Markdown runtime owns reuse and invalidation so consumers keep no parse cache. Reuse that changed the result would break that promise.
The harm. A round trip that changes the document loses content each time a user saves and reopens Markdown, or each time an AI reply goes through the codec. A streamed preview that differs from a fresh parse shows the user a document that changes when the stream ends, or a final document that differs from the source. Each Markdown fix before this check, such as e1fb01295e, covered only the cases someone had already found.
packages/platejs/scripts/markdown-round-trip.ts runs the real codec of createTestEditor in packages/platejs/src/markdown/lib/__tests__/createTestEditor.tsx. That editor has the plugins the Markdown stress cases use. The script checks the TypeScript code itself, not a model of it.
--inline tokens from 22 tokens: a, a space, *, **, _, a backtick, ~~, [a](b), <br/>, <u>, </u>, a backslash, a newline, $, !, #, <, &, https://a.b, [^1], | and :+1:.https://a.b.\na and https://a.b)\na put a bare URL before a line break, and <https://a.b'> a, <https://a.b"> a and <https://a.b;> a end a URL in a character that GFM trims from a bare URL. The link plugin refuses the URL of <https://a.b]> a, so the parse keeps it as text. The parse also keeps <div>www.a.b </div> and <div>a@b.cd </div> as text. Each must parse again as text, though GFM finds an autolink literal in it. They run alone and join no sequence, because as block forms they would almost double the 902 round-trip sequences. Two more forms leave a tag open that a count of tag names reads as closed, <callout> with </callout> in a code span and </callout> <callout>. They join only the streamed sequences.--blocks forms, each holding a, joined by one or two newlines. The round trip runs 902 sequences at the gate and the stream 1,080.refused and is not a failure. The script compares documents after nameDocumentListIds, because each parse gives every list a new id. A source that holds a line ending is also parsed with each one written as CRLF and then as CR, and each parse must give the document of the LF form.lossPolicy: 'allow', partial: true) and as a strict parse. Then it continues each reachable preview to its own prefix and every longer one, as a preview and as a strict parse, and compares each with the fresh result. A continuation reads its previous result through the runtime's checkpoint. Every stored state is a preview of a prefix of the same source, so the checkpoint's editor, formats, limits, policy and source test are the same for all of them, and each segment's nodes, diagnostics and last root follow from the segment ends. So the script records each checkpoint, merges previews whose segment ends match, and still covers every chunking without walking each chain. A failed preview merges with a checkpoint that has no segments, because neither reuses anything. When a preview has no recorded checkpoint, or a segment has no whole-number end, the script exits 2, because it can no longer read markdownCheckpoints in packages/platejs/src/markdown/lib/internal/markdownConversion.ts.markdown-line-endings: a source whose CRLF or CR line endings parse to another document than its LF line endings.markdown-round-trip: a source whose parse, serialized and parsed again, gives another document.markdown-serialize: a parsed document that does not serialize, or whose Markdown does not parse.markdown-stream: a preview that continues an earlier preview and differs from a fresh preview.markdown-stream-final: a strict parse that continues a preview and differs from a fresh strict parse.markdown-throws: a parse or serialize that throws.packages/platejs/scripts/markdown-round-trip-baseline.json through tooling/scripts/finding-baseline.mjs. Every finding is filed under packages/platejs/src/markdown/lib. An identity is the source alone, and the detail names the chunking, because which chain first reaches a state changes whenever the codec's segments do.Run every command from the repository root.
pnpm check:markdown-round-trip checks the gate bound: two inline tokens and two blocks. It compares 9,500 cases, 8,420 round trips and 1,080 streamed sources, in 3 to 17 s and 49 to 82 CPU-s, depending on the machine's load. It prints one JSON summary line, then each finding the baseline does not list. pnpm check markdown-round-trip runs the same step through the check runner, and pnpm check --affected runs it for a change under packages/platejs/src/ or packages/plitejs/src/, to the script or its baseline, to packages/plitejs/scripts/check-harness.ts, to config/plite-source-aliases.ts, to tooling/scripts/finding-baseline.mjs, or to a dependency through pnpm-lock.yaml.--inline and --blocks raise one part of the bound, and --part=round-trip or --part=stream runs one half. --inline=3 --part=round-trip compares 166,272 cases in 303 to 437 CPU-s, and --blocks=3 compares 95,036 cases. Its stream half, --blocks=3 --part=stream, compares 49,656 cases in 1,771 CPU-s on a loaded machine. A bound other than the gate's, --part or --case skips the baseline and exits 1 on any failure.--case='<identity or part of one>' prints the full diff and stack of each failure whose identity contains it, such as --case='"# a<br/>"'.--jobs=<n> sets the number of worker processes, the machine's cores up to 16 by default. Findings do not depend on it.GATE_CASES constant, so a change that makes parse refuse more sources cannot shrink the gate while the baseline still matches. Update the constant only with the reason the count moved.pnpm check:markdown-round-trip --lower. The baseline never grows past the base commit's copy. --init seeds only a rule the baseline lacks, and --lower and --init write nothing when the case count misses GATE_CASES.A finding prints its rule, identity and the first line of its detail:
markdown-round-trip "# a<br/>"
serialized "a\n\n"; the parse again differs at .children.0: {"type":"paragraph","children":[{"text":"a"}]}, not {"level":1,"type":"heading","children":[{"text":"a\n"}]}
markdown-stream "1. a\na\n\n1. a"
chunked at 1,9,10, the continued result differs at .slice.content.1.listId: "b", not "a"The first is a known finding, a heading that ends in a break and serializes as a paragraph. The second is what the gate prints when findMarkdownSegmentBoundaries stops keeping a list open, as What a pass proves says. The preview of the first 9 characters ends in 1, which is not a list marker yet. So the segment scanner commits a boundary after the lazy line, and the continuation splits one list in two. Run --case on the identity for the full documents.
Tell a real bug from a modeling error before you change code:
--lower.docs/vision/plate.md together. Never weaken a rule or add a baseline entry to get green.The gate finds 157: 156 markdown-round-trip and 1 markdown-throws. Read by their examples, the round-trip entries fall into these classes. No log proves that each class has a single cause:
<div>:+1:</div> comes back with the emoji glyph and escaped tags.<callout>\na<br/>\n</callout> loses its break.# a<br/> serializes as a paragraph, and so does a setext heading over a<br/>.# <br/>a serializes as a setext heading and loses the break.https://a.b:+1: gives a link whose URL holds the glyph.<br/>https://a.b serializes the break as a backslash and a space.<u>. The serializer closes it around the text after it, and <br/><u> loses its break.- [ ] <br/> loses its checkbox.- <br/><br/> gains a third newline.The other entry has one cause:
> [^1]:+1: throws Markdown node mapping "blockquote" expected block content. during serialize. 1 markdown-throws entry.--inline=3 --part=round-trip fails 4,894 round trips and 24 throws, and every throw is a footnote definition in a quote. --blocks=3 fails 232 round trips and 1 throw, and no streamed source fails. Neither run finds a markdown-line-endings case. The deep runs read no baseline.
Bugs planted on this tree showed where the gate holds:
markdown-round-trip and 74 markdown-throws findings, 199 of them new. - a<br/> drops its break, and a heading with an autolink throws heading expected inline content.!state.openList test in markdownSegments.ts gives 4 markdown-stream and 4 markdown-stream-final findings, such as 1. a\n\n1. a chunked at 1,7,8 and 1. a\na\n\n1. a chunked at 1,9,10, two lists where one belongs.markdownConversion.ts and the bare URL check in markdownIntrinsics.ts, gives 405 markdown-line-endings findings, such as - [ ] \n_, whose CRLF form keeps a leading line ending. It also fails the five bare URL sources that the script held then. https://a.b.\na serializes to https://a.b.\\\na, which parses again without the link. <https://a.b'> a serializes to https://a.b' a, which parses again with the ' cut from the URL.remarkSeparateEscapes from the parse in markdownConversion.ts gives 7 markdown-round-trip findings: the ], www. and email sources and four div sources, such as <div>https://a.b </div>. <https://a.b]> a serializes to https\://a.b] a, which parses again with a link to https://a.b, because mdast-util-gfm-autolink-literal finds the URL in the decoded text.MARKDOWN_DEFINITION.test(source) fallback in markdownConversion.ts gives stream findings on [a]\n\n[a]: b chunked at 1,11 and [^1]: a\n\n[^1]: a chunked at 1,13, a reference that a later definition does not resolve.markdownConversion.ts gives a stream finding on <div>a</div>\n\n<div>a</div> chunked at 1,18, two text runs that normalization would join.markdownConversion.ts gives 40 markdown-stream and 40 markdown-stream-final findings, such as </callout> <callout>\n\n--- chunked at 1,24, a body that falls out of its tag.Each run used the gate bound and restored the product files byte for byte. The remarkSeparateEscapes run used this sync's script. The openList and 0cfbdc701a runs used the script of fix(markdown): read CR line endings as LF and keep a bare URL a link, and the others ran at the earlier sync, test(platejs): check Markdown round trips and streamed parses on every small document.
Synced at fix(markdown): keep a streamed tag open until its real closer. The verification describes these files as they stand there:
| File | What the check exercises |
|---|---|
packages/platejs/src/markdown/lib/MarkdownPlugin.ts | parse, parseSlice and serialize |
packages/platejs/src/markdown/lib/internal/markdownConversion.ts | the full parse, the line-ending pass, checkpoints and segment reuse |
packages/platejs/src/markdown/lib/internal/markdownSegments.ts | where a streamed parse may commit a segment |
packages/platejs/src/markdown/lib/internal/ | the mappings, tags, lines and intrinsics both directions use |
packages/platejs/src/markdown/lib/deserializer/ | Markdown to nodes |
packages/platejs/src/markdown/lib/serializer/ | nodes to Markdown |
packages/platejs/src/markdown/lib/__tests__/createTestEditor.tsx | the plugins under test |
packages/platejs/src/markdown/lib/__tests__/nameListIds.ts | comparing documents up to list ids |
packages/plitejs/scripts/check-harness.ts | the flags, workers and gate the formal checks share |
To see whether the verification needs another look, find the sync commit with git log --oneline -1 --grep "keep a streamed tag open", then run git log --oneline <that commit>..HEAD -- with the table's paths and packages/platejs/scripts/markdown-round-trip.ts. For each commit it lists, read the diff and confirm three things. A continuation still reads its previous result only through the checkpoint's segments and the startsWith test on its source, so merging previews by segment ends stays sound. New Markdown syntax the codec supports has an inline token or a block form. A new plugin in the test editor that changes what a source parses to is in the gate. Move the sync commit in this file forward, naming its subject, only after that check and a run of pnpm check:markdown-round-trip.
a, so inline syntax streams only in the fixed bold, link and trailing-break forms.<callout>\r a loses its indented text. That bug is in the markdown subject's Open work. A round trip of these forms joins the gate once that bug is fixed.refused and gets no round trip./pstack:maintain-formal-verification-skill brings this verification back in line after changes that did not recheck it.
udecode/plate
f0c616a
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.