From d3115e3e07c689385a344f38ed2cc330db846388 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Thu, 28 May 2026 08:47:23 -0500 Subject: [PATCH 1/9] plan + beads: mermaid engine design session (bd-je48v) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open the design session for a minimal mermaid engine. First cut is intended to be tiny — `{mermaid}` blocks become `
` plus a once-per-doc jsdelivr script include — but writing that down forced an audit of the engine → pipeline integration story. The plan (claude-notes/plans/2026-05-28-mermaidjs-engine-design.md) documents the current substrate with file:line citations (ExecutionEngine trait, ExecuteResult flow, pipeline stage list, DocumentProfile checkpoint, trace + WASM replay path, VFS, filter_resolve) and surfaces four pre-existing gaps the mermaid case exposes: - G1 (bd-14rer): ExecuteResult.filters is written by Knitr but never read by any pipeline stage (rmarkdown/pagebreak.lua is silently dropped today). - G2 (bd-s8llm): no internal Rust API for pipeline producers to publish files into the VFS. - G3 (bd-mqk49): no mechanism for engines or extensions to declare additional pipeline stages. - G5/G6 (bd-cp3em): CaptureSpliceStage explicitly drops includes/filters/supporting_files from the recorded ExecuteResult — same shape as the CodeHighlightStage incident in CLAUDE.md. Open architectural questions for the design session (recorded as Q-A through Q-D in the plan): is mermaid an ExecutionEngine impl or an AST transform (engine selection is currently document-wide, so the engine framing requires per-block dispatch); where does the format-agnostic / HTML-specific split land; how does the script include reach the document; what's the WASM-replay coverage matrix. Beads tree: bd-je48v (epic) ├── bd-c6h96 design (resolve Q-A/Q-B/Q-C/Q-D) ├── bd-fztki audit (multi-engine + per-engine trace state) ├── bd-gwfdo impl (blocked-by design + audit) ├── bd-my0o5 preview e2e (blocked-by impl) └── bd-5ijtt docs (blocked-by impl) Follow-up gaps (discovered-from bd-c6h96): bd-14rer, bd-s8llm, bd-mqk49, bd-cp3em. Co-Authored-By: Claude Opus 4.7 (1M context) --- .beads/issues.jsonl | 10 + .../2026-05-28-mermaidjs-engine-design.md | 487 ++++++++++++++++++ 2 files changed, 497 insertions(+) create mode 100644 claude-notes/plans/2026-05-28-mermaidjs-engine-design.md diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index dc270902..8ea87796 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -18,6 +18,7 @@ {"id":"bd-12fpz","title":"D4/D5: HTML writer emits odd/even and header row classes","description":"## What\n\n`write_table_row` in `crates/pampa/src/writers/html.rs:1490` emits bare `` for every row. Pandoc's reference HTML writer (which Q1 inherits) adds:\n\n- `` on header (thead) rows.\n- `` / `` alternating on body rows.\n\n## Fix\n\nPass row index + is_header from the table-writing loop into `write_table_row` (it already gets `is_header`); add the class accordingly.\n\n## Tests (TDD)\n\nSnapshot test on a 3-row table (1 header + 2 body) showing:\n- header row: ``\n- first body row: ``\n- second body row: ``\n\n## Plan\n\nclaude-notes/plans/2026-05-20-table-default-rendering-parity.md (D4, D5 sections — bundled because they share the same writer code path)","status":"closed","priority":3,"issue_type":"bug","created_at":"2026-05-20T20:55:24.955808Z","created_by":"cscheid","updated_at":"2026-05-20T21:31:14.339774Z","closed_at":"2026-05-20T21:31:14.339617Z","close_reason":"Implemented in this commit: row classes (header/odd/even) emitted.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-12fpz","depends_on_id":"bd-hir7j","type":"parent-child","created_at":"2026-05-20T20:55:24.955808Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-12vrr","title":"Callout default-title synthesizer + popup-menu callout-type switching (Plan 6 §B)","description":"Plan 6's audit identified crates/quarto-core/src/transforms/callout_resolve.rs:267 (default callout title 'Note'/'Tip'/'Warning'/...) as an AST synthesizer that today emits SourceInfo::default(). To bring it into the Generated shape Plan 6 establishes, it needs: (a) a new By::callout() constructor in quarto-source-map, (b) an atomicity decision (is_atomic_kind), (c) the fix at callout_resolve.rs:267, (d) a per-transform test. Deferred from Plan 6 because it was not enumerated in the plan body and requires the new By constructor + atomicity decision.","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-22T16:19:01.484116Z","created_by":"gordon","updated_at":"2026-05-22T16:45:31.449690Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-12vrr","depends_on_id":"bd-129m3","type":"related","created_at":"2026-05-22T16:19:13.342003Z","created_by":"gordon","metadata":"{}","thread_id":""}],"comments":[{"id":27,"issue_id":"bd-12vrr","author":"Gordon Woodhull","text":"Why this matters beyond the audit fix: the React preview needs a UI affordance to **switch callout type** (Note → Tip → Warning → Important → Caution) via a popup menu on the callout block. For that to round-trip cleanly through Plan 7's writer, the default-title text (\"Note\", \"Tip\", …) must NOT be treated as user-editable Original content — otherwise typing-as-edit semantics would force the writer to materialize \"Note\" as literal text into source, even when the user only wanted to swap the type via the menu.\n\nThe `Generated { by: callout(), … }` stamping + the atomicity decision determine:\n\n1. **React-side**: whether the title region is gated read-only via Plan 2A's atomic check (so the user can't accidentally type into it; the menu is the only path to mutation).\n2. **Writer-side**: how a menu-driven type change serializes. The menu rewrites the Callout CustomNode's `plain_data` (e.g. `{\"type\": \"tip\"}`) and leaves the title-Generated either re-synthesized on next pipeline run or replaced with a fresh Generated whose `by.data` reflects the new type. Plan 7's let-user-win path handles the CustomNode rewrite naturally; the title region soft-drops on direct edits (Q-3-42-style).\n\nRecommend `is_atomic_kind = true` for `By::callout()`'s title-synthesizer use so the title is non-editable text and only menu-driven type changes mutate it. The popup-menu component lives in hub-client's React framework registry (post-Plan 2B); this beads owns the Rust-side provenance support that makes the round-trip behave.\n","created_at":"2026-05-22T16:45:31Z"}]} {"id":"bd-140x","title":"Contribute samod fix upstream with pure-rust reproduction test","description":"Cherry-pick the NotFound race condition fix (431333f) from quarto branch to main on cscheid/samod fork, and write a pure-rust integration test in samod-core/tests/ that reproduces the bug independently of quarto-hub. The test should demonstrate that a document synced from a client to a DontAnnounce server is dropped during handle_load when pending_sync_messages are ignored. PR target: alexjg/samod.","status":"open","priority":1,"issue_type":"task","created_at":"2026-03-04T00:58:02.303737Z","created_by":"cscheid","updated_at":"2026-03-04T00:58:02.303737Z","source_repo":".","compaction_level":0,"original_size":0} +{"id":"bd-14rer","title":"ExecuteResult.filters set by Knitr engine is never consumed downstream","description":"Workspace-wide grep for `result.filters` / `ExecuteResult.*filters` returns hits in tests, the Knitr write site, extension parsing, and a comment in capture-splice — but no production consumer.\n\nConcretely:\n- crates/quarto-core/src/engine/knitr/mod.rs:215 sets `filters: result.filters` from the R subprocess output (test fixture asserts `vec![\"rmarkdown/pagebreak.lua\"]`, knitr/types.rs:281).\n- crates/quarto-core/src/filter_resolve.rs only resolves filters from YAML `filters:` document metadata, not from ExecuteResult.filters.\n\nSo Knitr's pagebreak filter is silently dropped today. This also blocks any future engine (like a mermaid Lua-filter approach) that wants to declare filters dynamically.\n\nDecision needed: wire ExecuteResult.filters into the resolver, or remove the field if the architectural direction is different (e.g. all engine-side filters should be expressed as AST transforms instead).\n\nDiscovered during mermaid engine design — see claude-notes/plans/2026-05-28-mermaidjs-engine-design.md gap G1.","status":"open","priority":2,"issue_type":"bug","created_at":"2026-05-28T13:45:45.986759Z","created_by":"cscheid","updated_at":"2026-05-28T13:45:45.986759Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-14rer","depends_on_id":"bd-c6h96","type":"discovered-from","created_at":"2026-05-28T13:45:45.986759Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-15dw","title":"Navbar icon-only item enrichment tie-break","description":"Phase 3 enrichment fills navbar item.text from profile title only when text is None. An item that supplies icon but no text (common for social links) currently stays text-less. Confirmed intentional. Revisit if users ask for auto-text from titles even for icon-only items. See 2026-04-24-websites-phase-3.md §Follow-up beads.","status":"open","priority":4,"issue_type":"task","created_at":"2026-04-24T19:43:02.249186Z","created_by":"cscheid","updated_at":"2026-04-24T19:43:02.249186Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-15dw","depends_on_id":"bd-fqyg","type":"discovered-from","created_at":"2026-04-24T19:43:02.249186Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-18wn","title":"Fix file-locking test failures in quarto-hub on Windows","description":"2 storage tests fail because Windows uses mandatory file locking vs Unix advisory. File::create fails with sharing violation before try_lock_exclusive runs. Fix: use OpenOptions::new().write(true).create(true) instead of File::create, or map sharing violation error to HubAlreadyRunning. Affects test_storage_manager_prevents_double_lock and test_storage_manager_standalone_prevents_double_lock.","status":"open","priority":2,"issue_type":"bug","created_at":"2026-03-20T13:36:06.424470600Z","created_by":"cderv","updated_at":"2026-03-20T13:36:06.424470600Z","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-195t","title":"Lua attribute-mutation: proxy tables so cb.attr.attributes[k]=v persists","description":"Quarto 2's Lua bridge returns fresh copies of `cb.attr` (and of `cb.attr.attributes`) on every read. In-place mutation like `cb.attr.attributes[\"k\"] = v` — the idiomatic Pandoc-Lua pattern — silently hits an ephemeral copy and is discarded. Surfaced while exercising Phase 3.5's filter-authored spans fixture (04-filter-authored-spans); the workaround there rebuilds the whole Attr with pandoc.Attr(...) and assigns as one value. Before we document the Lua-filter path to syntax highlighting as a user-facing feature, the idiomatic pattern must persist. See claude-notes/plans/2026-04-20-syntax-highlighting-phase-3.5.md 'Follow-up task: Lua attribute-mutation proxy'. Plan: claude-notes/plans/2026-04-21-lua-attr-mutation-proxy.md","status":"open","priority":1,"issue_type":"bug","created_at":"2026-04-21T17:19:19.121444Z","created_by":"cscheid","updated_at":"2026-04-21T17:19:19.121444Z","source_repo":".","compaction_level":0,"original_size":0} @@ -109,6 +110,7 @@ {"id":"bd-55n0g","title":"JSON-OUTPUT-SCHEMA.md describes a stale Q-1-XX numbering scheme that does not match the catalog or src/error.rs","description":"crates/quarto-yaml-validation/JSON-OUTPUT-SCHEMA.md lines 134-152 list Q-1-13 = 'String too short', Q-1-14 = 'String too long', Q-1-16 = 'Number out of range', etc. — but the actual mapping in src/error.rs::ValidationErrorKind::error_code and the catalog assigns those numbers to other kinds (Q-1-13 = ArrayLengthInvalid, Q-1-16 = ObjectPropertyCountInvalid). The file is broadly stale and would mislead anyone reading it. Needs a wholesale rewrite to match the current catalog.\n\nDiscovered while fixing bd-gdzlq (Q-1-20 double-allocation). Only the Q-1-20 → Q-1-29 line was updated in that fix; the rest of the file stays stale until this issue is taken.","status":"open","priority":3,"issue_type":"bug","created_at":"2026-05-22T20:26:45.950001Z","created_by":"cscheid","updated_at":"2026-05-22T20:26:45.950001Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["documentation","error-reporting","yaml"],"dependencies":[{"issue_id":"bd-55n0g","depends_on_id":"bd-gdzlq","type":"discovered-from","created_at":"2026-05-22T20:26:45.950001Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-56b0","title":"Cross-doc dependency channel audit for q2 preview","description":"Enumerate every Quarto feature that creates a cross-document dependency the preview pipeline needs to react to: include shortcodes, listing content globs, bibliography/csl paths, theme SCSS imports, project-scoped resources, _extensions/ Lua filters, and anything else. For each: which DocumentProfile channel encodes it today (if any), which need to be added, which stay manual-refresh-only. Drives Phase B (channels already on DocumentProfile) and informs Phase D (new channels). Until this lands, the always-visible manual force-refresh button (Phase A.6) is the user escape hatch. See claude-notes/plans/2026-05-11-q2-preview-epic.md §Recommended next steps item 3 + Risk 2.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-11T15:40:54.896280Z","created_by":"cscheid","updated_at":"2026-05-11T15:40:54.896280Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-56b0","depends_on_id":"bd-kw93","type":"related","created_at":"2026-05-11T15:40:54.896280Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-57y4","title":"Vendor and integrate quarto-listing.scss with theme-CSS pipeline","description":"Per L3 D5 (decided 2026-05-06), the listing JS pair (list.min.js + quarto-listing.js) shipped via the artifact store as Project-scoped js: artifacts in the L3 phase 7 commit. The third asset, quarto-listing.scss, was deferred because it requires Bootstrap variable wiring + media-breakpoint mixins from the existing theme-CSS pipeline (CompileThemeCssStage / quarto_sass::SassLayer). That's a larger design task than the static JS bundling.\n\nThis issue: integrate the listing SCSS as a SassLayer that gets composed with the website's theme bundle. The Q1 source file lives at external-sources/quarto-cli/src/resources/projects/website/listing/quarto-listing.scss; copy to resources/listing/quarto-listing.scss per the External Sources Policy and add it to the theme bundle when at least one listing is rendered.\n\nWhile the SCSS isn't compiled, listings render with default browser styling — the markup and JS are unchanged. The list / grid / table layouts work but lack the polished card / table styling Q1 ships.\n\nDiscovered while shipping L3 phase 7 (bd-ml8z); see D5 in claude-notes/plans/2026-05-06-listings-L3-resolve-transform.md.\n\nL5 (bd-5vsr) lands the markup that consumes this SCSS; merging bd-57y4 restores Q1 visual parity for category sidebars and per-item category chips.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-06T20:52:15.894830Z","created_by":"cscheid","updated_at":"2026-05-07T13:46:18.157025Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-57y4","depends_on_id":"bd-ml8z","type":"discovered-from","created_at":"2026-05-06T20:52:15.894830Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-5ijtt","title":"User-facing docs for mermaid diagrams","description":"Add a user-facing docs page under docs/ explaining `{mermaid}` code blocks: syntax, what gets rendered, browser-runtime caveats (requires network access to jsdelivr; diagram appears after page load), known limitations (HTML output only in first cut). Render with cargo run --bin q2 -- render docs/ (Q2, not Q1) per CLAUDE.md.\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","status":"open","priority":3,"issue_type":"task","created_at":"2026-05-28T13:45:22.896182Z","created_by":"cscheid","updated_at":"2026-05-28T13:45:38.414661Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5ijtt","depends_on_id":"bd-gwfdo","type":"blocks","created_at":"2026-05-28T13:45:38.414195Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-5ijtt","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:22.896182Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-5qnj","title":"Manage trace size for use as replay/regression-test fixtures","description":"Spun out of bd-45yw (replay engine). Once traces double as replay fixtures (and as user-attached bug-report artifacts), trace size becomes a real constraint — they will be checked into the repo as regression fixtures and posted by users in issues.\n\nCurrent quarto-trace output captures per-stage pipeline state (see crates/quarto-trace/src/lib.rs and crates/quarto-core/src/stage/trace.rs::JsonTraceObserver), which is already on the heavy side for routine diagnostic use.\n\nInvestigate:\n- Where the bulk lives in current traces (per-stage AST snapshots? supporting_files content? something else?)\n- Which content is actually load-bearing for replay vs. diagnostics, and whether the two roles can share one artifact (the design preference noted in bd-45yw's plan)\n- Compression on disk; lazy loading on read\n- Whether per-stage AST snapshots can be elided/diffed when not needed\n- Size budgets — what is reasonable for (a) checked-in CI fixtures and (b) user-attached bug reports\n\nGoal: make 'one trace serves both diagnostic and replay roles' practical. If size cannot be bounded, fall back to two separate artifacts and document why.\n\nReferences:\n- crates/quarto-trace/src/lib.rs (TraceDocument, TraceEntry)\n- crates/quarto-core/src/stage/trace.rs (JsonTraceObserver)\n- claude-notes/plans/2026-05-03-replay-engine.md (open subquestion in Phase 1)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-03T21:11:19.859036Z","created_by":"cscheid","updated_at":"2026-05-03T23:29:05.405253Z","closed_at":"2026-05-03T23:29:05.405068Z","close_reason":"Phases 1, 2, 3, and 5 complete. End-to-end verified on bd-5qnj fixtures: big.qmd 16.3 MB pretty -> 62 KB gzipped+deduped (~265x). Unified-artifact promise realized post-merge with bd-45yw. Real-engine supporting_files re-measurement tracked as bd-sr73.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5qnj","depends_on_id":"bd-45yw","type":"related","created_at":"2026-05-03T21:11:19.859036Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-5vsr","title":"L5 — Categories sidebar","description":"Right-margin category list emitted from L3's resolved item set, grouped by category. Three Q1-parity styles: category-default, category-unnumbered, category-cloud. Templates embedded as doctemplate via MemoryResolver. Click-to-filter interactivity scoped out of v1. See claude-notes/plans/2026-05-05-listings-epic.md §L5.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-05-05T19:53:32.333630Z","created_by":"cscheid","updated_at":"2026-05-07T16:00:41.250061Z","closed_at":"2026-05-07T16:00:41.249924Z","close_reason":"L5 — categories sidebar landed on feature/listings via merge 9e8afa0d (impl 2750546b + follow-on 67a985f4). Per-item chips + right-margin sidebar with three modes (default/unnumbered/cloud); Q-12-11 and Q-12-12 diagnostics; aggregate across multi-listing pages. End-to-end CLI verification recorded in plan §End-to-end CLI verification record. cargo xtask verify clean (Rust + hub-client + WASM). Test count 8570 → 8621 (+51). Discovered-from follow-ups filed: bd-99ru (localize labels), bd-754f (review encoding scheme), bd-ra5j (hub-client browser smoke deferred), bd-nwyp (PandocInlines parser audit).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-5vsr","depends_on_id":"bd-61cd","type":"parent-child","created_at":"2026-05-05T19:53:32.333630Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-5vsr","depends_on_id":"bd-ml8z","type":"blocks","created_at":"2026-05-05T19:53:32.333630Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-5yff4","title":"Sequential multi-engine execution (engine: [a, b, ...])","description":"Investigate and design support for running multiple Quarto 2 execution engines in sequence for a single document.\n\nTwo coupled changes:\n\n1. YAML config: allow 'engine:' to be an ordered array (e.g. engine: [knitr, mermaidjs]) in addition to the current singular string/map forms. Distinct engines only; order is significant (engine N may emit code cells consumed by engine N+1). Array merge across config layers uses the existing default (!concat); no engine-specific tag default for now.\n\n2. Pipeline: thread N engines through EngineExecutionStage. The AST->text->engine->text->AST->reconcile loop already type-checks end-to-end; each subsequent engine starts from the AST after the prior engine's results were reconciled in. Generalize the FileId/intermediate-file slot handling (currently 2 slots: .qmd + one .rmarkdown) to N+1 slots.\n\nTracing/replay/preview redesign: TraceDocument.engine_capture (single Option slot, name-keyed replay) becomes per-engine. Capture one AST snapshot + one EngineCapture per engine invocation within the stage, mirroring the existing transform: sub-entry pattern. Preview record/replay (record_capture, with_replay, preview cache) generalize from one capture to an ordered list.\n\nValidation/testing uses a simple file-backed test engine (reads a results file and splices per-cell outputs in order) so multi-engine sequencing can be exercised deterministically without R/Python/Jupyter, and without committing to a real second engine (e.g. mermaidjs) yet.\n\nPlan: claude-notes/plans/2026-05-27-multi-engine-execution.md\n\nStatus: design/investigation. Implementation gated on user go-ahead after plan review.","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-05-27T14:07:37.941942Z","created_by":"cscheid","updated_at":"2026-05-27T14:33:39.118501Z","source_repo":".","compaction_level":0,"original_size":0} @@ -194,10 +196,12 @@ {"id":"bd-c263g","title":"Author error-docs pages for lua subsystem (1 codes)","description":"Author stub-quality pages for all 1 lua subsystem error codes under docs/errors/lua/. Follow template in docs/errors/yaml/Q-1-10.qmd. See claude-notes/plans/2026-05-22-error-docs-content.md.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-24T21:07:58.103168Z","created_by":"cscheid","updated_at":"2026-05-24T21:15:23.074966Z","closed_at":"2026-05-24T21:15:23.074602Z","source_repo":".","compaction_level":0,"original_size":0} {"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-c5u2g","title":"Cache engine-discovery so we don't re-spawn per document","description":"Replace JupyterEngine::find_executable's sh -c subprocess spawn with which::which (parity with KnitrEngine), and memoize both find_jupyter and find_rscript with OnceLock>. Per-doc engine-registry construction is 37% of main-thread CPU on quarto-web (bd-9eltv profile).\n\nPlan: claude-notes/plans/2026-05-22-engine-discovery-cache.md\nProfile: claude-notes/research/2026-05-21-quarto-web-render-profile.md","status":"closed","priority":1,"issue_type":"task","created_at":"2026-05-22T01:07:27.123486Z","created_by":"cscheid","updated_at":"2026-05-22T01:26:59.991346Z","closed_at":"2026-05-22T01:26:59.991170Z","close_reason":"Implemented and verified — per-process engine-discovery cache. quarto-web wall time 3.28s -> 2.30s (-30%); perf.engine-discover gauge added as regression tripwire.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-c5u2g","depends_on_id":"bd-9eltv","type":"discovered-from","created_at":"2026-05-22T01:07:27.123486Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-c6h96","title":"Mermaid engine design session: resolve architectural questions","description":"Design session to resolve the open questions in claude-notes/plans/2026-05-28-mermaidjs-engine-design.md:\n\n- Q-A: Is mermaid an ExecutionEngine impl or an AST transform? (Engine selection in Q2 is currently document-wide via YAML `engine:`; `{mermaid}` is per-block — so 'engine' framing requires per-block dispatch which doesn't exist.)\n- Q-B: Where does the format-agnostic / HTML-specific split land? Marker Div + separate HTML conversion vs. direct RawBlock emission.\n- Q-C: How does the jsdelivr ') at the end of the body with the jsdelivr mermaid@11 script and initialize({startOnLoad: true}).\n- Add an HTML-render conversion: marker Div ->
...
. Decide whether this lives in a sub-stage or in the HTML writer.\n- Tests: unit (transform on fixture AST) + integration (cargo run --bin q2 -- render fixture.qmd, grep for
) + record the end-to-end invocation in the plan per CLAUDE.md verification policy.\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","updated_at":"2026-05-28T13:45:36.396458Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-gwfdo","depends_on_id":"bd-c6h96","type":"blocks","created_at":"2026-05-28T13:45:35.978499Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-fztki","type":"blocks","created_at":"2026-05-28T13:45:36.396009Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-gz6k","title":"Cargo: upgrade sha1 v0.10.6 → v0.11.0","description":"Major upgrade surfaced by cargo-upgrade survey 2026-05-04. Current 0.10.6 is range-pinned in workspace; latest is 0.11.0. 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.170269Z","created_by":"cscheid","updated_at":"2026-05-04T20:30:44.941246Z","closed_at":"2026-05-04T20:30:44.941104Z","close_reason":"merged: 0812812e","source_repo":".","compaction_level":0,"original_size":0,"labels":["cargo","deps"],"dependencies":[{"issue_id":"bd-gz6k","depends_on_id":"bd-hb8h","type":"discovered-from","created_at":"2026-05-04T18:16:05.443720Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-h4l6","title":"[websites phase 5] Scoped artifact store and site_libs/ dedup","description":"Plan: claude-notes/plans/2026-04-23-website-project-epic.md § Phase 5.\n\nDeliverables:\n- Add scope (Page | Project) to artifact entries in ArtifactStore.\n- Project-aware artifact writer: Project-scoped artifacts emitted once to _site/site_libs/{name}/...\n- Relocator: rewrite per-page HTML to point at shared site_libs/ paths correctly (handles subdirs, offset computation).\n- Migrate theme CSS, Bootstrap, quarto-nav JS, etc. to Project scope when inside a website project.\n- Preservation: single-doc renders unchanged (both scopes resolve under {stem}_files/).\n- Sequence the change in two steps: (a) pure refactor introducing scope with identical behavior, (b) switch websites to use Project scope.\n\nBlocked by Phase 1.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-04-23T18:43:08.936956Z","created_by":"cscheid","updated_at":"2026-04-29T00:31:30.479325Z","closed_at":"2026-04-29T00:31:30.479019Z","close_reason":"Phase 5 (scoped artifact store + site_libs) implemented (commit dc4e81b0). Closed as part of Phase 9 cleanup.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-h4l6","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-23T18:43:08.936956Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-h4l6","depends_on_id":"bd-w5os","type":"blocks","created_at":"2026-04-23T18:43:44.336998Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-h5l7","title":"Diagnose and fix SourceInfo::eq hotspot in hub-client preview","description":"Chrome profile of hub-client on a moderately-sized document shows a large hotspot on ::eq, entered from parse_qmd_to_ast (crates/wasm-quarto-hub-client/src/lib.rs:757). Likely cause: SourceInfoSerializer::intern in crates/pampa/src/writers/json.rs:229 linearly scans content_map (Vec<(SourceInfo, usize)>) on every pointer-lookup miss, and SourceInfo is held by-value in AST nodes so pointer lookups almost always miss — giving O(n²) behavior per document. Also the first perf-profiling session on Quarto 2, so the plan deliberately establishes a repeatable native-side workflow (Criterion bench + samply flamegraph on the pampa binary) before any fix, since Chrome-side before/after is too noisy to iterate on. Plan: claude-notes/plans/2026-04-22-sourceinfo-eq-hotspot.md. Currently in draft — awaiting user review before any measurement work begins.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-04-22T20:00:48.019984Z","created_by":"cscheid","updated_at":"2026-04-22T23:23:38.315697Z","closed_at":"2026-04-22T23:23:38.315082Z","close_reason":"Landed on perf/2026-04-22-json-sourcemap in commit 8f6f21d9. Browser cross-validation confirmed SourceInfo::eq no longer a hotspot.","source_repo":".","compaction_level":0,"original_size":0}
@@ -283,6 +289,7 @@
 {"id":"bd-j9cf","title":"Recognize bare `<` as a Str token (currently a parse error)","description":"Today a literal `<` outside math/code/HTML produces a parse error. Minimal trigger: a single-line document containing `1 < 2`. Goal: bare `<` should parse as Str \"<\" when not the start of a recognized HTML element / autolink / raw-specifier / HTML comment. Plan: claude-notes/plans/2026-05-18-bare-lt-as-str.md","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2026-05-18T20:54:00.031438Z","created_by":"cscheid","updated_at":"2026-05-18T21:01:37.931022Z","source_repo":".","compaction_level":0,"original_size":0}
 {"id":"bd-jakt","title":"Investigate cargo xtask dev-setup --locked causing externref mismatch in TS Test Suite","description":"cargo xtask dev-setup installs wasm-bindgen-cli with --locked flag. When used in ts-test-suite.yml instead of the hardcoded cargo install wasm-bindgen-cli --version 0.2.108 (without --locked), the hub-client WASM tests fail with: CompileError: WebAssembly.instantiate(): call[0] expected type externref, found local.get of type i32. Reverted in PR 109. Root cause unknown — the --locked flag may produce a subtly different binary.","status":"open","priority":2,"issue_type":"bug","created_at":"2026-04-09T17:59:23.500651300Z","created_by":"cderv","updated_at":"2026-04-09T17:59:23.500651300Z","source_repo":".","compaction_level":0,"original_size":0}
 {"id":"bd-jbml","title":"Navbar index-forgiveness (about/ == about/index.html)","description":"Phase 3 Navbar active-state uses strict source-path equality. Q1's itemHasNavTarget additionally treats 'about/' and 'about/index.html' as equivalent for active-marking. Revisit if a real site hits the edge case. See 2026-04-24-websites-phase-3.md follow-ups.","status":"open","priority":3,"issue_type":"task","created_at":"2026-04-24T19:42:58.920114Z","created_by":"cscheid","updated_at":"2026-04-24T19:42:58.920114Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-jbml","depends_on_id":"bd-fqyg","type":"discovered-from","created_at":"2026-04-24T19:42:58.920114Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
+{"id":"bd-je48v","title":"Add mermaid diagram engine to Quarto 2","description":"Design + ship a minimal mermaid engine that handles `{mermaid}` code blocks. First cut: HTML-only, defers actual diagram rendering to the browser via jsdelivr mermaid.js script. Architectural goal: maintain Q2's format-agnostic AST processing vs. format-specific emission decomposition so the engine doesn't need to be reworked when non-HTML formats are added.\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md\n\nThis epic is intentionally also a design session — the mermaid case forces us to confront several engine-API gaps (see follow-up issues filed as discovered-from the design task).","status":"open","priority":2,"issue_type":"epic","created_at":"2026-05-28T13:44:39.930118Z","created_by":"cscheid","updated_at":"2026-05-28T13:44:39.930118Z","source_repo":".","compaction_level":0,"original_size":0}
 {"id":"bd-jfyl","title":"Footer Text-region project-link rewriting","description":"Phase 3 decision 8 excluded footer Text regions (string-valued left/center/right) from .qmd → .html rewriting — that's Phase 6's body-content link rewrite territory. Once Phase 6 lands, audit whether footer Text regions should get the same treatment or stay as literal markdown. Decision to defer was recorded in 2026-04-24-websites-phase-3.md plan §Decision 8 and §Risks.","status":"open","priority":3,"issue_type":"task","created_at":"2026-04-24T19:42:43.935902Z","created_by":"cscheid","updated_at":"2026-04-24T19:42:43.935902Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-jfyl","depends_on_id":"bd-fqyg","type":"discovered-from","created_at":"2026-04-24T19:42:43.935902Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-jgeu","title":"Home-link relativization for sidebar title + navbar brand","description":"Two related hardcoded fallbacks in crates/quarto-navigation/src/render_html.rs emit project-root-relative or current-dir-relative hrefs that don't account for the rendering page's depth:\n\n1. Sidebar title (line 211): hardcoded . From posts/aardvark.html, points back to posts/ instead of the site root. Reproduces in examples/websites/02-auto-sidebar (compare _site/ vs q1-site/).\n\n2. render_brand (line 297): navbar.logo_href.as_deref().unwrap_or(\"/\") falls back to absolute /, which is deployment-fragile (only works for domain-rooted hosts; breaks on file://, GitHub Pages project sites, sub-path deployments).\n\nPlus an adjacent gap in NavbarRenderTransform (crates/quarto-core/src/transforms/navbar_render.rs:114): rewrite_navigation_item_hrefs walks navbar.left/right/dropdown menus through resolve_href_for_html but doesn't touch navbar.logo_href, so user-supplied logo-href: about.qmd doesn't get the .qmd->.html rewrite or page-relative URL treatment.\n\nSame root-cause family as bd-swpy (closed). Sweep of render_html.rs + main HTML template confirmed these are the only home-link-style hardcodes; other unwrap_or(\"#\")/unwrap_or(\"\") fallbacks are correct no-op anchors and out of scope.\n\nFix plan: claude-notes/plans/2026-04-30-sidebar-title-home-link-relativize.md. Add ResourceResolverContext::page_url_for_site_root_dir; thread home_url: &str through sidebar_to_html and navbar_to_html; compute home_url from ctx.resource_resolver in SidebarRenderTransform and NavbarRenderTransform; add navbar.logo_href to the navbar transform's resolver-rewrite walk.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-04-30T19:31:25.256931Z","created_by":"cscheid","updated_at":"2026-04-30T19:52:29.349611Z","closed_at":"2026-04-30T19:52:29.349437Z","close_reason":"Fixed in feature/websites. Added ResourceResolverContext::page_url_for_site_root_dir; threaded home_url through sidebar_to_html and navbar_to_html (default ./ for no-resolver/single-doc); SidebarRenderTransform and NavbarRenderTransform compute home_url from ctx.resource_resolver; navbar.logo_href now goes through resolve_href_for_html (same .qmd->.html + page-relative treatment as ordinary nav items). 17 new unit tests pass. End-to-end verified on 02-auto-sidebar (sidebar-title: ./ at root, ../ in posts/), 04-navbar-footer (navbar-brand: ./ at root, ../ at depth-1), and 03-nested-sidebar (depth-1 sidebars all emit ../). 8140/8140 workspace tests pass; cargo xtask verify (full) passes.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-jgeu","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-30T19:31:28.472953Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-jgeu","depends_on_id":"bd-swpy","type":"discovered-from","created_at":"2026-04-30T19:31:25.256931Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-jjep","title":"q2: website.navbar / website.page-footer (nested form) not recognised by chrome transforms","description":"Discovered while smoke-testing Phase F.2 against docs/_quarto.yml. q2 NavbarGenerateTransform reads top-level meta.navbar (and FooterGenerateTransform reads top-level meta.page-footer), but TS Quarto and the docs site author these nested under website.navbar / website.page-footer (the historical schema). Result: docs/index.qmd renders without a navbar or footer under both q2 render AND q2 preview.\n\nTwo options:\n1. Add a metadata-normalize step that hoists website.navbar → navbar / website.page-footer → page-footer when the top-level form is absent.\n2. Update NavbarGenerateTransform / FooterGenerateTransform to also check the website.* nested paths.\n\nOption 1 is the smaller change. Path: crates/quarto-core/src/transforms/metadata_normalize.rs.\n\nEmpirical evidence (this worktree, 2026-05-14): running cargo run --bin q2 -- render docs/about.qmd produces an HTML file with 0 occurrences of 'navbar' or 'footer.footer'. The same fixture renders correctly under TS Quarto.\n\nNot a Phase F regression — q2 has never supported the nested form. Filed so the docs site authoring loop isn't blocked indefinitely.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-05-14T22:28:53.901801Z","created_by":"cscheid","updated_at":"2026-05-19T15:13:00.358360Z","closed_at":"2026-05-19T15:13:00.358215Z","close_reason":"Implemented in d66ff31c: quarto_config::resolve_website_value() merges meta. and meta.website. with top-level winning on conflicts. Rewired resolve_navbar, resolve_page_footer, resolve_sidebar_membership, SidebarGenerateTransform, and derive_doc_scss_layer. End-to-end verified against docs/_quarto.yml.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-jjep","depends_on_id":"bd-kw93.15","type":"discovered-from","created_at":"2026-05-14T22:28:53.901801Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
@@ -333,12 +340,14 @@
 {"id":"bd-mlj6","title":"Conditional render lists / _quarto-*.yml profiles","description":"Q1 supports profile-specific renders via _quarto-.yml (e.g. _quarto-prod.yml vs _quarto-dev.yml). Phase 1 of the website epic ignores profile files entirely — discovery excludes them because of the leading underscore, and config parsing only reads _quarto.yml. Follow-up: decide how profiles compose with the base config, add CLI flag for selecting profile, thread through ProjectContext::discover. See claude-notes/plans/2026-04-23-websites-phase-1.md §Decisions log.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-04-24T01:05:24.479391Z","created_by":"cscheid","updated_at":"2026-04-24T01:05:24.479391Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mlj6","depends_on_id":"bd-w5os","type":"discovered-from","created_at":"2026-04-24T01:05:24.479391Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-mot7","title":"qmd reader drops whitespace between code_span and html_element (issue #182)","description":"(see triage doc)","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-05-12T13:42:59.836284Z","created_by":"cscheid","updated_at":"2026-05-12T13:43:26.216838Z","closed_at":"2026-05-12T13:43:26.216709Z","close_reason":"Duplicate, accidental second create. Keeping bd-nkx4.","source_repo":".","compaction_level":0,"original_size":0}
 {"id":"bd-mqa4j","title":"Phase 8 — quarto-sync-client header pass-through + connection-manager integration","description":"Add auth?.getBearer option to client.connect(); new Node-only NodeWebSocketClientAdapter inside quarto-sync-client that constructs new WebSocket(url, [], { headers }). Browser path unchanged.\n\nConnection-manager try-then-fallback policy: read bundle, attempt WS with Bearer if present, on 401-with-creds forceRefresh+retry-once then ReauthRequired, on 401-without-creds AuthRequired.\n\nlastObservedAuthMode state machine ({no-auth, requires-auth, unknown}, process-local) drives Phase 7's short-circuit.\n\nInsecure-Bearer gate: refuse to send Bearer over plain HTTP/WS to non-loopback without QUARTO_HUB_MCP_ALLOW_INSECURE_AUTH=1; loud warning on every connect when set.\n\nFollow-up: upstream PR to thread headers through BrowserWebSocketClientAdapter — file separately.\n\nPlan §Phase 8: claude-notes/plans/2026-05-05-hub-mcp-device-flow-implementation.md","status":"open","priority":1,"issue_type":"task","created_at":"2026-05-20T14:27:25.451968Z","created_by":"shikokuchuo","updated_at":"2026-05-20T14:27:25.451968Z","source_repo":"kyoto","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mqa4j","depends_on_id":"bd-cmp48","type":"parent-child","created_at":"2026-05-20T14:27:25.451968Z","created_by":"shikokuchuo","metadata":"{}","thread_id":""}]}
+{"id":"bd-mqk49","title":"Design: how should engines/extensions declare additional pipeline stages?","description":"build_html_pipeline_stages() in crates/quarto-core/src/pipeline.rs:207-300 is a hardcoded 19-stage list. Stages can be gated by config branch (e.g. native-only), but no engine, extension, or plugin can register a new stage.\n\nFor format-specific work this is increasingly painful: a diagram engine like mermaid wants to add a marker -> HTML conversion that should fire only on HTML output. The current options are (a) add the stage to the canonical list and gate it, (b) put the work in AstTransformsStage which conflates engine-specific behavior with built-in transforms.\n\nA proper extension API would let engines/extensions declare:\n- a stage name + input/output kinds\n- a position (after-X / before-Y, or by precedence)\n- a per-format applicability predicate\n\nDiscovered during mermaid engine design — see claude-notes/plans/2026-05-28-mermaidjs-engine-design.md gap G3 and G4.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","updated_at":"2026-05-28T13:45:56.898020Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mqk49","depends_on_id":"bd-c6h96","type":"discovered-from","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-mre3","title":"[websites phase 2] Sidebar data model, generate, render, template","description":"Plan: claude-notes/plans/2026-04-23-website-project-epic.md § Phase 2.\n\nDeliverables:\n- Schema parsing for website.sidebar: Vec with id, title, contents, style, collapse-level.\n- Contents supports: string (path), {href, text, icon}, {section, contents}, {auto: ...}.\n- quarto-navigation data types: Sidebar, SidebarEntry, SidebarContents.\n- SidebarGenerateTransform: reads YAML config + ProjectIndex to resolve auto and expand entries.\n- SidebarRenderTransform: emits Bootstrap-5 HTML matching Q1 class names where possible.\n- Template slot: $rendered.navigation.sidebar$.\n- Sidebar-for-page selection logic.\n- Integration tests with manual, auto, and nested contents.\n\nBlocked by Phase 1 (needs ProjectIndex).","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-04-23T18:42:49.915763Z","created_by":"cscheid","updated_at":"2026-04-29T00:31:30.148471Z","closed_at":"2026-04-29T00:31:30.148151Z","close_reason":"Phase 2 (sidebar) implemented — see git log Phase 2 sub-phase commits and claude-notes/plans/2026-04-24-websites-phase-2.md (closed as part of Phase 9 cleanup; this should have been closed earlier).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mre3","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-23T18:42:49.915763Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-mre3","depends_on_id":"bd-w5os","type":"blocks","created_at":"2026-04-23T18:43:42.284820Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-mrx1","title":"Phase B.4: Acceptance bundle — _quarto.yml + posts/_metadata.yml propagation to preview","description":"Single Playwright spec pinning the two observable plan acceptance criteria for q2 preview (Phase B):\n\n1. Editing _quarto.yml (title:) re-renders the active page; new title visible in DOM within 5 s.\n2. Editing posts/_metadata.yml (subtitle:) re-renders pages under posts/; new subtitle visible in DOM within 5 s.\n\nFixture (single, dual-purpose): _quarto.yml + posts/_metadata.yml + posts/post1.qmd (no frontmatter, inherits both). Empirically verified both knobs flow through to the rendered title-block (2026-05-13 probe with target/debug/q2 render).\n\nReuses the multi-file fixture helper generalised in bd-pf63 (B.3).\n\nPlan acceptance criterion 3 ('unrelated sibling re-renders the active page') is deferred and tracked separately — its relaxed-contract form (any edit fires a re-render) is invisible at the DOM without SPA instrumentation. See discovered-from follow-up.\n\nPlan: claude-notes/plans/2026-05-13-q2-preview-phase-b.md §B.4.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-13T21:11:28.867999Z","created_by":"cscheid","updated_at":"2026-05-13T21:15:09.863020Z","closed_at":"2026-05-13T21:15:09.862892Z","close_reason":"Phase B.4 complete; zero production-code changes — new Playwright spec at q2-preview-spa/e2e/config-edits.spec.ts pins _quarto.yml + posts/_metadata.yml propagation. See commit df2f5f55 on beads/bd-mrx1-phase-b4-acceptance-bundle.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mrx1","depends_on_id":"bd-kw93","type":"parent-child","created_at":"2026-05-13T21:11:28.867999Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-mrx1","depends_on_id":"bd-pf63","type":"discovered-from","created_at":"2026-05-13T21:11:28.867999Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-msp0","title":"Hub-client preview resource resolution via service worker (epic)","description":"Long-term direction: replace the iframe post-processor's resource-rewriting passes (CSS data-URIs, image data-URIs, the disabled ') at the end of the body with the jsdelivr mermaid@11 script and initialize({startOnLoad: true}).\n- Add an HTML-render conversion: marker Div -> 
...
. Decide whether this lives in a sub-stage or in the HTML writer.\n- Tests: unit (transform on fixture AST) + integration (cargo run --bin q2 -- render fixture.qmd, grep for
) + record the end-to-end invocation in the plan per CLAUDE.md verification policy.\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","updated_at":"2026-05-28T13:45:36.396458Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-gwfdo","depends_on_id":"bd-c6h96","type":"blocks","created_at":"2026-05-28T13:45:35.978499Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-fztki","type":"blocks","created_at":"2026-05-28T13:45:36.396009Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
+{"id":"bd-gwfdo","title":"Implement MermaidEngine + MermaidHtmlEmitStage","description":"Implement mermaid handling per the ratified v2 design (see bd-c6h96 outcome and plan v2). **Gated on PR #238 merging.**\n\nExpected shape (Q-A → A1 engine impl, Q-B → B2c dedicated HTML-emit stage, Q-C → C1 inline script RawBlock):\n\n1. Add MermaidEngine implementing ExecutionEngine in crates/quarto-core/src/engine/mermaid/ (mirror engine/markdown.rs shape — closest precedent for a no-subprocess engine):\n   - name() == 'mermaidjs'.\n   - execute(input, ctx): parse input as QMD, walk for code cells with class 'mermaid', replace each with a Div.mermaid wrapping the original source as a code block (B2), append a once-per-doc RawBlock(HTML, '') at end of body (C1). Serialize back to QMD; return as ExecuteResult { markdown, ..Default }.\n   - Register in native + WASM EngineRegistry.\n\n2. Add MermaidHtmlEmitStage between AstTransformsStage (15) and RenderHtmlBodyStage (18) — exact slot resolved at impl time (whether user filters should see the marker Div or the rendered 
). The stage walks for Div.mermaid and rewrites to RawBlock(HTML, '
...
').\n\n3. Tests: unit (engine on fixture) + pipeline (full HTML render) + end-to-end per CLAUDE.md verification policy (cargo run --bin q2 -- render fixture.qmd; grep output; record in plan).\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","updated_at":"2026-05-28T16:16:38.836217Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-gwfdo","depends_on_id":"bd-c6h96","type":"blocks","created_at":"2026-05-28T13:45:35.978499Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-fztki","type":"blocks","created_at":"2026-05-28T13:45:36.396009Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-gz6k","title":"Cargo: upgrade sha1 v0.10.6 → v0.11.0","description":"Major upgrade surfaced by cargo-upgrade survey 2026-05-04. Current 0.10.6 is range-pinned in workspace; latest is 0.11.0. 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.170269Z","created_by":"cscheid","updated_at":"2026-05-04T20:30:44.941246Z","closed_at":"2026-05-04T20:30:44.941104Z","close_reason":"merged: 0812812e","source_repo":".","compaction_level":0,"original_size":0,"labels":["cargo","deps"],"dependencies":[{"issue_id":"bd-gz6k","depends_on_id":"bd-hb8h","type":"discovered-from","created_at":"2026-05-04T18:16:05.443720Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-h4l6","title":"[websites phase 5] Scoped artifact store and site_libs/ dedup","description":"Plan: claude-notes/plans/2026-04-23-website-project-epic.md § Phase 5.\n\nDeliverables:\n- Add scope (Page | Project) to artifact entries in ArtifactStore.\n- Project-aware artifact writer: Project-scoped artifacts emitted once to _site/site_libs/{name}/...\n- Relocator: rewrite per-page HTML to point at shared site_libs/ paths correctly (handles subdirs, offset computation).\n- Migrate theme CSS, Bootstrap, quarto-nav JS, etc. to Project scope when inside a website project.\n- Preservation: single-doc renders unchanged (both scopes resolve under {stem}_files/).\n- Sequence the change in two steps: (a) pure refactor introducing scope with identical behavior, (b) switch websites to use Project scope.\n\nBlocked by Phase 1.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-04-23T18:43:08.936956Z","created_by":"cscheid","updated_at":"2026-04-29T00:31:30.479325Z","closed_at":"2026-04-29T00:31:30.479019Z","close_reason":"Phase 5 (scoped artifact store + site_libs) implemented (commit dc4e81b0). Closed as part of Phase 9 cleanup.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-h4l6","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-23T18:43:08.936956Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-h4l6","depends_on_id":"bd-w5os","type":"blocks","created_at":"2026-04-23T18:43:44.336998Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-h5l7","title":"Diagnose and fix SourceInfo::eq hotspot in hub-client preview","description":"Chrome profile of hub-client on a moderately-sized document shows a large hotspot on ::eq, entered from parse_qmd_to_ast (crates/wasm-quarto-hub-client/src/lib.rs:757). Likely cause: SourceInfoSerializer::intern in crates/pampa/src/writers/json.rs:229 linearly scans content_map (Vec<(SourceInfo, usize)>) on every pointer-lookup miss, and SourceInfo is held by-value in AST nodes so pointer lookups almost always miss — giving O(n²) behavior per document. Also the first perf-profiling session on Quarto 2, so the plan deliberately establishes a repeatable native-side workflow (Criterion bench + samply flamegraph on the pampa binary) before any fix, since Chrome-side before/after is too noisy to iterate on. Plan: claude-notes/plans/2026-04-22-sourceinfo-eq-hotspot.md. Currently in draft — awaiting user review before any measurement work begins.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-04-22T20:00:48.019984Z","created_by":"cscheid","updated_at":"2026-04-22T23:23:38.315697Z","closed_at":"2026-04-22T23:23:38.315082Z","close_reason":"Landed on perf/2026-04-22-json-sourcemap in commit 8f6f21d9. Browser cross-validation confirmed SourceInfo::eq no longer a hotspot.","source_repo":".","compaction_level":0,"original_size":0} @@ -340,14 +340,14 @@ {"id":"bd-mlj6","title":"Conditional render lists / _quarto-*.yml profiles","description":"Q1 supports profile-specific renders via _quarto-.yml (e.g. _quarto-prod.yml vs _quarto-dev.yml). Phase 1 of the website epic ignores profile files entirely — discovery excludes them because of the leading underscore, and config parsing only reads _quarto.yml. Follow-up: decide how profiles compose with the base config, add CLI flag for selecting profile, thread through ProjectContext::discover. See claude-notes/plans/2026-04-23-websites-phase-1.md §Decisions log.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-04-24T01:05:24.479391Z","created_by":"cscheid","updated_at":"2026-04-24T01:05:24.479391Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mlj6","depends_on_id":"bd-w5os","type":"discovered-from","created_at":"2026-04-24T01:05:24.479391Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-mot7","title":"qmd reader drops whitespace between code_span and html_element (issue #182)","description":"(see triage doc)","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-05-12T13:42:59.836284Z","created_by":"cscheid","updated_at":"2026-05-12T13:43:26.216838Z","closed_at":"2026-05-12T13:43:26.216709Z","close_reason":"Duplicate, accidental second create. Keeping bd-nkx4.","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-mqa4j","title":"Phase 8 — quarto-sync-client header pass-through + connection-manager integration","description":"Add auth?.getBearer option to client.connect(); new Node-only NodeWebSocketClientAdapter inside quarto-sync-client that constructs new WebSocket(url, [], { headers }). Browser path unchanged.\n\nConnection-manager try-then-fallback policy: read bundle, attempt WS with Bearer if present, on 401-with-creds forceRefresh+retry-once then ReauthRequired, on 401-without-creds AuthRequired.\n\nlastObservedAuthMode state machine ({no-auth, requires-auth, unknown}, process-local) drives Phase 7's short-circuit.\n\nInsecure-Bearer gate: refuse to send Bearer over plain HTTP/WS to non-loopback without QUARTO_HUB_MCP_ALLOW_INSECURE_AUTH=1; loud warning on every connect when set.\n\nFollow-up: upstream PR to thread headers through BrowserWebSocketClientAdapter — file separately.\n\nPlan §Phase 8: claude-notes/plans/2026-05-05-hub-mcp-device-flow-implementation.md","status":"open","priority":1,"issue_type":"task","created_at":"2026-05-20T14:27:25.451968Z","created_by":"shikokuchuo","updated_at":"2026-05-20T14:27:25.451968Z","source_repo":"kyoto","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mqa4j","depends_on_id":"bd-cmp48","type":"parent-child","created_at":"2026-05-20T14:27:25.451968Z","created_by":"shikokuchuo","metadata":"{}","thread_id":""}]} -{"id":"bd-mqk49","title":"Design: how should engines/extensions declare additional pipeline stages?","description":"build_html_pipeline_stages() in crates/quarto-core/src/pipeline.rs:207-300 is a hardcoded 19-stage list. Stages can be gated by config branch (e.g. native-only), but no engine, extension, or plugin can register a new stage.\n\nFor format-specific work this is increasingly painful: a diagram engine like mermaid wants to add a marker -> HTML conversion that should fire only on HTML output. The current options are (a) add the stage to the canonical list and gate it, (b) put the work in AstTransformsStage which conflates engine-specific behavior with built-in transforms.\n\nA proper extension API would let engines/extensions declare:\n- a stage name + input/output kinds\n- a position (after-X / before-Y, or by precedence)\n- a per-format applicability predicate\n\nDiscovered during mermaid engine design — see claude-notes/plans/2026-05-28-mermaidjs-engine-design.md gap G3 and G4.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","updated_at":"2026-05-28T13:45:56.898020Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mqk49","depends_on_id":"bd-c6h96","type":"discovered-from","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-mqk49","title":"Design: how should engines/extensions declare additional pipeline stages?","description":"build_html_pipeline_stages() in crates/quarto-core/src/pipeline.rs is a hardcoded ~19-stage list. Stages can be gated by config (e.g. native-only), but no engine, extension, or plugin can register a new stage.\n\n**More relevant after PR #238 (sequential multi-engine execution).** With engines as first-class first-class pipeline citizens, the natural mechanism for 'format-agnostic engine output + format-specific emission' (Q-B in the mermaid plan) is to let an engine declare a per-format AST pass on its output. For mermaid: 'when emitting HTML, run this transform on my output blocks.' For graphviz/plantuml/dot later: same shape.\n\nA proper extension API would let engines/extensions declare:\n- a stage name + input/output kinds\n- a position (after-X / before-Y, or by precedence)\n- a per-format applicability predicate\n- a per-engine attribution back-reference (so trace events name the engine that contributed the stage)\n\nThe mermaid first ship (bd-gwfdo) will likely use a fixed MermaidHtmlEmitStage in the canonical pipeline (Q-B option B2c) instead of waiting on this. That's the right pragmatic move — but bd-mqk49 is the architecturally correct landing zone for diagram-like engines.\n\nDiscovered during mermaid engine design — see claude-notes/plans/2026-05-28-mermaidjs-engine-design.md gap G3.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","updated_at":"2026-05-28T16:16:25.594880Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mqk49","depends_on_id":"bd-c6h96","type":"discovered-from","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-mre3","title":"[websites phase 2] Sidebar data model, generate, render, template","description":"Plan: claude-notes/plans/2026-04-23-website-project-epic.md § Phase 2.\n\nDeliverables:\n- Schema parsing for website.sidebar: Vec with id, title, contents, style, collapse-level.\n- Contents supports: string (path), {href, text, icon}, {section, contents}, {auto: ...}.\n- quarto-navigation data types: Sidebar, SidebarEntry, SidebarContents.\n- SidebarGenerateTransform: reads YAML config + ProjectIndex to resolve auto and expand entries.\n- SidebarRenderTransform: emits Bootstrap-5 HTML matching Q1 class names where possible.\n- Template slot: $rendered.navigation.sidebar$.\n- Sidebar-for-page selection logic.\n- Integration tests with manual, auto, and nested contents.\n\nBlocked by Phase 1 (needs ProjectIndex).","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-04-23T18:42:49.915763Z","created_by":"cscheid","updated_at":"2026-04-29T00:31:30.148471Z","closed_at":"2026-04-29T00:31:30.148151Z","close_reason":"Phase 2 (sidebar) implemented — see git log Phase 2 sub-phase commits and claude-notes/plans/2026-04-24-websites-phase-2.md (closed as part of Phase 9 cleanup; this should have been closed earlier).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mre3","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-23T18:42:49.915763Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-mre3","depends_on_id":"bd-w5os","type":"blocks","created_at":"2026-04-23T18:43:42.284820Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-mrx1","title":"Phase B.4: Acceptance bundle — _quarto.yml + posts/_metadata.yml propagation to preview","description":"Single Playwright spec pinning the two observable plan acceptance criteria for q2 preview (Phase B):\n\n1. Editing _quarto.yml (title:) re-renders the active page; new title visible in DOM within 5 s.\n2. Editing posts/_metadata.yml (subtitle:) re-renders pages under posts/; new subtitle visible in DOM within 5 s.\n\nFixture (single, dual-purpose): _quarto.yml + posts/_metadata.yml + posts/post1.qmd (no frontmatter, inherits both). Empirically verified both knobs flow through to the rendered title-block (2026-05-13 probe with target/debug/q2 render).\n\nReuses the multi-file fixture helper generalised in bd-pf63 (B.3).\n\nPlan acceptance criterion 3 ('unrelated sibling re-renders the active page') is deferred and tracked separately — its relaxed-contract form (any edit fires a re-render) is invisible at the DOM without SPA instrumentation. See discovered-from follow-up.\n\nPlan: claude-notes/plans/2026-05-13-q2-preview-phase-b.md §B.4.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-13T21:11:28.867999Z","created_by":"cscheid","updated_at":"2026-05-13T21:15:09.863020Z","closed_at":"2026-05-13T21:15:09.862892Z","close_reason":"Phase B.4 complete; zero production-code changes — new Playwright spec at q2-preview-spa/e2e/config-edits.spec.ts pins _quarto.yml + posts/_metadata.yml propagation. See commit df2f5f55 on beads/bd-mrx1-phase-b4-acceptance-bundle.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mrx1","depends_on_id":"bd-kw93","type":"parent-child","created_at":"2026-05-13T21:11:28.867999Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-mrx1","depends_on_id":"bd-pf63","type":"discovered-from","created_at":"2026-05-13T21:11:28.867999Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-msp0","title":"Hub-client preview resource resolution via service worker (epic)","description":"Long-term direction: replace the iframe post-processor's resource-rewriting passes (CSS data-URIs, image data-URIs, the disabled ")` + at the end of the body, inline in its `markdown` output. Survives + the capture-splice path because `result.markdown` is the only + field the splice preserves. +- **C2.** The engine populates `ExecuteResult.includes` + (`include_after_body`). Cleaner on native but **silently lost on + q2 preview replay** until bd-cp3em is fixed. - **C3.** Engine returns the include in `ExecuteResult.includes` - on the engine's first invocation per document. Requires - Q-A to land on A1/A2 (engine path). + *and* we fix bd-cp3em as part of this work. Cleanest. Adds scope. -**Recommendation:** C1 for the first cut. It works uniformly on -native and q2-preview, doesn't depend on G6, and is observable in -the AST (debuggable). C2 is cleaner but blocked on G6. +**Recommendation for the first ship:** C1. Zero new infrastructure; +correct on both `q2 render` and `q2 preview` from day one; +debuggable in the AST. C2/C3 are a follow-up after bd-cp3em. ### Q-D. WASM replay coverage -Even with A3+B2+C1, the q2-preview path needs verification: - -- Native `q2 render` produces correct HTML — should be straightforward - to test. -- `q2 preview` of a doc containing both `{r}` (replayed) and - `{mermaid}` blocks should still produce a working diagram. Since - the mermaid transform runs *after* `EngineExecutionStage` and is - pure AST→AST, it should fire identically in the WASM pipeline. - But we have to confirm that: - - The WASM pipeline includes the mermaid transform stage. - - `CaptureSpliceStage` doesn't accidentally remove the mermaid - code block (it splices recorded *output*; the input mermaid - block should be passed through unchanged because the mermaid - handler is not the doc-level engine). -- Multi-engine work in flight (bd-5yff4) may already address some - of this. Read that plan before settling on the test matrix. +PR #238's bd-iq0hp documents that a browser E2E of *multi-engine* +preview was not possible at PR time — the default registry uses real +engines, `FixtureEngine` is test-only, and knitr/jupyter don't compose +cleanly. **Our preview-e2e task (bd-my0o5) closes bd-iq0hp**: mermaid ++ knitr is the first real-engine pair that composes cleanly because +cell-class ownership doesn't overlap. + +Test matrix: + +1. Document with mermaid block only — `q2 preview` renders the + diagram. +2. Document with `engine: [knitr, mermaidjs]`, knitr block + mermaid + block, with recorded captures — both render in preview. +3. Edit the mermaid block — preview updates without re-running + knitr's capture (capture invariance on engine 1's input). ## Proposed phased work ### Phase 0 — design ratification (this session) -- [ ] Confirm or revise the gap analysis (G1–G6) by reading the - actual code under the citations above. Anything that's been - fixed since the agent reports were generated needs updating. -- [ ] User decides on Q-A (architectural framing), Q-B (split), Q-C - (script include strategy). -- [ ] Update this plan with the decisions, then open implementation - beads. +- [x] v1 plan written +- [x] PR #238 surfaced; v2 revision applied +- [x] Decisions locked: A1 (engine impl), B2c (dedicated HTML-emit + stage), C1 (inline script RawBlock), D=bd-iq0hp closure +- [ ] User confirms Q-B recommendation (B2c) before impl ### Phase 1 — multi-engine current-state audit -- [ ] Read `claude-notes/plans/` entries for bd-5yff4 ("multi-engine - preview" work) and the related commits (c86e1d96, 51abb673, - 30a57abc, ed9cbbfe). Summarize the current state of per-engine - capture and what's still TBD. Add findings to this plan. -- [ ] Re-verify whether the `EngineRegistry` API has changed since - the research-agent report; recheck the registry construction - and engine name set. - -### Phase 2 — mermaid transform implementation (assuming A3/A4 + B2 + C1) - -- [ ] Add a new pipeline stage `MermaidTransformStage` (or inline - into `AstTransformsStage`) that: - - Walks the AST for `CodeBlock` nodes with class `mermaid`. - - Rewrites each into a `Div` with class `mermaid` wrapping the - code text (B2 format-agnostic step). - - On first sight of any mermaid block in a document, appends a - `RawBlock(HTML, …)` at the end of the body with the jsdelivr - script tag (C1). -- [ ] Add HTML-render conversion: either a sub-stage that turns the - `Div.mermaid` into `RawBlock(HTML, "
")`, - or special-case the HTML writer. +- [ ] Read `claude-notes/plans/2026-05-27-multi-engine-execution.md` + (on `feature/multi-engine`) and confirm: + - Where `MermaidEngine` would register in + `EngineRegistry::register_default`-equivalent (native + WASM + builds both need it — mermaid is pure-Rust, no subprocess, so + the WASM build can register it too). + - The per-engine FileId provenance scheme (`..rmarkdown`) + and what mermaid's intermediate name should be. + - That `result.markdown` is the QMD-text re-parsed for the next + engine — i.e. the mermaid engine should emit QMD, not Pandoc-JSON. +- [ ] Verify the new `capture_splice.rs` actually drops aux fields + (re-confirm bd-cp3em is post-PR-#238 relevant) — already done + in v2 revision, but spot-check at impl time. +- [ ] If PR #238's review surfaces design changes that affect mermaid, + reflect them here. + +### Phase 2 — `MermaidEngine` implementation (assumes Q-B → B2c, Q-C → C1) + +- [ ] Add `MermaidEngine` implementing `ExecutionEngine` in + `crates/quarto-core/src/engine/mermaid/` (mirror the + `markdown.rs` shape — it's the closest precedent for a + no-subprocess engine). + - `name() == "mermaidjs"`. + - `execute(input, ctx)`: parse `input` as QMD, walk for code cells + with class `mermaid`, replace each cell with a `Div.mermaid` + wrapping the original source as a code block (B2), and append a + once-per-doc `` `RawBlock` at + end of body (C1). Serialize back to QMD and return as + `ExecuteResult { markdown, ..Default }`. + - Register in native + WASM `EngineRegistry`s. +- [ ] Add `MermaidHtmlEmitStage` between `AstTransformsStage` and + `RenderHtmlBodyStage` (or between user-filters-post and + code-highlight — confirm exact slot during impl). The stage + walks for `Div.mermaid` and rewrites to + `RawBlock(HTML, "
")`. - [ ] Tests: - - Unit: transform on a fixture AST. - - Integration: full `q2 render` of a fixture qmd containing - `{mermaid}` blocks; inspect generated HTML for `
`
-    and the script tag.
-  - **End-to-end per CLAUDE.md**: actually run `cargo run --bin q2 -- render fixture.qmd`
-    and grep the output. Record the invocation + observed output in
-    this plan.
-
-### Phase 3 — q2-preview verification
-
-- [ ] Write a test that runs a mermaid-containing fixture through
-      the `q2-preview` pipeline (with and without a capture). Verify
-      the mermaid blocks survive and the script include is present.
-- [ ] If G6 or related capture-splice gaps block this, decide
-      between fixing the gap now vs. accepting a known limitation
-      tracked as a separate beads issue.
+  - Unit: mermaid engine on a fixture qmd containing `{mermaid}` and
+    non-mermaid blocks — only `{mermaid}` cells touched; script tag
+    appended.
+  - Pipeline: render a fixture qmd through the full HTML pipeline;
+    HTML contains `
` and the script tag. + - **End-to-end per CLAUDE.md**: `cargo run --bin q2 -- render fixture.qmd`, + grep the actual output, record invocation + observed output in + this plan before claiming done. + +### Phase 3 — q2-preview verification (closes bd-iq0hp) + +- [ ] Per the Q-D test matrix above. The fact that mermaid+knitr is + the first cleanly-composing real-engine pair makes this work + the canonical multi-engine browser preview E2E. ### Phase 4 — documentation -- [ ] User-facing docs page under `docs/` for the mermaid engine. -- [ ] Architecture note in `claude-notes/designs/` if the - ExecutionEngine extension story moves (i.e. if Q-A lands on - A1/A2 in a follow-up). - -### Follow-up issues (separate beads — not blockers for shipping mermaid via A3) - -- [ ] **G1**: Wire `ExecuteResult.filters` into `filter_resolve.rs` - (or remove the field if we decide the architectural direction - is different). -- [ ] **G2**: Define a `SystemRuntime`/`Vfs` publication API that - pipeline producers can call. -- [ ] **G3**: Define an engine→pipeline stage extension API - (probably tied to whichever direction Q-A lands on). -- [ ] **G6**: Make `CaptureSpliceStage` surface `includes` / - `filters` / `supporting_files` from the recorded - `ExecuteResult` through `StageContext`. Tracked separately - because it has its own design. +- [ ] User-facing docs page under `docs/`. Render with + `cargo run --bin q2 -- render docs/` (Q2, not Q1). + +### Follow-up issues (separate beads — not blockers for shipping +mermaid via A1/B2c/C1) + +- **G1** (bd-14rer): wire `ExecuteResult.filters` into resolver, or + remove the field. Knitr currently drops `rmarkdown/pagebreak.lua` + silently. +- **G2** (bd-s8llm): VFS publication API for pipeline producers. +- **G3** (bd-mqk49): engine→pipeline stage extension API. Upgrades + Q-B from B2c (dedicated stage) to B2e (engine-declared stage). +- **G6** (bd-cp3em): make capture-splice surface `includes` / + `filters` / `supporting_files`. Once landed, C1 → C2/C3 becomes + the cleaner mermaid implementation. ## Open questions -1. Is there a stronger reason than "Q1 did it this way" to model - mermaid as an *engine* rather than a transform? The user framed - it as an engine — was that mechanical-API language ("a thing - that handles `{mermaid}` blocks") or architectural intent ("a - first-class `ExecutionEngine` impl")? -2. Should the script-tag include be inside the document body - (`RawBlock` at end of body) or via template (`include-after-body` - metadata)? Body-inline is simpler and survives any template; - metadata is more idiomatic. We probably want body-inline for the - first cut. -3. The user mentioned `mermaid@11` pinned at jsdelivr. Should the - version be configurable per-document or hardcoded? Defer to a - user-facing config follow-up. -4. Mermaid supports init options (theme, fontFamily, etc.). The - first cut hardcodes `initialize({ startOnLoad: true })`. Defer - options to a follow-up. +1. Should the jsdelivr URL be configurable per-document + (`mermaid: { src: ..., version: 11 }`)? Defer. +2. Should `mermaid.initialize({...})` options be configurable + (theme, fontFamily, …)? Defer. +3. Does WASM build need to register `MermaidEngine` separately, or + does the engine being subprocess-free let us share a registration? + Resolve at impl time; expected: yes, share. +4. Where in the canonical pipeline does `MermaidHtmlEmitStage` slot? + After `AstTransformsStage` (15)? Between user-filters-post (16) + and code-highlight (17)? Resolve at impl time based on whether + user filters should see the marker `Div` or the + `
`.
 
 ## Beads issues
 
-Created 2026-05-28:
+Created 2026-05-28 (v1), revised 2026-05-28 (v2 after PR #238 review):
 
 - **`bd-je48v`** (epic, P2) — "Add mermaid diagram engine to Quarto 2."
 - **`bd-c6h96`** (task, P2, child of epic) — "Mermaid engine design
-  session: resolve architectural questions." This is where Q-A/Q-B/Q-C/Q-D
-  get ratified. Plan doc: this file.
+  session." v2: Q-A resolved (engine sequence via PR #238); Q-B
+  sharper (B1 vs B2a/B2c/B2e); Q-C unchanged (C1 recommended); Q-D
+  redirected at closing bd-iq0hp.
 - **`bd-fztki`** (task, P2, child of epic, blocks impl) — "Audit
-  current multi-engine + per-engine trace state (bd-5yff4)."
+  current multi-engine state." v2: retargeted at PR #238's plan +
+  code instead of a generic multi-engine investigation.
 - **`bd-gwfdo`** (task, P2, child of epic, blocked-by `bd-c6h96` +
-  `bd-fztki`) — "Implement mermaid AST transform + HTML emission."
+  `bd-fztki`, gated on PR #238 merge) — "Implement `MermaidEngine` +
+  `MermaidHtmlEmitStage`." v2: revised from "AST transform" to
+  "ExecutionEngine impl + dedicated HTML-emit stage."
 - **`bd-my0o5`** (task, P2, child of epic, blocked-by `bd-gwfdo`) —
-  "q2 preview end-to-end verification for mermaid blocks."
+  "q2 preview end-to-end verification for mermaid blocks." v2:
+  explicitly closes PR #238's bd-iq0hp.
 - **`bd-5ijtt`** (task, P3, child of epic, blocked-by `bd-gwfdo`) —
   "User-facing docs for mermaid diagrams."
 
-Follow-up issues (filed as discovered-from `bd-c6h96` — not blockers
-for shipping mermaid via the recommended A3/B2/C1 path, but separate
-bugs/features that the design exercise surfaced):
-
-- **`bd-14rer`** (bug, P2) — "ExecuteResult.filters set by Knitr
-  engine is never consumed downstream." Knitr's `rmarkdown/pagebreak.lua`
-  is silently dropped today.
-- **`bd-s8llm`** (feature, P3) — "No internal API for pipeline
-  producers to publish files into the VFS."
-- **`bd-mqk49`** (feature, P3) — "Design: how should
-  engines/extensions declare additional pipeline stages?"
-- **`bd-cp3em`** (bug, P2) — "CaptureSpliceStage drops
-  includes/filters/supporting_files from recorded ExecuteResult."
-  Same shape as the CodeHighlightStage incident in CLAUDE.md.
+Follow-up issues (discovered-from `bd-c6h96`):
+
+- **`bd-14rer`** (bug, P2) — Knitr's `filters` silently dropped.
+- **`bd-s8llm`** (feature, P3) — VFS publication API.
+- **`bd-mqk49`** (feature, P3) — Engine→stage extension. **More
+  relevant after PR #238**; the natural mechanism for Q-B's full
+  B2e form.
+- **`bd-cp3em`** (bug, P2) — Capture-splice aux-field drop;
+  verified still present in `feature/multi-engine`.
 
 ## References
 
 - ExecutionEngine trait: `crates/quarto-core/src/engine/traits.rs:56-127`
 - ExecuteResult: `crates/quarto-core/src/engine/context.rs:127-206`
-- EngineExecutionStage (includes/files drain): `crates/quarto-core/src/stage/stages/engine_execution.rs:241-296`
-- KnitrEngine filter write site: `crates/quarto-core/src/engine/knitr/mod.rs:215`
-- Capture-splice aux-field drop site: `crates/quarto-core/src/stage/stages/capture_splice.rs:121-126`
-- ReplayEngine: `crates/quarto-core/src/engine/replay.rs:53,95-111`
-- Pipeline stages: `crates/quarto-core/src/pipeline.rs:207-300`
+- EngineExecutionStage (single-engine, on `main`):
+  `crates/quarto-core/src/stage/stages/engine_execution.rs:150-401`
+- EngineExecutionStage (multi-engine, on `feature/multi-engine`):
+  per PR #238 file list (+625/-232)
+- Capture-splice aux-field drop (on `feature/multi-engine`):
+  `crates/quarto-core/src/stage/stages/capture_splice.rs` —
+  comment unchanged from `main` version
+- Pipeline stages: `crates/quarto-core/src/pipeline.rs`
 - Document profile contract: `claude-notes/designs/document-profile-contract.md`
 - Filter resolution: `crates/quarto-core/src/filter_resolve.rs`
 - VFS: `crates/quarto-system-runtime/src/wasm.rs:227-261`
 - q2-preview epic: `claude-notes/plans/2026-05-11-q2-preview-epic.md`
-- Multi-engine work in flight: `claude-notes/plans/` entries for bd-5yff4 (re-verify)
+- Multi-engine plan (on `feature/multi-engine`):
+  `claude-notes/plans/2026-05-27-multi-engine-execution.md`
+- Multi-engine PR: https://github.com/quarto-dev/q2/pull/238

From c73da3cd55b025e4c96d45ec66084da294952a2d Mon Sep 17 00:00:00 2001
From: Carlos Scheidegger 
Date: Thu, 28 May 2026 12:05:16 -0500
Subject: [PATCH 3/9] =?UTF-8?q?mermaid=20plan=20+=20beads:=20Q-B=20?=
 =?UTF-8?q?=E2=86=92=20B1=20(the=20hack),=20with=20bd-mqk49=20follow-up?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Decided to ship the mermaid engine with direct RawBlock(HTML, ...)
emission rather than the B2c "marker Div + dedicated
MermaidHtmlEmitStage" path the v2 plan recommended. Quarto 2 only
renders HTML today, so the format-agnostic decomposition would be
hypothetical correctness against a future cost. B1 + a TODO comment
captures the architectural intent without paying the impl cost now.

The follow-up lives in two places:

- bd-gwfdo (impl): description includes the explicit instruction to
  add a source-code comment at the RawBlock-emission site pointing
  at bd-mqk49. Title updated to "Implement MermaidEngine (B1:
  direct RawBlock HTML emission)".
- bd-mqk49 (engine→stage extension): description now names the
  mermaid engine as a known beneficiary, with concrete refactor
  steps when bd-mqk49 lands ("the engine should emit a marker Div
  and declare an HTML-conditional AST pass").

Also fixed a YAML frontmatter issue caught in review: the `status`
field's two-line value now uses `>` (folded block scalar) for
explicit multi-line handling.

Plan-doc edits:
- Q-B recommendation flipped from B2c to B1, with the rationale
  recorded.
- Phase 0 decision line updated.
- Phase 2 step list shortened: dropped the MermaidHtmlEmitStage
  step; engine emission step now includes the bd-mqk49 comment
  instruction.
- Beads-issues subsection updates for bd-gwfdo and bd-mqk49.

Co-Authored-By: Claude Opus 4.7 
---
 .beads/issues.jsonl                           |  4 +-
 .../2026-05-28-mermaidjs-engine-design.md     | 65 ++++++++++++-------
 2 files changed, 44 insertions(+), 25 deletions(-)

diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl
index d310884d..29e009f6 100644
--- a/.beads/issues.jsonl
+++ b/.beads/issues.jsonl
@@ -249,7 +249,7 @@
 {"id":"bd-gkqxl","title":"Author error-docs pages for project subsystem (3 codes)","description":"Author stub-quality pages for all 3 project subsystem error codes under docs/errors/project/. Follow template in docs/errors/yaml/Q-1-10.qmd. See claude-notes/plans/2026-05-22-error-docs-content.md.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-24T21:07:57.290373Z","created_by":"cscheid","updated_at":"2026-05-24T21:15:22.761525Z","closed_at":"2026-05-24T21:15:22.761176Z","source_repo":".","compaction_level":0,"original_size":0}
 {"id":"bd-gucj","title":"hub-client: thread project _quarto.yml into ProjectContext","description":"wasm-quarto-hub-client/src/lib.rs::create_wasm_project_context creates a single-file ProjectContext with default (empty) ProjectConfig. This means custom crossref types defined in a project's _quarto.yml (crossref.custom) are not applied to either render or outline. Fix: read the project's _quarto.yml content from the Automerge document set and populate ProjectContext.config.metadata before running the pipeline. Once fixed, both the render path and the new LSP analysis pipeline (added in bd-ascs) pick up custom ref types automatically.","status":"open","priority":2,"issue_type":"task","created_at":"2026-04-17T22:16:27.550804Z","created_by":"cscheid","updated_at":"2026-04-17T22:16:27.550804Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-gucj","depends_on_id":"bd-ascs","type":"discovered-from","created_at":"2026-04-17T22:16:27.550804Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-gvhe","title":"Block-crossref HTML output parity with Quarto 1","description":"Implement Q1-parity HTML rendering for theorems, lemmas, and sibling block-level crossref targets. See claude-notes/plans/2026-04-17-theorem-html-q1-parity.md. Two-step fix: (1) extend TheoremSugarTransform to match id-prefix in addition to class; (2) rewrite render_theorem / render_resolved_ref to emit theorem-title span + nbsp.","status":"in_progress","priority":1,"issue_type":"feature","created_at":"2026-04-17T17:11:23.700466Z","created_by":"cscheid","updated_at":"2026-04-17T17:11:32.888516Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-gvhe","depends_on_id":"bd-jsbg","type":"parent-child","created_at":"2026-04-17T17:11:23.700466Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
-{"id":"bd-gwfdo","title":"Implement MermaidEngine + MermaidHtmlEmitStage","description":"Implement mermaid handling per the ratified v2 design (see bd-c6h96 outcome and plan v2). **Gated on PR #238 merging.**\n\nExpected shape (Q-A → A1 engine impl, Q-B → B2c dedicated HTML-emit stage, Q-C → C1 inline script RawBlock):\n\n1. Add MermaidEngine implementing ExecutionEngine in crates/quarto-core/src/engine/mermaid/ (mirror engine/markdown.rs shape — closest precedent for a no-subprocess engine):\n   - name() == 'mermaidjs'.\n   - execute(input, ctx): parse input as QMD, walk for code cells with class 'mermaid', replace each with a Div.mermaid wrapping the original source as a code block (B2), append a once-per-doc RawBlock(HTML, '') at end of body (C1). Serialize back to QMD; return as ExecuteResult { markdown, ..Default }.\n   - Register in native + WASM EngineRegistry.\n\n2. Add MermaidHtmlEmitStage between AstTransformsStage (15) and RenderHtmlBodyStage (18) — exact slot resolved at impl time (whether user filters should see the marker Div or the rendered 
). The stage walks for Div.mermaid and rewrites to RawBlock(HTML, '
...
').\n\n3. Tests: unit (engine on fixture) + pipeline (full HTML render) + end-to-end per CLAUDE.md verification policy (cargo run --bin q2 -- render fixture.qmd; grep output; record in plan).\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","updated_at":"2026-05-28T16:16:38.836217Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-gwfdo","depends_on_id":"bd-c6h96","type":"blocks","created_at":"2026-05-28T13:45:35.978499Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-fztki","type":"blocks","created_at":"2026-05-28T13:45:36.396009Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-gwfdo","title":"Implement MermaidEngine (B1: direct RawBlock HTML emission)","description":"Implement mermaid handling per the ratified v2 design (see bd-c6h96 outcome and plan v2, Q-B → B1). **Gated on PR #238 merging.**\n\nAdd MermaidEngine implementing ExecutionEngine in crates/quarto-core/src/engine/mermaid/ (mirror engine/markdown.rs shape — closest precedent for a no-subprocess engine):\n\n- name() == 'mermaidjs'.\n- execute(input, ctx): parse input as QMD, walk for code cells with class 'mermaid', replace each with RawBlock(HTML, '
...
') (B1 — direct HTML emission), append a once-per-doc RawBlock(HTML, '') at end of body (C1). Serialize back to QMD; return as ExecuteResult { markdown, ..Default }.\n- **Add a source-code comment** at the RawBlock-emission site pointing at bd-mqk49: 'when engines can declare per-format AST passes, route this through a format-conditional transform instead of emitting HTML inline. Today Quarto 2 only renders HTML so the format-locked emission is acceptable.'\n- Register in native + WASM EngineRegistry.\n\nTests: unit (engine on fixture) + pipeline (full HTML render) + end-to-end per CLAUDE.md verification policy (cargo run --bin q2 -- render fixture.qmd; grep output; record in plan).\n\nThe B2c/B2e (dedicated HTML-emit stage / engine-declared per-format pass) path was rejected for the first ship — Quarto 2 is HTML-only today, so format-agnostic decomposition would be hypothetical correctness against a future cost. The follow-up lives on bd-mqk49.\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","updated_at":"2026-05-28T17:04:53.629281Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-gwfdo","depends_on_id":"bd-c6h96","type":"blocks","created_at":"2026-05-28T13:45:35.978499Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-fztki","type":"blocks","created_at":"2026-05-28T13:45:36.396009Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-gz6k","title":"Cargo: upgrade sha1 v0.10.6 → v0.11.0","description":"Major upgrade surfaced by cargo-upgrade survey 2026-05-04. Current 0.10.6 is range-pinned in workspace; latest is 0.11.0. 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.170269Z","created_by":"cscheid","updated_at":"2026-05-04T20:30:44.941246Z","closed_at":"2026-05-04T20:30:44.941104Z","close_reason":"merged: 0812812e","source_repo":".","compaction_level":0,"original_size":0,"labels":["cargo","deps"],"dependencies":[{"issue_id":"bd-gz6k","depends_on_id":"bd-hb8h","type":"discovered-from","created_at":"2026-05-04T18:16:05.443720Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-h4l6","title":"[websites phase 5] Scoped artifact store and site_libs/ dedup","description":"Plan: claude-notes/plans/2026-04-23-website-project-epic.md § Phase 5.\n\nDeliverables:\n- Add scope (Page | Project) to artifact entries in ArtifactStore.\n- Project-aware artifact writer: Project-scoped artifacts emitted once to _site/site_libs/{name}/...\n- Relocator: rewrite per-page HTML to point at shared site_libs/ paths correctly (handles subdirs, offset computation).\n- Migrate theme CSS, Bootstrap, quarto-nav JS, etc. to Project scope when inside a website project.\n- Preservation: single-doc renders unchanged (both scopes resolve under {stem}_files/).\n- Sequence the change in two steps: (a) pure refactor introducing scope with identical behavior, (b) switch websites to use Project scope.\n\nBlocked by Phase 1.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-04-23T18:43:08.936956Z","created_by":"cscheid","updated_at":"2026-04-29T00:31:30.479325Z","closed_at":"2026-04-29T00:31:30.479019Z","close_reason":"Phase 5 (scoped artifact store + site_libs) implemented (commit dc4e81b0). Closed as part of Phase 9 cleanup.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-h4l6","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-23T18:43:08.936956Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-h4l6","depends_on_id":"bd-w5os","type":"blocks","created_at":"2026-04-23T18:43:44.336998Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-h5l7","title":"Diagnose and fix SourceInfo::eq hotspot in hub-client preview","description":"Chrome profile of hub-client on a moderately-sized document shows a large hotspot on ::eq, entered from parse_qmd_to_ast (crates/wasm-quarto-hub-client/src/lib.rs:757). Likely cause: SourceInfoSerializer::intern in crates/pampa/src/writers/json.rs:229 linearly scans content_map (Vec<(SourceInfo, usize)>) on every pointer-lookup miss, and SourceInfo is held by-value in AST nodes so pointer lookups almost always miss — giving O(n²) behavior per document. Also the first perf-profiling session on Quarto 2, so the plan deliberately establishes a repeatable native-side workflow (Criterion bench + samply flamegraph on the pampa binary) before any fix, since Chrome-side before/after is too noisy to iterate on. Plan: claude-notes/plans/2026-04-22-sourceinfo-eq-hotspot.md. Currently in draft — awaiting user review before any measurement work begins.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-04-22T20:00:48.019984Z","created_by":"cscheid","updated_at":"2026-04-22T23:23:38.315697Z","closed_at":"2026-04-22T23:23:38.315082Z","close_reason":"Landed on perf/2026-04-22-json-sourcemap in commit 8f6f21d9. Browser cross-validation confirmed SourceInfo::eq no longer a hotspot.","source_repo":".","compaction_level":0,"original_size":0} @@ -340,7 +340,7 @@ {"id":"bd-mlj6","title":"Conditional render lists / _quarto-*.yml profiles","description":"Q1 supports profile-specific renders via _quarto-.yml (e.g. _quarto-prod.yml vs _quarto-dev.yml). Phase 1 of the website epic ignores profile files entirely — discovery excludes them because of the leading underscore, and config parsing only reads _quarto.yml. Follow-up: decide how profiles compose with the base config, add CLI flag for selecting profile, thread through ProjectContext::discover. See claude-notes/plans/2026-04-23-websites-phase-1.md §Decisions log.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-04-24T01:05:24.479391Z","created_by":"cscheid","updated_at":"2026-04-24T01:05:24.479391Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mlj6","depends_on_id":"bd-w5os","type":"discovered-from","created_at":"2026-04-24T01:05:24.479391Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-mot7","title":"qmd reader drops whitespace between code_span and html_element (issue #182)","description":"(see triage doc)","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-05-12T13:42:59.836284Z","created_by":"cscheid","updated_at":"2026-05-12T13:43:26.216838Z","closed_at":"2026-05-12T13:43:26.216709Z","close_reason":"Duplicate, accidental second create. Keeping bd-nkx4.","source_repo":".","compaction_level":0,"original_size":0} {"id":"bd-mqa4j","title":"Phase 8 — quarto-sync-client header pass-through + connection-manager integration","description":"Add auth?.getBearer option to client.connect(); new Node-only NodeWebSocketClientAdapter inside quarto-sync-client that constructs new WebSocket(url, [], { headers }). Browser path unchanged.\n\nConnection-manager try-then-fallback policy: read bundle, attempt WS with Bearer if present, on 401-with-creds forceRefresh+retry-once then ReauthRequired, on 401-without-creds AuthRequired.\n\nlastObservedAuthMode state machine ({no-auth, requires-auth, unknown}, process-local) drives Phase 7's short-circuit.\n\nInsecure-Bearer gate: refuse to send Bearer over plain HTTP/WS to non-loopback without QUARTO_HUB_MCP_ALLOW_INSECURE_AUTH=1; loud warning on every connect when set.\n\nFollow-up: upstream PR to thread headers through BrowserWebSocketClientAdapter — file separately.\n\nPlan §Phase 8: claude-notes/plans/2026-05-05-hub-mcp-device-flow-implementation.md","status":"open","priority":1,"issue_type":"task","created_at":"2026-05-20T14:27:25.451968Z","created_by":"shikokuchuo","updated_at":"2026-05-20T14:27:25.451968Z","source_repo":"kyoto","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mqa4j","depends_on_id":"bd-cmp48","type":"parent-child","created_at":"2026-05-20T14:27:25.451968Z","created_by":"shikokuchuo","metadata":"{}","thread_id":""}]} -{"id":"bd-mqk49","title":"Design: how should engines/extensions declare additional pipeline stages?","description":"build_html_pipeline_stages() in crates/quarto-core/src/pipeline.rs is a hardcoded ~19-stage list. Stages can be gated by config (e.g. native-only), but no engine, extension, or plugin can register a new stage.\n\n**More relevant after PR #238 (sequential multi-engine execution).** With engines as first-class first-class pipeline citizens, the natural mechanism for 'format-agnostic engine output + format-specific emission' (Q-B in the mermaid plan) is to let an engine declare a per-format AST pass on its output. For mermaid: 'when emitting HTML, run this transform on my output blocks.' For graphviz/plantuml/dot later: same shape.\n\nA proper extension API would let engines/extensions declare:\n- a stage name + input/output kinds\n- a position (after-X / before-Y, or by precedence)\n- a per-format applicability predicate\n- a per-engine attribution back-reference (so trace events name the engine that contributed the stage)\n\nThe mermaid first ship (bd-gwfdo) will likely use a fixed MermaidHtmlEmitStage in the canonical pipeline (Q-B option B2c) instead of waiting on this. That's the right pragmatic move — but bd-mqk49 is the architecturally correct landing zone for diagram-like engines.\n\nDiscovered during mermaid engine design — see claude-notes/plans/2026-05-28-mermaidjs-engine-design.md gap G3.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","updated_at":"2026-05-28T16:16:25.594880Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mqk49","depends_on_id":"bd-c6h96","type":"discovered-from","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-mqk49","title":"Design: how should engines/extensions declare additional pipeline stages?","description":"build_html_pipeline_stages() in crates/quarto-core/src/pipeline.rs is a hardcoded ~19-stage list. Stages can be gated by config (e.g. native-only), but no engine, extension, or plugin can register a new stage.\n\n**More relevant after PR #238 (sequential multi-engine execution).** With engines as first-class pipeline citizens, the natural mechanism for 'format-agnostic engine output + format-specific emission' is to let an engine declare a per-format AST pass on its output. For mermaid: 'when emitting HTML, run this transform on my output blocks.' For graphviz/plantuml/dot later: same shape.\n\nA proper extension API would let engines/extensions declare:\n- a stage name + input/output kinds\n- a position (after-X / before-Y, or by precedence)\n- a per-format applicability predicate\n- a per-engine attribution back-reference (so trace events name the engine that contributed the stage)\n\n**Known beneficiary: the mermaid engine (bd-gwfdo).** Mermaid ships first under B1 (the engine emits RawBlock(HTML, '
...
') directly — format-locked but acceptable while Q2 is HTML-only). A source-code comment in crates/quarto-core/src/engine/mermaid/ flags the TODO. **When bd-mqk49 lands, follow up on the mermaid engine** to refactor: the engine should emit a marker Div (e.g. Div.mermaid wrapping the source code block) and declare an HTML-conditional AST pass that turns the marker into the
 output. This removes the format coupling and makes the mermaid engine PDF-ready.\n\nDiscovered during mermaid engine design — see claude-notes/plans/2026-05-28-mermaidjs-engine-design.md gap G3.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","updated_at":"2026-05-28T17:05:01.753171Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mqk49","depends_on_id":"bd-c6h96","type":"discovered-from","created_at":"2026-05-28T13:45:56.898020Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-mre3","title":"[websites phase 2] Sidebar data model, generate, render, template","description":"Plan: claude-notes/plans/2026-04-23-website-project-epic.md § Phase 2.\n\nDeliverables:\n- Schema parsing for website.sidebar: Vec with id, title, contents, style, collapse-level.\n- Contents supports: string (path), {href, text, icon}, {section, contents}, {auto: ...}.\n- quarto-navigation data types: Sidebar, SidebarEntry, SidebarContents.\n- SidebarGenerateTransform: reads YAML config + ProjectIndex to resolve auto and expand entries.\n- SidebarRenderTransform: emits Bootstrap-5 HTML matching Q1 class names where possible.\n- Template slot: $rendered.navigation.sidebar$.\n- Sidebar-for-page selection logic.\n- Integration tests with manual, auto, and nested contents.\n\nBlocked by Phase 1 (needs ProjectIndex).","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-04-23T18:42:49.915763Z","created_by":"cscheid","updated_at":"2026-04-29T00:31:30.148471Z","closed_at":"2026-04-29T00:31:30.148151Z","close_reason":"Phase 2 (sidebar) implemented — see git log Phase 2 sub-phase commits and claude-notes/plans/2026-04-24-websites-phase-2.md (closed as part of Phase 9 cleanup; this should have been closed earlier).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mre3","depends_on_id":"bd-0tr6","type":"parent-child","created_at":"2026-04-23T18:42:49.915763Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-mre3","depends_on_id":"bd-w5os","type":"blocks","created_at":"2026-04-23T18:43:42.284820Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-mrx1","title":"Phase B.4: Acceptance bundle — _quarto.yml + posts/_metadata.yml propagation to preview","description":"Single Playwright spec pinning the two observable plan acceptance criteria for q2 preview (Phase B):\n\n1. Editing _quarto.yml (title:) re-renders the active page; new title visible in DOM within 5 s.\n2. Editing posts/_metadata.yml (subtitle:) re-renders pages under posts/; new subtitle visible in DOM within 5 s.\n\nFixture (single, dual-purpose): _quarto.yml + posts/_metadata.yml + posts/post1.qmd (no frontmatter, inherits both). Empirically verified both knobs flow through to the rendered title-block (2026-05-13 probe with target/debug/q2 render).\n\nReuses the multi-file fixture helper generalised in bd-pf63 (B.3).\n\nPlan acceptance criterion 3 ('unrelated sibling re-renders the active page') is deferred and tracked separately — its relaxed-contract form (any edit fires a re-render) is invisible at the DOM without SPA instrumentation. See discovered-from follow-up.\n\nPlan: claude-notes/plans/2026-05-13-q2-preview-phase-b.md §B.4.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-13T21:11:28.867999Z","created_by":"cscheid","updated_at":"2026-05-13T21:15:09.863020Z","closed_at":"2026-05-13T21:15:09.862892Z","close_reason":"Phase B.4 complete; zero production-code changes — new Playwright spec at q2-preview-spa/e2e/config-edits.spec.ts pins _quarto.yml + posts/_metadata.yml propagation. See commit df2f5f55 on beads/bd-mrx1-phase-b4-acceptance-bundle.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-mrx1","depends_on_id":"bd-kw93","type":"parent-child","created_at":"2026-05-13T21:11:28.867999Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-mrx1","depends_on_id":"bd-pf63","type":"discovered-from","created_at":"2026-05-13T21:11:28.867999Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
 {"id":"bd-msp0","title":"Hub-client preview resource resolution via service worker (epic)","description":"Long-term direction: replace the iframe post-processor's resource-rewriting passes (CSS data-URIs, image data-URIs, the disabled ` `RawBlock` at
-    end of body (C1). Serialize back to QMD and return as
+    with class `mermaid`, replace each cell with a
+    `RawBlock(HTML, "
")` (B1 — + direct HTML emission), and append a once-per-doc + `` `RawBlock` at end of body + (C1). Serialize back to QMD and return as `ExecuteResult { markdown, ..Default }`. + - **Add a source-code comment** at the RawBlock-emission site: + `// bd-mqk49: when engines can declare per-format AST passes, + // route this through a format-conditional transform instead of + // emitting HTML inline. Today, Quarto 2 only renders HTML, so + // the format-locked emission is acceptable.` - Register in native + WASM `EngineRegistry`s. -- [ ] Add `MermaidHtmlEmitStage` between `AstTransformsStage` and - `RenderHtmlBodyStage` (or between user-filters-post and - code-highlight — confirm exact slot during impl). The stage - walks for `Div.mermaid` and rewrites to - `RawBlock(HTML, "
")`. - [ ] Tests: - Unit: mermaid engine on a fixture qmd containing `{mermaid}` and non-mermaid blocks — only `{mermaid}` cells touched; script tag @@ -418,9 +431,11 @@ Created 2026-05-28 (v1), revised 2026-05-28 (v2 after PR #238 review): current multi-engine state." v2: retargeted at PR #238's plan + code instead of a generic multi-engine investigation. - **`bd-gwfdo`** (task, P2, child of epic, blocked-by `bd-c6h96` + - `bd-fztki`, gated on PR #238 merge) — "Implement `MermaidEngine` + - `MermaidHtmlEmitStage`." v2: revised from "AST transform" to - "ExecutionEngine impl + dedicated HTML-emit stage." + `bd-fztki`, gated on PR #238 merge) — "Implement `MermaidEngine`." + v2: revised from "AST transform" to "ExecutionEngine impl emitting + RawBlock HTML directly (B1)." A bd-mqk49 follow-up will refactor + to format-conditional emission once the engine→stage extension + API exists. - **`bd-my0o5`** (task, P2, child of epic, blocked-by `bd-gwfdo`) — "q2 preview end-to-end verification for mermaid blocks." v2: explicitly closes PR #238's bd-iq0hp. @@ -433,7 +448,11 @@ Follow-up issues (discovered-from `bd-c6h96`): - **`bd-s8llm`** (feature, P3) — VFS publication API. - **`bd-mqk49`** (feature, P3) — Engine→stage extension. **More relevant after PR #238**; the natural mechanism for Q-B's full - B2e form. + B2e form. The mermaid engine ships under B1 (direct + `RawBlock(HTML, …)` emission); when bd-mqk49 lands, the mermaid + engine should be refactored to declare a per-format AST pass + instead. A source-code comment in `engine/mermaid/` flags the + TODO. - **`bd-cp3em`** (bug, P2) — Capture-splice aux-field drop; verified still present in `feature/multi-engine`. From b6be654113c655f1307c05f306b66bbe10a063cf Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Thu, 28 May 2026 12:14:17 -0500 Subject: [PATCH 4/9] mermaid plan: close bd-c6h96 + bd-fztki; record Phase 1 audit findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the design (bd-c6h96, ratified by user) and the audit (bd-fztki) tasks for the mermaid engine epic. The audit pulled PR #238's plan + the multi-engine versions of engine_execution.rs, registry.rs, detection.rs, fixture.rs from the feature/multi-engine branch. Eight findings recorded in the plan doc's new Phase 1 findings sub-section, with implications fed into Phase 2: - F1: MermaidEngine registers in EngineRegistry::new's always-block (native + WASM) — mermaid is text-only, no subprocess, so the WASM build runs it live. This sidesteps bd-cp3em entirely for the mermaid case (no replay needed in q2 preview). - F2: Add "mermaidjs" to KNOWN_ENGINES (detection.rs:31) for the top-level-shortcut form. `engine: [mermaidjs]` works without it; this is nice-to-have. - F3: Per-engine FileId provenance is loop-driven. Mermaid's intermediate is `.mermaidjs.rmarkdown` automatically — no engine-side work. - F4: `result.markdown` is QMD text re-parsed for the next engine. Literal HTML in QMD round-trips as RawBlock(HTML) via pampa. - F5: Biggest finding — in-process engine convention is text-level fence scanning (per FixtureEngine), not AST parse-walk-serialize. Phase 2 refined to mirror fixture.rs rather than markdown.rs. - F6: jsdelivr script tag goes in the engine's markdown output (C1), not via ExecuteResult.includes. Trivial to do under F5's text-level approach. - F7: get_engine_with_fallback warn+skips unknown engine names, so landing MermaidEngine is purely additive. - F8: bd-cp3em verified still present in feature/multi-engine (capture-splice fold drops aux fields per iteration). Phase 2 plan step list rewritten to reflect F1-F8: text-level scanner, always-block registration, KNOWN_ENGINES update, fuller test matrix (single/multi/none/mixed/escaping + multi-engine integration via knitr or FixtureEngine). The plan is now ready for implementation. Implementation (bd-gwfdo) remains gated on PR #238 merging. Co-Authored-By: Claude Opus 4.7 --- .beads/issues.jsonl | 4 +- .../2026-05-28-mermaidjs-engine-design.md | 236 +++++++++++++++--- 2 files changed, 199 insertions(+), 41 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 29e009f6..f4fde5db 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -196,7 +196,7 @@ {"id":"bd-c263g","title":"Author error-docs pages for lua subsystem (1 codes)","description":"Author stub-quality pages for all 1 lua subsystem error codes under docs/errors/lua/. Follow template in docs/errors/yaml/Q-1-10.qmd. See claude-notes/plans/2026-05-22-error-docs-content.md.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-24T21:07:58.103168Z","created_by":"cscheid","updated_at":"2026-05-24T21:15:23.074966Z","closed_at":"2026-05-24T21:15:23.074602Z","source_repo":".","compaction_level":0,"original_size":0} {"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-c5u2g","title":"Cache engine-discovery so we don't re-spawn per document","description":"Replace JupyterEngine::find_executable's sh -c subprocess spawn with which::which (parity with KnitrEngine), and memoize both find_jupyter and find_rscript with OnceLock>. Per-doc engine-registry construction is 37% of main-thread CPU on quarto-web (bd-9eltv profile).\n\nPlan: claude-notes/plans/2026-05-22-engine-discovery-cache.md\nProfile: claude-notes/research/2026-05-21-quarto-web-render-profile.md","status":"closed","priority":1,"issue_type":"task","created_at":"2026-05-22T01:07:27.123486Z","created_by":"cscheid","updated_at":"2026-05-22T01:26:59.991346Z","closed_at":"2026-05-22T01:26:59.991170Z","close_reason":"Implemented and verified — per-process engine-discovery cache. quarto-web wall time 3.28s -> 2.30s (-30%); perf.engine-discover gauge added as regression tripwire.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-c5u2g","depends_on_id":"bd-9eltv","type":"discovered-from","created_at":"2026-05-22T01:07:27.123486Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} -{"id":"bd-c6h96","title":"Mermaid engine design session: resolve architectural questions","description":"Design session to resolve the open questions in claude-notes/plans/2026-05-28-mermaidjs-engine-design.md.\n\n**v2 (post PR #238 review):**\n\n- **Q-A — RESOLVED.** PR #238 lands engine: [knitr, mermaidjs] sequencing. Mermaid is a first-class ExecutionEngine impl. Cell-class ownership disjoint (`{r}` vs `{mermaid}`) so the two compose cleanly. The v1 'AST transform' framing is obsolete.\n\n- **Q-B — sharper.** With engines as first-class citizens, the format-agnostic vs HTML-specific split becomes: does the engine emit RawBlock(HTML) (B1, format-locked), a marker Div + HTML writer special-case (B2a, ugly coupling), a marker Div + dedicated MermaidHtmlEmitStage in the canonical pipeline (B2c, recommended for first ship), or a marker Div + engine-declared per-format AST pass (B2e, requires bd-mqk49)?\n\n- **Q-C — unchanged.** Recommend C1 (inline RawBlock(HTML) for the jsdelivr script tag at end of body, encoded into the engine's markdown output). Survives bd-cp3em (capture-splice aux drop).\n\n- **Q-D — redirected.** PR #238 documents bd-iq0hp ('no browser E2E of multi-engine preview was possible'). Mermaid + knitr is the first real-engine pair with disjoint cell ownership; our preview-e2e task (bd-my0o5) effectively closes bd-iq0hp.\n\nOutput: plan doc has v2 with these decisions recorded. User confirmation of B2c recommendation needed before impl.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-28T13:44:52.754334Z","created_by":"cscheid","updated_at":"2026-05-28T16:15:57.755030Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-c6h96","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:44:52.754334Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-c6h96","title":"Mermaid engine design session: resolve architectural questions","description":"Design session to resolve the open questions in claude-notes/plans/2026-05-28-mermaidjs-engine-design.md.\n\n**v2 (post PR #238 review):**\n\n- **Q-A — RESOLVED.** PR #238 lands engine: [knitr, mermaidjs] sequencing. Mermaid is a first-class ExecutionEngine impl. Cell-class ownership disjoint (`{r}` vs `{mermaid}`) so the two compose cleanly. The v1 'AST transform' framing is obsolete.\n\n- **Q-B — sharper.** With engines as first-class citizens, the format-agnostic vs HTML-specific split becomes: does the engine emit RawBlock(HTML) (B1, format-locked), a marker Div + HTML writer special-case (B2a, ugly coupling), a marker Div + dedicated MermaidHtmlEmitStage in the canonical pipeline (B2c, recommended for first ship), or a marker Div + engine-declared per-format AST pass (B2e, requires bd-mqk49)?\n\n- **Q-C — unchanged.** Recommend C1 (inline RawBlock(HTML) for the jsdelivr script tag at end of body, encoded into the engine's markdown output). Survives bd-cp3em (capture-splice aux drop).\n\n- **Q-D — redirected.** PR #238 documents bd-iq0hp ('no browser E2E of multi-engine preview was possible'). Mermaid + knitr is the first real-engine pair with disjoint cell ownership; our preview-e2e task (bd-my0o5) effectively closes bd-iq0hp.\n\nOutput: plan doc has v2 with these decisions recorded. User confirmation of B2c recommendation needed before impl.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-28T13:44:52.754334Z","created_by":"cscheid","updated_at":"2026-05-28T17:09:57.153827Z","closed_at":"2026-05-28T17:09:57.153681Z","close_reason":"Design ratified by user (2026-05-28). All four questions resolved in plan v2.1: Q-A=A1 (MermaidEngine as ExecutionEngine in sequence per PR #238), Q-B=B1 (direct RawBlock HTML emission with bd-mqk49 TODO comment), Q-C=C1 (inline script RawBlock in engine markdown output), Q-D=close PR #238's bd-iq0hp via bd-my0o5 preview e2e. Plan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-c6h96","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:44:52.754334Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-cfl67","title":"q2 render truncates source images referenced in qmd documents","description":"ResourceCollectorTransform stores image source paths as artifacts with empty content; write_artifacts then opens the source path for writing and truncates it to 0 bytes. Confirmed via 'q2 render docs/authoring/markdown/index.qmd' which truncates docs/authoring/markdown/elephant.png from 126124 bytes to 0. Plan doc: claude-notes/plans/2026-05-20-render-truncates-source-images.md","status":"open","priority":0,"issue_type":"bug","created_at":"2026-05-21T00:53:50.747194Z","created_by":"cscheid","updated_at":"2026-05-21T00:53:50.747194Z","source_repo":".","compaction_level":0,"original_size":0,"labels":["bug","data-loss"]} {"id":"bd-cjxlb","title":"Keyring error messages must not leak credential blob","description":"Plan §Phase 5 calls this out as a specifically-flagged risk: when CredentialStore.write/read/clear surfaces a backend error, the error message must NOT contain the credential blob (or any token-shaped substring).\n\nTest name: keyring_error_does_not_leak_blob_in_message in ts-packages/quarto-hub-mcp/test/auth/credential-store.test.ts.\n\nImplementation: wrap keyring errors via redact(err.message) at every site that touches a blob.\n\nPlan §Phase 5: claude-notes/plans/2026-05-05-hub-mcp-device-flow-implementation.md","status":"open","priority":1,"issue_type":"bug","created_at":"2026-05-20T14:27:50.747041Z","created_by":"shikokuchuo","updated_at":"2026-05-20T14:27:50.747041Z","source_repo":"kyoto","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-cjxlb","depends_on_id":"bd-sfr5l","type":"parent-child","created_at":"2026-05-20T14:27:50.747041Z","created_by":"shikokuchuo","metadata":"{}","thread_id":""}]} {"id":"bd-cmp48","title":"hub-mcp Google device-flow auth (Design C')","description":"Implements Design C' (Google as device-flow AS) for hub-mcp ↔ quarto-hub authentication over WebSocket. Hub-mcp persists Google ID + refresh tokens locally (OS keyring); hub keeps existing JWKS validator with audience allowlist.\n\nPlan: claude-notes/plans/2026-05-05-hub-mcp-device-flow-implementation.md\n\nPer the plan's Beads section: one issue per phase (1-10) parent-child to this epic, plus separate bug-tagged issues for the dual-credential CVE test (Phase 2), keyring-leak test (Phase 5), and canonical-URL constant test (Phase 7).","status":"open","priority":1,"issue_type":"epic","created_at":"2026-05-20T14:26:43.587791Z","created_by":"shikokuchuo","updated_at":"2026-05-20T14:26:43.587791Z","source_repo":"kyoto","compaction_level":0,"original_size":0} @@ -239,7 +239,7 @@ {"id":"bd-fx23","title":"Defensive percent-encoding of listing.id in L7 image marker","description":"L7's image-placeholder begin marker embeds `listing.id` unescaped between `:` separators:\n\n``\n\nToday the schema implicitly constrains listing ids to identifier-shape (no `:`, no whitespace). If a future schema change permits richer ids, the regex (`([^:]*)`) silently mis-parses.\n\nDefensive options:\n1. Validate listing ids at parse time, rejecting `:` / whitespace.\n2. Percent-encode the id segment in the marker, percent-decode at L7 substitution time.\n\nQ1 is silent here too. File only if a real user complaint surfaces, OR if the schema gains permissive id syntax.\n\nFiled at L7 close-out per L7 plan §D19.","status":"open","priority":4,"issue_type":"task","created_at":"2026-05-07T19:51:33.323112Z","created_by":"cscheid","updated_at":"2026-05-07T19:51:33.323112Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-fx23","depends_on_id":"bd-qf7r","type":"discovered-from","created_at":"2026-05-07T19:51:33.323112Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-fyb4z","title":"D1: list-table defaults header-rows to 1 (not 0)","description":"## What\n\nChange `transform_list_table_div` default for `header-rows` from 0 to 1 so a list-table with no explicit `header-rows` attribute promotes the first row to `` with `` cells — matching Quarto 1 behavior.\n\n## Where\n\n`crates/pampa/src/pandoc/treesitter_utils/postprocess.rs:308`\n\n```rust\nlet header_rows: usize = div\n .attr\n .2\n .get(\"header-rows\")\n .and_then(|v| v.parse().ok())\n .unwrap_or(0); // <- change to 1\n```\n\n## Tests (TDD)\n\nAdd fixtures under `crates/pampa/tests/`:\n\n1. `list_table_default_header.qmd` — bare `::: list-table`, no `header-rows` attr. Assert JSON output has `TableHead` with one row of cells.\n2. `list_table_no_header_rows.qmd` — same with explicit `header-rows: 0`. Assert `TableHead` is empty.\n3. `list_table_two_header_rows.qmd` — explicit `header-rows: 2`. Assert `TableHead` has 2 rows.\n\nRun failing first, then implement, then re-run.\n\n## Plan\n\nclaude-notes/plans/2026-05-20-table-default-rendering-parity.md (D1 section)","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-05-20T20:54:58.730903Z","created_by":"cscheid","updated_at":"2026-05-20T21:09:46.220920Z","closed_at":"2026-05-20T21:09:46.220751Z","close_reason":"Implemented in 87b5f236: bare list-table now defaults header-rows=1, matching Quarto 1. Tests added and green.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-fyb4z","depends_on_id":"bd-hir7j","type":"parent-child","created_at":"2026-05-20T20:54:58.730903Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-fyuo","title":"Cargo: upgrade hmac v0.12.1 → v0.13.0","description":"Major upgrade surfaced by cargo-upgrade survey 2026-05-04. Current 0.12.1 is range-pinned in workspace; latest is 0.13.0. 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:54.818590Z","created_by":"cscheid","updated_at":"2026-05-04T20:30:45.122298Z","closed_at":"2026-05-04T20:30:45.122157Z","close_reason":"merged: 0812812e","source_repo":".","compaction_level":0,"original_size":0,"labels":["cargo","deps"],"dependencies":[{"issue_id":"bd-fyuo","depends_on_id":"bd-hb8h","type":"discovered-from","created_at":"2026-05-04T18:16:04.861706Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} -{"id":"bd-fztki","title":"Audit PR #238 multi-engine state before mermaid impl","description":"Audit PR #238 (https://github.com/quarto-dev/q2/pull/238) and its plan claude-notes/plans/2026-05-27-multi-engine-execution.md (on feature/multi-engine branch). Confirm before MermaidEngine impl:\n\n1. Where MermaidEngine would register in the EngineRegistry — native + WASM. Mermaid is subprocess-free (pure-Rust transform), so it should be registrable in the WASM build too, unlike Knitr/Jupyter.\n2. The per-engine FileId provenance scheme (`..rmarkdown`) and what intermediate name mermaid should use.\n3. That `result.markdown` is QMD text re-parsed for the next engine — i.e. the mermaid engine emits QMD, not Pandoc-JSON.\n4. That capture_splice on feature/multi-engine still drops includes/filters/supporting_files (already spot-checked in plan v2; reconfirm at impl time).\n\nWrite findings back into claude-notes/plans/2026-05-28-mermaidjs-engine-design.md under Phase 1. Unblocks bd-gwfdo together with the design task.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:08.254090Z","created_by":"cscheid","updated_at":"2026-05-28T16:16:39.517880Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-fztki","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:08.254090Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-fztki","title":"Audit PR #238 multi-engine state before mermaid impl","description":"Audit PR #238 (https://github.com/quarto-dev/q2/pull/238) and its plan claude-notes/plans/2026-05-27-multi-engine-execution.md (on feature/multi-engine branch). Confirm before MermaidEngine impl:\n\n1. Where MermaidEngine would register in the EngineRegistry — native + WASM. Mermaid is subprocess-free (pure-Rust transform), so it should be registrable in the WASM build too, unlike Knitr/Jupyter.\n2. The per-engine FileId provenance scheme (`..rmarkdown`) and what intermediate name mermaid should use.\n3. That `result.markdown` is QMD text re-parsed for the next engine — i.e. the mermaid engine emits QMD, not Pandoc-JSON.\n4. That capture_splice on feature/multi-engine still drops includes/filters/supporting_files (already spot-checked in plan v2; reconfirm at impl time).\n\nWrite findings back into claude-notes/plans/2026-05-28-mermaidjs-engine-design.md under Phase 1. Unblocks bd-gwfdo together with the design task.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:08.254090Z","created_by":"cscheid","updated_at":"2026-05-28T17:14:00.844590Z","closed_at":"2026-05-28T17:14:00.844402Z","close_reason":"Audit complete (2026-05-28). Findings F1-F8 recorded in claude-notes/plans/2026-05-28-mermaidjs-engine-design.md Phase 1. Key results: (1) MermaidEngine registers in EngineRegistry::new always-block — works in both native and WASM; (2) per-engine FileId provenance is loop-driven, no engine participation needed; (3) result.markdown is QMD re-parsed for next engine — literal HTML round-trips via pampa's QMD reader; (4) capture-splice still drops aux fields but mermaid sidesteps the issue by registering directly in WASM (live in-browser execution); (5) BIG finding: in-process engine convention is text-level fence scanning per FixtureEngine, not AST parse-walk-serialize — Phase 2 refined accordingly; (6) jsdelivr script tag goes in engine markdown output not includes; (7) KNOWN_ENGINES should gain 'mermaidjs' for the top-level shortcut form; (8) bd-cp3em verified still present in feature/multi-engine.","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-fztki","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:08.254090Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-g18wu","title":"D8: q2 preview CSS bundle has same table rules as render","description":"## What\n\n`q2 preview`'s SPA bundles its own CSS (`assets/q2-preview-*.css`) that doesn't include Bootstrap table rules. After D7, render output looks correct; preview must match.\n\n## Approach (per plan)\n\nPrefer single-source-of-truth: have the preview server serve the same stylesheet the render pipeline produces, rather than bundling separately. Coordinate with `k-giyy` (Investigate style differences between WASM and CLI rendering) — its artifact-replacement approach may deliver this for free.\n\n## Tests\n\nChrome DevTools-driven e2e against a running `q2 preview` serving `tables.qmd`. Assert computed `font-weight: 700` on first `` and `padding: 8.5px` on cells.\n\n## Plan\n\nclaude-notes/plans/2026-05-20-table-default-rendering-parity.md (D8 section). Phase 3 — likely absorbed by k-giyy.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-05-20T20:55:25.188489Z","created_by":"cscheid","updated_at":"2026-05-20T22:51:48.168577Z","closed_at":"2026-05-20T22:51:48.168428Z","close_reason":"No code change needed. q2-preview SPA's bundled CSS already includes Bootstrap table rules. After bd-elgxx (D4/D5 react) and bd-tkamn (D6 react) added the markup hooks on the React side, the preview's computed table styles match Q1 byte-for-byte. The 'coordinate with k-giyy' concern was based on a wrong premise (the CSS was never missing; only the class hooks were).","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-g18wu","depends_on_id":"bd-hir7j","type":"parent-child","created_at":"2026-05-20T20:55:25.188489Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-g18wu","depends_on_id":"k-giyy","type":"related","created_at":"2026-05-20T20:55:25.188489Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-g1prx","title":"Phase 3: code folding decoration","description":"Phase 3 of bd-1tl09 (blocked by Phase 0 skeleton; should be sequenced after Phase 1 so the composition with filename is exercised).\n\n## Trigger\n\n`#| code-fold: true` (collapsed by default) or `#| code-fold: show` (expanded by default). Optional `#| code-summary: \"Custom Label\"` (default: \"Code\").\n\n## Output (HTML)\n\n
Code
\n\nImportant composition rule (from Q1's DecoratedCodeBlock pattern): when both filename AND fold are present, the fold's
wraps OUTSIDE the filename header, like this:\n\n
Code\n
\n
\n
\n
\n
\n\nThis means the Render transform applies the fold wrapper LAST, after filename wrapping.\n\n## Work\n\n1. Add fold: FoldMode { Off, Hide, Show } and summary: Option to CodeBlockDecoration.\n2. Generate transform: read per-block attrs. No doc default in Q1 — keep that.\n3. Render transform (HTML): emit
wrapper. Order with filename so the composition rule holds.\n4. CSS: port the .code-fold rules from _quarto-rules.scss.\n5. Mirror on React side.\n6. Composition test: a fixture with BOTH filename and code-fold renders with the correct nesting.\n\n## Tests\n\n- Native: single-feature snapshot (fold only).\n- Native: composition snapshot (fold + filename).\n- React: integration test for both single-feature and composed cases.\n- Visual: browser screenshot showing the
twirl works and the filename is visible inside it.\n\n## Acceptance\n\n- All tests pass.\n- cargo xtask verify passes.\n- Manual: clicking the disclosure triangle reveals/hides the code, filename header included.","notes":"Phase 2 (bd-j1trh) closed; Phase 3 unblocked. Read claude-notes/plans/2026-05-19-code-block-features.md §'Hand-off to next session — Phase 3 (code folding), bd-g1prx' FIRST. Key facts: (1) Render's wrap_in_place is now a single-pass cumulative wrap; just add wrap_with_fold_details as the outermost layer. (2) Q1 supports BOTH doc-default and per-block override for code-fold (different from Phase 2's doc-only mirror). (3)
RawBlock structuring (single vs split) needs to be cleared with user at kickoff — see plan open question. (4) SCSS is embedded via include_dir; rebuild q2 after .scss edits. (5) Pre-existing tree-sitter regression still trips xtask verify step 4/12 — use --skip-treesitter-tests --skip-treesitter-crlf-tests.","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-19T20:13:25.088564Z","created_by":"cscheid","updated_at":"2026-05-19T21:59:37.528460Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-g1prx","depends_on_id":"bd-1tl09","type":"parent-child","created_at":"2026-05-19T20:13:25.088564Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-g1prx","depends_on_id":"bd-ea5tl","type":"blocks","created_at":"2026-05-19T20:13:59.762892Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} {"id":"bd-gdhk","title":"Extract drain-and-flush-or-merge helper out of pass2 renderers","description":"Native `render_document_to_file` (`crates/quarto-core/src/render_to_file.rs:264-297`) and WASM `RenderToHtmlRenderer.render` (`crates/quarto-core/src/project/pass2_renderer.rs:343-355`) now have parallel logic: drain Project-scope artifacts, then either flush in-place via the per-page resolver (when `lib_dir` is empty) or merge into the orchestrator's accumulator (when non-empty). Extract a shared helper to dedupe — pure code-cleanup, no behavior change. Discovered while fixing bd-87fu.","status":"open","priority":3,"issue_type":"chore","created_at":"2026-05-01T22:49:06.984792Z","created_by":"cscheid","updated_at":"2026-05-01T22:49:06.984792Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-gdhk","depends_on_id":"bd-87fu","type":"discovered-from","created_at":"2026-05-01T22:49:06.984792Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} diff --git a/claude-notes/plans/2026-05-28-mermaidjs-engine-design.md b/claude-notes/plans/2026-05-28-mermaidjs-engine-design.md index 17fb4ea2..50e51685 100644 --- a/claude-notes/plans/2026-05-28-mermaidjs-engine-design.md +++ b/claude-notes/plans/2026-05-28-mermaidjs-engine-design.md @@ -330,54 +330,212 @@ Test matrix: - [x] Decisions locked: A1 (engine impl), B1 (direct RawBlock HTML emission with bd-mqk49 follow-up TODO), C1 (inline script RawBlock), D=bd-iq0hp closure +- [x] User ratified plan v2.1 (2026-05-28); bd-c6h96 closed ### Phase 1 — multi-engine current-state audit -- [ ] Read `claude-notes/plans/2026-05-27-multi-engine-execution.md` +- [x] Read `claude-notes/plans/2026-05-27-multi-engine-execution.md` (on `feature/multi-engine`) and confirm: - - Where `MermaidEngine` would register in - `EngineRegistry::register_default`-equivalent (native + WASM - builds both need it — mermaid is pure-Rust, no subprocess, so - the WASM build can register it too). - - The per-engine FileId provenance scheme (`..rmarkdown`) - and what mermaid's intermediate name should be. - - That `result.markdown` is the QMD-text re-parsed for the next - engine — i.e. the mermaid engine should emit QMD, not Pandoc-JSON. -- [ ] Verify the new `capture_splice.rs` actually drops aux fields - (re-confirm bd-cp3em is post-PR-#238 relevant) — already done - in v2 revision, but spot-check at impl time. + - [x] Where `MermaidEngine` would register + - [x] The per-engine FileId provenance scheme and mermaid's + intermediate name + - [x] That `result.markdown` is QMD-text re-parsed for the next + engine +- [x] Re-verify `capture_splice.rs` drops aux fields — confirmed + against `feature/multi-engine` (bd-cp3em remains valid) - [ ] If PR #238's review surfaces design changes that affect mermaid, - reflect them here. + reflect them here (deferred until #238 merges or stabilizes) + +#### Phase 1 findings (2026-05-28) + +Pulled from `feature/multi-engine` (PR #238 head `ed9cbbfe`) and the +multi-engine plan `claude-notes/plans/2026-05-27-multi-engine-execution.md`. + +**F1. Registration site is `EngineRegistry::new` +(`crates/quarto-core/src/engine/registry.rs:48-66`).** The current +shape is "always-register markdown; native-only register knitr + +jupyter": + +```rust +registry.register(Arc::new(MarkdownEngine::new())); + +#[cfg(not(target_arch = "wasm32"))] +{ + registry.register(Arc::new(KnitrEngine::new())); + registry.register(Arc::new(JupyterEngine::new())); +} +``` + +**Mermaid registers in the always-block alongside `MarkdownEngine`.** +Mermaid is text-only (no subprocess, no R/Python runtime), so the +same code can run in native + WASM. This is **strictly better than +the trace-replay route** for q2 preview: the WASM build executes +mermaid live in-browser, no recorded capture needed, and bd-cp3em's +aux-field-drop never bites mermaid because mermaid never executes +on the native side to need replaying. (Native render still uses the +local engine the same way; both paths converge on the same engine +code.) + +**F2. `KNOWN_ENGINES` const +(`crates/quarto-core/src/engine/detection.rs:31`)** is +`&["markdown", "knitr", "jupyter"]`. Used to gate the top-level +config shortcut (`jupyter: { kernel: python3 }` instead of +`engine: jupyter`). **Add `"mermaidjs"` to this list** so a +top-level `mermaidjs: { ... }` is recognized when no `engine:` is +declared. The `engine: [..., mermaidjs, ...]` path works regardless; +this is the nice-to-have for the bare top-level form. + +**F3. Per-engine FileId provenance is per-loop-iteration in the +stage, not per-engine.** `EngineExecutionStage::run` walks each +engine in `to_run` and appends one intermediate slot per executed +engine to `merged_context`. The intermediate filename is +`..rmarkdown` (per the multi-engine plan §2). **Mermaid +gets `.mermaidjs.rmarkdown` automatically** — no engine-side +participation required. `MermaidEngine::execute` only needs to +return `ExecuteResult { markdown, ..Default }`; the loop does the +rest. + +**F4. `result.markdown` is QMD text re-parsed for the next engine.** +Confirmed at `engine_execution.rs:255` (multi-engine version): + +```rust +let (qmd, qmd_source_info) = serialize_ast_to_qmd(&ast)?; +// ... +let mut result = engine.execute(&qmd, &exec_context)?; +// later: parse result.markdown as the next iteration's input +``` + +So mermaid emits QMD text. Literal HTML in QMD (e.g. +`
` on its own lines, blank-separated) +parses as `RawBlock(HTML, …)` via pampa's QMD reader (Pandoc +convention). No need to emit `\`\`\`{=html}` raw-block fences +unless we hit an edge case during impl. + +**F5. In-process engine convention is *text-level* fence scanning, +not AST parse-walk-serialize.** The biggest finding of the audit. +`FixtureEngine` (`crates/quarto-core/src/engine/fixture.rs:120-250`, +new in PR #238) is the only pure-Rust engine on the multi-engine +branch and it works text-level: a hand-rolled fence scanner finds +`{name}` cells and splices replacement text in. **Mermaid should +mirror this** rather than go through pampa's parser: + +- Simpler. ~100 lines of text-walking vs. AST manipulation + + `serialize_ast_to_qmd` (which is private to `engine_execution.rs`). +- Cheaper. No round-trip through the parser, no AST allocation. +- Less coupled. The mermaid engine never touches pampa internals or + Pandoc AST types; only `ExecuteResult`/`ExecutionContext`/`ExecutionError`. +- Matches the multi-engine plan's design pattern. Future graphviz/ + plantuml/dot engines would all follow the same template. + +The cell shape mermaid matches is exactly the FixtureEngine pattern: +opening fence `` ```{mermaid} `` ... source text ... closing fence +`` ``` ``. Replacement text is the literal HTML for the `
`
+wrapper, plus (once per document) the jsdelivr `` `RawBlock` at end of body
-    (C1). Serialize back to QMD and return as
+Refined after Phase 1 audit: mirror `fixture.rs` (text-level fence
+scanner), register in always-block (native + WASM), add `"mermaidjs"`
+to `KNOWN_ENGINES`.
+
+- [ ] Add `MermaidEngine` in
+      `crates/quarto-core/src/engine/mermaid.rs` (single file like
+      `markdown.rs` / `fixture.rs`; a directory is overkill).
+  - `name() == "mermaidjs"`. Always available
+    (`is_available() == true`).
+  - `execute(input, ctx)`: scan `input` line-by-line for opening
+    fences of the form `` ```{mermaid} ``; for each, find the
+    matching closing fence; replace the entire fenced block with a
+    literal HTML `
…source…
` + (HTML-escape the source). If any cell matched, append the + once-per-doc jsdelivr `` block + at end of output. Return as `ExecuteResult { markdown, ..Default }`. - - **Add a source-code comment** at the RawBlock-emission site: - `// bd-mqk49: when engines can declare per-format AST passes, - // route this through a format-conditional transform instead of - // emitting HTML inline. Today, Quarto 2 only renders HTML, so - // the format-locked emission is acceptable.` - - Register in native + WASM `EngineRegistry`s. -- [ ] Tests: - - Unit: mermaid engine on a fixture qmd containing `{mermaid}` and - non-mermaid blocks — only `{mermaid}` cells touched; script tag - appended. - - Pipeline: render a fixture qmd through the full HTML pipeline; - HTML contains `
` and the script tag. - - **End-to-end per CLAUDE.md**: `cargo run --bin q2 -- render fixture.qmd`, - grep the actual output, record invocation + observed output in - this plan before claiming done. + - **Source-code comment** at the HTML-emission site, pointing at + bd-mqk49: when engines can declare per-format AST passes, route + through a format-conditional transform instead. Today Q2 is + HTML-only so format-locked emission is acceptable. + - Reuse `fixture.rs`'s `parse_opening_fence` / `is_closing_fence` + helpers if they get factored out, or inline the same logic + (small enough). +- [ ] Register in `EngineRegistry::new` + (`crates/quarto-core/src/engine/registry.rs:48-66`) in the + always-block: + `registry.register(Arc::new(MermaidEngine::new()));` +- [ ] Add `"mermaidjs"` to `KNOWN_ENGINES` + (`crates/quarto-core/src/engine/detection.rs:31`) so the + top-level `mermaidjs:` shortcut is recognized. +- [ ] Module wiring: export `MermaidEngine` from + `crates/quarto-core/src/engine/mod.rs` (mirror how + `MarkdownEngine` is re-exported). +- [ ] Tests in `crates/quarto-core/src/engine/mermaid.rs`: + - Single-cell case: input with one `{mermaid}` cell produces the + `
` wrapper + the script tag.
+  - Multi-cell case: two cells, both wrapped; script tag emitted
+    once.
+  - No-cell case: input passes through unchanged; **no script tag**
+    emitted (only when at least one cell was matched).
+  - Mixed engines: input containing `{r}`, `{python}`, and
+    `{mermaid}` cells — only `{mermaid}` cells touched; others
+    pass through.
+  - HTML-escaping: source containing `<`, `>`, `&` in the diagram
+    is escaped in the output.
+- [ ] Integration test in `crates/quarto-core/tests/`:
+      render a fixture qmd with `engine: mermaidjs` (or
+      `engine: [mermaidjs]`) through the full HTML pipeline;
+      assert the rendered HTML contains `
`
+      and the script tag.
+- [ ] Multi-engine integration: a fixture with `engine: [knitr,
+      mermaidjs]` containing one `{r}` cell and one `{mermaid}`
+      cell — both render correctly (gated on the knitr R runtime
+      being available, or use the FixtureEngine pattern from PR #238
+      to substitute).
+- [ ] **End-to-end per CLAUDE.md**:
+      `cargo run --bin q2 -- render fixture.qmd`, grep the actual
+      output for `
` and the script tag,
+      record invocation + observed output in this plan before
+      claiming done.
 
 ### Phase 3 — q2-preview verification (closes bd-iq0hp)
 

From 206f98e5776c0d805ed585dc13689cffd8bda3cd Mon Sep 17 00:00:00 2001
From: Carlos Scheidegger 
Date: Thu, 28 May 2026 12:36:15 -0500
Subject: [PATCH 5/9] mermaid: MermaidEngine + integration tests (bd-gwfdo)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add the MermaidEngine `ExecutionEngine` impl ratified by the design
session (bd-c6h96): text-level fence scanner over QMD input,
rewrites `{mermaid}` code cells into raw HTML the browser-side
mermaid.js runtime picks up at page load, with a once-per-doc
jsdelivr `` block
-    at end of output. Return as
-    `ExecuteResult { markdown, ..Default }`.
-  - **Source-code comment** at the HTML-emission site, pointing at
-    bd-mqk49: when engines can declare per-format AST passes, route
-    through a format-conditional transform instead. Today Q2 is
-    HTML-only so format-locked emission is acceptable.
-  - Reuse `fixture.rs`'s `parse_opening_fence` / `is_closing_fence`
-    helpers if they get factored out, or inline the same logic
-    (small enough).
-- [ ] Register in `EngineRegistry::new`
-      (`crates/quarto-core/src/engine/registry.rs:48-66`) in the
-      always-block:
-      `registry.register(Arc::new(MermaidEngine::new()));`
-- [ ] Add `"mermaidjs"` to `KNOWN_ENGINES`
-      (`crates/quarto-core/src/engine/detection.rs:31`) so the
-      top-level `mermaidjs:` shortcut is recognized.
-- [ ] Module wiring: export `MermaidEngine` from
-      `crates/quarto-core/src/engine/mod.rs` (mirror how
-      `MarkdownEngine` is re-exported).
-- [ ] Tests in `crates/quarto-core/src/engine/mermaid.rs`:
-  - Single-cell case: input with one `{mermaid}` cell produces the
-    `
` wrapper + the script tag.
-  - Multi-cell case: two cells, both wrapped; script tag emitted
-    once.
-  - No-cell case: input passes through unchanged; **no script tag**
-    emitted (only when at least one cell was matched).
-  - Mixed engines: input containing `{r}`, `{python}`, and
-    `{mermaid}` cells — only `{mermaid}` cells touched; others
-    pass through.
-  - HTML-escaping: source containing `<`, `>`, `&` in the diagram
-    is escaped in the output.
-- [ ] Integration test in `crates/quarto-core/tests/`:
-      render a fixture qmd with `engine: mermaidjs` (or
-      `engine: [mermaidjs]`) through the full HTML pipeline;
-      assert the rendered HTML contains `
`
-      and the script tag.
+- [x] Add `MermaidEngine` in
+      `crates/quarto-core/src/engine/mermaid.rs` — text-level fence
+      scanner, B1 emission, once-per-doc script append, HTML-escaped
+      source, bd-mqk49 TODO comment at the emission site.
+- [x] Register in `EngineRegistry::new` always-block
+      (`crates/quarto-core/src/engine/registry.rs:52-67`).
+- [x] Add `"mermaidjs"` to `KNOWN_ENGINES`
+      (`crates/quarto-core/src/engine/detection.rs:31`).
+- [x] Module wiring: export `MermaidEngine` from `engine/mod.rs`;
+      doc-comment table updated for the new "always-available"
+      mermaidjs row.
+- [x] Unit tests in `engine/mermaid.rs` (12 tests, all passing):
+      `name_is_mermaidjs`, `always_available`,
+      `single_cell_emits_pre_and_script`,
+      `multiple_cells_share_one_script`, `no_cells_means_no_script`,
+      `other_engine_cells_pass_through`,
+      `does_not_match_inside_other_fenced_blocks`,
+      `html_escapes_lt_gt_amp_in_source`,
+      `errors_on_unterminated_mermaid_cell`,
+      `unterminated_non_mermaid_fence_is_passthrough`,
+      `longer_fences_round_trip`, `script_appended_only_once_after_body`.
+- [x] Full `quarto-core` test suite passes (2170 tests); full
+      workspace passes (9496 tests). No regressions from
+      `KNOWN_ENGINES` change.
+- [x] Integration test in `crates/quarto-core/tests/mermaid_pipeline.rs`
+      — 4 tests routed through `render_to_file` (the same path
+      `q2 render` uses): single-doc emits pre+script, no-cells
+      omits script, multiple cells share one script, array engine
+      form works. All passing.
 - [ ] Multi-engine integration: a fixture with `engine: [knitr,
       mermaidjs]` containing one `{r}` cell and one `{mermaid}`
       cell — both render correctly (gated on the knitr R runtime
       being available, or use the FixtureEngine pattern from PR #238
-      to substitute).
-- [ ] **End-to-end per CLAUDE.md**:
-      `cargo run --bin q2 -- render fixture.qmd`, grep the actual
-      output for `
` and the script tag,
-      record invocation + observed output in this plan before
-      claiming done.
+      to substitute). Deferred to a follow-up — single-doc and
+      array-form coverage is sufficient for the first ship.
+- [x] **End-to-end per CLAUDE.md** verification (recorded below).
+
+#### Phase 2 finding: emission must be `\`\`\`{=html}`-fenced, not bare HTML
+
+A finding the audit missed: pampa's QMD reader treats *bare* ``
+markup at block position as a sequence of `RawInline` nodes — not a
+block-level raw HTML element — and tries to parse the interior as
+Markdown. That works for a `
` with simple content but breaks
+hard on the script block: `mermaid.initialize({ startOnLoad: true });`
+contains `:`, which the parser treats as a definition-list-like
+construct and rejects with `unexpected character or token here`.
+
+The fix is to emit the explicit Pandoc raw-block form so the reader
+treats the whole thing as opaque raw HTML and skips Markdown parsing
+inside it:
+
+```text
+```{=html}
+
+…HTML-escaped source…
+
+``` +``` + +…and the same wrapping for the ` +``` + +This output was inspected at the file system; the rendered HTML is +exactly the markup the user's hub-client browser session would +receive, and the mermaid runtime would pick up both diagrams at +page load. ### Phase 3 — q2-preview verification (closes bd-iq0hp) diff --git a/crates/quarto-core/src/engine/detection.rs b/crates/quarto-core/src/engine/detection.rs index b62f2188..00ad434f 100644 --- a/crates/quarto-core/src/engine/detection.rs +++ b/crates/quarto-core/src/engine/detection.rs @@ -28,7 +28,7 @@ use quarto_pandoc_types::ConfigValue; /// Known execution engine names. -pub const KNOWN_ENGINES: &[&str] = &["markdown", "knitr", "jupyter"]; +pub const KNOWN_ENGINES: &[&str] = &["markdown", "mermaidjs", "knitr", "jupyter"]; /// Result of engine detection. /// diff --git a/crates/quarto-core/src/engine/mermaid.rs b/crates/quarto-core/src/engine/mermaid.rs new file mode 100644 index 00000000..1aa278d5 --- /dev/null +++ b/crates/quarto-core/src/engine/mermaid.rs @@ -0,0 +1,490 @@ +/* + * engine/mermaid.rs + * Copyright (c) 2026 Posit, PBC + * + * Mermaid diagram engine: rewrites `{mermaid}` code cells into the + * `
` markup that the mermaid.js browser runtime
+ * picks up at page load.
+ */
+
+//! Mermaid diagram engine — direct HTML emission (B1).
+//!
+//! [`MermaidEngine`] is a pure-Rust, in-process [`ExecutionEngine`]
+//! that rewrites Quarto's `{mermaid}` code cells into raw HTML the
+//! browser-side `mermaid.js` runtime can pick up. It does **no** server-
+//! side diagram rendering: no SVG generation, no PNG export, no
+//! subprocess. The actual diagram drawing happens in the browser when
+//! `mermaid.initialize({ startOnLoad: true })` runs.
+//!
+//! # What it does
+//!
+//! 1. Scans the input QMD for executable code cells whose info string
+//!    is exactly `{mermaid}` — i.e. fences of the form
+//!    ```` ```{mermaid} ````. (pampa keeps the braces in the class
+//!    name, matching the `{r}` / `{python}` / `{fixture-…}` shape.)
+//! 2. Rewrites each matched cell as a raw HTML block
+//!    `
…HTML-escaped source…
`. The browser +//! then reads the `textContent` of the element (entity-decoding the +//! escapes) and hands it to `mermaid.render`. +//! 3. If at least one mermaid cell was found, appends a single +//! ` +```"; + +/// Mermaid diagram engine — emits browser-runtime markup for `{mermaid}` +/// code cells. See module docs. +/// +/// # Characteristics +/// +/// - Always available (no external dependencies). +/// - Available in both native and WASM builds (no subprocess work). +/// - Does not support freeze/thaw (output is a deterministic function +/// of input — caching the trace via `engine: replay` would be silly). +/// - Produces no intermediate files. +#[derive(Debug, Clone, Default)] +pub struct MermaidEngine; + +impl MermaidEngine { + /// Create a new mermaid engine instance. + pub fn new() -> Self { + Self + } +} + +impl ExecutionEngine for MermaidEngine { + fn name(&self) -> &str { + "mermaidjs" + } + + fn execute( + &self, + input: &str, + _ctx: &ExecutionContext, + ) -> Result { + let output = render_mermaid_cells(input) + .map_err(|msg| ExecutionError::execution_failed(self.name(), msg))?; + Ok(ExecuteResult::new(output)) + } + + fn is_available(&self) -> bool { + true + } +} + +/// Rewrite each ```` ```{mermaid} ```` executable cell in `input` as a +/// raw HTML `
` block. If any cell was +/// matched, append the once-per-doc jsdelivr `') at end of body (C1). Serialize back to QMD; return as ExecuteResult { markdown, ..Default }.\n- **Add a source-code comment** at the RawBlock-emission site pointing at bd-mqk49: 'when engines can declare per-format AST passes, route this through a format-conditional transform instead of emitting HTML inline. Today Quarto 2 only renders HTML so the format-locked emission is acceptable.'\n- Register in native + WASM EngineRegistry.\n\nTests: unit (engine on fixture) + pipeline (full HTML render) + end-to-end per CLAUDE.md verification policy (cargo run --bin q2 -- render fixture.qmd; grep output; record in plan).\n\nThe B2c/B2e (dedicated HTML-emit stage / engine-declared per-format pass) path was rejected for the first ship — Quarto 2 is HTML-only today, so format-agnostic decomposition would be hypothetical correctness against a future cost. The follow-up lives on bd-mqk49.\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","status":"open","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","updated_at":"2026-05-28T17:04:53.629281Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-gwfdo","depends_on_id":"bd-c6h96","type":"blocks","created_at":"2026-05-28T13:45:35.978499Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-fztki","type":"blocks","created_at":"2026-05-28T13:45:36.396009Z","created_by":"cscheid","metadata":"{}","thread_id":""},{"issue_id":"bd-gwfdo","depends_on_id":"bd-je48v","type":"parent-child","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","metadata":"{}","thread_id":""}]} +{"id":"bd-gwfdo","title":"Implement MermaidEngine (B1: direct RawBlock HTML emission)","description":"Implement mermaid handling per the ratified v2 design (see bd-c6h96 outcome and plan v2, Q-B → B1). **Gated on PR #238 merging.**\n\nAdd MermaidEngine implementing ExecutionEngine in crates/quarto-core/src/engine/mermaid/ (mirror engine/markdown.rs shape — closest precedent for a no-subprocess engine):\n\n- name() == 'mermaidjs'.\n- execute(input, ctx): parse input as QMD, walk for code cells with class 'mermaid', replace each with RawBlock(HTML, '
...
') (B1 — direct HTML emission), append a once-per-doc RawBlock(HTML, '') at end of body (C1). Serialize back to QMD; return as ExecuteResult { markdown, ..Default }.\n- **Add a source-code comment** at the RawBlock-emission site pointing at bd-mqk49: 'when engines can declare per-format AST passes, route this through a format-conditional transform instead of emitting HTML inline. Today Quarto 2 only renders HTML so the format-locked emission is acceptable.'\n- Register in native + WASM EngineRegistry.\n\nTests: unit (engine on fixture) + pipeline (full HTML render) + end-to-end per CLAUDE.md verification policy (cargo run --bin q2 -- render fixture.qmd; grep output; record in plan).\n\nThe B2c/B2e (dedicated HTML-emit stage / engine-declared per-format pass) path was rejected for the first ship — Quarto 2 is HTML-only today, so format-agnostic decomposition would be hypothetical correctness against a future cost. The follow-up lives on bd-mqk49.\n\nPlan: claude-notes/plans/2026-05-28-mermaidjs-engine-design.md","notes":"Topic branch: beads/bd-gwfdo-implement-mermaidengine-b1-direct (commit 93418945). Working tree clean. cargo nextest run --workspace passes (9496 tests). End-to-end verified via cargo run --bin q2 -- render. cargo xtask verify --skip-hub-build in flight (background).","status":"closed","priority":2,"issue_type":"task","created_at":"2026-05-28T13:45:14.944001Z","created_by":"cscheid","updated_at":"2026-05-28T17:43:58.846739Z","closed_at":"2026-05-28T17:43:58.846606Z","close_reason":"Implementation complete (2026-05-28).\n\nMermaidEngine landed on feature/mermaid-engine via merge commit da29062f (topic 93418945).\n\nWhat shipped:\n- crates/quarto-core/src/engine/mermaid.rs — text-level fence scanner, B1 direct HTML emission via ```{=html} raw-block fence, HTML-escaped source, once-per-doc jsdelivr script include.\n- Registered in EngineRegistry::new always-block (native + WASM).\n- 'mermaidjs' added to KNOWN_ENGINES.\n- 13 unit tests in mermaid.rs.\n- 4 integration tests in tests/mermaid_pipeline.rs (single-doc, no-cells, multi-cell, array engine form).\n- bd-mqk49 follow-up comment in code at the HTML emission site.\n\nVerification:\n- cargo nextest run --workspace: 9501 tests pass.\n- cargo xtask verify (full, with WASM): all 12 steps green after npm install. Hub-client build + 79 tests + 61 smoke-all WASM tests pass.\n- End-to-end via real binary: 'cargo run --bin q2 -- render /tmp/mermaid-e2e/test.qmd' produces
 wrappers + jsdelivr script tag in rendered HTML. Recorded in plan.\n\nPhase 2 surprise documented in plan: bare 
/
 ```";
 
diff --git a/crates/quarto-core/tests/mermaid_pipeline.rs b/crates/quarto-core/tests/mermaid_pipeline.rs
index c177a427..b1f91a75 100644
--- a/crates/quarto-core/tests/mermaid_pipeline.rs
+++ b/crates/quarto-core/tests/mermaid_pipeline.rs
@@ -81,8 +81,9 @@ fn single_doc_mermaid_emits_pre_and_script() {
          html:\n{html}"
     );
     assert!(
-        html.contains("startOnLoad: true"),
-        "expected initialize call in rendered HTML. html:\n{html}"
+        html.contains("mermaid.run({ querySelector: 'pre.mermaid' })"),
+        "expected explicit mermaid.run() call in rendered HTML. \
+         html:\n{html}"
     );
     // The diagram source survives HTML-escaped: `-->` becomes `-->`.
     assert!(
diff --git a/hub-client/src/components/render/ReactAstSlideRenderer.test.tsx b/hub-client/src/components/render/ReactAstSlideRenderer.test.tsx
index b141d254..76b415cb 100644
--- a/hub-client/src/components/render/ReactAstSlideRenderer.test.tsx
+++ b/hub-client/src/components/render/ReactAstSlideRenderer.test.tsx
@@ -1,4 +1,6 @@
 /**
+ * @vitest-environment jsdom
+ *
  * Regression test for the slide-renderer migration to the framework
  * `extractMetaString` helper (Plan 2D Phase 6.0d).
  *
@@ -15,8 +17,10 @@
  */
 
 import { describe, expect, it } from 'vitest';
-import type { PandocAST } from '@quarto/preview-renderer/framework';
-import { parseSlides } from './ReactAstSlideRenderer';
+import { render } from '@testing-library/react';
+import React from 'react';
+import type { PandocAST, RawBlock } from '@quarto/preview-renderer/framework';
+import { parseSlides, renderBlock } from './ReactAstSlideRenderer';
 
 describe('ReactAstSlideRenderer slide-title meta coercion', () => {
     it('preserves inline emphasis text in MetaInlines title (Plan 2D 6.0d)', () => {
@@ -57,3 +61,75 @@ describe('ReactAstSlideRenderer slide-title meta coercion', () => {
         expect(slides[0].title).toBe('My Doc');
     });
 });
+
+/**
+ * Regression tests for the `RawBlock(html, …)` script-re-execution
+ * shim added in bd-my0o5.
+ *
+ * Without the shim, scripts inserted via `dangerouslySetInnerHTML`
+ * remain inert (the HTML spec only executes script elements that
+ * the parser sees in the initial document, or that are created via
+ * `document.createElement`). That broke engines that emit in-band
+ * `'],
+        };
+
+        const { container } = render(renderBlock(rawBlock, 0, '', () => {}));
+
+        const scripts = container.querySelectorAll('script');
+        expect(scripts.length).toBe(1);
+        // The text body and id attribute are copied onto the
+        // replacement script; the replacement is what's in the DOM
+        // now (the original was removed by `replaceWith`).
+        expect(scripts[0].getAttribute('id')).toBe('orig-marker');
+        expect(scripts[0].textContent).toContain('inline body');
+    });
+
+    it('copies script attributes (e.g. type="module") onto the recreated tag', () => {
+        // We cannot easily exercise an actual ES-module load in jsdom,
+        // but we can assert the recreated script keeps the type
+        // attribute — which is what makes the browser parse the source
+        // as a module rather than a classic script.
+        const rawBlock: RawBlock = {
+            t: 'RawBlock',
+            c: ['html', ''],
+        };
+
+        const { container } = render(renderBlock(rawBlock, 0, '', () => {}));
+
+        const script = container.querySelector('script');
+        expect(script).not.toBeNull();
+        expect(script!.getAttribute('type')).toBe('module');
+        expect(script!.hasAttribute('data-mermaid-init')).toBe(true);
+    });
+
+    it('passes through non-html raw blocks as null (no behaviour change)', () => {
+        const rawBlock: RawBlock = { t: 'RawBlock', c: ['latex', '\\section{Foo}'] };
+        const result = renderBlock(rawBlock, 0, '', () => {});
+        expect(result).toBeNull();
+    });
+});
diff --git a/hub-client/src/components/render/ReactAstSlideRenderer.tsx b/hub-client/src/components/render/ReactAstSlideRenderer.tsx
index 59cb29fd..d0bddd24 100644
--- a/hub-client/src/components/render/ReactAstSlideRenderer.tsx
+++ b/hub-client/src/components/render/ReactAstSlideRenderer.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
 import { AspectRatioScaler } from '../render/AspectRatioScaler';
 import katex from 'katex';
 import 'katex/dist/katex.min.css';
@@ -484,6 +484,73 @@ function parseStyleString(styleString: string): React.CSSProperties {
 // BlockNode Rendering
 // ============================================================================
 
+/**
+ * Render a Pandoc `RawBlock(html, …)` so embedded `');
+        const { container } = render();
+
+        const scripts = container.querySelectorAll('script');
+        expect(scripts.length).toBe(1);
+        // Attributes and body were copied onto the replacement.
+        expect(scripts[0].getAttribute('id')).toBe('orig-marker');
+        expect(scripts[0].getAttribute('type')).toBe('module');
+        expect(scripts[0].textContent).toContain('module body');
+    });
+
+    it('preserves the mermaid engine\'s emission shape end-to-end through the renderer', () => {
+        // Verbatim of what MermaidEngine emits after a render cycle:
+        // a 
 for each cell and one
+        // ',
+        ].join('\n');
+        const node = rb('html', html);
+        const { container } = render();
+
+        expect(container.querySelectorAll('pre.mermaid').length).toBe(1);
+        const script = container.querySelector('script[type="module"]');
+        expect(script).not.toBeNull();
+        // The recreated script must still carry the import; this is
+        // what a real browser would execute and that would set
+        // `window.mermaid` and run the diagram render.
+        expect(script!.textContent).toContain('mermaid.esm.min.mjs');
+        expect(script!.textContent).toContain('startOnLoad: true');
+    });
+
+    it('passes through non-html raw blocks as 
', () => {
+        const node = rb('latex', '\\section{Foo}');
+        const { container } = render();
+        const pre = container.querySelector('pre');
+        expect(pre).not.toBeNull();
+        expect(pre!.textContent).toContain('\\section{Foo}');
+    });
+});
diff --git a/ts-packages/preview-renderer/src/q2-preview/blocks/RawBlock.tsx b/ts-packages/preview-renderer/src/q2-preview/blocks/RawBlock.tsx
index f361e96a..c603ee59 100644
--- a/ts-packages/preview-renderer/src/q2-preview/blocks/RawBlock.tsx
+++ b/ts-packages/preview-renderer/src/q2-preview/blocks/RawBlock.tsx
@@ -1,9 +1,12 @@
+import { useEffect, useRef } from 'react';
 import type { NodeArgs, RawBlock as RawBlockType } from '../../framework';
 
 /**
  * RawBlock semantics:
  *  - format === 'html' (or 'html5'): inject raw HTML via
- *    `dangerouslySetInnerHTML` so users can embed exact markup.
+ *    `dangerouslySetInnerHTML` so users can embed exact markup. After
+ *    mount we re-execute any `