preview-render-parity
Skill302 starsChanged 2 months agoMITView on GitHub
Diagnose and fix DOM / style differences between `q2 preview` and `q2 render` so the two pipelines produce visually-equivalent output. Use when the user reports that the preview's appearance, spacing, classes, or DOM structure doesn't match the rendered site — phrases like "preview vs render differs", "preview shows X differently", "spacing/margin/padding off", "DOM doesn't match", "missing class in preview", or shows a specific computed-style mismatch (e.g. "17px in preview, 34px in render"). Also invoked explicitly via `/preview-parity`.
- Deletes or force-pushes
- Commits and pushes
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- autonomously-fix-small-issueSkill
- braidSkill
- ejs-listing-portSkill
- investigate-beadsSkill
- reader-expectations-proseSkill
- snyk-prSkill
- triageSkill
- upgrade-cargo-depsSkill
- upgrade-vendored-depsSkill
- weekly-summarySkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
13 rules. About 0 are common, 5 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 24
Do not: use for:
- Distinctive: line 46
Before opening Chrome, try to reproduce the divergence with the automated parity runner — it's faster to iterate on and gives you a …
- Very specific: line 48
Write a minimal fixture under crates/quarto/tests/smoke-all/q2-preview/ (or find an existing one that already shows the symptom).
- Very specific: line 49
Add dom-parity: true under its _quarto.tests.html block.
- Very specific: line 50
Run it in isolation: cd hub-client && SMOKE_FILTER=<fixture-name> npm run test:wasm -- --reporter=verbose.
- Very specific: line 51
Read the normalised output at hub-client/test-results/parity/<fixture-path-with-__>/{render,preview}.norm.txt — the diff between these two …
- Very specific: line 69
When computing styles in preview, use iframe.contentWindow.getComputedStyle(target), not the parent window's.
- Distinctive: line 115
If the symptom looks like more than one category at once, file separate braid sub-strands and tackle them one at a time (the bd-y1fs3 work …
- Distinctive: line 123
Read the matching arm. Pay attention to:
- Very specific: line 126
Helpers that prepend / mutate the attr: before emission (e.g. write_code_container_attr adds sourceCode when data-hl-spans is present).
- Distinctive: line 184
Run the failing test:
- Very specific: line 211
All 12 steps must pass. The verify rebuilds the WASM + the q2-preview SPA bundle, which is what q2 preview embeds in its binary.
- Very specific: line 263
iframe
getComputedStyle: Always iframe.contentWindow.getComputedStyle(target) — never the parent window's.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
preview-render-parity Skill
q2 preview's React renderer (ts-packages/preview-renderer/src/q2-preview/...) is supposed to produce the same DOM as q2 render's native HTML writer (crates/pampa/src/writers/html.rs) for the same input. Every divergence — wrong tag, classes on the wrong element, missing classes, attribute leakage, missing pipeline stage — costs the user visible style drift, because the Quarto theme CSS is the same in both places and assumes the native writer's DOM shape.
This skill turns a "preview looks slightly wrong" report into a closed braid strand with a regression test, a verified-in-browser fix, and a --no-ff merge into the integration branch.
When to use
User says any of:
- "preview shows X differently from render"
- "spacing / margin / padding / indentation / etc. is different between preview and render"
- "DOM doesn't match"
- "this class is in render but missing in preview" (or vice versa)
- "X is on
<pre>in render but on<code>in preview" (or similar tag/element placement complaint) - describes a specific
getComputedStylemismatch ("17px vs 34px") - shows a screenshot comparison of the two pipelines
- explicitly:
/preview-parityor/preview-render-parity
Do not use for:
- Preview-server bugs that aren't about rendered output (file watcher misses, samod sync failures, capture pipeline failures, port conflicts).
- Render-side bugs in
q2 render(when the user wants the render output changed, not preview brought into line). - Quarto theme CSS changes (changing the rules themselves, not making preview match them).
- Replay-engine / capture splice work (those have their own design space — see
claude-notes/plans/2026-05-18-q2-preview-project-replay-engine.md).
Preconditions
- A running
q2 previewURL (the user usually provides one, or you can start it:cargo run --bin q2 -- preview <path> --no-browser --port <p>aftercargo build --bin q2 --releasefor the latest WASM). - A running comparison
q2 renderURL (typicallypython -m http.serverover the fixture's_site/directory). - Chrome via the chrome-devtools MCP (
mcp__plugin_chrome-devtools-mcp_chrome-devtools__*).
The native HTML writer is the canonical contract. Whenever the spec is unclear, read crates/pampa/src/writers/html.rs for the Block::* or Inline::* arm in question — that's what q2 render actually does, and q2 preview must mirror it. Discrepancies between Pandoc's convention and pampa's are common; trust the pampa code, not your memory of Pandoc.
The standing fixture
~/Desktop/daily-log/2026/05/15/q2-preview-test-website/ is a 2-page website with one R code cell that the user keeps using for these comparisons. Use it as the default fixture unless the user names another. Render it via q2 render ., serve _site/ over python -m http.server, run q2 preview . against it, and compare in Chrome.
Diagnosis workflow
0. Reproduce with the harness first
Before opening Chrome, try to reproduce the divergence with the automated parity runner — it's faster to iterate on and gives you a normalised text diff instead of manual DOM inspection:
- Write a minimal fixture under
crates/quarto/tests/smoke-all/q2-preview/(or find an existing one that already shows the symptom). - Add
dom-parity: trueunder its_quarto.tests.htmlblock. - Run it in isolation:
cd hub-client && SMOKE_FILTER=<fixture-name> npm run test:wasm -- --reporter=verbose. - Read the normalised output at
hub-client/test-results/parity/<fixture-path-with-__>/{render,preview}.norm.txt— the diff between these two files usually points straight at the divergent tag/attribute/class.
Reach for Chrome (steps 1-6 below) only when the symptom is a computed-style mismatch (margins, colors, layout) that the harness's DOM-only comparison can't see, or when the harness can't reproduce it at all. See claude-notes/instructions/testing.md § "4. Preview ↔ render DOM parity" for the runner's full contract (normalisation rules, opt-in policy, known open divergences).
1. Locate the element on both pages
Open both URLs in Chrome via the MCP. Use mcp__plugin_chrome-devtools-mcp_chrome-devtools__select_page to switch between them.
For q2 render, the target is in document directly.
For q2 preview, the rendered document is inside an iframe:
const iframe = document.querySelector('iframe');
const doc = iframe.contentDocument;
// ...querySelector on `doc`When computing styles in preview, use iframe.contentWindow.getComputedStyle(target), not the parent window's.
2. Compare DOM shape
For the same logical element on both sides, capture: outerHTML.slice(0, 500), tagName, className, id, attribute list, and the parent chain (up to body). The parent chain often surfaces a structural difference one level up that explains the symptom.
3. Compare computed styles (when the symptom is visual)
const cs = getComputedStyle(target); // or iframe.contentWindow for preview
return {marginTop: cs.marginTop, marginBottom: cs.marginBottom, /* ... */};A value difference (17px vs 34px) is usually a selector difference — one side matches a more specific rule. Enumerate the matching rules:
// Scan all stylesheets for rules that match the target and have margin/padding.
const matches = [];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules || []) {
if (!rule.selectorText) continue;
try {
if (target.matches(rule.selectorText) && /margin|padding/.test(rule.cssText)) {
matches.push({selector: rule.selectorText, css: rule.cssText.slice(0, 200), href: sheet.href});
}
} catch {}
}
} catch {}
}The render side will typically have a more-specific selector that doesn't match in preview because some attribute or tag differs (next-sibling tag, an absent class, etc.).
4. Classify the divergence
The five categories observed so far:
| Category | Symptom | Where the fix lives |
| Wrong tag | <div> vs <section>, <pre> vs <code>, etc. | React component in q2-preview/blocks/ or q2-preview/inlines/ |
| Wrong attribute placement | classes on <code> instead of <pre> | Same — mirror write_*_attr in crates/pampa/src/writers/html.rs |
| Missing classes | sourceCode, cell-output, etc. absent in preview | React component (often a conditional add — if highlight present, prepend sourceCode) |
| Stage exclusion | Some pipeline-emitted attribute (data-hl-spans, data-loc) absent from the AST entirely | Q2_PREVIEW_STAGE_EXCLUDED in crates/quarto-core/src/pipeline.rs |
| Attribute leakage | A data-* attr the writer consumes leaks to the DOM in preview | React component — filter the consumed key (e.g. data-hl-spans) |
If the symptom looks like more than one category at once, file separate braid sub-strands and tackle them one at a time (the bd-y1fs3 work surfaced bd-coffj this way).
5. Find the canonical native behavior
grep -n "Block::<NodeType>\|fn write_<thing>\|<tag\|class=\"<class>" crates/pampa/src/writers/html.rsRead the matching arm. Pay attention to:
- Which element gets the
Attr(id + classes + kvs). Pampa's convention is: classes on the OUTER container (<pre>,<section>,<figure>), inner<code>/ etc. bare. This is the opposite of vanilla Pandoc and a recurring gotcha. - Helpers that prepend / mutate the attr before emission (e.g.
write_code_container_attraddssourceCodewhendata-hl-spansis present). - Tag elevation based on classes (e.g.
Divwith"section"class →<section>). - Attribute filtering (consumed keys like
data-hl-spans).
6. Find the React component handling the same AST node
Block-level:
ts-packages/preview-renderer/src/q2-preview/blocks/<NodeType>.tsxInline:
ts-packages/preview-renderer/src/q2-preview/inlines/<NodeType>.tsxPipeline stages: crates/quarto-core/src/stage/stages/<stage>.rs + the q2-preview pipeline at crates/quarto-core/src/pipeline.rs::build_q2_preview_pipeline_stages (and the exclusion list Q2_PREVIEW_STAGE_EXCLUDED).
TDD workflow
File a braid strand + topic branch
braid create "q2 preview: <one-line symptom>" \
-t bug -p 2 \
-l preview-parity \
-d "$(cat <<EOF
<symptom — what the user sees>
q2 render produces: <DOM/style>
q2 preview produces: <DOM/style>
Root cause: <where the divergence is>. Native writer: <file:line>.
Fix: mirror the writer — <one-line plan>.
EOF
)"
# braid prints the new strand id on stdout. Capture it as <id>.
braid update <id> --status in_progress
git switch -c braid/<id>-<short-slug> mainParity strands branch off main directly and carry the preview-parity label (no parent-child epic dependency — the former parent epic, bd-kw93, is closed and its branch merged via PR #214). (The git branch prefix is braid/ — a plain git namespace, renamed from the historical beads/ in bd-yjh1y117.)
Write the failing test FIRST
Prefer the parity harness's dom-parity: true opt-in as the regression test whenever the fixture from step 0 can be made minimal — it exercises the real render and preview pipelines end-to-end (not a hand-mounted AST fragment) and stays green forever once the fix lands, with no separate assertion to keep in sync. Add/reuse the fixture under crates/quarto/tests/smoke-all/q2-preview/, confirm it fails RED before the fix (the parity diff should name the divergent tag/attribute/class), then implement. Fall back to one of the three surfaces below only when the symptom can't be captured as a minimal fixture (e.g. it needs SPA-level state, or a pipeline-stage inclusion check with no DOM to compare).
Three test surfaces, pick the right one:
| Test surface | Use when |
ts-packages/preview-renderer/src/q2-preview/q2-preview.integration.test.tsx | DOM/component shape — most common. Mount a small AST fixture via mount(blocks), assert on the rendered DOM. |
crates/quarto-core/src/pipeline.rs (tests module) | Pipeline stage inclusion / exclusion. Pattern: q2_preview_pipeline_includes_<stage_name> asserts the name appears in build_q2_preview_pipeline_stages(None, None).iter().map(s => s.name()). |
q2-preview-spa/src/PreviewApp.integration.test.tsx | SPA-app-level behavior (boot, document.title, capture wiring) — when the symptom is about the outer SPA, not a single AST node. |
For the SPA-app integration tests, the existing render-error tests at lines 415/460 of PreviewApp.integration.test.tsx override renderPageForPreview with mockImplementation. vi.clearAllMocks() only clears call history, not implementations, so later tests inherit the stale stub. Always restore the default closure-over-state mock at the top of your test (see the bd-iuzmk pattern with resetRenderPageForPreviewMockToDefault).
Run the failing test:
# preview-renderer (component / DOM tests)
(cd ts-packages/preview-renderer && npm run test:integration)
# q2-preview-spa (SPA-app tests)
(cd q2-preview-spa && npm run test:integration)
# quarto-core (pipeline / Rust tests)
cargo nextest run -p quarto-core --lib <test-name>Confirm RED for the right reason — the assertion message should name the missing tag / class / attribute, not a generic JSON-shape error.
Implement the fix
Mirror the native writer's behavior in the React component (or update the exclusion list / add a stage). Keep the diff minimal and the comment dense — link bd-id and the native-writer file:line so future maintainers see the contract.
Verify GREEN
Same commands. Then full suite:
cargo xtask verifyAll 12 steps must pass. The verify rebuilds the WASM + the q2-preview SPA bundle, which is what q2 preview embeds in its binary.
E2E browser verification
Tests passing alone is NECESSARY BUT NOT SUFFICIENT — AGENTS.md mandates real-binary check for CLI / UI features. After verify:
cargo build --bin q2 --release # picks up the freshly-built SPA
pkill -f "q2 preview" 2>/dev/null; sleep 1
cd <fixture> && rm -rf .quarto
/Users/cscheid/repos/github/quarto-dev/q2/target/release/q2 preview . --port <p> --no-browser &Then load http://127.0.0.1:<p>/?page=<file> in Chrome via the MCP and run the SAME assertion against the live DOM (computed style, querySelector match, outerHTML substring). Record the snippet in the commit body.
Ship
# Close the strand (braid syncs the skein automatically — nothing to commit)
braid close <id> --reason "Fixed: <one-line>"
# Commit (component fix + test in one commit when they're tightly coupled)
git add <component> <test>
git commit -m "...(bd-<id>)"
# Push (only after explicit user OK, per AGENTS.md GIT PUSH POLICY) and open a PR
# against main — no integration branch to merge into (see worktrees.md § Pushing for PR)
git push -u origin braid/<id>-<slug>:feature/<id>-<slug>
# Later pushes need the same refspec: local and remote names differ, so a
# bare `git push` fails. Confirm with `git ls-remote origin feature/<id>-<slug>`.Commit-message style (per repo convention — read git log -5 --pretty=format:"%h %s%n%b%n---" on main if in doubt):
- Title with bd-id:
<imperative one-line summary> (bd-<id>). - Body: user-visible symptom → root cause (cite native-writer file:line) → fix → verification recipe (include test counts + the
cargo xtask verify N/N greenline + a DOM-snippet from the live browser). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>footer.
Landmines
These caught me on previous fixes; check before assuming "it's broken":
- Pampa puts classes on the OUTER container. For
CodeBlock, classes go on<pre>,<code>is bare. This is the opposite of vanilla Pandoc. SeeBlock::CodeBlockincrates/pampa/src/writers/html.rsandwrite_code_container_attr. bd-y1fs3. sourceCodeclass is conditional. The native writer prepends it to a code container's class list only whendata-hl-spansis present (i.e. the code-highlight stage actually annotated something). Idempotent: don't add if the author already has it. Seewrite_code_container_attrlines 487-495.data-hl-spansis consumed, not forwarded. The ReactCodeBlockreads it to emit<span class="hl-...">markup and must not leak the raw attr to the DOM. Otherdata-*keys (e.g.data-loc) still pass through. bd-nxslt.- Tag elevation for Divs.
Div.attr.classes.contains("section")→ emit<section>, not<div>(sectionize transform output). Quarto theme CSS keys off<section>. The constantSECTION = 'section'lives ints-packages/preview-renderer/src/q2-preview/quartoClasses.ts. bd-coffj. Q2_PREVIEW_STAGE_EXCLUDEDonly drops HTML-emission stages. AST-level stages (they write annotations onto existing nodes, not raw HTML) belong in q2-preview.CodeHighlightStagewas wrongly excluded for the same wrong reason. Before adding to the exclusion list, confirm the stage actually produces an HTML string. bd-nxslt.- iframe
getComputedStyle. Alwaysiframe.contentWindow.getComputedStyle(target)— never the parent window's. Wrong window returns the parent's<body>styles applied to whatevertargethappens to inherit. Vitest
clearAllMocksdoes NOT clearmockImplementation. It clears call history only. Earlier tests inPreviewApp.integration.test.tsxoverriderenderPageForPreview; your new tests inherit the stale impl. Top-of-test reset:async function resetRenderPageForPreviewMockToDefault() { const runtime = await import('@quarto/preview-runtime'); (runtime.renderPageForPreview as ReturnType<typeof vi.fn>).mockImplementation( async () => runtimeMockState.renderResult, ); }bd-iuzmk.
- Pampa MetaValue shape:
{t: 'MetaString' | 'MetaInlines' | 'MetaBlocks' | 'MetaMap' | 'MetaList' | 'MetaBool', c: ...}. Use@quarto/preview-renderer/framework'sextractMetaString/extractMetaStringListrather than reading rawc. - WASM safety. Stages live in
quarto-coreand run on both native andwasm32-unknown-unknown. Anything WASM-incompatible must becfg(not(target_arch = "wasm32"))-gated.quarto-highlight's user-grammar machinery is native-only; built-in grammars are WASM-safe.
Sub-strands to file when discovered
If diagnosis surfaces a second divergence in the same area, file a sibling braid strand rather than expanding the current fix's scope. The repo's convention is one small focused PR per parity fix. bd-y1fs3 surfaced bd-coffj this way; both shipped as separate --no-ff merges. The skill optimizes for cycle time per fix, not for batched mega-PRs.
Cross-references
- Former parent epic (closed, merged via PR #214):
bd-kw93— q2 preview. Parity strands no longer depend on it; they branch offmainand carry thepreview-paritylabel. - Capture-splice design (separate scope):
claude-notes/plans/2026-05-18-q2-preview-project-replay-engine.md. - Phase F (chrome-rendering parity):
claude-notes/plans/2026-05-14-q2-preview-phase-f.md. - Native HTML writer:
crates/pampa/src/writers/html.rs. Read this. Often. - Q2-preview pipeline build:
crates/quarto-core/src/pipeline.rs::build_q2_preview_pipeline_stages. - Q2-preview React block components:
ts-packages/preview-renderer/src/q2-preview/blocks/. - Q2-preview React inline components:
ts-packages/preview-renderer/src/q2-preview/inlines/. - Class-name constants shared between Rust + React:
ts-packages/preview-renderer/src/q2-preview/quartoClasses.ts.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

