From 5540a540bdbf8f7d78febcb7b9f2b8834e418dff Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 04:03:10 +0000 Subject: [PATCH] fix(scripts): give a blockquoted doc snippet a reachable fragment marker (objectui#7099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit objectui#7086 brought a fence opened inside a blockquote under this gate's contract, but that contract has two halves — a collected block must compile, OR it must carry a fragment marker declaring why it cannot. Only the first half reached blockquotes: `FRAGMENT_MARKER` was anchored `^[ \t]*`, so `> {/* doc-snippet: fragment ... */}` did not register as a marker at all, and the attachment walk wanted the nearest non-blank line above the fence while `'>'.trim()` is `'>'`, not the empty string. A quoted block that legitimately cannot compile was collected, failed, and had no declared way to say so; the author's only outs were to unindent the callout or to grow `UNGATED_DOCS`. Both mechanisms move together, because widening the anchor alone would pass in tests and fail on the callout shape real pages use: - the marker is matched against the line with its blockquote prefix stripped, and keeps the depth it was written at; - the attachment walk judges blankness AT the fence's depth, so it crosses a callout's bare `>` spacer; - a marker declares only a fence at its own depth — a depth-0 marker above a quoted fence sits outside the callout the block lives in, and a quoted marker above an unquoted fence sits inside one the block is not in. Depth 0 keeps the identity path, so every unquoted fence scans exactly as before: the corpus verdict lines are unchanged by this commit (227 documents, 625 covered blocks, 467 to compile, 158 declared fragments, 0 failed). A failing collected block at depth > 0 now also names the depth its declaration must be written at, so a marker that did not attach is explained rather than guessed at. The population this reaches is 0 today and stays 0 (0 blockquoted fragment markers over the gate's own 227-document scan set, with the controls holding in the same query: 2978 plain fence openers, 158 plain fragment markers), so the behaviour proves itself in the pins rather than in the corpus. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_013uAaxiwgYDybsTNV9xwa1M --- .../__tests__/check-doc-snippet-types.test.ts | 102 ++++++++++++++++++ scripts/check-doc-snippet-types.mjs | 73 +++++++++++-- 2 files changed, 169 insertions(+), 6 deletions(-) diff --git a/scripts/__tests__/check-doc-snippet-types.test.ts b/scripts/__tests__/check-doc-snippet-types.test.ts index a34c2f498..164f9edfe 100644 --- a/scripts/__tests__/check-doc-snippet-types.test.ts +++ b/scripts/__tests__/check-doc-snippet-types.test.ts @@ -250,6 +250,108 @@ describe('fence scanning', () => { } }); + it('attaches a blockquoted fragment marker to the fence directly beneath it', () => { + // objectui#7099: the marker anchor read `^[ \t]*`, so `> {/* doc-snippet: + // fragment ... */}` did not register as a marker at all. objectui#7086 had + // already brought the quoted fence under the gate's contract, and that + // contract has two halves — compile, OR declare why you cannot. Only the + // first half reached blockquotes, so a quoted block that legitimately cannot + // compile had no declared way to say so. + const { blocks, markers } = scanFences( + [ + '> **Note:** the renderer is already mounted above.', + '> {/* doc-snippet: fragment \u2014 continues the block above */}', + `> ${FENCE}ts`, + '> renderer.mount(el);', + `> ${FENCE}`, + ].join('\n'), + ); + expect(markers, 'a quoted marker must register as a marker').toHaveLength(1); + expect(blocks).toHaveLength(1); + expect(blocks[0].fragmentReason).toBe('continues the block above'); + }); + + it('attaches across a bare `>` spacer — the callout shape real pages use', () => { + // This is the case a half-fix misses. Widening the marker anchor alone makes + // the pin above pass and leaves this one failing: the attachment walk wants + // the nearest NON-BLANK line above the fence, and `'>'.trim()` is `'>'`, not + // the empty string — so the walk stops on the very spacer that separates a + // callout's prose from its fence. Passing in tests and failing on the shape + // real pages use is why both mechanisms move together. + const { blocks } = scanFences( + [ + '> **Note:** the renderer is already mounted above.', + '>', + '> {/* doc-snippet: fragment \u2014 continues the block above */}', + '>', + `> ${FENCE}ts`, + '> renderer.mount(el);', + `> ${FENCE}`, + ].join('\n'), + ); + expect(blocks).toHaveLength(1); + expect(blocks[0].fragmentReason).toBe('continues the block above'); + }); + + it('does not attach a marker written at a different quote depth than its fence', () => { + // The rule: a marker declares the fence at its OWN depth. A depth-0 marker + // above a quoted fence is not inside the callout the block lives in; a quoted + // marker above an unquoted fence is not outside it. Neither attaches, and the + // unattached marker is reported rather than silently dropped. + const outside = scanFences( + [ + '{/* doc-snippet: fragment \u2014 continues the block above */}', + `> ${FENCE}ts`, + '> renderer.mount(el);', + `> ${FENCE}`, + ].join('\n'), + ); + expect(outside.blocks).toHaveLength(1); + expect(outside.blocks[0].fragmentReason).toBeNull(); + expect(outside.markers.map((m) => m.consumed)).toEqual([false]); + + const inside = scanFences( + [ + '> {/* doc-snippet: fragment \u2014 continues the block above */}', + `${FENCE}ts`, + 'renderer.mount(el);', + FENCE, + ].join('\n'), + ); + expect(inside.blocks).toHaveLength(1); + expect(inside.blocks[0].fragmentReason).toBeNull(); + expect(inside.markers.map((m) => m.consumed)).toEqual([false]); + }); + + it('leaves the unquoted path exactly as it was — depth 0 is the identity path', () => { + const { blocks, markers } = scanFences( + [ + '{/* doc-snippet: fragment \u2014 continues the block above */}', + '', + '', + `${FENCE}ts`, + 'renderer.mount(el);', + FENCE, + ].join('\n'), + ); + expect(blocks).toHaveLength(1); + expect(blocks[0].quoteDepth).toBe(0); + expect(blocks[0].fragmentReason).toBe('continues the block above'); + expect(markers.map((m) => m.consumed)).toEqual([true]); + }); + + it('reports a blockquoted marker that declares nothing, instead of never seeing it', () => { + const root = tempTree({ + 'content/docs/a.mdx': [ + '> {/* doc-snippet: fragment \u2014 nothing follows this */}', + '>', + '> Just prose.', + ].join('\n'), + }); + const findings = analyze({ root, ungated: {} }).findings as Finding[]; + expect(findings.map((f) => f.reason)).toContain('stale-fragment-marker'); + }); + it('reports a marker that declares nothing rather than ignoring it', () => { const root = tempTree({ 'content/docs/a.mdx': ['{/* doc-snippet: fragment — nothing follows this */}', '', 'Just prose.'].join('\n'), diff --git a/scripts/check-doc-snippet-types.mjs b/scripts/check-doc-snippet-types.mjs index 32cd6533d..6d3b43ba7 100644 --- a/scripts/check-doc-snippet-types.mjs +++ b/scripts/check-doc-snippet-types.mjs @@ -788,7 +788,36 @@ function stripQuotePrefix(line, depth) { return out; } -/** The declaration a fragment carries; see FRAGMENT_MARKER_EXAMPLES. */ +/** + * How many levels of blockquote `line` opens with, read with the same prefix + * shape the fence opener matches — so a marker's depth and its fence's depth are + * counted by one rule and cannot disagree. + */ +function quoteDepthOf(line) { + // Every quantifier here is `*`, so the match cannot fail and there is no + // no-match branch to write: an empty prefix IS depth 0. + return (/^[ \t]*(?:>[ \t]*)*/.exec(line)[0].match(/>/g) ?? []).length; +} + +/** + * Whether `line` is blank AT `depth`: empty, or nothing left once the blockquote + * markers carrying the quote down the callout are stripped. A bare `>` is the + * spacer a callout puts between its prose and its fence, and `'>'.trim()` is + * `'>'`, not the empty string — so without this the marker attachment walk stops + * on that spacer and a quoted declaration can never reach the block it declares. + * `depth === 0` is `line.trim() === ''`, the same test the walk used before. + */ +function isBlankAtDepth(line, depth) { + return stripQuotePrefix(line, depth).trim() === ''; +} + +/** + * The declaration a fragment carries; see FRAGMENT_MARKER_EXAMPLES. Matched + * against the line with its blockquote markers stripped, so a marker written + * inside the callout its block lives in registers exactly as an unquoted one + * does; the depth it was written at is kept, and `scanFences` requires it to + * equal the depth of the fence it declares. + */ const FRAGMENT_MARKER = /^[ \t]*(?:\{\/\*|)[ \t]*$/; @@ -847,14 +876,25 @@ export const FRAGMENT_MARKER_EXAMPLES = [ * line, so the compiler sees the snippet the reader sees and not the `>` around * it. Depth 0 — every unquoted fence — takes the identity path and scans exactly * as it did before blockquotes were recognised. + * + * A fragment marker reaches those blocks at the same depth: it is read through + * the quote prefix, the walk that attaches it treats a bare `>` as blank, and it + * declares only a fence at its own depth. Both halves of the gate's contract — + * compile, OR declare why you cannot — therefore apply inside a blockquote. Only + * ONE of them reaching there would leave a quoted block that legitimately cannot + * compile with no way to say so (objectui#7099), and widening the marker anchor + * alone would not fix it: the walk would still stop on the `>` spacer that + * separates a callout's prose from its fence, which is the shape real pages use. */ export function scanFences(source) { const lines = source.split('\n'); const blocks = []; const markers = []; for (let i = 0; i < lines.length; i++) { - const marker = FRAGMENT_MARKER.exec(lines[i]); - if (marker) markers.push({ line: i + 1, reason: marker[1].trim(), consumed: false }); + const markerDepth = quoteDepthOf(lines[i]); + const marker = FRAGMENT_MARKER.exec(stripQuotePrefix(lines[i], markerDepth)); + if (marker) + markers.push({ line: i + 1, depth: markerDepth, reason: marker[1].trim(), consumed: false }); const open = /^([ \t]*(?:>[ \t]*)*)(`{3,})(.*)$/.exec(lines[i]); if (!open) continue; const ticks = open[2]; @@ -870,14 +910,20 @@ export function scanFences(source) { const info = open[3].trim(); const language = (info.split(/\s+/)[0] || '').toLowerCase(); if (TS_FENCE_LANGUAGES.has(language)) { - // The marker must be the nearest non-blank line above the fence. + // The marker must be the nearest non-blank line above the fence, and must + // be written at the fence's own quote depth. Blankness is judged at that + // depth, so the walk crosses a callout's bare `>` spacer; the depths must + // match, because a depth-0 marker above a quoted fence sits outside the + // callout the block lives in, and a quoted marker above an unquoted fence + // sits inside one the block is not in. Neither declares that block. let k = i - 1; - while (k >= 0 && lines[k].trim() === '') k--; - const above = k >= 0 ? markers.find((m) => m.line === k + 1) : undefined; + while (k >= 0 && isBlankAtDepth(lines[k], depth)) k--; + const above = k >= 0 ? markers.find((m) => m.line === k + 1 && m.depth === depth) : undefined; if (above) above.consumed = true; blocks.push({ fenceLine: i + 1, language, + quoteDepth: depth, body: lines .slice(i + 1, close) .map((line) => stripQuotePrefix(line, depth)) @@ -1809,6 +1855,21 @@ function main() { ); } + // A failing block inside a blockquote: name the depth its declaration must be + // written at. The escape hatch reaches it, but only at its own depth, and a + // marker written at depth 0 above it is silent about why it did not attach. + const quotedFailures = new Map(); + for (const { block } of [...run.parseFailures, ...run.semanticFailures, ...run.boundFailures]) { + if (block.quoteDepth > 0) quotedFailures.set(`${block.doc}:${block.fenceLine}`, block.quoteDepth); + } + for (const [site, depth] of quotedFailures) { + console.error( + ` [quoted] ${site} this block is fenced inside a blockquote (depth ${depth}). If it cannot ` + + 'compile, its fragment marker must be written at that SAME depth — one at depth 0 above the ' + + `callout does not declare it:\n ${'> '.repeat(depth)}${FRAGMENT_MARKER_EXAMPLES[0]}`, + ); + } + // ── the summary always states semantic COVERAGE, never just a verdict ───── const parseFailedBlocks = run.parseFailures.length; const coveredWithBlocks = new Set([