Skip to content

feat(tooling): pin the fence vocabulary so no spelling hides a TypeScript block (#6135) - #6144

Merged
yinlianghui-tw merged 5 commits into
mainfrom
claude/issue-6135-fence-language-population
Aug 24, 2026
Merged

feat(tooling): pin the fence vocabulary so no spelling hides a TypeScript block (#6135)#6144
yinlianghui-tw merged 5 commits into
mainfrom
claude/issue-6135-fence-language-population

Conversation

@yinlianghui-tw

@yinlianghui-twyinlianghui-tw commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6135

Route A then C as ruled. A — the lane's population is re-derived with text / plain (and txt, and the empty info string) treated as synonyms of plaintext, and the remainder is restated below. C — the durable half lands with it, as scripts/check-doc-fence-languages.mjs.

⛔ Triage's classifier is quoted verbatim, never widened. ⛔ No document is re-fenced. UNGATED_DOCS, the ledger and check-doc-snippet-types.mjs are untouched; no FRAGMENT_MARKER is 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 Check job:

❌ check:doc-fences — 9 stale KNOWN_UNHIGHLIGHTED_TS_FENCES entry/entries:
content/docs/blocks/block-schema.mdx — baselined at 10, now carries 0
content/docs/fields/auto-number.mdx — baselined at 2, now carries 0
content/docs/fields/object.mdx — baselined at 2, now carries 0
content/docs/plugins/plugin-charts.mdx — baselined at 1, now carries 0
…six in total under plugins/

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 bfdb9f906 and 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. diff of 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/main

Derived 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-coded TS_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:

fence-language setat 133e2ea1ecard says
plaintext146 blocks / 102 files146 / 102 ✓
plaintext + text + plain147 blocks / 103 files147 / 103 ✓

Measured 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:

fence-language setat bfdb9f906at 273537957 (this base)
plaintext (what every batch has used)127 / 91104 blocks / 82 files
plaintext + text + plain128 / 92105 blocks / 83 files
…+ txt128 / 92105 / 83
…+ no info string at all128 / 92105 / 83

The 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 ```text fence opening interface FileUploadSchema {. txt and 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.

groupblocksfilesstate
content/docs/components/**7272⛔ blocked on #6132
content/docs/layout/**123⛔ blocked on #6120
content/docs/core/**111⛔ blocked on #6121
named in UNGATED_DOCS — re-fencing adds zero to the compile population76leave
content/docs/plugins/**31plugin-dashboard.mdx, recorded on #6122
total10583

content/docs/blocks/** and content/docs/fields/** are gone from the table entirely — batch 4 and #6137 took them, which is what §0 caught.

The components count is 72, not 71: it is the group's 71 plaintext blocks 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 fixable

Named here rather than touched, for the two independent reasons the ruling gives:

  1. it is in the components group, 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;
  2. more decisively and newer than the card, finding(docs): every content/docs/fields page's "Field Schema" block declares its OWN interface, so it compiles vacuously — and the ones measured name keys the exported *FieldMetadata types do not have #6138 measured that a block declaring its owninterface and then being checked against it compiles vacuously. file-upload.mdx:27 is 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:

A fenced block whose body is TypeScript must be fenced ts / tsx / typescript.

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: T is code. Because the fence spelling is never consulted to decide whether a block fails, txt, console, raw and 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

modewhenwhy it is separate
SYNONYMthe fence is a known spelling of an unhighlighted block — plaintext, text, plain, txt, or no info string (UNHIGHLIGHTED_SPELLINGS)the gate knows exactly what this is: #5867's population. The remedy is mechanical — re-fence ```ts. So it is the only mode a baseline entry can describe.
UNKNOWNany other spellingthe gate cannot auto-classify it: raw might be a sixth synonym, or a real highlighter language whose block happens to open with import. 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_SPELLINGS therefore 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 for KNOWN_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), no paths filter, merge_group subscribed — because the defect arrives in a docs-only pull request, which is exactly the shape ci.yml and lint.yml skip. It is install-free and build-free: it re-implements the snippet gate's document walk rather than importing it (that gate imports typescript), 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 own TS_FENCE_LANGUAGES export.


3. Non-vacuity — the whole deliverable

Beyond §0, which is the unstaged half.

Durable half, in CI.--self-test runs 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. Plus scripts/__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 --porcelain was empty afterwards.

legon diskgate
clean treeexit 0
```text TS block, unbaselined filemarker×1, ```text at :1368exit 1content/docs/guide/ci-cd-pipeline.md:1368 ```text — interface ObjectUI6135ProbeSchema {
```txt TS blockmarker×1, ```txt at :1368exit 1 — same line, ```txt
no info string TS blockmarker×1, ``` at :1368exit 1 — same line, ```(no info string)
the same block corrected to ```tsmarker×1, ```ts at :1368exit 0
```console TS block (unknown spelling)marker×1exit 1 — reported as the UNKNOWN mode, with both remedies and "⛔ never baselined"
an 11th hidden block in a file baselined at 10marker×1exit 1block-schema.mdx is baselined at 10; it now carries 11
one hidden block removed from that file```plaintext count 11 → 10exit 1STALE, baselined at 10, now carries 9
prose under a ```text fencemarker×1exit 0 — the classifier is quoted, not widened

The 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 plaintext block in the tree and on the text one, 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.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. The new workflow carries no trigger-level path filter, so every pull request produces Doc Fence Language Check. Caught by scripts/__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 --porcelain empty 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 (273537957228909995), which lands after §0 and edits five content/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 and check:doc-fences is 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.

checkexitits own verdict line
check:doc-fences --self-test0✓ check-doc-fence-languages self-test: 26 cases pass — …the shrink-only baseline is pinned in every direction it can move.
check:doc-fences0✅ 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-guard0✓ check:entry-guard: 42 scripts/ file(s) — no entry guard outside the baseline; …
check:doc-types0✅ Every documented component type is registered.
docs:check-links0Links are valid across 15 scan roots.
check:control-bytes0✅ check-control-bytes: OK (scanned 5094 tracked text file(s); skipped 85 binary).
check:changeset-presence0✅ No source of a released package changed in this range, so no changeset is owed.
check:changeset-fixed0✅ privatePackages declared: version=true, tag=false.
check:changeset-no-major0✅ No changeset declares a major bump.
check:lint-coverage0✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
type-check:scripts0tsc -p tsconfig.scripts.json, no diagnostics
vitest, repo root, scripts/0Test 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, so KNOWN_HAND_TYPED_GUARDS does not grow.

Changeset: none owed, and that is a measurement rather than a judgement — check-changeset-presence reports 0 of them published source of a package the release covers. This PR's own diff is scripts/, .github/workflows/, root package.json and 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:

  1. the population is read from eslint's own configuration resolution, not from a guess. eslint --print-config on each changed file: scripts/__tests__/check-doc-fence-languages.test.ts resolves 116 configured rules; scripts/check-doc-fence-languages.mjs and scripts/dependabot-merge-gate.mjs resolve 0eslint.config.js names only **/*.{ts,tsx} globs, so this repository's .mjs gate 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.md and package.json each come back File ignored because no matching configuration was supplied;
  2. file counts read from --format json: eslint exit 0, 0 errors / 0 warnings across the three files it was given;
  3. type-aware linting is not enabledeslint.config.js declares no parserOptions.project and no projectService — so this diff cannot move the verdict on any untouched file.

⚠️ Stated plainly rather than left to be inferred from a green: exactly one file in this diff is rule-linted. The first draft of this section said all three were, which the --print-config reading above disproves. The .mjs half's correctness therefore rests on type-check:scripts (which compiles the pin test with allowJs, 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-snippets was 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.md holds 0ts/tsx/typescript fences 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.mdx is 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

…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
`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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(docs): #5867's population is derived from plaintext fences only, so a TS block fenced text is invisible to both the gate and the batch lane

2 participants

@yinlianghui-tw@claude