Uh oh!
There was an error while loading. Please reload this page.
feat(tooling): pin the fence vocabulary so no spelling hides a TypeScript block (#6135) - #6144
Merged
yinlianghui-tw merged 5 commits intoAug 24, 2026
Merged
Conversation
…ript block
`check-doc-snippet-types` compiles `ts` / `tsx` / `typescript` fences and
nothing else, so a TypeScript block fenced any other way is invisible to it —
objectui#5867, whose remediation lane collects its population from ```plaintext
fences only. `plaintext` is not the only spelling of an unhighlighted fence:
objectui#6135 measured a ```text block opening `interface FileUploadSchema {`
sitting outside the gate AND outside the lane that exists to close it.
Adds `scripts/check-doc-fence-languages.mjs`, which reads block BODIES with
#5867's own binding triage classifier rather than pinning a list of languages —
so `txt`, `console`, `raw` or a bare fence with no info string at all cannot
reopen the gap. Two failure modes are reported apart, because only the first can
be auto-classified: a KNOWN unhighlighted spelling (#5867's population,
mechanical remedy, baselineable) and a spelling nobody has thought of (a human's
call, never baselined; zero in the tree today).
`KNOWN_UNHIGHLIGHTED_TS_FENCES` is #5867's remaining population per file — 92
files, 128 blocks — shrink-only in the shape objectui#6133 landed for
`KNOWN_HAND_TYPED_GUARDS`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe…abot gate `scripts/dependabot-merge-gate.mjs` partitions every check a pull request produces into required and optional buckets, and an unclassified blocking check is one the gate would let a Dependabot merge past. `doc-fence-languages.yml` carries no trigger-level path filter, so every pull request produces it. Found by `scripts/__tests__/dependabot-merge-gate.test.ts`, which is the pin that exists for exactly this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
…ce-language-population
`check:doc-fences` went red on its FIRST CI run, reporting nine of its own entries as STALE — `block-schema.mdx` baselined at 10 now carries 0, and the same for `fields/auto-number`, `fields/object` and six `plugins/` pages. Nothing was wrong with the tree: #6136 (#5867 batch 4) and #6137 merged while this branch was open and re-fenced 23 blocks across those 9 files. That is the shrink-only baseline doing exactly what it is for, unstaged, on its first run, with the remedy printed. The remedy is followed literally: the nine zero lines are deleted and nothing else moves. The population is RE-DERIVED on the merged base rather than subtracted by hand — 105 blocks / 83 files, and the diff against the old map is nine deletions with no line added and no number raised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
…ce-language-population
yinlianghui-tw
marked this pull request as ready for review
August 24, 2026 21:33
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 24, 2026
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 freeto 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#6135
Route A then C as ruled. A — the lane's population is re-derived with
text/plain(andtxt, and the empty info string) treated as synonyms ofplaintext, and the remainder is restated below. C — the durable half lands with it, asscripts/check-doc-fence-languages.mjs.⛔ Triage's classifier is quoted verbatim, never widened. ⛔ No document is re-fenced.
UNGATED_DOCS, the ledger andcheck-doc-snippet-types.mjsare untouched; noFRAGMENT_MARKERis declared.0. ⭐ The guard failed its own first CI run, and nobody staged it
The strongest non-vacuity evidence in this PR is not one of the injected probes below. It is this, from the very first
Doc Fence Language Checkjob:Nothing was wrong with the tree. #6136 (#5867 batch 4) and #6137 merged while this branch was open and re-fenced 23 blocks across exactly those 9 files. The baseline had been measured at
bfdb9f906and the tree had moved under it — which is the one failure this design exists to make loud, arriving unprompted, in the good direction (the debt shrank), naming every file and printing the remedy.A guard that only ever passes is indistinguishable from no guard. This one did not get to be that, on day one.
The remedy is followed literally: the nine zero lines are deleted, nothing else moves, and the population is re-derived on the merged base rather than subtracted by hand.
diffof the old map against the new one is nine deletions, no line added, no number raised — shrink-only held in both directions. ⛔ The staleness check was not weakened and the baseline is not pinned to a commit.1. The population, re-derived on current
origin/mainDerived with
check-doc-snippet-types's ownlistDocuments(imported, not re-implemented) and a byte-for-byte copy of its own run-length fence walk, with exactly one thing changed — the language set is a parameter instead of the hard-codedTS_FENCE_LANGUAGES. That single difference is what makes the delta attributable.Control first. Run against the card's own commit
133e2ea1e, the same derivation reproduces the card's numbers to the block:133e2ea1eplaintextplaintext+text+plainMeasured twice, because #5867's batches were landing underneath this card while it was written. The delta is stable; the totals are not — which is itself the argument for keeping the number in the repository rather than in a handback:
bfdb9f906273537957(this base)plaintext(what every batch has used)plaintext+text+plaintxtThe delta is +1 block / +1 file at both readings, and it is the block the card named:
content/docs/components/form/file-upload.mdx:27, a```textfence openinginterface FileUploadSchema {.txtand the empty info string add nothing today — worth measuring precisely because the card predicted them as the next spellings, and the answer is that the gap they would open is unrealised rather than already realised.105 / 83 is a measurement, not the arithmetic. The PM predicted
128 − 23 = 105 / 83; the re-derivation on the merged base returns exactly that, so prediction and measurement agree.The remainder, in batch 4's handback shape
105 blocks in 83 files. This table is generated from
KNOWN_UNHIGHLIGHTED_TS_FENCES— it is not a figure re-derived by hand, and a #5867 batch that re-fences blocks without lowering the map goes red, as §0 demonstrates.content/docs/components/**content/docs/layout/**content/docs/core/**UNGATED_DOCS— re-fencing adds zero to the compile populationcontent/docs/plugins/**plugin-dashboard.mdx, recorded on #6122content/docs/blocks/**andcontent/docs/fields/**are gone from the table entirely — batch 4 and #6137 took them, which is what §0 caught.The
componentscount is 72, not 71: it is the group's 71plaintextblocks plusfile-upload.mdx:27. That is deliverable A's whole visible effect on the arithmetic.content/docs/components/form/file-upload.mdx— in the population, not fixableNamed here rather than touched, for the two independent reasons the ruling gives:
componentsgroup, blocked on [Decision] Five documented action props have no declared slot at all — delete them, move them onto the item, or is this a types gap? #6132;content/docs/fieldspage's "Field Schema" block declares its OWN interface, so it compiles vacuously — and the ones measured name keys the exported*FieldMetadatatypes do not have #6138 measured that a block declaring its owninterfaceand then being checked against it compiles vacuously.file-upload.mdx:27is exactly that form. Re-fencing it would add a block that reports green while checking nothing — the finding: 两个结构不兼容的CalendarEvent同名并存(@object-ui/types授权型 vs@object-ui/plugin-calendar运行时型),已造成一处文档示例不可编译 #5044 class this lane exists to close, arriving by the lane's own hand.Its baseline line is labelled in the map so nobody re-derives that judgement: it comes down when #6138 rules on what such a block should be, not when someone re-fences it.
2. The guard — the mechanism, and why this one
The ruling deliberately did not prescribe the mechanism. Three were measured.
Rejected — widen the derivation's language set. This is A without C. It fixes one block and pins nothing:
console,raw,output, a bare fence with no info string are all still free, and the card gets refiled under a different fence name.Rejected — enumerate the allowed fence languages and ban the rest (the "normalisation that makes alternate spellings not expressible" route). The measured objection is that the enumeration is the thing that rots: the tree carries 20 distinct fence languages today, the list needs extending every time a page picks up a new highlighter language, and the failure direction is silent — a spelling nobody added is a spelling nobody notices. It also cannot be landed inside this card's scope: banning the empty info string alone would demand re-fencing 36 blocks, and ⛔ re-fencing is out of scope.
Chosen — read block BODIES, so no list of languages is on the enforcement path. The rule is one sentence:
The question put to every fence is triage's, quoted rather than extended — a block whose first line starts with
import/export/interface/type X =/const x: Tis code. Because the fence spelling is never consulted to decide whether a block fails,txt,console,rawand a bare ``` are all closed by construction, without any of them being named anywhere in the enforcement path. That is the outcome the ruling asked for: after this lands, no spelling of an unhighlighted fence can hide a TypeScript block from the lane again.The two failure modes, kept apart because only one can be auto-classified
plaintext,text,plain,txt, or no info string (UNHIGHLIGHTED_SPELLINGS)```ts. So it is the only mode a baseline entry can describe.rawmight be a sixth synonym, or a real highlighter language whose block happens to open withimport. That is a human's call, so this mode ⛔ can never be baselined and fails on sight. Zero in the tree today, which is what makes "never baselined" affordable rather than aspirational.UNHIGHLIGHTED_SPELLINGStherefore chooses a message, never a verdict. Adding a spelling to it moves a finding from UNKNOWN to SYNONYM; it does not excuse the block, because SYNONYM findings still have to sit inside a shrink-only baseline that no supported route adds to. There is no edit to this file that makes a new hidden TypeScript block pass.The baseline
KNOWN_UNHIGHLIGHTED_TS_FENCES,path -> count, ⛔ SHRINK-ONLY, in the exact shape #6133 landed forKNOWN_HAND_TYPED_GUARDS— including that card's third reason, which is the one that matters: a path-only baseline silently accepts a second hidden block smuggled into an already-owed file.It is not a debt list this gate invented. It is#5867's remaining population per file, in the repository. Every batch of that lane now lowers these numbers in the same pull request that re-fences the blocks, which is what stops the arithmetic being a figure re-derived by hand in each handback and trusted by the next dispatch. §0 is what that looks like when it is not done in the same pull request. When the last entry goes, so does the map.
Where it runs
Its own workflow (
doc-fence-languages.yml), nopathsfilter,merge_groupsubscribed — because the defect arrives in a docs-only pull request, which is exactly the shapeci.ymlandlint.ymlskip. It is install-free and build-free: it re-implements the snippet gate's document walk rather than importing it (that gate importstypescript), and the copy is held to the original by a test that imports both walks and compares them, plus a comparison of its TS-fence set against that gate's ownTS_FENCE_LANGUAGESexport.3. Non-vacuity — the whole deliverable
Beyond §0, which is the unstaged half.
Durable half, in CI.
--self-testruns before the verdict, driving the real scanner over fixture sources: 26 cases, covering the three spellings the ruling named, the corrected forms, the second failure mode, the classifier's limbs, the run-length walk, and the baseline in every direction it can move. Plusscripts/__tests__/check-doc-fence-languages.test.ts, 20 tests.One-off half, on disk, through the shipped gate process. Each leg proved its mutation on disk by grepping the injected marker before any result was read, and restored under
trap … EXIT INT TERM;git status --porcelainwas empty afterwards.```textTS block, unbaselined file```textat :1368content/docs/guide/ci-cd-pipeline.md:1368 ```text — interface ObjectUI6135ProbeSchema {```txtTS block```txtat :1368```txt```at :1368```(no info string)```ts```tsat :1368```consoleTS block (unknown spelling)block-schema.mdx is baselined at 10; it now carries 11```plaintextcount 11 → 10baselined at 10, now carries 9```textfenceThe last two rows are the ones that make the rest mean something: the baseline moves in exactly one direction, and the gate says nothing about prose whatever its fence says. (The staleness row was a probe; §0 is the same thing happening for real, nine files at once.)
A measured note on the classifier's edge. "First line" is read literally — line 1 of the body, no leading whitespace tolerated. A variant that trims first agrees with this one on every
plaintextblock in the tree and on thetextone, and disagrees on exactly one block:content/docs/guide/architecture-overview.md:123, an ASCII-art plugin-lifecycle diagram whose first line is an indentedimport 'plugin-kanban'inside a box drawing. It is not TypeScript, and the strict reading is the one that says so. Pinned as a self-test case.4. A second defect this found
scripts/dependabot-merge-gate.mjspartitions every check a pull request produces into required and optional buckets, and an unclassified blocking check is one the gate would let a Dependabot merge past. The new workflow carries no trigger-level path filter, so every pull request producesDoc Fence Language Check. Caught byscripts/__tests__/dependabot-merge-gate.test.ts— the pin that exists for exactly this — and registered in its own commit.Verification
All at final head
072dfe8f9, pushed at that same head,git status --porcelainempty at run time. Exit codes captured before any pipe; every gate quoted by its own verdict line. Heavy runs went through the container's shared verify lock. vitest from the repository ROOT, never package-scoped (objectui#3378).The branch was brought up to date with
git merge origin/main, twice — ⛔ never rebase, never force-push. The second merge takes #6142 (273537957→228909995), which lands after §0 and edits fivecontent/docs/components/**pages this baseline owes lines for. It moves none of them: those edits change block bodies, not fences, so the map is byte-identical across that merge andcheck:doc-fencesis green on the merged tree. That is worth stating rather than assuming, because it is the same hazard §0 is about, checked instead of hoped for.check:doc-fences --self-test✓ check-doc-fence-languages self-test: 26 cases pass — …the shrink-only baseline is pinned in every direction it can move.check:doc-fences✅ check:doc-fences — every TypeScript block in 222 document(s) is fenced ts/tsx/typescript, except 83 declared file(s) carrying 105 block(s) of objectui#5867's remaining population (⛔ SHRINK-ONLY). No unknown fence spelling hides one.check:entry-guard✓ check:entry-guard: 42 scripts/ file(s) — no entry guard outside the baseline; …check:doc-types✅ Every documented component type is registered.docs:check-linksLinks are valid across 15 scan roots.check:control-bytes✅ check-control-bytes: OK (scanned 5094 tracked text file(s); skipped 85 binary).check:changeset-presence✅ No source of a released package changed in this range, so no changeset is owed.check:changeset-fixed✅ privatePackages declared: version=true, tag=false.check:changeset-no-major✅ No changeset declares a major bump.check:lint-coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).type-check:scriptstsc -p tsconfig.scripts.json, no diagnosticsscripts/Test Files 70 passed (70)/Tests 1914 passed (1914)The new gate's own file also carries the entry-guard's canonical predicate (
isEntrypoint(import.meta.url)) rather than a hand-typed one, soKNOWN_HAND_TYPED_GUARDSdoes not grow.Changeset: none owed, and that is a measurement rather than a judgement —
check-changeset-presencereports0 of them published source of a package the release covers. This PR's own diff isscripts/,.github/workflows/, rootpackage.jsonand one docs page.Lint, narrowed with the narrowing measured rather than asserted — and the measurement corrected one of my own claims. Three pieces of evidence, all three required:
eslint --print-configon each changed file:scripts/__tests__/check-doc-fence-languages.test.tsresolves 116 configured rules;scripts/check-doc-fence-languages.mjsandscripts/dependabot-merge-gate.mjsresolve 0 —eslint.config.jsnames only**/*.{ts,tsx}globs, so this repository's.mjsgate scripts sit outside every rule block. That is a pre-existing property of the repo, not something this diff created..github/workflows/doc-fence-languages.yml,content/docs/guide/ci-cd-pipeline.mdandpackage.jsoneach come backFile ignored because no matching configuration was supplied;--format json:eslintexit 0, 0 errors / 0 warnings across the three files it was given;eslint.config.jsdeclares noparserOptions.projectand noprojectService— so this diff cannot move the verdict on any untouched file.--print-configreading above disproves. The.mjshalf's correctness therefore rests ontype-check:scripts(which compiles the pin test withallowJs, so the gate's exported signatures are inferred from the gate itself and cannot drift from it), on the gate's own--self-test, and on the 20-test pin file — not on eslint.check:doc-snippetswas not run locally, and the narrowing is declared with its evidence. It needs a filtered turbo build; this PR's own diff cannot move it, measured rather than asserted:content/docs/guide/ci-cd-pipeline.mdholds 0ts/tsx/typescriptfences before and after, and the diff on that file adds no fence-opening line at all.UNGATED_DOCS, the ledger and the gate's source are untouched. CI runs it unfiltered regardless.Refs: #5867 (the lane) · #6136 / #6137 (what moved the tree under §0) · #6142 (checked against the baseline, moves nothing) · #6133 (the shrink-only baseline precedent) · #6138 (why
file-upload.mdxis worse than blocked) · #6132 / #6120 / #6121 / #6122 (what blocks the remainder) · #5044 (the class) · #5174 (why the scan surface is stated and pinned).Generated by Claude Code