fix(scripts): read a fence opener as a run, not as three backticks (#9194) - #9332
Merged
Merged
Conversation
…9194) Both doc gates carried the same opening-fence predicate, verbatim, and the same wrong answer with it: a capture greedy over non-space read a four-backtick opener as a three-backtick fence whose language was the leftover backtick plus the real one. On the page that legitimately teaches nested fences the nesting then inverted -- the four-backtick line opened, the real inner opener closed, and the code between them sat outside any fence, invisible to both gates, while two empty non-fences were counted as successfully parsed in the coverage figures and a language that does not exist got its own column. The rule now lives once, in scripts/markdown-fence-scan.mjs, as CommonMark states it: a run of three or more backticks or tildes opens, and only a run of the same character that is at least as long, carrying no info string, closes. A backtick-bearing language is no longer detectable-after-the-fact but unrepresentable -- the run is consumed greedily and an info string holding a backtick is not an opener at all. Extraction rather than a two-way pin, because the two gates already shared a module boundary: one imports the other's scan surface. The new pin test is the recurrence guard -- it fails if either gate grows a private fence predicate again, and its lit control carries an ODD number of stray four-backtick markers, which is the shape that desynchronises pairing for the rest of a file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #9194.
Fixes, notPart of: all four acceptance items from the triage comment are met in this PR and nothing about the card is left for a second change. The follow-up found while measuring is filed separately as #9331 and is ⛔ not touched here.What was wrong
Two doc gates carried the same opening-fence predicate, verbatim, and the same wrong answer with it:
(\S*)is greedy over non-space, so a four-backtick opener matched as a three-backtick opener whose language was the leftover backtick plus the real one. Read out of the gates' own scanners onorigin/main— ⛔ not from a grep:A language that does not exist, two phantom fences both reporting a successful parse (the object-body retry wraps
''into{}, which parses), and the nesting inverted — line 150 opened, the real inner opener at 151 closed, and the JavaScript on 152–154 sat outside any fence, invisible to both gates.What this does — A, with B folded into its structure
Per the triage ruling: A, the CommonMark rule. The predicate now lives once, in
scripts/markdown-fence-scan.mjs: a run of three or more backticks or tildes opens, and only a run of the same character that is at least as long, carrying no info string, closes. A shorter run inside a longer one is ordinary body text, which is exactly how a page teaches nested fences.B did not need a separate check. B was "reject a language capture containing a backtick and report it". Here the run is consumed greedily and — per CommonMark — an info string containing a backtick is not an opener at all, so a backtick-bearing language is no longer detected after being counted; it is unrepresentable. That is the repair order the ruling asked for (remove the construct that permits the error → make the correct form the only spelling → only then add a check), and the check that remains is an assertion in the pin test, not a runtime report on a state that can no longer occur.
⛔ C was not taken, as ruled: the affected page is legitimately teaching nested fences.
Single authority, not two patched copies — and which fork that was
Triage's fork: extract, or pin the two copies against each other if their module boundaries make extraction too costly. It was the cheap case, and the answer is extraction. The two gates already share a module boundary —
check-doc-expression-carriage.mjsimports its scan surface fromcheck-doc-component-types.mjs(objectui#7878) rather than carrying a copy — so there was no boundary to force. Both now importopenFence/closesFence, and each site carries a ⛔ comment naming the authority.What keeps them consistent from now on is
scripts/__tests__/markdown-fence-scan.test.ts, which fails if either gate grows a private fence predicate again: it masks comments (prose about a regex is not a regex) and looks for the shape of an anchored fence-matching regex literal, not for the one pattern that was removed — with a positive control asserting the guard still matches both the removed spelling and the run-aware one, so a guard that stopped matching anything cannot pass by describing nothing.Prior art is named rather than re-derived:
body-dialect-census.mjs(keepFencedCodeOnly) andcheck-doc-fence-languages.mjswere already run-aware. They are offset-based and self-contained respectively; this module is the line-based spelling the two doc gates needed, and it is the one a third gate should import instead of writing a fourth.Acceptance — all four, measured
1. The instance. Read back out of
scanFencesafter the fix:Language⚠️ One precision on the acceptance wording: under CommonMark there is no inner fence — the inner
markdown, no phantom fences (the fence at 155 is gone), and the JavaScript on 152–154 is inside the fence.```javascriptand its closer are literal body text of the four-backtick fence, which is why the body is 5 lines and not 3. What the acceptance is about holds either way: the JavaScript is no longer outside every fence.2. The lit control — an ODD number of stray markers. A fixture carrying exactly one stray four-backtick marker, inside a five-backtick wrapper that is legitimately teaching it (the even-count corpus instance resynchronises by accident and cannot show this). Both gates driven over it:
json,markdown, `json`,,plaintextjson,markdown,jsonjsonfenceomega)jsonfence3. No backtick-bearing language column. The fence inventory the census reads held one such language before (
`markdown) and holds none after;--listover the real surface greps zero.4. Both gates. Both verified, and the whole-corpus diff of both scanners' output, before vs after, is exactly three lines — the two phantom fences replaced by one real one. No other fence, no type site, and no counter moved:
Two deliberate divergences from strict CommonMark, both measured
Both are documented in the module header:
.md/.mdxin the tree: 12 marker lines are indented past three spaces, all of them in.github/prompts/component.prompt.md, which is on neither gate's scan surface. A separate card if anyone wants it.Reverse verification
The implementation was committed first, then ablated: the module's rule was swapped back to the replaced semantics (exactly three, greedy info, no info guard), proven on disk (three injected markers, zero original spellings remaining), and the pins re-run.
The defect is reproduced exactly, and the seven reds include both gate-level desync pins and the corpus invariant. Restored with
git checkout HEAD --, verified byte-identical by blob hash, 16/16 green again.Commands run
node scripts/check-doc-component-types.mjsnode scripts/check-doc-expression-carriage.mjsnode scripts/check-doc-expression-carriage.mjs --self-testnode scripts/markdown-fence-scan.mjs --self-testpnpm exec vitest run scripts/__tests__(153 files)pnpm type-check:scriptspnpm exec eslinton the six changed filesnode scripts/check-changeset-presence.mjsnode scripts/check-control-bytes.mjsnode scripts/check-new-cross-file-line-citations.mjsnode scripts/check-lint-coverage.mjs·check-type-check-coverage.mjs·check-unreferenced-sources.mjspnpm type-check:scriptsis run by name and deliberately: it is a separate command from per-packagetype-check, it is the only thing that compilesscripts/, and a PR on this lane shipped red this shift by running the latter and not the former.Two other files moved, both because the change made an existing claim stale, and both predicted by the claim's own comment:
scripts/__tests__/check-doc-expression-carriage.test.ts— the orphan-copy list for the "is LOUD when the instrument itself is broken" pin. Its comment says in as many words that falling behind the gate's imports turns that test into a module-resolution stack trace; this PR adds a fourth import, so the list gains a fourth file.scripts/markdown-test-inputs.mjs— the new pin test names a document in the corpus, so it owes a row in the adjudicated ledger.Changeset: empty frontmatter — tooling only, no package is released.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code