docs(fields): blank line before ## Field Schema on four field pages (objectui#9427) - #9435
Conversation
`date.mdx`, `rich-text.mdx`, `text.mdx` and `textarea.mdx` each placed a
self-closing `SchemaExample` tag immediately before `## Field Schema`, with
no blank line between them.
`packages/plugin-markdown`'s `MarkdownImpl` (react-markdown; the chain
`apps/console`'s `DocPage` uses) follows CommonMark's HTML-block rule — an
HTML block ends only at a blank line — so the tag's block swallowed the
heading and no `field-schema` anchor was rendered, while `extractToc` still
listed one. The `#field-schema` ToC link therefore resolved to nothing.
`@mdx-js/mdx` (the compiler `fumadocs-mdx` uses for `apps/site`) closes a
self-closing MDX JSX flow element immediately, so it never swallowed the
heading; its emitted id lists are byte-identical before and after this
change. The renderer that disagreed is the one reading CommonMark
correctly, so the fix is in the docs, not in either renderer.
Measured on this base over 223 files (`content/docs/**/*.{md,mdx}` plus
`packages/*/README.md`, frontmatter stripped): the rendered-vs-`extractToc`
divergence set was exactly these four files and is now empty; rendered
headings 2984 -> 2988; the 22 field pages that already carried the blank
line emitted `field-schema` throughout, as the lit control.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
PM pre-merge probe —
|
| files | |
|---|---|
| defect shape — self-closing JSX tag immediately followed by an ATX heading, pre | 4 — fields/date.mdx, fields/rich-text.mdx, fields/text.mdx, fields/textarea.mdx |
| same, post | 0 ✅ |
| CONTROL — healthy shape (tag + true blank line + heading), pre | 129 ← HITS |
⭐ The control is what makes the 0 meaningful: a run where the healthy shape also read 0 would be a broken regex, ⛔ not a clean corpus.
Leg 2 — ⭐ the inserted BYTES, not merely 「the file changed」
| file | separator pre | separator post | bytes |
|---|---|---|---|
date.mdx |
b'\n' |
b'\n\n' |
+1 |
rich-text.mdx |
b'\n' |
b'\n\n' |
+1 |
text.mdx |
b'\n' |
b'\n\n' |
+1 |
textarea.mdx |
b'\n' |
b'\n\n' |
+1 |
NEGATIVE control: files whose separator is LF + spaces/tabs + LF → 0. Diff is 4 files, 4 insertions, every one an empty line. ⛔ Nothing else touched.
Leg 3 — the divergence set was RE-DERIVED, and the corpus moved under it
⭐ The dev re-derived rather than inheriting, and the check earned itself: the corpus moved — 2984 rendered headings on their base against the 2981 the card read hours earlier (+3) — while the divergence set stayed exactly those four. ⇒ no fifth file appeared, none dropped out. ⛔ A card's population figure is a reading with a date on it, and this one was re-taken.
Population named precisely: 223 files = content/docs/**/*.{md,mdx} (184) + packages/*/README.md (39), YAML frontmatter stripped — ⭐ because leaving it in makes the closing --- a setext h2 in every frontmatter file and would have faked ~144 divergences. That is a reader chosen to match the question.
Leg 4 — BOTH renderers, in opposite directions
react-markdown (MarkdownImpl, what apps/console uses) — absent → present, all four, each at the index extractToc already claimed:
date.mdx [basic-usage, with-default-value, |field-schema|, date-formats, use-cases, cell-renderer, datetime-variant] idx 2
rich-text.mdx idx 2 · text.mdx idx 4 · textarea.mdx idx 3
@mdx-js/mdx (what apps/site uses) — NON-change, id lists printed both sides, byte-identical for all four (7/10/7/6 ids, all non-empty). ⭐ This leg agrees on both sides, so per this seat's standing rule it carries its printed lists as the guard rather than a bare 「no change」.
⇒ the card's claim that the insertion costs apps/site nothing is verified, ⛔ not inherited.
⭐ LIT control with a known direction: of 26 field pages carrying a ## Field Schema heading, the 22 that already had the blank line emitted field-schema in the PRE run (22/26), rising to 26/26 POST. ⇒ the PRE zero on the four targets is a measurement, ⛔ not a dead instrument. NONSENSE control field-schema-zzz-not-a-real-anchor: 0 in every run.
extractToc agreement POST is exact and corpus-wide — the divergence set over all 223 files is empty.
⭐ Why the docs changed and not the renderer
MarkdownImpl is the one following CommonMark correctly — an HTML block ends only at a blank line, so the tag's block swallowed the heading. @mdx-js/mdx closes a self-closing MDX JSX flow element immediately. ⇒ the renderer that disagreed is the correct one, and the docs were authoring a shape it cannot turn into a heading. objectui#7666's triage already priced fixing extractToc instead and routed it here. ⛔ packages/plugin-markdown, apps/site, fumadocs-mdx and all config: untouched.
⭐ A gate distinction the dev got right, and most get wrong
check:doc-snippets and check:doc-examples first returned exit 2, whose own output reads 「PRECONDITION NOT MET — the snippet program was NOT run」 (packages unbuilt). They ⛔ did not report that as a red gate — they read it as NOT MEASURED, ran the scoped build the gate itself names, and only then recorded PRE 0 / POST 0 (650 of 650 blocks judged; 124 blocks). ⭐ An unrun gate is not a passing gate and is not a failing one — conflating either way is how a probe lies.
CI on 6386d9dfd4
32 runs — all 9 required green, and ZERO other non-green. ⭐ Not even Bundle Analysis, which is red across the rest of this lane. check-changeset-presence exits 0 (「no changeset is owed」 — 4 files changed, 0 of them published source). check-governed-queue-guard --test: NOT GOVERNED.
⚠️ Carried forward — ⛔ NOT filed, ⛔ not fixed
- ⭐ Nothing mechanically prevents the shape returning. The divergence set is 0/223 today, but no gate reads it:
toc-anchor-parity.test.tsxpinsextractToc-vs-renderer parity on synthetic heading strings only, so a fifth field page authored with the tag flush against its heading reopens this silently. A corpus-level parity gate — the exact 223-file scan this card ran, asserting the divergence set is empty — would close the class.⚠️ This is the THIRD 「a defence that works, and nothing watches it」 finding on this lane today (objectui#9422; objectui#3535's residual; this). Needs its own card. - objectui#7666 is now fully settled by this measurement — ⛔ triage owns re-grading or closing it; the dev correctly did not touch it.
- This repo has no
scripts/pm/os-verify-lock.sh(an objectstack artifact), so the heavy scoped build ran without the shared lock — worth knowing with parallel seats in one container.
⇒ Flipping ready and enqueueing once the governed guard settles.
Generated by Claude Code
✅ LANDED —
|
pre (fb097b8479) |
post (on main) |
|
|---|---|---|
| defect shape — self-closing JSX tag immediately followed by an ATX heading | 4 — fields/date.mdx, rich-text.mdx, text.mdx, textarea.mdx |
0 ✅ |
| CONTROL — healthy shape (tag + true blank line + heading) | 129 files ← HITS | 129 files |
Separator bytes on main, all four: b'\n\n' — a true empty line.
What the fix settles
MarkdownImpl (react-markdown, apps/console's DocPage) follows CommonMark's HTML-block rule — a block ends only at a blank line — so the tag's block swallowed the heading and console emitted no field-schema anchor while extractToc still listed one. @mdx-js/mdx (apps/site) closes a self-closing MDX JSX flow element immediately and never swallowed it.
⇒ ⭐ the renderer that disagreed is the one reading CommonMark correctly, so the docs changed, not the renderer. objectui#7666's triage had already priced fixing extractToc instead and routed it here. ⛔ packages/plugin-markdown, apps/site, fumadocs-mdx and all config: untouched.
Both renderers were proven, in opposite directions: react-markdown absent → present on all four at the indices extractToc already claimed, and @mdx-js/mdx id lists byte-identical before and after — ⭐ that second leg agrees on both sides, so it carried its printed id lists as its guard rather than a bare 「no change」. LIT control with a known direction: 22 of 26 field pages already emitted field-schema pre, rising to 26/26 post.
⭐ The corpus moved under the card, and the re-derivation caught it
The dev re-derived the divergence set rather than inheriting it: 2984 rendered headings on their base against the 2981 the card read hours earlier (+3), while the divergence set stayed exactly those four. ⇒ no fifth file appeared, none dropped out. ⛔ A card's population figure is a reading with a date on it.
Population named precisely: 223 files = content/docs/**/*.{md,mdx} (184) + packages/*/README.md (39), frontmatter stripped — ⭐ because leaving it in makes the closing --- a setext h2 in every frontmatter file and would have faked ~144 divergences.
⭐ A gate distinction worth copying
check:doc-snippets and check:doc-examples first returned exit 2, whose own output reads 「PRECONDITION NOT MET — the snippet program was NOT run」. The dev ⛔ did not record that as a red gate — they read it as NOT MEASURED, ran the scoped build the gate itself names, and only then recorded PRE 0 / POST 0. ⭐ An unrun gate is neither passing nor failing; collapsing it into either is how a probe lies.
⚠️ Carried forward — ⛔ NOT filed here, and being routed
- ⭐ Nothing mechanically prevents the shape returning. The divergence set is 0/223 today, but no gate reads it:
toc-anchor-parity.test.tsxpins parity on synthetic heading strings only, so a fifth field page authored with the tag flush against its heading reopens this silently.⚠️ Third 「a defence that works, and nothing watches it」 finding on this lane today — objectui#9422, objectui#3535's residual, and this. Being routed as its own card. - objectui#7666 is now fully settled by this measurement — ⛔ triage owns re-grading or closing it; the dev correctly did not touch it.
- This repo has no
scripts/pm/os-verify-lock.sh(an objectstack artifact), so the heavy scoped build ran without the shared lock — worth knowing with parallel seats in one container.
Generated by Claude Code
Fixes #9427
Four field pages placed a self-closing
SchemaExampletag immediately before## Field Schema, with no blank line between them. This adds that one blankline to each — the diff is four empty lines and nothing else.
Why a four-line docs patch needed measuring
Two real pipelines in this monorepo disagree on that exact shape, so the fix
had to be proved on both.
Renderer 1 —
packages/plugin-markdown'sMarkdownImpl(react-markdown;the chain
apps/console'sDocPagepairs withextractToc). It followsCommonMark's HTML-block rule: an HTML block ends only at a blank line, so the
tag's block swallowed the heading and no
field-schemaanchor was everrendered — while
extractTocstill listed one, so the ToC's#field-schemalink resolved to nothing.
Renderer 2 —
@mdx-js/mdx(the compilerfumadocs-mdxuses forapps/site). A self-closing MDX JSX flow element closes immediately, so itnever swallowed the heading.
apps/sitewas already correct.The renderer that disagreed is the one reading CommonMark correctly, so the
docs are what change.
packages/plugin-markdownis untouched (objectui#7666'striage already priced and rejected teaching
extractTocthe HTML-blockcontinuation rule — that would fix the
MarkdownImplpairing and flip thedivergence for every MDX consumer).
Measurements (all re-derived on this branch's base,
7098eed365)Population, named once: 223 files —
content/docs/**/*.{md,mdx}(184) pluspackages/*/README.md(39), YAML frontmatter stripped (console serves docbodies without it). Reader:
MarkdownImplrendered withrenderToStaticMarkup, ids read off theidattribute of each rendered h1–h6element — the same reader
packages/plugin-markdown/src/toc-anchor-parity.test.tsxalready uses, becausereading the rendered attribute proves the anchor exists rather than re-deriving
the slug rules.
Divergence set re-derived, not inherited. Files where the rendered heading
ids differ from
extractToc's: PRE exactly the four named in the card, POSTempty. Rendered headings across the corpus 2984 → 2988 (+4, the four new
headings). The card read 2981 on the same corpus a few hours earlier — the
corpus moved again by +3 headings, and the divergence set did not.
Renderer 1 — the diagnostic flip (absent → present, all four):
field-schemaPREextractTocpositioncontent/docs/fields/date.mdxcontent/docs/fields/rich-text.mdxcontent/docs/fields/text.mdxcontent/docs/fields/textarea.mdxdate.mdxrendered ids, both sides:extractTocagreement — the point of the fix: POST, the rendered id listequals
extractToc's id list exactly (h1–h6, document order) for all fourfiles. That is the same comparison whose failure defined the divergence set,
so the empty POST set is that agreement measured corpus-wide.
Renderer 2 — the non-change, with both sides printed. Compiled in situ
through
@mdx-js/mdx3.1.1 +rehype-slug, rendered with the samerenderToStaticMarkupreader. Id lists are identical PRE and POST for allfour files — the insertion costs
apps/sitenothing:Controls. Every zero above is paired with a control of known direction that
hits in the same run:
## Field Schemaheading; the 22 that already had the blank line emittedfield-schemathroughMarkdownImplin the same PRE run (22/26 → 26/26POST). A run where those also read absent would be a broken instrument.
field-schema-zzz-not-a-real-anchor— 0 hits in everyrun, both renderers, both phases.
tag line immediately followed by an ATX heading) over the same 223 files:
4 hits PRE → 0 POST, while the healthy shape (tag + true blank line +
heading) reads 250 PRE → 254 POST.
Inserted bytes verified, not just "the file changed." A
trailing-whitespace-only line is not a blank line to CommonMark's HTML-block
rule, so the bytes were read back: each file grew by exactly 1 byte, and the
separator between tag and heading is
b'\n\n'in all four.git diff --checkis clean, and
cat -Ashows$with nothing before it on each inserted line.Gates (redirect-then-capture; PRE = the four files reverted to base, POST = as committed)
docs:check-linkscheck:doc-fencescheck:doc-snippetscheck:doc-examplescheck:doc-typescheck:doc-example-idscheck:control-bytescheck:new-line-citationsvitest packages/plugin-markdown/ apps/console/src/pages/docs-portal.test.tsxvitest packages/types/(these readcontent/docs)check:doc-snippetsandcheck:doc-examplesfirst exited 2, which theirown output spells out as
PRECONDITION NOT MET — the snippet program was NOT run; that is "could not run", not a red gate. After the scoped build theyname (
turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2, 35/35 successful) both read 0 in both phases.node scripts/check-changeset-presence.mjsexits 0 and prints "no changeset isowed" — 4 files changed, 0 of them published source of a released package.
node scripts/check-governed-queue-guard.mjs --teston the four paths printsNOT GOVERNED.
Draft on purpose: the PM seat lands this.
Bundle Analysisis red across thislane and is not related to this change.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code