diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 362cb0eb..0ca695f6 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":""}]} @@ -237,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":"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":""}]} 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..1e908ec9 --- /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 + +- [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 + +(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}