From d191740da2f1e92b86d72a747d9e2cda6591ee6b Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 12 May 2026 13:39:50 -0500 Subject: [PATCH 1/5] sync beads: bd-cpzp (qmd writer trailing newline, issue #180) --- .beads/issues.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 362cb0eb..c83eaadb 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -128,6 +128,7 @@ {"id":"bd-bwwv","title":"Navbar sub-row (book-style secondary navbar)","description":"Epic-excluded for MVP but worth tracking. Q1 renders a second row on book sites for chapter/part navigation. Requires ProjectType-specific extension to WebsiteProjectType (or its book successor). See 2026-04-24-websites-phase-3.md §Follow-up beads.","status":"open","priority":4,"issue_type":"feature","created_at":"2026-04-24T19:43:00.017912Z","created_by":"cscheid","updated_at":"2026-04-24T19:43:00.017912Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-bwwv","depends_on_id":"bd-fqyg","type":"discovered-from","created_at":"2026-04-24T19:43:00.017912Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-c083","title":"Cargo: upgrade tree-sitter v0.25.10 → v0.26.8","description":"Major upgrade surfaced by cargo-upgrade survey 2026-05-04. Current 0.25.10 is range-pinned in workspace; latest is 0.26.8. Type: pre-1.0 minor (semver-breaking). Review changelog and bump deliberately. See claude-notes/plans/2026-05-04-cargo-upgrade-survey.md and bd-hb8h.","status":"closed","priority":3,"issue_type":"chore","created_at":"2026-05-04T18:15:55.321199Z","created_by":"cscheid","updated_at":"2026-05-04T20:30:44.759296Z","closed_at":"2026-05-04T20:30:44.759137Z","close_reason":"merged: 61b01cd4","source_repo":".","compaction_level":0,"original_size":0,"labels":["cargo","deps"],"dependencies":[{"issue_id":"bd-c083","depends_on_id":"bd-hb8h","type":"discovered-from","created_at":"2026-05-04T18:16:05.692502Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-c3jh","title":"Phase 9 follow-up: GC stale VFS artifacts at session end","description":"When the hub-client preview re-renders, the WASM Pass-2 produces new artifact paths (theme-css fingerprints change when content changes) but the *old* artifacts under /.quarto/project-artifacts/... linger in VFS storage. The new HTML never references them — so they don't poison the page — but they do leak.\n\nGC pass at session-end (or periodically): walk /.quarto/project-artifacts/, drop any entry whose path doesn't appear in a 'live' set (the union of artifact paths from the most-recent project render).\n\nPhase 9 plan §Risks: 'Add a follow-up to GC /.quarto/project-artifacts/... entries with no live references at session end. Not a Phase-9 blocker.'","status":"open","priority":4,"issue_type":"task","created_at":"2026-04-29T00:32:31.194561Z","created_by":"cscheid","updated_at":"2026-04-29T00:32:31.194561Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-c3jh","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-29T00:32:31.194561Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-c3jh","depends_on_id":"bd-ayj6","type":"discovered-from","created_at":"2026-04-29T00:32:31.194561Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-cpzp","title":"qmd writer: implicit-figure path drops trailing newline, collapses next block (issue #180)","description":"Triage doc: claude-notes/issue-reports/180/triage.md on branch issue-180.\n\nRoot cause: write_figure (crates/pampa/src/writers/qmd.rs:759) implicit-figure branch returns directly from write_image without emitting the trailing \\n that every block writer is expected to produce. When such a Figure is followed by another block (top-level or inside a Div), the inter-block separator collapses to a single \\n instead of a blank line, and the re-parser glues the two blocks into one Para. Affects every layout/subfigure div in the docs corpus.\n\nReports covered (both same root cause):\n- Bug A: top-level Figure followed by Para -> one Para after round-trip\n- Bug B: Div with multiple Figure children + caption -> one Para inside the Div after round-trip\n\nFix (TDD-first per crates/pampa/CLAUDE.md):\n1. Add failing roundtrip tests under tests/roundtrip_tests/qmd-json-qmd/\n - figure_implicit_then_para.qmd\n - layout_div_subfigures.qmd\n - figure_implicit_then_figure.qmd (extra coverage)\n2. Verify they fail.\n3. Fix write_figure: replace 'return write_image(...)' with 'write_image(...)?; writeln!(buf)?; Ok(())'.\n4. Verify tests pass.\n5. cargo nextest run --workspace to catch downstream snapshot updates.\n\nNot a duplicate of bd-emr4 — that is about non-implicit Figure shapes hitting the fallback fenced-div path. This bug is in the implicit-figure code path; different code, different fix.","status":"open","priority":2,"issue_type":"bug","created_at":"2026-05-12T18:39:12.847875Z","created_by":"cscheid","updated_at":"2026-05-12T18:39:15.853892Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-cpzp","depends_on_id":"bd-emr4","type":"related","created_at":"2026-05-12T18:39:15.853594Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-creo","title":"quarto render: fail strictly on Pass-1 failures (CI-friendly contract)","description":"Sibling of bd-rqba. Once Pass-1 failures are wired through as a dedicated pass1_failures field on the render summary surfaces (D1 in plan), give 'quarto render' a strict policy: any pass1_failures entry causes a non-zero exit. Remove the current string-matching of 'profile-pass skipped …' warning text in favor of the structured field.\n\nRationale: 'quarto render' is often used in headless CI; partial-progress leniency belongs to 'quarto preview' / hub-client, not render. The engine stays policy-free; consumers choose strict (render) vs lenient (preview).\n\nAlso: document the strict-vs-lenient contract in claude-notes/designs/document-profile-contract.md so future consumers (e.g., the planned hub-client-based 'quarto preview' binary) inherit it.\n\nPlan: claude-notes/plans/2026-05-01-hub-client-website-render-ux.md (Decision D1).","status":"open","priority":1,"issue_type":"task","created_at":"2026-05-01T14:16:43.115104Z","created_by":"cscheid","updated_at":"2026-05-01T14:16:43.115104Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-creo","depends_on_id":"bd-lk66","type":"parent-child","created_at":"2026-05-01T14:16:43.115104Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-creo","depends_on_id":"bd-rqba","type":"related","created_at":"2026-05-01T14:16:43.115104Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-d8go","title":"L9 follow-up: date_format doctemplate pipe","description":"Originally specified as part of L9's L4 enhancements; deferred at impl-start because L9's RFC 822 pubDate is computed server-side in feed/binding.rs (no template-level need). Adding the pipe later requires a tree-sitter grammar change in crates/tree-sitter-doctemplate/grammar/grammar.js (the pipe set is grammar-fixed; the existing date_format must be a custom rule like pipe_left/pipe_center/pipe_right because it takes an argument), plus a match arm in crates/quarto-doctemplate/src/pipes.rs. Useful for L8's existing custom-template authors who want to format dates in the host page.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-05-08T17:33:55.509676Z","created_by":"cscheid","updated_at":"2026-05-08T17:33:55.509676Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-d8go","depends_on_id":"bd-o90m","type":"discovered-from","created_at":"2026-05-08T17:33:55.509676Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-dhtw","title":"Phase 1: gh-pages provider end-to-end","description":"Phase 1 of bd-t3ny. Implement the gh-pages provider end-to-end on top of the Phase 0 scaffolding: common::git wrappers, common::github context discovery, _publish.yml reader, GhPagesProvider (prepare/commit/verify with .nojekyll poll), --dry-run cleanup with no residue, mkdocs-style summary, and an end-to-end test against a bare local remote (dry-run + real-run + json-run).\n\nPlan: claude-notes/plans/2026-05-03-publish-command-and-gh-pages.md (Phase 1 section)\n\nBlocked on Phase 0 (bd-068k).","status":"completed","priority":1,"issue_type":"task","created_at":"2026-05-03T14:37:39.274734Z","created_by":"cscheid","updated_at":"2026-05-03T15:18:38.648426Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-dhtw","depends_on_id":"bd-068k","type":"blocks","created_at":"2026-05-03T14:37:39.274734Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-dhtw","depends_on_id":"bd-t3ny","type":"parent-child","created_at":"2026-05-03T14:37:39.274734Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} From 4ccf5163a0e41fb02d4eff4b34743d34aeca9909 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 12 May 2026 14:32:34 -0500 Subject: [PATCH 2/5] sync beads: bd-qpa2 (labeled display math edge cases, issue #181 follow-up) --- .beads/issues.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index c83eaadb..bf91c43f 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -238,6 +238,7 @@ {"id":"bd-qb4o","title":"L11 — Listings epic close-out","description":"Compile per-phase follow-up bd log into single epic report. Confirm cargo xtask verify on a fresh checkout. Update document-profile-contract.md change log. Confirm hub-client renders listings end-to-end via WASM (real browser smoke test): multi-page project with listings, edit a content page, see listing host preview update with L1 fallbacks. See claude-notes/plans/2026-05-05-listings-epic.md §L11.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","updated_at":"2026-05-05T19:54:06.937522Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-qb4o","depends_on_id":"bd-5vsr","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-61cd","type":"parent-child","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-hzsi","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-o90m","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-qf7r","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-xbnf","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-qf7r","title":"L7 — Post-render placeholder upgrade (engine-rendered previews; BRACKETED)","description":"BRACKETED FEATURE — read epic §L7 'Bracketing rules' before extending. Upgrades L1-fallback description+image to engine-rendered firstPara+previewImage by reading sibling output files in WebsiteProjectType::post_render. Single module home, mandatory file-header discipline, CLI-only by construction (hub-client and quarto preview show L1 fallbacks), no cross-feature reuse, mandatory L1-fallback contract. See claude-notes/plans/2026-05-05-listings-epic.md §L7.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-05-05T19:53:43.884886Z","created_by":"cscheid","updated_at":"2026-05-07T21:11:33.486906Z","closed_at":"2026-05-07T21:11:33.486767Z","close_reason":"L7 (bd-qf7r) implemented + merged: post-render placeholder upgrade. impl d4877142, merge dc3a0f7b. CLI render now substitutes engine-rendered first paragraphs and preview images into listing entries via the bracketed post_render step. Hub-client preview retains L1 fallbacks per the bracketing rules. +50 tests over 8647 baseline → 8697 total. Follow-ups: bd-rvpd (span threading), bd-bpdz (L9 reader extension), bd-399t (docs callout), bd-fx23 (defensive id encoding).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-qf7r","depends_on_id":"bd-61cd","type":"parent-child","created_at":"2026-05-05T19:53:43.884886Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qf7r","depends_on_id":"bd-ml8z","type":"blocks","created_at":"2026-05-05T19:53:43.884886Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-qhkp","title":"New projects not added to Automerge project set (stale closure in App.tsx)","description":"handleProjectCreated and share link handler in App.tsx capture stale projectSetState/projectSetActions due to missing useCallback/useEffect dependencies. The status check always sees 'loading' instead of 'connected', so addProject is never called on the synced set. Plan: claude-notes/plans/2026-04-06-fix-project-set-stale-closure.md","status":"open","priority":1,"issue_type":"bug","created_at":"2026-04-06T20:27:45.091230Z","created_by":"cscheid","updated_at":"2026-04-06T20:27:45.091230Z","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-qpa2","title":"Labeled display math ($$ ... $$ {#label}) writer emits bracketed Span, breaks round trip (issue #181 follow-up)","description":"Follow-up to bd-q6ed / issue #181. After the column-strip fix landed for unlabeled display math in blockquotes, @rundel reported two related regressions involving **labeled** display math (`$$ ... $$ {#eq-p}`):\n\n1. **Labeled math inside a blockquote** — `> ` prefix leaks into Math.text on the second round trip.\n2. **Labeled math at top level** — interior whitespace lines lose one column of leading space on each round trip.\n\nBoth share the same root cause, which is in the **qmd writer**, not the parser:\n\nThe reader correctly post-processes `$$...$$ {#label}` into a `Span ( id , [\"quarto-math-with-attribute\", ...] , [] ) [Math DisplayMath ...]` (see `crates/pampa/src/pandoc/treesitter_utils/postprocess.rs:1232-1278`).\n\nThe writer's `write_span` (`crates/pampa/src/writers/qmd.rs:1586+`) does not recognise this marker class and renders the Span with the generic bracket syntax `[$$...$$]{#label .quarto-math-with-attribute}`. The leading `[` shifts the column of `$$` by 1, so on re-parse the column-strip from bd-q6ed mis-fires:\n- top-level: `start_col` becomes 1, strips one space from interior lines.\n- blockquote: `start_col` becomes 3, can't strip the `> ` prefix safely (correctly refuses to chew the math content), and the prefix bytes leak into Math.text.\n\n**Fix shape:** `write_span` should detect the marker pattern (Span whose class list contains `quarto-math-with-attribute` and whose content is a single `Inline::Math(DisplayMath)`) and emit:\n\n```\n$$\n\n$$ {}\n```\n\ninstead of the bracketed Span form. The marker class itself is a reader-side bookkeeping artefact and should be dropped from the emitted attribute set.\n\n**Regression coverage:** add two fixtures to `crates/pampa/tests/roundtrip_tests/qmd-json-qmd/`:\n- `labeled_display_math_in_blockquote.qmd`\n- `labeled_display_math_with_indented_lines.qmd`\n\nReference inputs preserved in worktree at `claude-notes/issue-reports/181/exp-labeled-math-in-bq.qmd` and `exp-labeled-math-toplevel.qmd`. Full diagnosis in `claude-notes/issue-reports/181/triage.md` (Follow-up section).\n\nWorktree branch: `issue-181`. Upstream: https://github.com/quarto-dev/q2/issues/181 (rundel comment 2026-05-12).","status":"open","priority":1,"issue_type":"bug","created_at":"2026-05-12T19:32:00.197200Z","created_by":"cscheid","updated_at":"2026-05-12T19:32:00.197200Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-qpa2","depends_on_id":"bd-q6ed","type":"related","created_at":"2026-05-12T19:32:00.197200Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-r7v2","title":"Browser smoke: confirm MathJax/KaTeX typeset in real browser","description":"Phase 4.3 of bd-w5ov could not be completed in-session because chrome-devtools-mcp disconnected after a stale browser process was killed. Implementation is complete and Phase 4.2 (CLI exercise) verified the rendered HTML markup is correct. What remains: load the rendered fixtures in a real Chromium session and confirm the math is actually typeset, not just present as raw \\(x^2\\) text.\n\nAcceptance:\n- Render /tmp/q2-math-cli/inline_math.html, display_math.html, labelled_eq.html via http.server.\n- chrome-devtools-mcp: navigate, evaluate document.querySelectorAll('mjx-container').length > 0 (MathJax fixtures).\n- Render katex.html, evaluate document.querySelectorAll('.katex, .katex-display').length > 0.\n- Confirm zero console errors from the math runtime (404s on jsDelivr would fail this).\n- Record observations in claude-notes/plans/2026-05-04-math-mode.md.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-05-04T23:58:12.775314Z","created_by":"cscheid","updated_at":"2026-05-05T00:04:57.347714Z","closed_at":"2026-05-05T00:04:57.347571Z","close_reason":"Browser smoke completed in same session as bd-w5ov implementation: 5 fixtures verified live in Chromium via chrome-devtools-mcp. MathJax 3.2.2 typesets inline + display + labelled equations from jsDelivr; KaTeX typesets via auto-render; math-free pages stay clean; user URL override honored. Recorded in plan §Browser smoke log.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-r7v2","depends_on_id":"bd-w5ov","type":"discovered-from","created_at":"2026-05-04T23:58:12.775314Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-r82e","title":"DocumentProfile: add includes: Vec for incremental-rebuild invalidation","description":"Surfaced during the main -> feature/websites merge (bd-xfwx) that threaded IncludeExpansionStage before the DocumentProfile checkpoint.\n\nProblem: after the merge, DocumentProfile.outline (and any other AST-derived fields) can be populated from headings / code / crossref targets spliced in via {{< include child.qmd >}}. The profile therefore depends on the contents of every included file, but the profile struct itself records no trail back to those files.\n\nFor incremental rebuilds (Phase 8 of the websites epic, bd-*) and for the future 'freeze' feature, the cache-key computation needs to invalidate a parent document's cached profile when ANY of its (transitive) includes change. Without tracking the include set on the profile, that is impossible.\n\nProposal: add a field roughly of the form\n\n includes: Vec // or Vec\n\nto DocumentProfile. Populated by IncludeExpansionStage (or by DocumentProfileStage reading a side-channel set that IncludeExpansionStage populated on the DocumentAst). Bump profile_version on the serialized shape.\n\nScope:\n- Decide on the shape (bare PathBuf list vs. (path, content-hash) pairs). For Phase 8 we'll likely want the hash too so nav-state invalidation can compare without re-reading the files.\n- Populate from IncludeExpansionStage.\n- Extend contract doc (claude-notes/designs/document-profile-contract.md).\n- Tests: profile records every included file (direct + transitive); round-trip serialization.\n\nNon-blocking for the website-epic MVP; becomes a hard prerequisite for Phase 8.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-24T20:05:53.778909Z","created_by":"cscheid","updated_at":"2026-04-28T00:43:08.078191Z","closed_at":"2026-04-28T00:43:08.077941Z","close_reason":"Closed as part of Phase 8.0 — DocumentProfile.includes added (v2). Verified by editing_include_invalidates_parent_profile (incremental_rebuild.rs).","source_repo":".","compaction_level":0,"original_size":0,"labels":["websites"],"dependencies":[{"issue_id":"bd-r82e","depends_on_id":"bd-0tr6","type":"related","created_at":"2026-04-24T20:05:53.778909Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-r82e","depends_on_id":"bd-fegm","type":"blocks","created_at":"2026-04-27T22:21:01.025552Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-r82e","depends_on_id":"bd-xfwx","type":"related","created_at":"2026-04-24T20:05:53.778909Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-r9hs","title":"Cargo: upgrade ureq v2.12.1 → v3.3.0","description":"Major upgrade surfaced by cargo-upgrade survey 2026-05-04. Current 2.12.1 is range-pinned in workspace; latest is 3.3.0. Type: major. Review changelog and bump deliberately. See claude-notes/plans/2026-05-04-cargo-upgrade-survey.md and bd-hb8h.","status":"closed","priority":3,"issue_type":"chore","created_at":"2026-05-04T18:15:55.424872Z","created_by":"cscheid","updated_at":"2026-05-04T20:30:45.584646Z","closed_at":"2026-05-04T20:30:45.584505Z","close_reason":"merged: b0ee0f18","source_repo":".","compaction_level":0,"original_size":0,"labels":["cargo","deps"],"dependencies":[{"issue_id":"bd-r9hs","depends_on_id":"bd-hb8h","type":"discovered-from","created_at":"2026-05-04T18:16:05.861885Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} From 43ddd6aa19ce1cc3099bce290e18992af7812184 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 12 May 2026 14:56:24 -0500 Subject: [PATCH 3/5] sync beads: bd-qpa2 description updated with unified reader-side diagnosis --- .beads/issues.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index bf91c43f..0ca695f6 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -238,7 +238,7 @@ {"id":"bd-qb4o","title":"L11 — Listings epic close-out","description":"Compile per-phase follow-up bd log into single epic report. Confirm cargo xtask verify on a fresh checkout. Update document-profile-contract.md change log. Confirm hub-client renders listings end-to-end via WASM (real browser smoke test): multi-page project with listings, edit a content page, see listing host preview update with L1 fallbacks. See claude-notes/plans/2026-05-05-listings-epic.md §L11.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","updated_at":"2026-05-05T19:54:06.937522Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-qb4o","depends_on_id":"bd-5vsr","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-61cd","type":"parent-child","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-hzsi","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-o90m","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-qf7r","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qb4o","depends_on_id":"bd-xbnf","type":"blocks","created_at":"2026-05-05T19:54:06.937522Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-qf7r","title":"L7 — Post-render placeholder upgrade (engine-rendered previews; BRACKETED)","description":"BRACKETED FEATURE — read epic §L7 'Bracketing rules' before extending. Upgrades L1-fallback description+image to engine-rendered firstPara+previewImage by reading sibling output files in WebsiteProjectType::post_render. Single module home, mandatory file-header discipline, CLI-only by construction (hub-client and quarto preview show L1 fallbacks), no cross-feature reuse, mandatory L1-fallback contract. See claude-notes/plans/2026-05-05-listings-epic.md §L7.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-05-05T19:53:43.884886Z","created_by":"cscheid","updated_at":"2026-05-07T21:11:33.486906Z","closed_at":"2026-05-07T21:11:33.486767Z","close_reason":"L7 (bd-qf7r) implemented + merged: post-render placeholder upgrade. impl d4877142, merge dc3a0f7b. CLI render now substitutes engine-rendered first paragraphs and preview images into listing entries via the bracketed post_render step. Hub-client preview retains L1 fallbacks per the bracketing rules. +50 tests over 8647 baseline → 8697 total. Follow-ups: bd-rvpd (span threading), bd-bpdz (L9 reader extension), bd-399t (docs callout), bd-fx23 (defensive id encoding).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-qf7r","depends_on_id":"bd-61cd","type":"parent-child","created_at":"2026-05-05T19:53:43.884886Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-qf7r","depends_on_id":"bd-ml8z","type":"blocks","created_at":"2026-05-05T19:53:43.884886Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-qhkp","title":"New projects not added to Automerge project set (stale closure in App.tsx)","description":"handleProjectCreated and share link handler in App.tsx capture stale projectSetState/projectSetActions due to missing useCallback/useEffect dependencies. The status check always sees 'loading' instead of 'connected', so addProject is never called on the synced set. Plan: claude-notes/plans/2026-04-06-fix-project-set-stale-closure.md","status":"open","priority":1,"issue_type":"bug","created_at":"2026-04-06T20:27:45.091230Z","created_by":"cscheid","updated_at":"2026-04-06T20:27:45.091230Z","source_repo":".","compaction_level":0,"original_size":0} -{"id":"bd-qpa2","title":"Labeled display math ($$ ... $$ {#label}) writer emits bracketed Span, breaks round trip (issue #181 follow-up)","description":"Follow-up to bd-q6ed / issue #181. After the column-strip fix landed for unlabeled display math in blockquotes, @rundel reported two related regressions involving **labeled** display math (`$$ ... $$ {#eq-p}`):\n\n1. **Labeled math inside a blockquote** — `> ` prefix leaks into Math.text on the second round trip.\n2. **Labeled math at top level** — interior whitespace lines lose one column of leading space on each round trip.\n\nBoth share the same root cause, which is in the **qmd writer**, not the parser:\n\nThe reader correctly post-processes `$$...$$ {#label}` into a `Span ( id , [\"quarto-math-with-attribute\", ...] , [] ) [Math DisplayMath ...]` (see `crates/pampa/src/pandoc/treesitter_utils/postprocess.rs:1232-1278`).\n\nThe writer's `write_span` (`crates/pampa/src/writers/qmd.rs:1586+`) does not recognise this marker class and renders the Span with the generic bracket syntax `[$$...$$]{#label .quarto-math-with-attribute}`. The leading `[` shifts the column of `$$` by 1, so on re-parse the column-strip from bd-q6ed mis-fires:\n- top-level: `start_col` becomes 1, strips one space from interior lines.\n- blockquote: `start_col` becomes 3, can't strip the `> ` prefix safely (correctly refuses to chew the math content), and the prefix bytes leak into Math.text.\n\n**Fix shape:** `write_span` should detect the marker pattern (Span whose class list contains `quarto-math-with-attribute` and whose content is a single `Inline::Math(DisplayMath)`) and emit:\n\n```\n$$\n\n$$ {}\n```\n\ninstead of the bracketed Span form. The marker class itself is a reader-side bookkeeping artefact and should be dropped from the emitted attribute set.\n\n**Regression coverage:** add two fixtures to `crates/pampa/tests/roundtrip_tests/qmd-json-qmd/`:\n- `labeled_display_math_in_blockquote.qmd`\n- `labeled_display_math_with_indented_lines.qmd`\n\nReference inputs preserved in worktree at `claude-notes/issue-reports/181/exp-labeled-math-in-bq.qmd` and `exp-labeled-math-toplevel.qmd`. Full diagnosis in `claude-notes/issue-reports/181/triage.md` (Follow-up section).\n\nWorktree branch: `issue-181`. Upstream: https://github.com/quarto-dev/q2/issues/181 (rundel comment 2026-05-12).","status":"open","priority":1,"issue_type":"bug","created_at":"2026-05-12T19:32:00.197200Z","created_by":"cscheid","updated_at":"2026-05-12T19:32:00.197200Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-qpa2","depends_on_id":"bd-q6ed","type":"related","created_at":"2026-05-12T19:32:00.197200Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-qpa2","title":"Display math column-strip uses wrong column source, mishandles inline-wrapped and labeled math (issue #181 follow-up)","description":"Follow-up to bd-q6ed / issue #181, with revised diagnosis after a second-pass investigation (see `claude-notes/issue-reports/181/triage.md` § \"Second-pass investigation\").\n\n## Symptom\n\nTwo reporter-supplied edge cases (rundel, 2026-05-12):\n\n1. **Labeled display math inside a blockquote** — `> $$\\n> p(x)\\n> $$ {#eq-p}` round-trips with a literal `> ` left in `Math.text` after the second pass.\n2. **Labeled display math at top level** — `$$\\na\\n b\\n$$ {#eq-x}` loses one column of leading whitespace from interior lines on each round trip.\n\nSubsequent probing revealed both are instances of a broader brittleness in the column-strip introduced by bd-q6ed:\n\n3. `_$$\\na\\n b\\n$$_` (emph wrapping multi-line displaymath) loses one space on **first** parse — no round trip needed.\n4. `> _$$\\n> a\\n> b\\n> $$_` (same inside a blockquote) leaks `> ` AND loses interior whitespace on first parse.\n\n## Root cause\n\n`strip_continuation_prefix` in `crates/pampa/src/pandoc/treesitter.rs` uses `node.start_position().column` (the column of `$$`) as the strip width. That column equals the cumulative block-continuation prefix width **only when `$$` is the leftmost non-prefix character on its line**. Anything that precedes `$$` on the same line (`_`, `**`, `[`, the writer's own `[` for `quarto-math-with-attribute` Spans, etc.) shifts the column while the body bytes' source layout does not change. The strip then either eats real content (when the prefix is all whitespace/`>`) or refuses to strip real prefix (when the column overshoots into real content).\n\n## Constraint\n\n`DisplayMath` must remain an inline AST node — there are large existing corpora with display math nested inside paragraphs, and the grammar restructure proposed in the original triage would break those documents.\n\n## Fix shape (reader-side, no grammar change, no AST shape change)\n\nChange the strip-width source from `math_node.start_position().column` to the start column of the **enclosing block-leaf ancestor** (`pandoc_paragraph` / `pandoc_plain`). The cumulative block-continuation prefix width equals the paragraph's start column, which is constant across all interior lines of the paragraph regardless of what precedes `$$` on the opening line.\n\nPandoc's markdown reader uses this exact strategy (verified empirically). The fix matches Pandoc's behaviour on all probed cases — including lazy-continuation, nested blockquotes, list-item indent, and inline-wrapped multi-line math. The existing conservative `bytes ∈ {>, space, tab}` guard in the helper continues to handle lazy continuation correctly.\n\nThis single-input change subsumes:\n- bd-q6ed canonical case (paragraph col = math col, identical result)\n- bd-qpa2 edges A and B (paragraph col reflects true bq/top-level width)\n- Inline-wrapped multi-line math at any block context\n\n## Regression coverage to add\n\nFixtures under `crates/pampa/tests/roundtrip_tests/qmd-json-qmd/`:\n\n- `labeled_display_math_in_blockquote.qmd` (bd-qpa2 edge A)\n- `labeled_display_math_top_level_indented.qmd` (bd-qpa2 edge B)\n- `emph_around_multiline_display_math.qmd`\n- `emph_around_multiline_display_math_in_blockquote.qmd`\n\nReference inputs preserved at `claude-notes/issue-reports/181/exp-labeled-math-in-bq.qmd` and `exp-labeled-math-toplevel.qmd`.\n\n## Worktree / branch\n\nImplementation on its own branch with plan file at `claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md`. Upstream: https://github.com/quarto-dev/q2/issues/181.\n\n## Out of scope (separate issue if desired)\n\nThe writer's `[$$…$$]{attr}` emission for `quarto-math-with-attribute` Spans is suboptimal — Pandoc emits the natural form `$$\\n…\\n$$ {attr}` for the same AST. Switching to that form would improve readability and Pandoc compatibility, but with the reader fix above it is no longer required for round-trip correctness.","status":"open","priority":1,"issue_type":"bug","created_at":"2026-05-12T19:32:00.197200Z","created_by":"cscheid","updated_at":"2026-05-12T19:55:56.757118Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-qpa2","depends_on_id":"bd-q6ed","type":"related","created_at":"2026-05-12T19:32:00.197200Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-r7v2","title":"Browser smoke: confirm MathJax/KaTeX typeset in real browser","description":"Phase 4.3 of bd-w5ov could not be completed in-session because chrome-devtools-mcp disconnected after a stale browser process was killed. Implementation is complete and Phase 4.2 (CLI exercise) verified the rendered HTML markup is correct. What remains: load the rendered fixtures in a real Chromium session and confirm the math is actually typeset, not just present as raw \\(x^2\\) text.\n\nAcceptance:\n- Render /tmp/q2-math-cli/inline_math.html, display_math.html, labelled_eq.html via http.server.\n- chrome-devtools-mcp: navigate, evaluate document.querySelectorAll('mjx-container').length > 0 (MathJax fixtures).\n- Render katex.html, evaluate document.querySelectorAll('.katex, .katex-display').length > 0.\n- Confirm zero console errors from the math runtime (404s on jsDelivr would fail this).\n- Record observations in claude-notes/plans/2026-05-04-math-mode.md.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-05-04T23:58:12.775314Z","created_by":"cscheid","updated_at":"2026-05-05T00:04:57.347714Z","closed_at":"2026-05-05T00:04:57.347571Z","close_reason":"Browser smoke completed in same session as bd-w5ov implementation: 5 fixtures verified live in Chromium via chrome-devtools-mcp. MathJax 3.2.2 typesets inline + display + labelled equations from jsDelivr; KaTeX typesets via auto-render; math-free pages stay clean; user URL override honored. Recorded in plan §Browser smoke log.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-r7v2","depends_on_id":"bd-w5ov","type":"discovered-from","created_at":"2026-05-04T23:58:12.775314Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-r82e","title":"DocumentProfile: add includes: Vec for incremental-rebuild invalidation","description":"Surfaced during the main -> feature/websites merge (bd-xfwx) that threaded IncludeExpansionStage before the DocumentProfile checkpoint.\n\nProblem: after the merge, DocumentProfile.outline (and any other AST-derived fields) can be populated from headings / code / crossref targets spliced in via {{< include child.qmd >}}. The profile therefore depends on the contents of every included file, but the profile struct itself records no trail back to those files.\n\nFor incremental rebuilds (Phase 8 of the websites epic, bd-*) and for the future 'freeze' feature, the cache-key computation needs to invalidate a parent document's cached profile when ANY of its (transitive) includes change. Without tracking the include set on the profile, that is impossible.\n\nProposal: add a field roughly of the form\n\n includes: Vec // or Vec\n\nto DocumentProfile. Populated by IncludeExpansionStage (or by DocumentProfileStage reading a side-channel set that IncludeExpansionStage populated on the DocumentAst). Bump profile_version on the serialized shape.\n\nScope:\n- Decide on the shape (bare PathBuf list vs. (path, content-hash) pairs). For Phase 8 we'll likely want the hash too so nav-state invalidation can compare without re-reading the files.\n- Populate from IncludeExpansionStage.\n- Extend contract doc (claude-notes/designs/document-profile-contract.md).\n- Tests: profile records every included file (direct + transitive); round-trip serialization.\n\nNon-blocking for the website-epic MVP; becomes a hard prerequisite for Phase 8.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-04-24T20:05:53.778909Z","created_by":"cscheid","updated_at":"2026-04-28T00:43:08.078191Z","closed_at":"2026-04-28T00:43:08.077941Z","close_reason":"Closed as part of Phase 8.0 — DocumentProfile.includes added (v2). Verified by editing_include_invalidates_parent_profile (incremental_rebuild.rs).","source_repo":".","compaction_level":0,"original_size":0,"labels":["websites"],"dependencies":[{"issue_id":"bd-r82e","depends_on_id":"bd-0tr6","type":"related","created_at":"2026-04-24T20:05:53.778909Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-r82e","depends_on_id":"bd-fegm","type":"blocks","created_at":"2026-04-27T22:21:01.025552Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-r82e","depends_on_id":"bd-xfwx","type":"related","created_at":"2026-04-24T20:05:53.778909Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-r9hs","title":"Cargo: upgrade ureq v2.12.1 → v3.3.0","description":"Major upgrade surfaced by cargo-upgrade survey 2026-05-04. Current 2.12.1 is range-pinned in workspace; latest is 3.3.0. Type: major. Review changelog and bump deliberately. See claude-notes/plans/2026-05-04-cargo-upgrade-survey.md and bd-hb8h.","status":"closed","priority":3,"issue_type":"chore","created_at":"2026-05-04T18:15:55.424872Z","created_by":"cscheid","updated_at":"2026-05-04T20:30:45.584646Z","closed_at":"2026-05-04T20:30:45.584505Z","close_reason":"merged: b0ee0f18","source_repo":".","compaction_level":0,"original_size":0,"labels":["cargo","deps"],"dependencies":[{"issue_id":"bd-r9hs","depends_on_id":"bd-hb8h","type":"discovered-from","created_at":"2026-05-04T18:16:05.861885Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} From bc060eec594e969e07df69cf43d7401587b59945 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 12 May 2026 15:11:18 -0500 Subject: [PATCH 4/5] fix(pampa): source displaymath strip-column from enclosing paragraph (bd-qpa2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bd-q6ed's column-strip for display-math content used the column of the opening `$$` as the block-continuation prefix width. That column only equals the true prefix width when `$$` is the leftmost non-prefix character on its line. When anything precedes `$$` on the same line — `_`, `**`, `[` (e.g. the writer's bracket-form for `quarto-math-with-attribute` Spans), or any other inline construct — the column shifts but the body bytes do not, and the strip either eats real content (when the prefix is all whitespace) or refuses to strip real prefix (when the column overshoots into real content). Symptoms reported by @rundel on issue #181: * `$$ ... $$ {#eq-x}` round-tripping loses one column of interior whitespace per pass at top level. * `> $$ ... $$ {#eq-p}` round-tripping leaks `> ` into Math.text on the second parse. Probing surfaced a broader instance of the same brittleness: * `_$$\na\n b\n$$_` loses interior whitespace on *first* parse. * `> _$$\n> a\n> b\n> $$_` leaks `> ` and loses whitespace on first parse, with no round-trip involved. Pandoc's markdown reader handles all of these correctly while keeping `DisplayMath` as an inline AST node (verified empirically). Pandoc strips the cumulative block-continuation prefix width *in columns* from each interior line, where the width is derived from the enclosing block context — not from where `$$` sits on the opening line. The width is constant across all interior lines of the paragraph. Fix: change a single input to `strip_continuation_prefix`. The new `block_continuation_column` helper walks `parent()` from the math node to the nearest `pandoc_paragraph` ancestor and returns its start column. That column equals the cumulative block-continuation prefix width, independent of what precedes `$$` on the opening line. Falls back to the math node's own column when no paragraph ancestor is found (single-line contexts like table cells / captions, which have no interior body lines to strip). DisplayMath stays as an inline AST node — no grammar change, no AST shape change. The conservative `bytes ∈ {>, space, tab}` guard in the existing helper continues to handle lazy-continuation lines correctly. Four new round-trip fixtures cover the cases above: - labeled_display_math_in_blockquote.qmd - labeled_display_math_top_level_indented.qmd - emph_around_multiline_display_math.qmd - emph_around_multiline_display_math_in_blockquote.qmd Each one fails the round-trip consistency test before this change and passes after. The original bd-q6ed canonical case continues to round trip cleanly. End-to-end verification on the Poisson and Black-Scholes in-the-wild patterns rundel cited also passes. Full pampa suite (3687 tests), full workspace (8851 tests), and `cargo xtask verify --skip-hub-build --skip-hub-tests` all green. Plan + Pandoc-informed analysis: claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md Upstream: https://github.com/quarto-dev/q2/issues/181 --- ...2026-05-12-displaymath-column-strip-fix.md | 248 ++++++++++++++++++ crates/pampa/src/pandoc/treesitter.rs | 56 +++- .../emph_around_multiline_display_math.qmd | 4 + ...d_multiline_display_math_in_blockquote.qmd | 4 + .../labeled_display_math_in_blockquote.qmd | 5 + ...abeled_display_math_top_level_indented.qmd | 4 + 6 files changed, 312 insertions(+), 9 deletions(-) create mode 100644 claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md create mode 100644 crates/pampa/tests/roundtrip_tests/qmd-json-qmd/emph_around_multiline_display_math.qmd create mode 100644 crates/pampa/tests/roundtrip_tests/qmd-json-qmd/emph_around_multiline_display_math_in_blockquote.qmd create mode 100644 crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_in_blockquote.qmd create mode 100644 crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_top_level_indented.qmd diff --git a/claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md b/claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md new file mode 100644 index 00000000..fc350df8 --- /dev/null +++ b/claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md @@ -0,0 +1,248 @@ +# 2026-05-12 — Fix displaymath column-strip to use enclosing paragraph column + +- **Beads:** [bd-qpa2](https://example/none) — *Display math column-strip uses wrong column source, mishandles inline-wrapped and labeled math (issue #181 follow-up)* +- **Related:** bd-q6ed (the original column-strip fix), upstream GH #181 +- **Worktree:** `.worktrees/bd-qpa2-display-math-column-strip/` +- **Branch:** `beads/bd-qpa2-display-math-column-strip` +- **Triage record:** `claude-notes/issue-reports/181/triage.md` on the `issue-181` branch + +## Overview + +bd-q6ed added a column-strip in `pampa/src/pandoc/treesitter.rs` to remove the block-continuation prefix (`> `, list-item indent, etc.) that the `pandoc_display_math` grammar regex captures verbatim from the source bytes. The strip uses `node.start_position().column` (the column of the opening `$$`) as the prefix width. That column equals the cumulative block-continuation prefix width **only** when `$$` is the leftmost non-prefix character on its line. As soon as anything precedes `$$` on the same line — `_`, `**`, `[`, the writer's own `[` for `quarto-math-with-attribute` Spans, or an explicit ID/label expression — the column is wrong and the strip mis-fires. + +Pandoc's markdown reader handles every probed case correctly while keeping `DisplayMath` as an inline AST node (constraint from the user: there are large existing corpora with display math nested inside paragraphs, so a grammar restructure that promoted display math to a block element is off the table). Pandoc strips the cumulative block-continuation prefix width *in columns* from each interior line, where the width is derived from the enclosing block context — not from where `$$` sits on the opening line. + +**Fix shape:** change the strip-width source from the math node's start column to the **enclosing block-leaf ancestor's** start column (`pandoc_paragraph` / `pandoc_plain`). One single-input change to the existing helper. No grammar change, no AST shape change. + +This subsumes: + +- The canonical bd-q6ed case (paragraph col equals math col, identical result). +- bd-qpa2 edge A: `> $$ ... $$ {#eq-p}` round-tripping with `> ` leakage on second parse. +- bd-qpa2 edge B: `$$ ... $$ {#eq-x}` losing interior whitespace on each round trip. +- `_$$\nmulti\n$$_` and similar inline-wrapping (which fail on **first** parse, no writer needed). +- `> _$$\nmulti\n$$_` (combined blockquote + inline wrap). + +Pandoc reference behaviour table is in `claude-notes/issue-reports/181/triage.md` § *Second-pass investigation*. + +## Constraints + +1. **`DisplayMath` must remain an inline AST node.** Large existing corpora put display math inside paragraphs. +2. **No regressions in the existing bd-q6ed fixtures** (canonical blockquote + nested + list combinations). +3. **Cross-platform:** the helper must not assume Unix-only behaviour. Existing byte-level helper is already cross-platform; the change is purely in input sourcing. +4. **Pandoc compatibility:** the goal is to match Pandoc's reader behaviour on the probed cases. + +## Out of scope + +- **Writer change** to emit `$$\n…\n$$ {attr}` instead of `[$$…$$]{attr}` for `quarto-math-with-attribute` Spans. Nice-to-have for readability / Pandoc-output compatibility, but not required for round-trip correctness once the reader fix lands. Should be its own beads issue at lower priority. +- Grammar restructure to make `pandoc_display_math` a block element. Ruled out by constraint 1. + +## Work items + +### Phase 0 — Setup + +- [x] Triage doc updated on `issue-181` branch with second-pass investigation +- [x] bd-qpa2 description rewritten with unified diagnosis +- [x] Worktree created (`.worktrees/bd-qpa2-display-math-column-strip/`) +- [x] HEAD green: `cargo xtask verify --skip-hub-build --skip-hub-tests` passes +- [ ] Plan file written (this document) and pointer added to CLAUDE.local.md + +### Phase 1 — Inspect tree-sitter CST for representative inputs + +- [x] `> _$$\n> a\n> b\n> $$_` (blockquote + emph + multiline math) +- [x] `_$$\na\n b\n$$_` (top-level emph + multiline math) +- [x] `- $$\n a\n b\n $$` (list item + multiline math) +- [x] `> [$$\n> p(x)\n> $$]{#eq-p}` (writer's bd-qpa2 round-trip output) +- [x] `> > $$\n> > a\n> > $$` (nested blockquote) +- [x] `| $$x$$ |` in pipe table cell + +Confirmed: `pandoc_paragraph` is the right ancestor for every multi-line case. Tight and loose lists both use `pandoc_paragraph` (no `pandoc_plain` rule in this grammar). Inline ancestor kinds between math and paragraph: `pandoc_emph`, `pandoc_span`. Helper should walk `parent()` past anything that isn't `pandoc_paragraph`. Full table in *Implementation notes → CST inspection notes* below. + +### Phase 2 — Add failing fixtures (TDD) + +- [x] Add `crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_in_blockquote.qmd` +- [x] Add `labeled_display_math_top_level_indented.qmd` +- [x] Add `emph_around_multiline_display_math.qmd` +- [x] Add `emph_around_multiline_display_math_in_blockquote.qmd` +- [x] Run `cargo nextest run -p pampa test_qmd_roundtrip_consistency` and confirm: + - new fixtures fail with divergent JSON between parses 1 and 3 ✓ (all 4 fail before fix) + - existing fixtures (bd-q6ed and the rest) still pass ✓ + +### Phase 3 — Implementation + +- [x] Read `crates/pampa/src/pandoc/treesitter.rs` around the existing `pandoc_display_math` arm and `strip_continuation_prefix` helper +- [x] Add `block_continuation_column(&Node) -> usize` helper that walks `parent()` until it finds a `pandoc_paragraph` ancestor and returns its start column; falls back to math node's own column if no such ancestor (single-line contexts like table cells / captions) +- [x] Update the `pandoc_display_math` arm to source `start_col` from the new helper +- [x] Run `cargo nextest run -p pampa test_qmd_roundtrip_consistency` — all fixtures pass ✓ +- [x] Run `cargo nextest run -p pampa` — full pampa suite passes (3687/3687, 2 skipped) ✓ +- [x] Inspect any newly-emitted code paths for cross-platform correctness — pure byte-level, no platform calls + +### Phase 4 — Regression sweep + +- [x] `cargo nextest run --workspace` — full workspace passes (8851 tests, 195 skipped) ✓ +- [x] `cargo xtask verify --skip-hub-build --skip-hub-tests` — all verification steps passed ✓ (hub-client tests skipped per pre-existing `ERR_MODULE_NOT_FOUND` on HEAD; unrelated to this change) + +### Phase 5 — End-to-end CLI verification + +Per `CLAUDE.md` § *End-to-end verification before declaring success*. All commands run from the worktree root. + +#### Fixture 1 — `labeled_display_math_in_blockquote.qmd` (bd-qpa2 edge A) + +``` +$ cargo run --bin pampa -- crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_in_blockquote.qmd +[ BlockQuote [Para [Str "Let", Space, Str "x.", SoftBreak, + Span ( "eq-p" , ["quarto-math-with-attribute"] , [] ) + [Math DisplayMath "\np(x)\n"], SoftBreak, Str "Done."]] ] + +$ … | cargo run --bin pampa -- -t qmd +> Let x. +> [$$ +> p(x) +> $$]{#eq-p .quarto-math-with-attribute} +> Done. + +$ … -t qmd | cargo run --bin pampa -- +[ BlockQuote [Para [Str "Let", Space, Str "x.", SoftBreak, + Span ( "eq-p" , ["quarto-math-with-attribute"] , [] ) + [Math DisplayMath "\np(x)\n"], SoftBreak, Str "Done."]] ] +``` + +Math.text on parse 1 and 3 are identical (`"\np(x)\n"`) — the `> ` leak is gone. + +#### Fixture 2 — `labeled_display_math_top_level_indented.qmd` (bd-qpa2 edge B) + +``` +$ cargo run --bin pampa -- … (top-level $$ a\n b\n$$ {#eq-x}) +[ Para [Span ( "eq-x" , ["quarto-math-with-attribute"] , [] ) + [Math DisplayMath "\na\n b\n"]] ] + +$ … -t qmd +[$$ +a + b +$$]{#eq-x .quarto-math-with-attribute} + +$ … -t qmd | … +[ Para [Span ( "eq-x" , ["quarto-math-with-attribute"] , [] ) + [Math DisplayMath "\na\n b\n"]] ] +``` + +` b` (two leading spaces) preserved across the round trip. + +#### Fixture 3 — `emph_around_multiline_display_math.qmd` (inline-wrap, top) + +``` +$ cargo run --bin pampa -- … +[ Para [Emph [Math DisplayMath "\na\n b\n"]] ] + +$ … -t qmd +*$$ +a + b +$$* + +$ … -t qmd | … +[ Para [Emph [Math DisplayMath "\na\n b\n"]] ] +``` + +The two-space indent on ` b` survives both parses — the brittleness on **first** parse that the second-pass investigation surfaced is gone. + +#### Fixture 4 — `emph_around_multiline_display_math_in_blockquote.qmd` + +``` +$ cargo run --bin pampa -- … +[ BlockQuote [Para [Emph [Math DisplayMath "\na\n b\n"]]] ] + +$ … -t qmd +> *$$ +> a +> b +> $$* + +$ … -t qmd | … +[ BlockQuote [Para [Emph [Math DisplayMath "\na\n b\n"]]] ] +``` + +Both the `> ` blockquote prefix and the ` b` interior indent are handled correctly. + +#### Regression check — bd-q6ed canonical case + +``` +$ cargo run --bin pampa -- crates/pampa/tests/roundtrip_tests/qmd-json-qmd/display_math_in_blockquote.qmd +[ BlockQuote [Para [Str "Before", SoftBreak, + Math DisplayMath "\np = q\n", SoftBreak, Str "After"]] ] + +$ … -t qmd +> Before +> $$ +> p = q +> $$ +> After +``` + +Identical behaviour to before this change. ✓ + +#### In-the-wild — Poisson (quarto-web `_equations.qmd`) + +``` +> Let $X_1, X_2, \ldots, X_n$ be a Poisson random variable, then +> +> $$ +> p(x) = e^{-\lambda} \frac{\lambda^x}{x!}, x = 0, 1, 2 ,\ldots, n +> $$ {#eq-poisson} +``` + +First parse and reparse both produce `Math DisplayMath "\np(x) = e^{-\lambda} \\frac{\\lambda^x}{x!}, x = 0, 1, 2 ,\\ldots, n\n"`. Round-trip stable. ✓ + +#### In-the-wild — Black-Scholes (quarto-web `cross-references.qmd`) + +``` +$$ +\frac{\partial \mathrm C}{ \partial \mathrm t } + \frac{1}{2}\sigma^{2} \mathrm S^{2} + + \mathrm r \mathrm S \frac{\partial \mathrm C}{\partial \mathrm C} + \mathrm r \mathrm C +$$ {#eq-black-scholes} +``` + +The interior ` + \mathrm r …` and ` \mathrm r \mathrm C` lines (each with two leading spaces) are preserved on both parses — confirmed by inspecting the `DisplayMath` content in the AST output. ✓ + +All outputs inspected. All checks pass. + +- [x] Each new fixture exercised through the CLI end-to-end; outputs match expected values +- [x] bd-q6ed canonical fixture re-verified +- [x] Poisson + Black-Scholes in-the-wild patterns verified + +### Phase 6 — Commit and report + +- [ ] Stage and commit on the bd-qpa2 worktree branch +- [ ] Sync beads on main (close bd-qpa2 with reference to the commit) +- [ ] Report to user: diff summary, end-to-end verification output, snapshot/fixture counts +- [ ] Optionally file the writer-polish issue (out-of-scope) at lower priority + +## Implementation notes + +(filled in as we work) + +### CST inspection notes + +Confirmed `pandoc_paragraph` is the right block-leaf ancestor for every multi-line display math case. Math under a single-line context (table cell, caption, heading) sits in containers that don't need stripping (no interior body lines). + +| Input | Path math → block ancestor | Math col | Paragraph col | Notes | +|---|---|---|---|---| +| `> _$$\n> a\n> b\n> $$_` | math → emph → **paragraph** → bq | 3 | **2** | math col wrong (after `> _`); paragraph col right (after `> `) | +| `_$$\na\n b\n$$_` (top) | math → emph → **paragraph** | 1 | **0** | math col wrong (after `_`); paragraph col right | +| `- $$\n a\n b\n $$` | math → **paragraph** → list_item → list | 2 | 2 | same — canonical | +| `> [$$\n> p(x)\n> $$]{#eq-p ...}` | math → span → **paragraph** → bq | 3 | **2** | math col wrong (after `> [`); paragraph col right | +| `> > $$\n> > a\n> > $$` | math → **paragraph** → bq → bq | 4 | 4 | same — canonical | +| pipe table cell `\| $$x$$ \|` | math → pipe_table_cell → … | 2 | n/a | single-line, no body splits, no strip needed | + +**Decision**: walk `parent()` from the math node and return the start column of the first ancestor with kind `"pandoc_paragraph"`. If no such ancestor is found (table cells, captions, headings — all single-line for display math purposes), fall back to the math node's own column. The conservative `{>, space, tab}` byte check in `strip_continuation_prefix` continues to guard against mis-stripping in any unforeseen edge case. + +Other inline ancestor kinds encountered between math and paragraph: `pandoc_emph`, `pandoc_span`. The loop walks past these transparently — only the `pandoc_paragraph` kind is the target. + +### Helper signature decision + +*(pending Phase 3)* + +### Edge cases discovered + +*(pending — add anything we run into while implementing)* diff --git a/crates/pampa/src/pandoc/treesitter.rs b/crates/pampa/src/pandoc/treesitter.rs index 898ce24f..d417a144 100644 --- a/crates/pampa/src/pandoc/treesitter.rs +++ b/crates/pampa/src/pandoc/treesitter.rs @@ -316,8 +316,39 @@ fn process_list_item( ) } +/// Find the column at which interior lines of a `pandoc_display_math` node +/// "should" start — i.e. the cumulative block-continuation prefix width +/// imposed by all enclosing block-level containers (blockquotes, list +/// items, etc.). That width equals the start column of the nearest +/// enclosing `pandoc_paragraph` ancestor. +/// +/// Walking the math node's own start column is *not* sufficient: when an +/// inline construct precedes `$$` on the opening line (e.g. `_$$`, +/// `[$$` for `quarto-math-with-attribute` Spans, `**$$`), the math +/// column overshoots the actual continuation prefix width and the strip +/// either mis-eats real content or fails to strip real prefix. The +/// paragraph's start column is constant across all interior lines of the +/// paragraph regardless of what precedes `$$` on the opening line, which +/// is what we want. This matches Pandoc's markdown reader behaviour. +/// +/// Falls back to the math node's own column for the (theoretical) case +/// where no `pandoc_paragraph` ancestor is found. In practice display +/// math always sits inside a paragraph when it has multi-line body +/// content; single-line contexts like table cells / captions have no +/// interior lines to strip. +fn block_continuation_column(node: &tree_sitter::Node) -> usize { + let mut current = *node; + while let Some(parent) = current.parent() { + if parent.kind() == "pandoc_paragraph" { + return parent.start_position().column; + } + current = parent; + } + node.start_position().column +} + /// Strip the block-continuation prefix from each interior line of -/// display-math content (issue #181 / bd-q6ed). +/// display-math content (issue #181 / bd-q6ed; refined for bd-qpa2). /// /// The grammar matches the math body as a single regex token, so any /// continuation prefixes that enclosing blocks (blockquotes, list items, @@ -326,13 +357,17 @@ fn process_list_item( /// qmd writer then re-prefixes every line on output, so the prefixes /// double on round trip. /// -/// `start_col` is the column of the opening `$$` (i.e. `node.start_position().column`). +/// `start_col` is the cumulative block-continuation prefix width — i.e. +/// the column where math content should land on every interior line. It +/// is sourced from the enclosing `pandoc_paragraph` ancestor (see +/// `block_continuation_column`), not from the column of the opening `$$` +/// (which can be shifted by preceding inline constructs). +/// /// On every interior line of the math, the bytes at columns `0..start_col` /// are the accumulated continuation prefix added by the chain of enclosing -/// blocks; the math content "should" start at column `start_col`. We -/// strip those bytes — but only if they look like continuation: the only -/// characters that ever appear in a continuation prefix are `>`, space, -/// and tab. Anything else means the line was matched via lazy +/// blocks. We strip those bytes — but only if they look like continuation: +/// the only characters that ever appear in a continuation prefix are `>`, +/// space, and tab. Anything else means the line was matched via lazy /// continuation (no explicit `> `), and we leave it alone rather than /// chewing bytes off real content. /// @@ -560,9 +595,12 @@ fn native_visitor( // block-continuation prefix that enclosing blocks (blockquotes, // list items, etc.) would normally consume is captured verbatim // on interior lines. Strip those prefix bytes column-wise so the - // qmd writer doesn't double-prefix on round trip - // (issue #181 / bd-q6ed). - let start_col = node.start_position().column; + // qmd writer doesn't double-prefix on round trip (issue #181 / + // bd-q6ed). The strip width comes from the enclosing paragraph, + // not the math node, so that inline constructs preceding `$$` + // on the opening line (e.g. `_`, `[`, `**`) don't shift the + // column away from the true continuation prefix width (bd-qpa2). + let start_col = block_continuation_column(node); let text = strip_continuation_prefix(content, start_col); PandocNativeIntermediate::IntermediateInline(Inline::Math(Math { diff --git a/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/emph_around_multiline_display_math.qmd b/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/emph_around_multiline_display_math.qmd new file mode 100644 index 00000000..99787d2f --- /dev/null +++ b/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/emph_around_multiline_display_math.qmd @@ -0,0 +1,4 @@ +_$$ +a + b +$$_ diff --git a/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/emph_around_multiline_display_math_in_blockquote.qmd b/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/emph_around_multiline_display_math_in_blockquote.qmd new file mode 100644 index 00000000..4f3296ac --- /dev/null +++ b/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/emph_around_multiline_display_math_in_blockquote.qmd @@ -0,0 +1,4 @@ +> _$$ +> a +> b +> $$_ diff --git a/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_in_blockquote.qmd b/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_in_blockquote.qmd new file mode 100644 index 00000000..542d03d4 --- /dev/null +++ b/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_in_blockquote.qmd @@ -0,0 +1,5 @@ +> Let x. +> $$ +> p(x) +> $$ {#eq-p} +> Done. diff --git a/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_top_level_indented.qmd b/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_top_level_indented.qmd new file mode 100644 index 00000000..c2f2040c --- /dev/null +++ b/crates/pampa/tests/roundtrip_tests/qmd-json-qmd/labeled_display_math_top_level_indented.qmd @@ -0,0 +1,4 @@ +$$ +a + b +$$ {#eq-x} From 92f291bc1eab967c719ec5513fac99122dd4441c Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 12 May 2026 15:11:35 -0500 Subject: [PATCH 5/5] Update plan with commit reference and review-pending status --- .../plans/2026-05-12-displaymath-column-strip-fix.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md b/claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md index fc350df8..1e908ec9 100644 --- a/claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md +++ b/claude-notes/plans/2026-05-12-displaymath-column-strip-fix.md @@ -213,10 +213,10 @@ All outputs inspected. All checks pass. ### Phase 6 — Commit and report -- [ ] Stage and commit on the bd-qpa2 worktree branch -- [ ] Sync beads on main (close bd-qpa2 with reference to the commit) -- [ ] Report to user: diff summary, end-to-end verification output, snapshot/fixture counts -- [ ] Optionally file the writer-polish issue (out-of-scope) at lower priority +- [x] Stage and commit on the bd-qpa2 worktree branch (commit `bc060eec`) +- [ ] Awaiting user review + push approval before closing bd-qpa2 on main +- [ ] Report to user: diff summary, end-to-end verification output, fixture counts +- [ ] (Optional) file the writer-polish issue for natural-form labeled math at lower priority ## Implementation notes