Skip to content

Execution records in use: preview and hub run from records; one presenter for every engine (RIU + RFU + OPR) - #1171

Merged
gordonwoodhull merged 43 commits into
mainfrom
feature/one-presenter
Nov 12, 2026
Merged

gordonwoodhull merged 43 commits into
mainfrom
feature/one-presenter

Conversation

@gordonwoodhull

@gordonwoodhull gordonwoodhull commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

Execution records: preview and hub run from records, and every engine goes through one presenter

Preview and provide-hub now work from an ipynb execution record instead of the legacy capture cache. A record can come from an engine run, a file, or a Positron cache. One presenter turns a recorded cell into page output for Jupyter, Julia and knitr alike, and plain q2 render of Julia and knitr takes the same route. Three epics, developed on one integration branch, land together because each builds on the last. Main has the record writer behind an off-by-default gate; none of the consumer side below exists on main, so the command and metadata names are new, not renames:

  1. RIU, records in use. Records replace the preview capture cache and can come from places other than an engine.
  2. RFU, RIU follow-up. Closes the gaps the Positron demo exposed: a q2 convert command, knitr export, tests, and docs.
  3. OPR, one presenter path. Every recorded cell goes through one presenter that reads what the cell supplies, not which engine made it.

The branch changes about 390 files, mostly in quarto-core and quarto-preview, and no .snap files.

What changes for users

  • Record writing is on by default for local preview and provide-hub. The record is the only thing the viewer needs; the legacy capture remains only for an engine that builds no record, and a diagnostic says so.
  • execute: record-sources chooses where a document's record comes from. The source list (engine, cache, a directory, and so on) is one metadata key, set with -M on the command line. preview, provide-hub, convert and render read it through one reader, so a document gets its record from the first source that has one.
  • q2 convert --to positron-ipynb exports a record as a Positron cache file, for Python, Julia and knitr documents.
  • Positron cache files import as records, and the import gate asks whether the record declares kernel equivalence instead of testing the engine name.
  • Plain q2 render of Julia and knitr builds the record in memory and presents it through the Rust presenter, writing nothing under .quarto. A cached knitr chunk becomes a record cell by reading its sidecar (level 3 cache hit), so knitr's cache still works. The LaTeX family, pptx, context and unknown knitr targets stay on the engine route.
  • Julia gains the capture facts knitr has (expression starts, anchors, output kinds), through one runner change. Julia still shows only its last value, so it does not interleave as knitr does.
  • Presentation fixes that apply to every engine: ANSI escapes are stripped in one function, group captions and panel ids are numbered, page dependencies for widget outputs are derived on every route, image size is read from output metadata, and engines can set cell and output attributes through quarto_attrs.

Jupyter output stays byte-identical unless a cell sets a new option, except for two documented preview and hub fixes (OPR-1a, OPR-1b) and one group-caption fix (OPR-7j). Marimo keeps the legacy capture splice and is outside this work.

How it is built

  • One cell step, one document function. Each cell runs through a single presenter step that takes optional inputs (anchors, starts, kinds, figure count, resolved options, echoed code). A single document-level function takes matched unit and cell pairs and returns a replacement, a skip with a reason, or an error. Callers keep their own matching and error policy.
  • Presentation is chosen by offer and acknowledgement, not by engine name. A presenter feature switches on when a cell supplies the input it needs and degrades one input at a time when it does not.
  • warning and message are enforced exactly where the record's hash definition lists them as display-only. When an option is in the hash, capture already enforced it.
  • A Quarto 1 parity harness gates the plain-render switch per target, with the Julia targets graduated and the interleaving entries signed.
  • The Julia engine extension is vendored as a subtree of the fork's q2/node-port. It is restored by cargo xtask pull-extension-subtree julia-engine as the last step before review, so the history ends in a subtree merge commit.

What to check

  • The Windows run is not done. aws-lc-sys cannot cross-compile from macOS, so RFU-6 needs a native Windows run of the changed crates and of the tests that exercise paths and file handles.
  • OPR-8 (final docs and end-to-end pass) is still open. The user docs for q2 convert, execution-records and preview are written, but the embedded-docs refresh (cargo xtask build-agents-docs) is that plan's last step.
  • OPR-7d was cancelled. Its work moved into OPR-7a through 7h, so a reader following the plan numbers will find a gap.
  • Positron fork work is outside this repo. RFU-7 changed the fork's extension and demo guide on local commits that were never pushed.
  • Reviewers who want the shape before the diff should read the three epic notes in claude-notes/plans/: 2026-11-02-records-in-use-epic.md, 2026-11-08-rfu-epic-riu-follow-up.md, 2026-11-07-one-presenter-path-epic.md.

Testing

  • cargo xtask lint and cargo clippy -p quarto-core -p pampa --all-targets -- -D warnings pass.
  • cargo nextest run -p quarto-core -p pampa: 13772 run, 13772 passed, 36 skipped, with pandoc 3.13 on PATH (the Julia subtree included). Under pandoc 3.12.1, 252 tests fail on the 3.13 version floor that main introduced; that is not a regression from this branch.
  • cargo xtask verify (full, with the hub-client build) was started locally; results are posted below when it finishes.
  • Not done: the Windows run (RFU-6).

@posit-snyk-bot

posit-snyk-bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ 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.

A capture ref restored from disk was trusted as fresh until the next edit, so a
document changed while the preview was down showed stale output. The preview
now recomputes staleness once at startup. The RIU-0 spikes S0-S7 (Python,
Julia, knitr, headless Chromium, the real preview and hub, record-only cache
entries, Positron export) and the records-in-use epic and plans RIU-0 to
RIU-6 they fed are included as notes.

Fixtures: 9 files added, none modified, none removed, under
crates/quarto-preview/tests/fixtures/records-in-use (6, plus 3 under project/).
No .snap files.
… entries (RIU-2)

One publish seam encodes a record for the cache and refuses a record with no
legacy form when the gate is off. The cache wrapper stores and hits a
record-only entry; record sources are walked first-wins with per-source
outcomes; re-execute carries a config and an execution guard; record_one walks
the sources and policy-off still reads records. Reuse across a restart is
pinned.

Also here: five review rounds and the RIU-0b/0c spike results that rewrote the
epic and plans, and the pointers from the execution-records plans (decisions 4
and 8, EXR-15, EXR-17) at the records-in-use decisions. No fixtures or
snapshots added or changed.
export_for_positron filters a record down to what Positron's cache accepts,
with one representation per image. Tests run against real Python and Julia
records and cover mirror and import round trips.

Fixtures: 8 files added under crates/quarto-core/tests/fixtures/records-export,
none modified. No .snap files.
…e 0)

Research notes only: links, live update, console, stop and clear behavior, the
Quarto 1 source reading, and the finding that the pane does not sync. Points
at the strand for printing the URL when the server is ready. No code, no
fixtures or snapshots.
… (RIU-1)

Each surface (preview, provider) has its own writer gate. A document whose
engine builds no execution record is stored whole in the legacy capture format
and gets the warning Q-18-15 (Q-18-14 went to knitr child documents on main),
once per capture run; record copy fails soft. The preview config takes the
Preview surface and the provider returns engines_without_record. The
provide-hub flip is deferred to RIU-7.

Also here: the epic's lanes and ready board, the exit criterion for a native
Windows run, and the EXR decision-4 pointer to the runbook gate section.
Catalog entry Q-18-15, docs page docs/errors/engine/Q-18-15.qmd and its
sidebar line are added. No fixtures or .snap files.
record_from_positron converts a Positron cache into a record; a native-only
locator finds the cache (URI-hash name, quarto_source scan, content-hash
fallback). The walk gains Positron and directory arms, imported records get a
staleness rule via an opt-in language ownership map, and the preview
publishes and re-checks them. A minimal --records flag and the provider's
publish_record_source are included.

Fixtures: 7 files added under crates/quarto-core/tests/fixtures/positron-cache,
none modified. No .snap files.
Adds scripts/riu-6-check-export-mirror.sh and records the results of running
commands 1 and 3 in a Positron window (directory override, child process, Stop
ends q2), plus the plan's resume state. No fixtures or snapshots.
--records takes record sources (a Positron cache directory or the q2 cache) on
both commands, with start-up validation, per-command defaults, suggestions for
bad entries, and a rule that no engine means never execute. The provider walks
the sources through ProviderLookup and keeps the engine-only path unchanged.
The share banner and help stop claiming guests can re-run when no engine can
run, and the preview shows a per-source diagnostic when no source supplies a
record (silent when a source failed). Documented in the guides.
No fixtures or snapshots added or changed.
Browser, e2e and two-binary share checks, the guest re-execute run against a
scratch ipykernel venv, and provide-hub runs against a scratch hub (gate off,
consent refusal, default engine path under --dangerously-accept-requests). The
no-record warning now carries its per-source reasons and the fix in the problem
line. Plan reconciliation and the board tick are included.

Fixtures/e2e: 1 spec added and 1 helper modified under q2-preview-spa/e2e. No
.snap files.
Command that writes a record as a Positron cache file, using export_for_positron.
Includes the recorded Julia error-document run through it. No fixtures or
snapshots added.
Command 2 and the runner, Python and part of Julia done via Playwright, the
findings note and a demo script; Phase D and the resume notes are brought up
to date. No fixtures or snapshots.
…U-3b)

Spliced record output carries a jupyter-output stamp so the viewer can tell a
generated output node from the author's text. The AST reconcile remap is
reworked for it, record_splice stamps nodes, and the preview renderer treats
the new kind as atomic (documented in the provenance contract and the guides).
The epic's closing reconciliation (statuses, board, exit criteria, Julia
results) lands with it.

Tests: a 675-line stamp test module and a preview-renderer integration test
are added. No .snap files and no fixture directories.
Cell presentation had separate loops per route (plain render, record
replay, splice), each deciding for itself what to emit. A single
per-cell step now takes a struct of optional inputs (record, source,
options, target) and produces the wrapper and output divs, so a route
only decides which inputs it has.

Pinning tests for the existing loops, scope, spans and entry paths land
first so the unification can be checked against today's behavior.
Julia declares its display-only options and acknowledges that core now
presents its output.

Also adds the planning documents for the one-presenter epic and the
follow-up to the execution-records work: the plan DAG, lanes, agent
protocol and recorded decisions.
Exporting a record for Positron is a format conversion, so it belongs
under `q2 convert` next to the other targets rather than in a one-off
`record` subcommand. The old command is removed (it never shipped).
…uarto 1 parity harness

The document-level function now returns per-cell outcomes instead of
only output, so callers can report what was presented, refused or
skipped.

The parity harness renders a corpus with both q2 and Quarto 1, normalizes
generated ids and empty wrappers, and diffs the two. It separates
documents q2 rejects from execution failures, and keeps known
differences in an allowlist with explicit sign-off state. The baseline
run is recorded.
… instead of --records

A cached knitr chunk keeps its cache and is left out of the record, so
a record never claims output that was not produced in this run.

Warning enforcement now comes from one rule derived from the hash
declaration. Record sources are selected by the `execute:
record-sources` key (layered across project, directory and document)
instead of the `--records` flag, which is removed from preview,
provide-hub and convert. The key is excluded from the execution hash
(hash definition version 2) since choosing where records come from must
not invalidate them.
…unner

Render reads `execute: record-sources` per document, with selection, a
skip guard and a shared staleness wrapper; a single-file book flags the
key as not honored. Preview routes use the `--share` ceiling. The
staleness inputs are merged for the render's format, since Julia records
otherwise read as stale.

QuartoNotebookRunner is pinned to a known revision and re-resolved when
the pinned source revision changes, and the three capture facts are read
from the cell annotation. A conformance sweep pushes 21 cases through
import and splice.
…alence fact

Julia capture facts are written as cell metadata under the knitr keys,
and `warning: false` hides only Warning-kind stderr when the record
carries output kinds.

The Positron import gate used to compare engine names. Engines now
declare kernel equivalence (trait method, manifest key, oracle), and
export no longer skips by engine, so knitr records export too.
An engine offers presentation and core acknowledges it, so deciding
who presents a cell no longer involves an engine-name test. Presenting
from a record is a separate flag from recording.

Also documents `q2 convert` and the Positron export, records the first
parity-gate run with two proposed allowlist entries, and notes the
results of verifying the Julia reproductions and browser checks.
A knitr chunk served from the cache never re-executes, so the record had
no entry for it. A sidecar beside the cache entry now holds what is
needed to record the hit, keeping level 1 and 2 behavior distinct.
Tests were written first and pin the sidecar checks.

Includes the end-to-end notes from running R and Julia through the
fork, and the plans for engine-controlled attributes.
An `execute_result` image takes its width and height from the output
metadata, so a recorded PNG renders at the size the engine produced.

Level-3 cache hits are pinned for `dependson`, `autodep`,
`cache.lazy`, `cache.rebuild`, callouts, `child=`, deleted `_files`
and the nonce marker. The three non-Julia parity allowlist entries are
recorded as signed off.
Engines can now control the attributes on the cell wrapper and on every
output div, and the figure file names, instead of core hard-coding
Julia's. `quarto_attrs` is validated by an `engine_attrs` module;
refused entries are reported as a presentation note. The attributes are
display data: they are hashed, survive staleness checks and round trip
through splice and render.

Julia uses the channel to stamp the plain route's wrapper id, counts,
figure names and figure class, and its to-markdown no longer forwards
`metadata.quarto` as an attribute. That removes the five Julia entries
from the parity allowlist. The engine-host bundle is rebuilt.
ANSI color codes in stream and error output are removed in a single
function at presentation, driven by one shared table, so every engine
gets the same result. Parity cases cover Jupyter, knitr and Julia.
Widget outputs need their JavaScript and CSS on the page, which Quarto 1
discovers by scanning the output. The scan is now implemented in core
and runs on every route (plain, replay, splice), with oracle fixtures
generated from Quarto 1. Engines can pass `dependencyData`, which is
stored on the record entry together with the presentation
acknowledgement and checked by the wire-parity guard.

A widget shown in a view that cannot load it raises Q-18-16 once per
document. Replay derives dependencies again so older records keep
working.
Figure groups (`fig-subcap`, layouts) got their caption and panel ids
only on the engine-specific route. The group rule now lives in core, so
Jupyter, knitr and Julia all number captions and panels the same way.
Later figures keep their numbers, and a panel-id clash stays an author
error. Edge-case, widget and fallback-chunk tests cover the rule.

Includes the reviews and plans that came out of this work: the split of
the whole-document target flip into smaller steps, and the subfloat
parity draft.
The parity gate now compares output per target rather than only HTML.
A document/pin schema describes what to compare, a normalizer handles
Julia's interleaved cells, and an ordered diff reports differences.
docx, pptx and epub are unpacked by a zip extractor; a target with no
extractor fails loudly instead of passing vacuously.

The gate asserts on blockers, on disagreement between runs and on stale
allowlist entries, and the report has per-entry columns. Spike results
for the target-following work are folded into the plans.
…ell id

The cell presenter picks the display format by the render target's
priority, the figure directory is named after the output format, and the
engine's automatic cell id goes on the wrapper only for html-family
targets, matching Quarto 1. A stored Jupyter html-plus-image bundle
follows the target.

`quarto-unit-id` is reserved on knitr chunks (Q-18-17), so a chunk
cannot claim the key the record uses to identify cells.
Julia documents are checked against Quarto 1 for eight targets,
including epub with a pinned interleaving entry, echo-false classes and
a date-regex fix. A `JULIA_PRESENTED_TARGETS` allowlist decides which
targets offer presentation. The gate report proposes five more
interleaving entries and a capture-facts pin; all six entries are now
signed off.
Plain render of Julia and knitr documents now goes through the recorded
route and the one presenter, instead of each engine's own output path.
WASM replay tests cover captures stored before the change so they keep
rendering.
…execute keys

cfe44f6 put record-sources in the Jupyter hash definition's presentationExecuteKeys; the
13 shadow .record.json goldens were not updated because they only run with a Python kernel
(skipped locally, caught by macOS CI). Hand-patched; the patched files equal the CI actual
output byte for byte. No .snap files changed.
pptx joins the knitr structured targets (html, revealjs, epub, typst, docx,
gfm), so its chunks go through the shared presenter instead of keeping knitr's
finished markdown.

Evidence:
- The KNR corpus run under pptx (every fixture, KNR_FORMAT override) matched
  docx exactly: 256 accepted, 6 allowlisted, 5 aborted by design, 4 unexpected
  blockers. The 4 are not pptx: two are the html cache=1/2 gap, two are a
  random R temp path in webshot's "screenshot completed" line on fixtures that
  only run under html.
- Real render with pandoc 3.13: one document (print, plot with fig-cap, kable
  with tbl-cap, fig-subcap pair, cross-references) rendered to pptx down both
  routes. All five slides identical as pretty-printed XML, the one media file
  byte-identical, same two Q-11-1 warnings. A fallback notice appears only on
  the old route; the new route's record holds 4 structured cells.
- Shared limits, not caused by this change: the kable table and the fig-subcap
  group reach no slide on either route (pandoc/q2 pptx writer).

Tests (written first, seen failing): target_route covered_targets_are_structured
and gating_tests::the_fallback_table ("pptx target" structured; the uncovered
case now uses context). Corpus manifest: pptx added to the four fixtures that
already run docx. Snapshots: 4 new frozen oracle files, live/rich-output/
{kable,df_print_default,knit_asis,include_graphics}.pptx.md; no existing
snapshot changed (288 matched).

Verified: cargo nextest run -p quarto-core (target_route, the_fallback_table);
cargo clippy -p quarto-core --all-targets -- -D warnings. Not run: the Windows
cross-clippy (aws-lc-sys does not build for the target here; the change is
constants, tests and fixtures only). Not run: the full corpus gate passes for
every pptx row but still reports four html-only failures that predate this
change (s1/14_cache_level1/2 and two stale indent allowlist entries); the
corpus report only runs when KNR_REPORT is set, so CI does not see them.
Corpus tool: KNR_FORMAT=<fmt> overrides each fixture's formats for one report run, to measure a candidate target across the whole corpus without editing the manifest. Report-only; the gate and the default run are unchanged.
A page for the knitr engine, the sibling of the Jupyter kernels page, built
from Quarto 1's "Using R" and caching articles and made about Quarto 2:
overview and a complete example, installation (R, rmarkdown, knitr 1.44,
QUARTO_R), how Quarto picks knitr, cell options (`#|` only, `!expr`, labels,
execution options, `output: asis`), `df-print`, the `knitr:` key with
`opts_chunk` and `R.options`, inline expressions, caching, and the chunks
that keep knitr's markdown (child=, other languages, replaced hooks, formats
the record cannot present yet, cache levels 1 and 2). Differences from
Quarto 1 are callouts beside each feature.

Checked by real renders with the built q2: `knitr:` in the project file and
the document, `R.options`, `output: asis`, `df-print: kable`, both inline
spellings, and `!expr` in `echo` (works) and in `fig-cap` / `tbl-cap` (shown
literally; documented as a difference). The page was rendered with
`q2 render --strict` in a copy of the docs site (the `examples/` resource is
generated and absent in a fresh worktree): no errors or warnings for the page,
every link resolves.

Not verified: the list of formats the record presents is as of this branch
(pptx and context are being added separately); the Positron statement for
cells that keep knitr's markdown comes from the KNR plans and the existing
execution-records page, not a Positron run. `--cache` and `--cache-refresh`
are documented as having no effect because nothing reads them in the code.
The page listed pptx among the formats that keep knitr's markdown. It joined the presented formats in 9ac905b22 on this branch.
context stays on knitr's finished markdown, like latex, beamer and pdf, and now
gets the same diagnostic. Before, it was an "uncovered target", which fell back
the same way but under the generic wording and with no stated reason.

knitr treats context as a TeX target: pandoc_to in knitr's pandoc.R maps
latex, beamer, context and texinfo alike to the pdf figure device, so its
output depends on the format the way LaTeX does and the LaTeX epic owns that
presentation. A markdown-level corpus run under context cannot see that (it
compared like html), so it is not evidence for presenting context from the
record. Context is also not a q2 output format today (`q2 render --to context`
stops with "Unknown format: context").

The diagnostic now names LaTeX and ConTeXt. Tests (written first, seen
failing): target_route the_latex_family_falls_back and
the_diagnostic_names_the_target_and_the_reason; gating_tests the_fallback_table
gains a "context target" row (it passes before and after, since both fallback
reasons write the same record; it pins the behavior, not the reason). The
uncovered-target examples now use odt.

Verified: cargo nextest run -p quarto-core (target_route, the_fallback_table,
gating); cargo clippy -p quarto-core --all-targets -- -D warnings; cargo fmt
--check. Not run: the Windows cross-clippy (aws-lc-sys does not build for the
target here; constants, tests and a message only).
a_record_only_cache_entry_is_published's second record_one call fell through
from the refused cache source to the engine source. With a Jupyter kernel
installed (CI's ubuntu job) that ran the document's python cell for real and
panicked with 'Cannot start a runtime from within a runtime'; without one the
engine source declined and the test passed. Turn the engine off for that call
so the outcome does not depend on what is installed.
…4dceefd..493762536

493762536 julia-engine declares dependencyData with the presentation acknowledgement
237876f02 Declare the kernel-equivalence fact in _extension.yml
8044c1a9b Pin QuartoNotebookRunner ffe5dab; re-resolve when the pinned source revision changes
bb9d7b09c Exclude `execute: record-sources` from the hash definition (version 2)
9f110c875 Julia declares display-only options and acknowledges core presentation

git-subtree-dir: resources/extension-subtrees/julia-engine
git-subtree-split: 4937625361d77ac0890cd12da06246e4613b7ea9
@gordonwoodhull
gordonwoodhull marked this pull request as ready for review November 12, 2026 05:30
@gordonwoodhull
gordonwoodhull merged commit a76c96a into main Nov 12, 2026
17 checks passed
@gordonwoodhull
gordonwoodhull deleted the feature/one-presenter branch November 12, 2026 06:25

This branch was previously deployed

1 inactive deployment
github-pages — a76c96ab Deployed Nov 12, 2026 by gordonwoodhull via deploy #140
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