Skip to content

Preview: transport engine-generated figures in the capture doc (bd-qbhp2cvv)#410

Merged
cscheid merged 6 commits into
mainfrom
bugfix/bd-qbhp2cvv-preview-engine-images
Jul 23, 2026
Merged

Preview: transport engine-generated figures in the capture doc (bd-qbhp2cvv)#410
cscheid merged 6 commits into
mainfrom
bugfix/bd-qbhp2cvv-preview-engine-images

Conversation

@cscheid

@cscheid cscheid commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary

Knitr/jupyter execution results that include images appeared in q2 render output but were broken images in q2 preview and the hub-client q2-preview format. Root cause: the EngineCapture recorded for preview replay serialized ExecuteResult.supporting_files as paths only — the replaying machine (or browser VFS) can never see those files, and the hub even records captures in a temp dir that is deleted right after. The only code that turned those paths into bytes (copy_resources_to_output_dir) is native-render-only.

Fix: carry the bytes inside the capture binary doc and materialize them into the (WASM) VFS at splice time, where the existing image resolvers already look.

  • quarto-trace: CaptureFile { path, contents_base64 } + serde-defaulted EngineCapture.files. Wire-compatible both ways (old captures deserialize; file-less captures serialize byte-identically).
  • quarto-core / recording: EngineExecutionStage embeds supporting-file bytes (recursive dir walk, doc-relative forward-slash paths, fail-soft), gated by new PipelineObserver::wants_engine_capture_files() — only the preview capture recorder opts in, so q2 render pays zero extra I/O.
  • quarto-core / replay: CaptureSpliceStage materializes embedded files next to the live doc via ctx.runtime (in WASM: the preview VFS). Path-escape validation since captures are remote data. Zero TypeScript changes needed — assetWalker.ts / iframePostProcessor.ts pick the files out of the VFS as-is.
  • Writers: shared gzip_captures helper (+ 10 MB size warning) replaces three duplicated gzip blocks (eager driver, re-execute handler, hub-provider exec server).

End-to-end verification

  • knitr repro (ggplot cell): capture embeds the 22 KB PNG; in a real Chrome session the preview renders <img src="blob:…"> at 672×480 with no console errors. Record in claude-notes/plans/2026-07-23-preview-engine-supporting-files.md.
  • jupyter: blocked upstream — the engine emits only text reprs for matplotlib figures even under q2 render (no supporting files to transport). Filed bd-rwz8kwia (discovered-from bd-qbhp2cvv); this transport is engine-agnostic and picks up jupyter figures once that engine gap is fixed.

Test plan

  • 3 wire-format tests (quarto-trace roundtrip), 10 unit tests on collect/materialize/gzip edge cases, end-to-end recording test (figure-writing engine) and splice-materialization test (temp-dir project through render_qmd_to_html) — each behavior test written first and observed failing.
  • cargo xtask verify passes end to end (Rust + clippy -D warnings + ts-packages + hub-client build:all + test:ci + q2-preview-spa build).

🤖 Generated with Claude Code

cscheid and others added 6 commits July 23, 2026 14:21
Adds CaptureFile { path, contents_base64 } and a serde-defaulted
files: Vec<CaptureFile> to quarto_trace::EngineCapture, the carrier
for engine-generated supporting-file bytes (knitr/jupyter figures)
so preview replay can materialize them into the WASM VFS.

Wire-format compatible both ways: old captures deserialize (default),
and captures without files serialize without the key (skip_serializing_if),
byte-identical to the pre-field format. Pinned by three new tests in
quarto-trace/tests/roundtrip.rs. All EngineCapture literal sites
(tests/fixtures) updated with files: Vec::new().

Also switches this worktree's plan symlink/context to
claude-notes/plans/2026-07-23-preview-engine-supporting-files.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New quarto-core module engine/capture_files.rs:
- collect_capture_files: resolves ExecuteResult.supporting_files
  entries (absolute or doc-relative; files or directories, walked
  recursively via SystemRuntime) against the doc dir, reads bytes,
  and returns CaptureFiles keyed by doc-relative forward-slash paths.
  Fail-soft: unreadable or outside-doc-dir entries warn and skip.
  Sorted listings keep capture bytes deterministic for the
  hash-keyed capture cache.
- materialize_capture_files: the splice-time counterpart (wired in
  the next commit); validates paths stay under the doc dir since
  captures are remote data.

EngineExecutionStage embeds the collected files in the EngineCapture
aux payload, gated by the new
PipelineObserver::wants_engine_capture_files() (default false — plain
q2 render and trace observers pay zero extra I/O). Only the preview
recorder's CaptureCollector opts in; its captures replay on machines
(hub peers, browser VFS) that cannot see the recording disk — and the
hub records in a temp dir deleted right after, so recording time is
the only moment the bytes exist.

Tests: 8 unit tests on collect/materialize edge cases + an
end-to-end record_capture test with a figure-writing engine
(written first, observed failing with files=[]).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CaptureSpliceStage now writes each capture's embedded supporting
files (CaptureFile bytes from Phase 2) next to the live document
before splicing, via ctx.runtime. In WASM that runtime IS the
preview VFS — the exact place the SPA/hub-client image resolvers
(assetWalker.ts blob URLs, iframePostProcessor.ts data URIs) read
project-relative image srcs — so spliced engine output like
![](doc_files/figure-html/fig.png) resolves without any TS changes.

Doc dir = doc_ast.path.parent() (absolute in the WASM VFS), with a
project-dir fallback for bare source names in native harnesses.
Materialization is fail-soft and path-validated (no escape from the
doc dir; captures are remote data).

Test (written first, observed failing with NotFound): a
render_qmd_to_html run over a temp-dir project with a capture whose
files carry fake PNG bytes must both splice the image ref into the
HTML and materialize the bytes at <docdir>/test_files/figure-html/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gzip_captures + capture_doc_size_warning (pure, unit-tested;
threshold 10MB per plan decision) live in
quarto_core::engine::capture_files. The three capture writers —
quarto-preview's eager driver + re-execute handler and
quarto-hub-provider's exec server — previously duplicated the
serialize+gzip block; all now call the shared helper, so oversized
capture docs (embedded figure bytes can inflate multi-plot docs)
warn uniformly at every write site.

Dependency cleanup: flate2 promoted to a regular dep of quarto-core
(already in its graph via quarto-trace); dropped to dev-dep in
quarto-hub-provider (only its integration test still gunzips).

Full workspace suite: 10370 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

The Phase 4 flate2 dependency line had landed in quarto-core's
native-only [target.'cfg(not(wasm32))'.dependencies] section, breaking
the WASM build (capture_files.rs compiles on both targets). Moved to
[dependencies]; flate2 builds cleanly on wasm32 (miniz_oxide) and the
WASM client already uses it to gunzip captures.

E2E (recorded in the plan): knitr repro verified in a real browser —
capture embeds the 22KB ggplot PNG, preview renders it via a blob URL
(672x480, loaded, no console errors). jupyter is blocked upstream:
the engine emits only text reprs for matplotlib figures even under
q2 render (no supporting files to transport) — filed bd-rwz8kwia,
discovered-from bd-qbhp2cvv.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
map_or_else in CaptureSpliceStage's doc-dir fallback and
sort_unstable in the capture_files test, both flagged by the
cargo xtask verify clippy leg. Full verify now passes end to end
(Rust build + nextest, ts-packages, hub-client build:all + test:ci,
q2-preview-spa build).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Jul 23, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cscheid
cscheid merged commit 58f9d27 into main Jul 23, 2026
8 checks passed
@cscheid
cscheid deleted the bugfix/bd-qbhp2cvv-preview-engine-images branch July 23, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants