Skip to content

(hub-client) Allow specifying sync server when creating new project#4

Merged
cscheid merged 2 commits into
kyotofrom
hub-client
Jan 15, 2026
Merged

(hub-client) Allow specifying sync server when creating new project#4
cscheid merged 2 commits into
kyotofrom
hub-client

Conversation

@shikokuchuo
Copy link
Copy Markdown
Member

We may not want this enabled eventually, but useful when developing to be able to test against any automerge-repo compliant backend e.g. one served by the autosync R package.

@shikokuchuo shikokuchuo changed the title Allow specifying sync server when creating new project (hub-client) Allow specifying sync server when creating new project Jan 15, 2026

This comment was marked as resolved.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@cscheid cscheid merged commit d27d420 into kyoto Jan 15, 2026
2 checks passed
@shikokuchuo shikokuchuo deleted the hub-client branch January 15, 2026 19:47
cscheid added a commit that referenced this pull request Apr 17, 2026
Closes out Phase 1 of the crossref plan.

- `crossref::codeblock_shorthand`: detect `#| label: <reftype>-..` in
  `CodeBlock.text`, partition cell options into consumed (label,
  <reftype>-cap/-scap/-alt) vs pass-through (echo, eval, engine-
  specific), rewrite the text to drop consumed keys, and wrap the code
  block in a `Div(#<label>)` scaffold with the caption paragraph.
  Driven from `PreEngineSugaringStage` so the engine then sees plain
  code blocks; the Div survives QMD round-trip (pinned by the Phase 0.7
  fixture).
- `transforms::crossref_render` (finalization phase): converts
  `CustomNode("FloatRefTarget")` with `ref_type=fig` into Pandoc's
  native `Figure` so the HTML writer emits
  `<figure><figcaption>Figure N: ...</figcaption></figure>`; other
  ref_types become a `Div` wrapping content with a trailing numbered
  caption paragraph. `CustomNode("CrossrefResolvedRef")` becomes a
  `Link` with class `quarto-xref` pointing at `#<id>`; text is
  "<Kind> <N>" for resolved refs or "?id?" for unresolved ones so the
  failure is visible in output.
- `crossref::metadata` now uses `ConfigValue::as_plain_text` so
  `crossref.custom` values parsed by pampa as `PandocInlines` resolve
  to the underlying string (was reading as None before).
- `tests/crossref_fixtures.rs`: 11 integration fixtures parsing qmd
  through pampa + running the crossref pipeline, asserting over the
  resulting `CrossrefIndex` per plan success criterion #4 (structured
  data, not HTML snapshots). Covers all four authoring shapes, counter
  behavior, section paths, duplicate ids, unresolved refs,
  @-disambiguation between crossref and citation prefixes, custom
  categories via `crossref.custom`, and the code-block shorthand
  end-to-end.

End-to-end `quarto render` on a hand-written qmd produces the expected
HTML: `<a href="#fig-foo" class="quarto-xref">Figure 1</a>` and
`<figure id="fig-foo"><p>Figure 1: <caption></p>...</figure>`.

Full workspace: 7362 tests pass; `cargo xtask verify` clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cscheid added a commit that referenced this pull request May 13, 2026
Drafts the engine-less CLI skeleton for `q2 preview` after bd-hfjj
landed the decomposition. Seven sub-tasks (A.0 through A.6) plus an
e2e smoke (A.7):

  A.0  Lift hub-client/src/wasm-js-bridge/ to a new
       @quarto/wasm-js-bridge workspace package; alias `/src/wasm-
       js-bridge` in hub-client + q2-preview-spa + preview-renderer
       tests. Resolves Q-A1 the right way (no duplication, no
       cross-platform symlink fragility).
  A.1  Stub `quarto preview` clap subcommand.
  A.2  New crates/quarto-preview/ shell crate with the
       quarto-trace-server-shaped include_dir! + build.rs
       placeholder.
  A.3  Fill in q2-preview-spa/src/main.tsx so the SPA actually
       drives a render. <PreviewApp> stays local for now.
  A.4  `cargo xtask build-q2-preview-spa` + wire into `build-all`.
  A.5  End-to-end boot test (tempdir lifecycle, samod handshake,
       URL-fragment carrier).
  A.6  Always-visible "force re-render" button (epic resolution #4).
  A.7  Playwright smoke (real-browser flow incl. DOM-stability
       invariant).

Q-A3 source-code audit corrected an inherited claim: the epic and my
initial draft both said the SPA reads `indexDocId`/`wsUrl` from a
server-injected `<meta>` tag "the same trick share links use." Both
halves wrong. hub-client's share links use URL *fragments*
(`hub-client/src/App.tsx:272`, `utils/routing.ts`); there's no
meta-injection in the codebase. trace-server serves `index.html`
unmodified (`crates/quarto-trace-server/src/lib.rs:185-187`). Phase
A now uses the same URL-fragment pattern: the CLI opens
`http://<host>:<port>/#/preview/<indexDocId>`, the SPA reads
`window.location.hash`, the WebSocket URL derives from
`window.location`. No new server-side patterns introduced.

Q-A5 captures a future direction: persistent samod storage inside
`.quarto/preview/samod/` to amortize the "blank screen at boot" cost
on large projects. Phase A measures the cost; future phases act.

TDD: every sub-task is structured tests-first.

Status: approved 2026-05-13 (all open questions resolved); ready to
file beads sub-issues and begin A.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cscheid added a commit that referenced this pull request May 13, 2026
…hase A.6)

Adds a small circular `↻` control overlaid on the top-right of the
preview pane (`aria-label="Refresh preview"`). Click → PreviewApp
bumps its `contentTick` so the render `useEffect` re-fires —
reusing the one channel that the sync handler's `onFileContent`
already uses, so there's a single path through the render pipeline
regardless of who asks for the re-render.

The button satisfies the epic's resolution #4 (force-refresh
invariant): the user always has an escape hatch when the dep-graph
misses a cross-doc relationship. Phase A has no dep-graph at all,
so it's largely redundant today, but the affordance is part of the
preview UX contract going forward; Phase C will extend the click
handler to trigger server-side re-execution where applicable.

Tests:
- New integration test in `PreviewApp.integration.test.tsx`
  pins the contract at the runtime seam: click → at least one
  additional `renderPageForPreview` call. Match by accessible
  name rather than test-id so the UX label is part of the test.

Manual smoke (Chrome DevTools): button rendered at (1876, 12) on
a 1920x1080 viewport; click runs without console error; iframe
content stays consistent; refresh affordance survives clicks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cscheid added a commit that referenced this pull request May 14, 2026
Adds the §C.1 integration test #3 the plan asks for: drive
`quarto_preview::run_with_on_ready` (new) against a tempdir fixture
with a `{test-passthrough}` code cell, wait for the sidecar entry to
appear, then verify the linked binary doc round-trips back to a
parseable `EngineCapture`.

Surface changes:

- New `quarto_preview::run_with_on_ready(config, extra_on_ready)`
  variant. Same lifecycle as `run`, plus an extra callback fired after
  the eager-capture driver has been enqueued. Tests use it with a
  oneshot channel to lift `Arc<HubContext>` out of the server task and
  poll the IndexDocument's sidecar through real samod reads — no
  duplicated wire-protocol code, no HTTP shim. `run` delegates to it
  with a no-op extra hook so the CLI is unchanged.
- `capture_driver::read_capture_from_doc` promoted from `#[cfg(test)]`
  to plain `pub`. The on-the-wire format (gzipped JSON in a
  binary-doc envelope) is shared with the Phase C.4 WASM/SPA reader,
  so a public server-side reader is valuable for tests today and for
  any future inspection/debug surface.
- `automerge` + `samod` moved from dev-dependencies to regular
  dependencies (versions pinned to match quarto-hub's). Required by
  the now-public `read_capture_from_doc`.

Tests (2 new):

- `tests/eager_capture.rs::eager_capture_populates_index_sidecar` —
  Spawn run_with_on_ready, oneshot the ctx out, poll
  `ctx.index().get_capture("doc.qmd")` up to 30 s, fetch the capture
  binary doc, verify `engine_name == "test-passthrough"`, `input_qmd`
  contains the cell body sentinel, and `result.markdown` contains the
  engine's passthrough comment.
- `tests/eager_capture.rs::prose_only_doc_leaves_sidecar_empty` —
  Negative path (§C.1 integration #4): drive the full server against
  a prose-only fixture, sleep 2 s, assert `has_capture` is still
  false.

End-to-end binary smoke (per CLAUDE.md "End-to-end verification
before declaring success"):

  $ cargo run --bin q2 -- preview /tmp/c1-smoke --no-browser
  ...
  INFO quarto_hub::context: Discovered project files qmd_count=1
  INFO quarto_hub::context: samod repo initialized
  INFO quarto_hub::index: Created new index document doc_id=...
  INFO quarto_hub::server: Hub server listening (project mode) addr=127.0.0.1:64574
  INFO quarto_hub::server: Starting filesystem watcher
  (SIGTERM)
  INFO quarto_hub::server: Server shutting down...

The eager-capture driver's on_ready hook fires inside `run_with_on_ready`;
a prose-only doc produces no capture (verified by the integration
test). Real-engine end-to-end (jupyter/knitr) was NOT verified this
session — no R/Python runtime configured. The integration test
substitutes a test-passthrough engine via PreviewConfig::engine_registry
to exercise the full lifecycle deterministically.

quarto-preview: 11/11 tests pass (5 unit + 4 smoke/boot + 2 new
eager-capture).

Plan: claude-notes/plans/2026-05-13-q2-preview-phase-c.md §C.1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cscheid added a commit that referenced this pull request May 19, 2026
* docs(q2-preview): epic plan + beads (bd-kw93) for q2 preview workstream

Architecture: ephemeral local hub-client (samod + file watcher) serves
a pared-down React SPA that renders via the q2-preview format. Engines
run server-side via `engine: replay` and the EngineCapture flows
through automerge to the browser, which replays in WASM. No DOM
rebuild on edit — Bootstrap / MathJax / reveal.js state survives.

Phasing: A (skeleton + standalone SPA serving), B (broadened watcher +
dep-graph invalidation), C (record-on-demand engine + replay), D
(polish), E (stretch). All seven open questions resolved in the
2026-05-11 reviews — see plan §Resolutions from 2026-05-11 review #2.

Two follow-ups filed:
- bd-hfjj (epic, blocks bd-kw93): hub-client decomposition — shared
  preview-pane package so hub-client and the preview SPA stay in
  lockstep by construction.
- bd-56b0 (task, related to bd-kw93): cross-doc dependency channel
  audit. Phase A's always-visible force-refresh button is the user
  escape hatch until this lands.

Branch: feature/q2-preview. No code changes; this is the plan +
beads index update.

* docs(q2-preview): hub-client decomposition sub-epic plan (bd-hfjj)

Concretizes the bd-hfjj sub-epic of bd-kw93 with package boundaries
(two packages: @quarto/preview-renderer + @quarto/preview-runtime),
SPA location (top-level q2-preview-spa/), MVP scope (React preview
path only; Preview.tsx / slides / debug stay in hub-client), WASM
access (existing symlink + per-consumer alias), and a 7-phase
sequencing that keeps tests green across each move.

This branch holds both this sub-epic plan and the parent epic plan
(via the 03fc1238 ancestor commit). Parked off feature/q2-preview
while the other workstream there settles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(q2-preview): scaffold preview-renderer + preview-runtime workspaces (bd-hfjj Phase 1)

Creates two empty workspace packages mirroring the
ts-packages/quarto-sync-client pattern:

- `@quarto/preview-renderer` — pure React; jsdom-based integration
  config; deps on react, react-dom, morphdom.
- `@quarto/preview-runtime` — WASM + automerge glue; both vitest
  configs declare the `wasm-quarto-hub-client` alias pointing at
  hub-client's existing symlink; deps on @automerge/automerge,
  @automerge/automerge-repo, @quarto/quarto-sync-client,
  @quarto/quarto-automerge-schema, web-tree-sitter.

Each package ships an `export {}` index, a trivial placeholder test
(green on both), and a small jsdom polyfill `test-utils/setup.ts` so
its integration config is self-contained. Both pass `test`,
`typecheck`, and `build`.

Phase 0 pre-flight is also captured in this commit:
- catalogue audit added customRegistry, atomicCustomNodes, and
  utils/sourceInfo to the renderer move list (drift discovered by
  grep against the current tree);
- test-helper placement section records dispositions
  (mockSyncClient/mockWasm → runtime; visibility/setup/userSettings
  stay in hub-client);
- plan status flipped from "draft — awaiting review" to "approved;
  Phases 0–1 complete; Phase 2 next".

Hub-client probe imports (added to main.tsx, compiled cleanly through
both `tsc --noEmit` and `vite build`, then reverted) verified that
both packages resolve via npm's workspace symlinks + tsc's bundler
moduleResolution + vite's `source` condition. No changes to
hub-client landed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(q2-preview): move types + utils to preview-renderer (bd-hfjj Phase 2)

Moves 5 type modules and 7 utility modules out of hub-client into the
new @quarto/preview-renderer workspace, behind sub-path exports:

  ts-packages/preview-renderer/src/types/
    project.ts (+ test), diagnostic.ts, artifactPaths.ts,
    sourceInfo.ts, intelligence.ts

  ts-packages/preview-renderer/src/utils/
    vfsPaths.ts (+ test),
    iframeLinkHandlers.ts (+ integration test),
    componentPath.ts (+ test),
    stripAnsi.ts (+ test),
    customRegistry.ts (+ test),
    atomicCustomNodes.ts,
    sourceInfo.ts (+ test)

`git mv` preserves history. All 64 importers across 44 hub-client
files are rewritten from `../<dir>/types/<m>` / `../<dir>/utils/<m>`
to `@quarto/preview-renderer/types/<m>` / `/utils/<m>`. The package's
exports map uses wildcard sub-paths because `types/diagnostic` and
`types/intelligence` both legitimately export `Diagnostic` /
`DiagnosticDetail` (the former @deprecated), and a single barrel
would force every importer to alias one side.

Two files originally scheduled for Phase 2 were deferred after audit:

- `components/ThemeContext.tsx` — no moving file consumes it, but it
  imports `services/preferences/` (localStorage). The sound move is a
  DI refactor, deferred until the SPA actually needs theme switching.
  Tracked as follow-up `bd-hfjj-fu-theme`.
- `utils/iframePostProcessor.ts` — imports `vfsReadFile` /
  `vfsReadBinaryFile` from `services/wasmRenderer`, which itself
  moves in Phase 5. Moving the postprocessor with wasmRenderer then
  is cleaner than adding a DI seam now. Its remaining internal import
  of `./vfsPaths` is updated to `@quarto/preview-renderer/utils/vfsPaths`.

Hub-client's three vitest configs gain a
`@quarto/preview-renderer` → `ts-packages/preview-renderer/src` alias.
Vitest doesn't honor the package.json `source` condition the same way
Vite's prod build does on fresh clones; the alias matches the
existing sync-client / automerge-schema pattern.

preview-renderer adds `@quarto/quarto-automerge-schema` as a workspace
dep (required by `types/project`), and its own vitest.config.ts gains
the same alias so its tests resolve workspace deps to source.

Hub-client's test:ci, build:all, and `cargo xtask verify
--skip-rust-tests` all pass. Preview-renderer's unit (81 tests across
7 files) and integration (7 tests) suites pass.

Snapshot tests: no snapshot files changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(hub-client): changelog entry for bd-hfjj Phase 2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(q2-preview): move framework subtree to preview-renderer (bd-hfjj Phase 3)

Moves the entire components/render/framework/ subtree out of hub-client
into @quarto/preview-renderer:

  ts-packages/preview-renderer/src/framework/
    Ast.tsx, RegistryContext.tsx, dispatch.tsx,
    customNode.ts (+ test), meta.ts (+ test),
    plainText.ts (+ test), types.ts, index.ts

framework is exposed as a single barrel via `./framework` in
package.json's exports map (not a wildcard sub-path). Rationale: the
subtree mixes .tsx and .ts files, and Node's exports map can't
pattern-match both extensions in a single wildcard entry cleanly.
Every symbol re-exports through framework/index.ts already, so
consumers don't need direct sub-file imports.

107 imports across 60 hub-client files rewritten to
`from '@quarto/preview-renderer/framework'`. One dynamic
`await import('./framework')` in parity.integration.test.tsx also
caught and rewritten.

Framework's internal cross-dir imports — Phase 2 had rewritten
Ast/RegistryContext/dispatch to use self-package
`@quarto/preview-renderer/...` paths since their targets had moved
already — are converted to idiomatic relative paths
(`../types/sourceInfo`, `../utils/sourceInfo`, etc.) now that
framework itself lives inside the package.

preview-renderer's test suite now exercises 133 tests across 10 files
(adds customNode/meta/plainText tests). hub-client test:ci, build:all,
and `cargo xtask verify --skip-rust-tests` all green.

Snapshot tests: no snapshot files changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(hub-client): changelog entry for bd-hfjj Phase 3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(q2-preview): move services to preview-runtime (bd-hfjj Phase 5, swapped before Phase 4)

Moves the WASM-bridge / automerge-sync / user-grammar services out of
hub-client into the new @quarto/preview-runtime workspace, plus pulls
iframePostProcessor (deferred from Phase 2) into @quarto/preview-renderer
now that its `vfsReadFile` / `vfsReadBinaryFile` dependency lives in
runtime.

To preview-runtime:
  src/wasmRenderer.ts (+ test)
  src/automergeSync.ts (+ test)
  src/userGrammar/Discovery.ts (renamed; + test)
  src/userGrammar/Cache.ts (renamed; + test)
  src/userGrammar/Highlight.ts (renamed; + WASM-init test)
  src/test-utils/mockSyncClient.ts (test helper)
  src/test-utils/mockWasm.ts (test helper)
  src/wasm-quarto-hub-client.d.ts (mirrored from hub-client so the
    package typechecks standalone)
  src/vite-shims.d.ts (`*.wasm?url` and `/src/wasm-js-bridge/*.js`
    ambient declarations)

To preview-renderer:
  src/utils/iframePostProcessor.ts (+ unit and integration tests)

Phase reordering: the original plan had Phase 4 (q2-preview / iframe
wrappers / overlays) before Phase 5 (services). Swapping is *safer*:
most Phase-4 files import `services/wasmRenderer` or
`utils/iframePostProcessor`, so moving services first means the
remaining Phase-4 movers already point at @quarto/preview-runtime
when their turn comes. Hub-client's full test surface keeps validating
the renderer-side code throughout this swap, so any service regression
surfaces immediately. See "Phase ordering note" in the plan.

Re-decided 2026-05-13: `assetWalker.ts` does NOT move to preview-runtime.
It stays inside `q2-preview/` and moves with it in Phase 4. Rationale:
assetWalker is an AST walker that *uses* VFS, not a VFS service; moving
it to runtime would create a circular preview-runtime → preview-renderer
dependency via `vfsPaths`. The plan's prior signal ("the test moves
alongside since it tests the runtime function") wasn't load-bearing.

Cross-package wiring changes:
  - preview-renderer gains @quarto/preview-runtime as a workspace dep
    (iframePostProcessor imports `vfsReadFile`).
  - preview-runtime gains @quarto/preview-renderer + @quarto/pandoc-types
    + @quarto/quarto-automerge-schema as workspace deps. The
    preview-renderer dep is for the wire-format `Diagnostic` /
    `RenderResponse` re-exports from `types/diagnostic`.
  - preview-runtime/package.json adds sub-path exports
    `./userGrammar/*` and `./test-utils/*`, plus a `test:wasm` script
    (foundation for moving the wasm-test runner later if useful).

WASM build-time path resolution:
  `wasmRenderer.ts`'s lazy `import('../wasm-js-bridge/sass.js')` was
  rewritten to `'/src/wasm-js-bridge/sass.js'` — the same Vite-root
  convention the Rust WASM module already uses via
  `wasm-bindgen raw_module = "/src/wasm-js-bridge/sass.js"`. Every
  consumer (hub-client today, the future q2-preview SPA) must host the
  bridge files at `src/wasm-js-bridge/` and Vite resolves uniformly.
  An ambient `declare module '/src/wasm-js-bridge/*.js'` shim in
  `preview-runtime/src/vite-shims.d.ts` keeps tsc happy.

Hub-client import rewrites:
  Two passes via `/tmp/rewrite-phase5-imports.py`:
    - Pass 1: `<rel>/services/wasmRenderer`, `<rel>/services/automergeSync`,
      `<rel>/utils/iframePostProcessor`, and the renamed userGrammar
      paths.
    - Pass 2: short-form intra-services imports (`./wasmRenderer`,
      `./automergeSync`) that the first pass's pattern didn't cover.
  Manually fixed three categories the regex couldn't:
    - `vi.mock('./wasmRenderer', ...)` and `vi.mock('./automergeSync', ...)`
      in test files (rewritten to mock '@quarto/preview-runtime').
    - Inline `import('../services/automergeSync').ActorIdentity` in
      Editor.tsx.
    - `await import('./framework')` in parity.integration.test.tsx
      (Phase 3 leftover that the framework rewrite caught here).

Hub-client's three vitest configs gain `@quarto/preview-runtime` aliases
to source (same pattern as `@quarto/preview-renderer`).

`Highlight.wasm.test.ts`'s `repoRoot` computation updated from
`../../..` to `../../../..` to account for the deeper directory.

All green:
  - preview-runtime: 60 tests / 6 files, tsc build clean
  - preview-renderer: 133 tests / 10 files (unit) + 8 integration,
    tsc build clean
  - hub-client: 525 unit tests, 173 integration tests, 79 wasm tests,
    `build:all` clean
  - cargo xtask verify --skip-rust-tests: all 9 steps pass

Snapshot tests: no snapshot files changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(hub-client): changelog entry for bd-hfjj Phase 5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(q2-preview): move q2-preview + iframe + overlays to preview-renderer (bd-hfjj Phase 4)

The biggest single move in the decomposition — 94 files touched. Lifts
the q2-preview format components, the iframe wrappers, and the
overlays out of hub-client and into @quarto/preview-renderer.

To preview-renderer/src/q2-preview/ (entire subtree):
  AssetManifestContext, NoteNumberingContext, PreviewContext,
  PreviewDocument (+ integration test), assetWalker (+ test),
  dispatchers, entry (+ integration test), index.ts (barrel),
  quartoClasses, registry (+ test), theoremEnvs, utils,
  q2-preview.integration.test, custom-components.integration.test,
  + blocks/, inlines/, custom/ subtrees (each ~15-20 files).

To preview-renderer/src/iframe/:
  Q2PreviewIframe (moved out of q2-preview/, + integration test),
  MorphIframe, DoubleBufferedIframe.

To preview-renderer/src/overlays/:
  PreviewErrorOverlay (+ integration test), PreviewStaticInfoViews.

`PreviewErrorOverlay` was DI-refactored as part of this move. Its
previous internal `usePreference('errorOverlayCollapsed')` call coupled
it to hub-client's localStorage layer; that wasn't viable inside the
shared package. The component now takes optional `collapsed` +
`onToggleCollapsed` props (controlled), with an internal
`useState(true)` fallback for the uncontrolled case. The two hub-client
call sites in `ReactPreview.tsx` and `Preview.tsx` wrap with
`usePreference` and pass the value down — same UX, same persistence,
cleaner boundary. The future q2-preview SPA can pass any state or
omit the props entirely.

Import wiring:

  - Self-package `@quarto/preview-renderer/<X>` imports inside the
    moved subtree were converted to depth-aware relative paths via
    `/tmp/relativize-self-imports.py` (100 rewrites across 55 files).
    Idiomatic and matches the Phase 3 framework treatment.
  - `Q2PreviewIframe.tsx`'s `./assetWalker` import was rewritten to
    `../q2-preview/assetWalker` (it moved out of q2-preview into
    iframe/ as one of the few cross-subtree moves).
  - Hub-client importers of moved files were rewritten via
    `/tmp/rewrite-phase4-imports.py` (9 imports / 6 files in the
    first pass, plus 2 more after the regex was broadened to handle
    paths with intermediate `components/render/` segments).
  - One `vi.mock('./q2-preview/Q2PreviewIframe', ...)` in
    `ReactRenderer.integration.test.tsx` was retargeted to
    `'@quarto/preview-renderer/iframe/Q2PreviewIframe'`.

Hub-client iframe-entry shim:

  `hub-client/q2-preview.html` has a `<script type="module"
  src="/src/components/render/q2-preview/entry.tsx">` tag — Vite
  resolves that path relative to hub-client's project root. The real
  entry now lives in preview-renderer; we keep the original path
  stable by adding a one-line stub file at the hub-client location
  that just re-imports `@quarto/preview-renderer/q2-preview/entry`.
  `parity.integration.test.tsx`'s dynamic
  `import('./q2-preview/entry')` also goes through this stub.

  This required adding a specific `./q2-preview/entry` sub-path
  export to `preview-renderer/package.json` (the q2-preview barrel
  alone wouldn't expose `entry.tsx` since q2-preview/index.ts is the
  barrel, not entry).

preview-renderer public API surface:

  Top-level `src/index.ts` becomes a real barrel exposing
  `Q2PreviewIframe`, `MorphIframe` (+ Handle), `DoubleBufferedIframe`
  (+ Handle), `PreviewErrorOverlay`, `ErrorView` / `FallbackView` /
  `NonQmdPlaceholderView`, plus a `export * from './q2-preview'` for
  the rest of the q2-preview surface (Block, Inline, PreviewDocument,
  previewRegistry, PreviewContext, AssetManifestContext,
  buildAssetManifest, ManifestCacheEntry). Phase 6's q2-preview-spa
  placeholder will use this barrel directly.

Test-config plumbing in `preview-renderer/vitest.integration.config.ts`:
  - Workspace-package aliases: `@quarto/quarto-sync-client`,
    `@quarto/preview-runtime`.
  - `wasm-quarto-hub-client` → hub-client's symlink (JS shim resolves
    at transform; the WASM binary itself isn't invoked).
  - `/src/wasm-js-bridge` → hub-client's bridge dir, so
    `wasmRenderer.ts`'s lazy `import('/src/wasm-js-bridge/sass.js')`
    resolves at Vite's transform step. Scoped to that sub-tree (not
    plain `/src`, which would also intercept test files' own
    absolute paths).

All green:
  - preview-renderer: 156 unit / 13 files + 129 integration / 9 files
  - preview-runtime: 60 tests / 6 files (unchanged)
  - hub-client: 525 unit + 58 integration + 79 wasm tests; build:all
    clean.
  - `cargo xtask verify --skip-rust-tests`: all 9 steps pass.

End-to-end UI smoke: NOT run this session (no browser available in
the worktree). Per CLAUDE.md §End-to-end verification — tests pass,
the real render path was not exercised in a browser. To verify,
`cd hub-client && npm run dev`, open a Quarto project in the
returned URL, and confirm the q2-preview format still renders.

Snapshot tests: no snapshot files changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(hub-client): changelog entry for bd-hfjj Phase 4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(q2-preview): q2-preview-spa skeleton + cargo xtask verify integration (bd-hfjj Phases 6 + 7)

Closes the bd-hfjj sub-epic. Phases 6 and 7 are mechanical follow-ups
to the big moves in Phases 2–5.

Phase 6 — q2-preview-spa skeleton:

  Creates a new workspace package `q2-preview-spa/` (sibling to
  hub-client/, trace-viewer/) that's the future host of the
  `quarto preview` CLI command from bd-kw93. Today it's a 4-file
  placeholder:

    package.json   — name, react/preview-renderer/preview-runtime deps
    vite.config.ts — react + wasm plugins, source-condition aliases
    tsconfig.{,app,node}.json — trace-viewer-shaped TS project refs
    index.html     — minimal shell
    src/main.tsx   — renders <PreviewErrorOverlay> with a placeholder
                     message, proving the cross-package boundary works.

  Bundle audit (after `npm run build`):
    - Total: 196K, 30 modules.
    - grep'd for editor-only symbols (Monaco, GoogleOAuthProvider,
      FileSidebar, ProjectSelector, automergeSync, createSyncClient,
      monaco-editor): all zero. The §Crate/SPA invariant from the
      epic is enforced by construction — editor code cannot be
      transitively pulled in because the SPA only imports from
      shared packages.

  Caveat: the placeholder uses the *sub-path* overlay import
  (`@quarto/preview-renderer/overlays/PreviewErrorOverlay`) rather
  than the top-level barrel. Importing through the barrel pulls in
  the q2-preview tree, which pulls in `@quarto/preview-runtime` and
  its `/src/wasm-js-bridge/` consumer-provided files. The SPA
  placeholder doesn't yet host those bridge files at its root;
  Phase A of bd-kw93 will revisit when the SPA actually drives a
  render. The current shape proves the cross-package boundary
  works without paying that cost.

Phase 7 — cargo xtask verify integration + cleanup:

  - TOTAL_STEPS bumped 9 → 11. Steps 10 (shared preview-* package
    tests: preview-renderer unit + integration + preview-runtime
    unit) and 11 (q2-preview-spa build) added. Skip flags added:
    `--skip-shared-package-tests`, `--skip-q2-preview-spa-build`.
  - `cargo xtask verify --skip-rust-tests` confirmed green across
    all 11 steps.
  - `hub-client/src/test-utils/index.ts` cleaned up: the mockSyncClient
    and mockWasm re-exports were dead (those moved to preview-runtime
    in Phase 5). The barrel now only carries `@testing-library/react`
    helpers and `visibility.ts` exports.
  - Audit confirmed no empty directories left behind in hub-client/src/;
    the one-file q2-preview/entry.tsx stub there is intentional
    (kept so q2-preview.html and parity.integration.test.tsx keep
    their existing paths).

Also fixes two qmd-render-blocking characters in earlier Phase 4 +
Phase 2 changelog entries (`PreviewErrorOverlay`'s apostrophe and
the `(~50 files)` tilde — both confused Quarto's parser into
"Unclosed Single Quote" / "Unclosed Subscript"; rephrased to plain
text).

Plan flipped from "Phases 0–5 complete; Phase 6 next" to "COMPLETE".
The bd-hfjj sub-epic is closed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(hub-client): changelog entry for bd-hfjj Phases 6+7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(q2-preview): Phase A plan (bd-kw93 sub-epic kickoff)

Drafts the engine-less CLI skeleton for `q2 preview` after bd-hfjj
landed the decomposition. Seven sub-tasks (A.0 through A.6) plus an
e2e smoke (A.7):

  A.0  Lift hub-client/src/wasm-js-bridge/ to a new
       @quarto/wasm-js-bridge workspace package; alias `/src/wasm-
       js-bridge` in hub-client + q2-preview-spa + preview-renderer
       tests. Resolves Q-A1 the right way (no duplication, no
       cross-platform symlink fragility).
  A.1  Stub `quarto preview` clap subcommand.
  A.2  New crates/quarto-preview/ shell crate with the
       quarto-trace-server-shaped include_dir! + build.rs
       placeholder.
  A.3  Fill in q2-preview-spa/src/main.tsx so the SPA actually
       drives a render. <PreviewApp> stays local for now.
  A.4  `cargo xtask build-q2-preview-spa` + wire into `build-all`.
  A.5  End-to-end boot test (tempdir lifecycle, samod handshake,
       URL-fragment carrier).
  A.6  Always-visible "force re-render" button (epic resolution #4).
  A.7  Playwright smoke (real-browser flow incl. DOM-stability
       invariant).

Q-A3 source-code audit corrected an inherited claim: the epic and my
initial draft both said the SPA reads `indexDocId`/`wsUrl` from a
server-injected `<meta>` tag "the same trick share links use." Both
halves wrong. hub-client's share links use URL *fragments*
(`hub-client/src/App.tsx:272`, `utils/routing.ts`); there's no
meta-injection in the codebase. trace-server serves `index.html`
unmodified (`crates/quarto-trace-server/src/lib.rs:185-187`). Phase
A now uses the same URL-fragment pattern: the CLI opens
`http://<host>:<port>/#/preview/<indexDocId>`, the SPA reads
`window.location.hash`, the WebSocket URL derives from
`window.location`. No new server-side patterns introduced.

Q-A5 captures a future direction: persistent samod storage inside
`.quarto/preview/samod/` to amortize the "blank screen at boot" cost
on large projects. Phase A measures the cost; future phases act.

TDD: every sub-task is structured tests-first.

Status: approved 2026-05-13 (all open questions resolved); ready to
file beads sub-issues and begin A.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: file 7 Phase A sub-issues under bd-kw93

Files bd-0xmt (A.0 wasm-js-bridge), bd-yxqt (A.1+A.2 CLI + crate),
bd-o5wd (A.3 SPA main.tsx), bd-501n (A.4 xtask build), bd-mflk (A.5
boot test), bd-b5hf (A.6 force-refresh), bd-vpsy (A.7 Playwright)
as parent-child children of bd-kw93. Plan:
claude-notes/plans/2026-05-13-q2-preview-phase-a.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(wasm-js-bridge): lift to @quarto/wasm-js-bridge workspace package (bd-0xmt, Phase A.0)

First step of the q2-preview Phase A (bd-kw93). Moves the 4 JS bridge
modules + their .d.ts/.test companions out of hub-client/src/wasm-js-
bridge/ into a new @quarto/wasm-js-bridge workspace package. Each
consumer (hub-client today + q2-preview-spa later) aliases the
Vite-root path `/src/wasm-js-bridge/` to the package's src/ — the
Rust WASM module's `wasm-bindgen raw_module = "/src/wasm-js-bridge/
sass.js"` annotation is unchanged because resolution happens at
consumer build time. One copy in the workspace, no per-consumer
duplication, no symlinks.

Why this is A.0: Phase A.3 (filling in q2-preview-spa/src/main.tsx)
will boot the WASM module from the SPA's Vite root. Without this
move, the SPA would either need its own copy of the bridge files or
a symlink to hub-client's. Doing the lift now lets A.3 inherit a
clean answer.

Package details:
  ts-packages/wasm-js-bridge/
    package.json   — `@quarto/wasm-js-bridge`, private, type module.
                     Deps `ejs`, `sass` (template.js + sass.js).
    src/sass.js, sass.d.ts, cache.js, cache.d.ts, cache.test.ts,
        fetch.js, template.js — git-mv'd from hub-client.
    src/sass.test.ts (new) — pins the four public function names
        (setVfsCallbacks, jsSassAvailable, jsSassCompilerName,
        jsCompileSass). The Rust WASM module + wasmRenderer.ts both
        reference these by name; renaming any silently breaks the
        bridge at runtime. The TypeScript surface is `any`-typed
        because sass.js is JS, so this guard test is the only
        compile-or-test signal we have for the contract.

Consumer wiring:
  - hub-client/vite.config.ts: alias `/src/wasm-js-bridge` →
    `../ts-packages/wasm-js-bridge/src`. The leading `/` was
    previously resolving via Vite project root to a directory that
    no longer exists.
  - hub-client/vitest.{,integration}.config.ts: inherit the alias
    via mergeConfig from vite.config.ts (resolve.alias deep-merges
    cleanly).
  - hub-client/vitest.wasm.config.ts: still has `/src` →
    `hub-client/src` for the WASM tests that need general /src/
    paths; the more-specific `/src/wasm-js-bridge` alias from
    vite.config.ts wins via longest-prefix match. Comment added.
  - q2-preview-spa/vite.config.ts: same `/src/wasm-js-bridge` alias.
    Not strictly required until A.3 fills in main.tsx with real WASM
    init, but landing it now is cheap and means A.3 doesn't need
    config changes.
  - ts-packages/preview-renderer/vitest.integration.config.ts:
    retarget the existing alias from `hub-client/src/wasm-js-bridge`
    (now gone) to `../wasm-js-bridge/src` (within ts-packages).
  - Three hub-client wasm tests (`smokeAll`, `themeCss`,
    `themeFingerprint`) imported the bridge via the relative path
    `../wasm-js-bridge/sass.js`. Rewritten to the Vite-root form
    `/src/wasm-js-bridge/sass.js`, matching how wasmRenderer.ts
    handles it. Now alias-driven, location-independent.

Hub-client cleanup:
  - Removed `ejs` (^3.1.10), `sass` (^1.77.0), `@types/ejs`
    (^3.1.5) from hub-client/package.json. The bridge files were
    the only consumers; with the move complete, these npm deps
    belong to @quarto/wasm-js-bridge instead.

Verification (all 11 cargo xtask verify steps pass):
  - hub-client: 525 unit + 173 integration + 79 wasm tests
    (test:wasm exercises the real sass compile through the bridge —
     the strongest signal that the alias works end-to-end).
  - preview-renderer: 156 unit + 129 integration tests.
  - preview-runtime: 60 tests.
  - q2-preview-spa: builds (placeholder still doesn't touch WASM).
  - hub-client/dist/assets/sass-*.js confirmed to contain the bridge
    code (grep'd for `dart-sass`, `setVfsCallbacks`, jsSassAvailable
    function signature).

Snapshot tests: no snapshot files changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(hub-client): changelog entry for bd-0xmt (q2-preview Phase A.0)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-0xmt (q2-preview Phase A.0)

A.0 landed on branch beads/bd-0xmt-wasm-js-bridge-package
(commits ea1bb889 + 043f65e1). @quarto/wasm-js-bridge workspace
package now hosts the bridge files; hub-client + q2-preview-spa
alias /src/wasm-js-bridge to the package's src.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(xtask): add `switch-task` for in-place sub-task transitions

Two-pattern worktree workflow: `create-worktree` for parallel /
investigation work (fresh isolation; pays the npm install + cargo
cold-cache cost on purpose); `switch-task` for sequential
implementation work within an epic (reuses the current worktree's
warm caches by branch-switching in place).

`cargo xtask switch-task <bd-id> [--from <branch>]`:
  1. If `--from <branch>` given: `git switch <branch>` + `git pull
     --ff-only` so the topic branch starts from the latest tip
     (siblings' merges land here first).
  2. `git switch -c beads/<id>-<slug>` off the resulting HEAD.
  3. `br update <id> --status in_progress` (skippable via
     `--no-claim`).
  4. Rewrites the `<!-- BEGIN/END WORKTREE CONTEXT -->` block in
     `CLAUDE.local.md` so the next Claude Code session sees the new
     sub-task's metadata (same template as create-worktree).

Reuses create_worktree's BeadsMetadata + derive_slug + build_section
+ update_claude_local_md helpers — the shape is the same; only the
git-side changes (no worktree add, just an in-place branch switch).

Companion changes to `.claude/rules/worktrees.md`:
- Documents the two patterns side-by-side at the top of the file so
  Claude (or any contributor) picks the right command for the shape
  of the work.
- New "Integration-line convention" section formalising that ready
  sub-task work merges into a long-lived `feature/<name>` branch
  (--no-ff so each sub-task is a single merge commit); `switch-task
  --from <branch>` expects to find that integration line already
  current.
- Adds the in-place sub-task pattern to the branch-naming section
  (same `beads/<id>-<slug>` shape, no `.worktrees/` dir).

No change to existing commands; `cargo xtask verify` still 11/11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(xtask switch-task): branch off ref directly, don't switch first

Previous implementation did `git switch <from>` + `git pull --ff-only`
+ `git switch -c <new>`. Fails when `<from>` is already checked out
in another worktree — which is exactly the common case (main repo on
`feature/<epic>`, in-place sub-task work in a sibling worktree).

New shape: `git fetch origin <from>` (best-effort), then
`git switch -c <new> <from>` — the trailing start-point creates the
new branch at `<from>`'s tip without checking out `<from>` itself.
Worktree-friendly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(xtask switch-task): write CLAUDE.local.md to the current worktree, not the workspace root

The first end-to-end run of `switch-task` from inside a worktree
clobbered the *main repo's* `CLAUDE.local.md` instead of the
worktree's. Cause: switch_task used `create_worktree::repo_root()`,
which walks up to find `Cargo.toml` with `[workspace]`. For a
worktree of a workspace, that `[workspace]` declaration is shared
across all worktrees, so the walk lands on the main repo regardless
of which worktree the command is run from.

Add a `current_worktree_root()` helper that calls
`git rev-parse --show-toplevel` instead — that returns the
*invoking worktree's* top, which is the right anchor for the
per-worktree CLAUDE.local.md file. Use it in switch_task.

The bug only existed in `switch_task`; `create-worktree` is
unaffected because it explicitly constructs `repo_root.join(".worktrees")
.join(<leaf>)` and uses that for CLAUDE.local.md path, never the
caller's worktree root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: bd-yxqt → in_progress (Phase A.1+A.2 work begins)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(quarto-preview): CLI subcommand + new crate with embedded SPA (bd-yxqt, Phase A.1+A.2)

Two halves of the q2-preview phase-A skeleton:

A.1 — CLI surface
  - Replaces the existing `Preview` clap stub (Q1-shaped placeholder
    that returned NotImplemented) with the Q2 shape from
    `claude-notes/plans/2026-05-13-q2-preview-phase-a.md` §A.1.
    Drops the Q1-era flags (--render, --no-serve, --no-navigate,
    --no-watch-inputs, --timeout, --log*, --quiet, --profile) since
    Q2 always serves, always watches, and logs at the binary level.
    Keeps [path] / --port / --host / --no-browser, adds --data-dir
    (samod storage override), --preview-dir (SPA-from-disk override,
    same shape as QUARTO_TRACE_VIEWER_DIR), --no-project (standalone
    mode, Q5 resolution).
  - Tests-first: `crates/quarto/tests/preview_cli.rs` pins the flag
    set via `q2 preview --help` (3 tests). Started red (the
    inherited Q1 flags failed the Phase-A flag-list assertion);
    landed green after the replacement.
  - `commands/preview.rs` grows a `PreviewArgs` struct carrying the
    Phase-A flags. `execute()` is still a NotImplemented stub; the
    fields are `#[allow(dead_code)]`-marked because A.5 (bd-mflk)
    will consume them.

A.2 — quarto-preview crate
  - New `crates/quarto-preview/`. Cargo.toml depends on quarto-hub,
    include_dir, axum, tokio (the deps needed for the future
    hub-layered router). Currently lib.rs is just the SPA shell;
    A.5 layers the hub server on top.
  - `build.rs` mirrors `quarto-trace-server/build.rs` exactly: looks
    for `q2-preview-spa/dist/index.html`; if present, embeds that
    directory via `include_dir!`; otherwise emits an OUT_DIR
    placeholder with the same `<div id="root">` mount point (so the
    smoke test works either way) plus a build warning telling the
    user to run `cargo xtask build-q2-preview-spa` (added in A.4).
  - `src/lib.rs` exposes `PreviewConfig` + `run(config)` + `router(
    &config)`. `router()` is `pub` so the smoke test can exercise
    the SPA-fallback chain without spinning the listener through
    run()'s indefinite serve loop. `run()` itself uses
    `axum::serve(...).with_graceful_shutdown(ctrl_c)`.
  - Smoke tests (`tests/smoke.rs`, 2 tests):
      1. `spa_root_serves_html_with_react_mount` — GET / returns
         200 with the React mount point.
      2. `unknown_path_falls_back_to_index_html` — GET on an
         arbitrary path falls back to index.html (client-side
         routing), matching what `q2-debug.html` /
         `q2-preview.html` rely on in hub-client.
  - Both run green; the bound port comes back via
    `listener.local_addr()` so the tests are port-clash-free.

Verification: `cargo xtask verify --skip-hub-build --skip-hub-tests
--skip-trace-viewer-build --skip-trace-viewer-tests
--skip-shared-package-tests --skip-q2-preview-spa-build
--skip-treesitter-tests` green — Rust-only because the change
touches no Rust code that hub-client transitively consumes (no
quarto-core, no quarto-pandoc-types, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-yxqt (Phase A.1+A.2 complete)

q2 preview CLI surface + crates/quarto-preview shell crate landed
via merge commit. Ready: bd-o5wd (A.3 — fill in q2-preview-spa
main.tsx), bd-501n (A.4 — xtask build helper).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(q2-preview-spa): real boot path via PreviewApp + integration test (bd-o5wd, Phase A.3)

Replaces bd-hfjj Phase 6's placeholder main.tsx (which just rendered
`<PreviewErrorOverlay>` with static text to prove the package
boundary) with the actual SPA bootstrap: WASM init, samod connect,
first-.qmd selection, and `<Q2PreviewIframe>` driven off automerge.

New `src/PreviewApp.tsx`:
  - Three boot states (loading / ready / error) tracked in a single
    state object so the render branches stay declarative.
  - Parses `window.location.hash` for `#/preview/<indexDocId>` per
    Q-A3 of the Phase A plan. The CLI synthesises that fragment when
    opening the browser; the SPA reads it client-side — no server
    HTML rewriting needed.
  - Derives the websocket URL from `window.location` (same host:port
    serves SPA + samod ws), so there's a single source of truth for
    where to connect.
  - On mount: `initWasm()` → `setSyncHandlers()` → `connect(wsUrl,
    indexDocId)`. Files come back from `connect()`'s resolved promise
    AND via the `onFilesChange` handler (the runtime fires both).
  - Active page = first `.qmd` in the project. URL-driven file
    selection is a Phase-D polish item.
  - `onFileContent` bumps a `contentTick` counter; a second
    `useEffect` keyed on `[activeFile, contentTick]` re-runs
    `renderPageInProject()` and feeds the resulting `ast_json` into
    Q2PreviewIframe. That's the "edit re-renders without DOM
    rebuild" promise of the q2-preview format.
  - `setAst` on Q2PreviewIframe is a deliberate no-op for Phase A.
    The iframe expects it because Phase 2 of q2-preview anticipated
    a WYSIWYG round-trip into the .qmd source. Wiring that needs an
    editor surface the SPA doesn't yet have.

New `src/PreviewApp.integration.test.tsx` (3 tests, all green):
  1. Boot path: mocked runtime returns one .qmd; assert
     Q2PreviewIframe receives `astJson` + `currentFilePath` matching
     the mock.
  2. Loading state: `connect()` held in a never-resolving promise;
     assert "Initializing…" UI is visible *before* the iframe.
  3. Connection error path: `connect()` throws; assert the message
     surfaces through `<PreviewErrorOverlay>` and no iframe renders.

Test infra:
  - `vitest.config.ts` + `vitest.integration.config.ts` mirroring
    preview-renderer's pattern: node env for unit, jsdom for
    integration. Workspace-package aliases (preview-renderer,
    preview-runtime, quarto-automerge-schema, quarto-sync-client)
    listed explicitly because vitest doesn't honour the `source`
    exports condition on fresh clones.
  - `src/test-utils/setup.ts`: jest-dom + fake-indexeddb polyfill +
    minimal ResizeObserver shim. Mirrors hub-client's setup
    (deliberate small duplication — see bd-hfjj's catalogue note).

Bookkeeping:
  - q2-preview-spa now has its own `.gitignore` (dist/, node_modules,
    *.tsbuildinfo) — matches `trace-viewer/` and `hub-client/`. The
    prior placeholder dist/ that landed via bd-hfjj Phase 6 is
    untracked accordingly.
  - dev deps grow with jsdom + @testing-library/{react,jest-dom} +
    vitest. `@quarto/quarto-automerge-schema` added as devDep for
    the FileEntry import in the test.

Bundle audit (production build):
  - Editor symbols (Monaco, GoogleOAuthProvider, FileSidebar,
    ProjectSelector, monaco-editor): zero. The §invariant from the
    epic still holds — SPA imports only from shared packages, editor
    code can't transitively leak in.
  - SPA-side code (Q2PreviewIframe, PreviewErrorOverlay,
    renderPageInProject, "Initializing") present. The full WASM +
    sass + automerge chain ships now (~40MB of dist incl. WASM
    binary) — that's the cost of being a real preview host rather
    than a placeholder.

`cargo xtask verify` 11/11 green (including the SPA build).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-o5wd (Phase A.3 complete)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(xtask): `build-q2-preview-spa` + `build-all` chain (bd-501n, Phase A.4)

Mirror of `build-trace-viewer` for the q2-preview SPA. Two pieces:

- `cargo xtask build-q2-preview-spa` (new): runs `npm run build` in
  `q2-preview-spa/`. Useful when only the SPA changed — the
  `quarto-preview` crate's `include_dir!` picks up the fresh dist on
  the next Rust compile.
- `cargo xtask build-all` extended: a new step between the
  trace-viewer build and the Rust workspace build invokes
  `npm run build` in `q2-preview-spa/` so a fresh-clone `build-all`
  produces a `quarto` binary with the real SPA bundle embedded.
  Skip with `--skip-q2-preview-spa-build`. Same `<name>_exists()`
  guard as trace-viewer so the step is a no-op on branches that
  predate bd-hfjj Phase 6's SPA scaffold.

Implementation mirrors `build_trace_viewer.rs` line-for-line —
project-root lookup, npm spawn, success check, banner. The
`BuildAllConfig` gains a `skip_q2_preview_spa_build` field plumbed
through the Command enum + match arm in main.rs.

Verify: `cargo xtask verify --skip-rust-tests` green (11/11).
`cargo xtask build-q2-preview-spa` runs cleanly. `cargo xtask
build-all --help` advertises the new flag. The end-to-end "fresh
clone" smoke (a full `cargo xtask build-all` from a clean checkout)
is exercised by CI rather than this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-501n (Phase A.4 complete)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(quarto-preview): q2 preview boots, renders, and styles end-to-end (bd-mflk, Phase A.5)

Wires quarto_preview::run() to wrap the hub: SPA owns `/`, hub keeps
`/ws` + `/health` + `/api/*` + `/auth/*`. The CLI shim sets up an
ephemeral TempDir data_dir, probes a free port, prints the URL, and
hands off to quarto-hub's run_server_with() with a SPA-fallback
closure. quarto-hub gains three small composability seams:
`HubConfig::register_root_ws`, `StorageManager::new_with_data_dir`,
and `run_server_with<F>` accepting an optional router extender.

Driving the binary against a real fixture surfaced five gaps the
plan didn't anticipate; the plan file documents each with rationale
under "Status (bd-mflk, 2026-05-13)":

- PreviewApp fetches `index_document_id` from `/health` and
  normalizes to the `automerge:<id>` form that
  `@quarto/preview-runtime`'s `connect()` expects (the raw samod
  form makes `repo.find()` hang). Replaces the URL-fragment
  carrier originally chosen in Q-A3.
- New `#[wasm_bindgen] render_page_for_preview` entry point maps
  the default-when-absent `html` format to `q2-preview` so a
  bare-markdown file flows through the AST pipeline and returns
  `ast_json`. Hub-client's `render_page_in_project` is unchanged.
- `quarto-sync-client.connect()` grows an optional `peerTimeoutMs`
  (default 1ms preserved); the SPA passes 5000ms to avoid an
  "Document … is unavailable" cold-start race when no IndexedDB
  cache exists.
- Multi-entry Vite build (mirroring hub-client): adds
  `q2-preview.html` + entry stub for the sandboxed renderer
  iframe `Q2PreviewIframe` loads.
- `theme_fingerprint` threaded through PreviewApp →
  `Q2PreviewIframe`, three-way (string / null / undefined) so the
  iframe mints a blob URL for `/.quarto/project-artifacts/styles.css`
  and posts `UPDATE_THEME`. Fixes the unstyled-DOM symptom.

Boot reliability: `q2-preview-spa/index.html` forces
`html/body/#root { height: 100% }` so the embedded iframe doesn't
collapse to its intrinsic content height.

Tests:
- New `crates/quarto-preview/tests/boot.rs` — in-process spawn of
  `quarto_preview::run()`, asserts `/`, `/health`, SPA-fallback.
- Updated `crates/quarto-preview/tests/smoke.rs` — exercises
  `extend_with_spa()` in isolation (3 cases).
- `q2-preview-spa/src/PreviewApp.integration.test.tsx` — 7 cases
  covering fetch-then-connect chain, /health-failure surfacing,
  docId normalization, and theme-fingerprint passthrough.

End-to-end verified per CLAUDE.md §End-to-end verification:
`q2 preview <tempdir>` → browser via Chrome DevTools shows
`<h1>Hello, q2 preview!</h1>` and both paragraphs rendered through
the AST iframe with Bootstrap typography applied; editing the
source `.qmd` on disk re-renders within ~2s via the file watcher
→ samod → SPA path.

`cargo xtask verify` green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-mflk (Phase A.5 complete)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(q2-preview-spa): always-visible force-refresh button (bd-b5hf, Phase A.6)

Adds a small circular `↻` control overlaid on the top-right of the
preview pane (`aria-label="Refresh preview"`). Click → PreviewApp
bumps its `contentTick` so the render `useEffect` re-fires —
reusing the one channel that the sync handler's `onFileContent`
already uses, so there's a single path through the render pipeline
regardless of who asks for the re-render.

The button satisfies the epic's resolution #4 (force-refresh
invariant): the user always has an escape hatch when the dep-graph
misses a cross-doc relationship. Phase A has no dep-graph at all,
so it's largely redundant today, but the affordance is part of the
preview UX contract going forward; Phase C will extend the click
handler to trigger server-side re-execution where applicable.

Tests:
- New integration test in `PreviewApp.integration.test.tsx`
  pins the contract at the runtime seam: click → at least one
  additional `renderPageForPreview` call. Match by accessible
  name rather than test-id so the UX label is part of the test.

Manual smoke (Chrome DevTools): button rendered at (1876, 12) on
a 1920x1080 viewport; click runs without console error; iframe
content stays consistent; refresh affordance survives clicks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-b5hf (Phase A.6 complete)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(q2-preview-spa): Playwright E2E smoke for q2 preview (bd-vpsy, Phase A.7)

Adds a Playwright suite that drives the real `q2 preview` binary
end-to-end against a temp fixture and pins four behaviours:

  1. Initial render contains the fixture heading + paragraph.
  2. Editing the .qmd on disk re-renders the iframe within ~2s
     (file watcher → samod → SPA round-trip).
  3. The force-refresh button (bd-b5hf) is reachable + clickable.
  4. DOM-stability invariant: the `<h1>` keeps the SAME DOM node
     instance across an edit that doesn't touch the heading
     (`===` reference equality via shared `window.__preEditH1`).

Plan-deviation note: §A.7 mentioned a `data-stable-id` attribute,
which doesn't actually exist in the rendered DOM. The intent there
was the React-reconciler-level invariant — when an edit doesn't
add/remove an element, React reuses the same DOM node — and the
test pins that directly on the heading.

Each test spins up its own `target/debug/q2` instance against a
fresh tempdir project (helpers/previewServer.ts spawns + parses
the printed launch URL). globalSetup just asserts the binary is
built so the failure mode is a copy-pasteable command rather than
a 90 s cargo-compile.

xtask wiring:
- New Step 12 in `cargo xtask verify --e2e` runs `npm run test:e2e`
  from q2-preview-spa. Gated on `--e2e` so the default `verify`
  stays fast and browser-free; skipped if q2-preview-spa/ doesn't
  exist.
- `npm run test:e2e` (in q2-preview-spa) runs Playwright directly.

Verified locally: 4/4 pass under both `npm run test:e2e` and
`cargo xtask verify --skip-hub-build --skip-hub-tests --e2e`
(hub-client's own e2e suite is broken on this branch due to a
pre-existing `@quarto/quarto-sync-client/dist` resolution issue,
not introduced here).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-vpsy (Phase A.7 complete)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* beads: file bd-u6ef (hub-client e2e fails: quarto-sync-client/dist resolution)

Discovered while wiring q2-preview-spa's Playwright suite (bd-vpsy).
Pre-existing on main; not introduced by Phase A work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan(q2-preview): Phase B file-watcher + remap broadening (bd-z529, bd-pf63, bd-91da)

claude-notes/plans/2026-05-13-q2-preview-phase-b.md lays out the
work to close the "everything that should trigger a re-render does"
loop:

  - B.1 (bd-z529): broaden the FileWatcher allow-list to include
    `_quarto.yml`, `_metadata.yml`, image extensions, and `.tsx`
    (defer `_extensions/**` per plan §Q-B1). Plumb a WatchFilter
    enum through HubConfig so hub keeps `.qmd`-only and
    quarto-preview opts into broad.
  - B.3 (bd-pf63): verify cross-doc + include-shortcode edits
    propagate via the existing onFileContent → contentTick path.
    Likely empirical / Playwright-only work.
  - B.4 (bd-91da): acceptance Playwright spec pinning all three
    Phase B scenarios.

bd-pf63 and bd-91da are `blocks`-gated on bd-z529 (the broader
watcher is the precondition for the cross-doc + acceptance work).

Phase A's `render_page_for_preview` (A.5.4c) already resolved
what the epic plan called Phase B item B.2 (format-html →
q2-preview remap), so it isn't a sub-task here. The plan
recommends dropping the proposed B.5 "opt-out of remap" knob
unless a real user complains.

Plan is plan-only — implementation in a later session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(quarto-hub): WatchFilter enum + PreviewBroad for q2 preview (bd-z529, Phase B.1)

Replaces the bare `is_qmd_file` check in `FileWatcher` with a `WatchFilter`
enum so `quarto hub` keeps its `.qmd`-only semantics while `q2 preview` can
opt into a broader allow-list.

`WatchFilter::PreviewBroad` accepts, in addition to `.qmd`:
  - `_quarto.{yml,yaml}` and `_metadata.{yml,yaml}` (basename match)
  - `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.webp` (extension match)
  - `.tsx` (custom React components)

Both `.yml` and `.yaml` are matched because Quarto canonicalizes to `.yml`
but nothing prevents `.yaml`; silently dropping the alt would be a
surprising failure mode. `_extensions/**` is intentionally deferred per
plan Q-B1 — that channel needs gitignore-style ignore-patterns.

Wiring: new `HubConfig::watch_filter` field (default `QmdOnly`).
`quarto-preview::build_hub_config` overrides to `PreviewBroad`. Both
hub-CLI entry points spell out `Default::default()` so the narrow
semantics are explicit. `server::run_server_with` reads the filter
before the `HubConfig` move and threads it through `WatchConfig`.

Tests (`crates/quarto-hub/src/watch.rs`): 4 new predicate tests covering
both modes, accept cases, reject cases (including `.tsx.bak`,
`_quarto.yml.bak`, non-canonical YAML), and 2 new integration tests
that spawn a real `FileWatcher` and verify a `_quarto.yml` edit
surfaces only under `PreviewBroad`.

End-to-end verified through the binary: `q2 preview` boot log shows
`Started filesystem watcher … filter=PreviewBroad`, and a live edit
to `_quarto.yml` produced `DEBUG File change detected
…/_quarto.yml` followed by `Sync complete: filesystem → automerge
new_len=48`. Driving samod-side propagation to a browser re-render
remains Phase B.3 / B.4 work.

Plan: claude-notes/plans/2026-05-13-q2-preview-phase-b.md §B.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-z529 (Phase B.1 complete)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(q2-preview-spa): cross-doc include-shortcode propagation (bd-pf63, Phase B.3)

Adds the empirical proof that editing an included `.qmd` re-renders
the includer in the live preview. Per Phase B plan §B.3 the verification
was expected to need zero production-code changes — confirmed: the
Phase A wiring (watcher → samod sync → `onFileContent` → VFS update →
`contentTick` bump → render useEffect) plus the B.1 broadened watcher
already covers the case.

- Generalise `q2-preview-spa/e2e/helpers/previewServer.ts` from a
  single `fixtureQmd` to a multi-file `fixtureFiles: Array<{path,
  content}>`, with nested-path mkdir. Migrates the one existing
  callsite (`basic-preview.spec.ts`). B.4 will need the same shape.

- New `q2-preview-spa/e2e/include-shortcode.spec.ts`. Fixture:
  `index.qmd` containing `{{< include x.qmd >}}` + `x.qmd` carrying a
  unique sentinel. Two cases:

  1. Initial render expands x.qmd into index.qmd (h1 from index, h2
     and sentinel from x; would fail loudly if the SPA ever picked
     x.qmd as the active page).
  2. Editing x.qmd on disk surfaces the new sentinel in the includer
     within 5 s (plan budget 2 s; matches `basic-preview.spec.ts`'s
     CI ceiling).

  All 6 q2-preview-spa Playwright tests pass (4 pre-existing + 2 new).

- Plan §B.3 rewritten to record the agreed approach (multi-file
  fixture, strict include-shortcode scope, stop-and-report on red)
  and tick the checklist. Empirical result: zero production-code
  changes, contingency not triggered.

- `package-lock.json` drift correction — `npm install` synced entries
  for `@playwright/test` and `@types/node` that were already declared
  in `q2-preview-spa/package.json` but missing from the lockfile.
  Unrelated to B.3 but harmless to include.

Verification:
- Direct: `npx playwright test` in q2-preview-spa → 6/6 pass.
- Indirect: `cargo xtask verify --skip-hub-build` → 12/12 steps green
  (step 12 skipped without --e2e flag).
- `cargo xtask verify --e2e` is blocked at step 7 by the pre-existing
  hub-client e2e failure tracked in bd-u6ef, which explicitly notes
  the q2-preview-spa suite is unaffected. Not my fault to fix here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-pf63 (Phase B.3 complete)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(q2-preview-spa): config-file edit propagation (bd-mrx1, Phase B.4)

Adds the empirical proof that editing project-level and section-level
metadata files re-renders the active preview page with the new
metadata applied. Per Phase B plan §B.4 the verification was
expected to need zero production-code changes — confirmed: the
Phase A wiring (watcher → samod → onFileContent → contentTick →
render useEffect → MetadataMergeStage re-merge) plus B.1's
broadened watcher already cover both layers.

- New `q2-preview-spa/e2e/config-edits.spec.ts`. Fixture:
  _quarto.yml (project title), posts/_metadata.yml (section
  subtitle), posts/post1.qmd (no frontmatter, inherits both).
  Three cases:

  1. Initial render — both inherited values appear in the
     rendered title-block. Load-bearing check that both
     metadata layers are read on first render.
  2. Editing _quarto.yml title — new title in DOM within 5 s;
     section subtitle unchanged.
  3. Editing posts/_metadata.yml subtitle — new subtitle in DOM
     within 5 s; project title unchanged.

  Each edit test cross-checks the OTHER layer to prevent an
  over-eager edit trivially passing.

- Plan §B.4 rewritten with the agreed approach (single dual-
  purpose fixture, criteria 1+2 only). Criterion 3 ("unrelated
  sibling re-renders only on dep edge") is deferred to bd-0mji,
  which also covers the Phase D dep-graph filter regression tests
  once that lands.

Phase B closeout: all sub-tasks (B.1, B.3, B.4) land without
touching production rendering or sync code, matching the plan's
"verification, not implementation" framing.

Verification:
- Direct: `npx playwright test` in q2-preview-spa → 9/9 pass
  (4 basic-preview + 2 include-shortcode + 3 config-edits).
- Indirect: `cargo xtask verify --skip-hub-build` → 12/12 green
  (step 12 skipped without --e2e).
- `cargo xtask verify --e2e` is still blocked at step 7 by the
  pre-existing hub-client e2e failure tracked in bd-u6ef.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-mrx1 (Phase B.4 complete), file bd-0mji follow-up

Also closes accidental duplicate bd-hytl (created by a stray
double-run during B.4 setup; no work done against it).

Phase B is now closed for verification work — all of B.1 / B.3 / B.4
have landed without touching production rendering or sync code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan(q2-preview): Phase C engine execution + replay (draft)

Drafts claude-notes/plans/2026-05-13-q2-preview-phase-c.md covering
the seven sub-tasks (C.1–C.7) the epic plan names for Phase C. Per
user direction, this commit is plan-only — sub-task bd-issues will
be filed in a follow-up session after plan review.

Highlights and decisions captured in the plan:
- Six Phase-C-specific open questions (Q-C1..Q-C6), each with a
  recommended resolution: sidecar IndexDocument schema (additive,
  no migration of existing `files` values); reuse the full
  EngineExecutionStage pipeline for capture recording; whole-QMD
  byte-equality for staleness (matches ReplayEngine's miss policy);
  explicit `state` enum on the sidecar; reject concurrent
  re-execute requests with 409.
- Explicit dependency order — C.3 (transport) blocks C.1 + C.4;
  C.5 needs C.1 + C.2; C.6 + C.7 are parallelisable polish.
- Per-sub-task TDD test plans, seams (file:line for the existing
  code), and acceptance criteria.
- Pre-flight investigation receipts so future sessions don't
  re-derive the seams: EngineExecutionStage's existing capture
  emission at line 256, EngineRegistry::with_replay, WASM
  render_page_for_preview signature, IndexDocument Rust + TS
  shapes, the file-watcher → sync_file hook, the
  extend_with_spa router-extension point.

Phase-level decisions Q1–Q7 from the epic are reconfirmed as
settled and folded into the plan rather than re-litigated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads + plan: file Phase C sub-tasks (bd-kw93.1–bd-kw93.7)

Seven parent-child sub-tasks under bd-kw93 with blocks edges per the
dependency graph in the Phase C plan:

  C.3 (bd-kw93.1) — schema + WASM signature   foundational
  C.1 (bd-kw93.2) — server eager capture      blocked by C.3
  C.4 (bd-kw93.3) — browser-side replay       blocked by C.3
  C.2 (bd-kw93.4) — staleness detection       blocked by C.1
  C.5 (bd-kw93.5) — stale-capture UX overlay  blocked by C.4 + C.2
  C.6 (bd-kw93.6) — preview.engine config     blocked by C.5
  C.7 (bd-kw93.7) — per-doc capture cache     blocked by C.5

Plan updated to drop the bd-XXXX placeholders and reflect filed status.
bd-kw93.1 is the only Phase C item currently ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(automerge-schema): IndexDocument capture sidecar (V2) — bd-kw93.1

Phase C.3 schema half. Adds the typed `captures` sidecar that Phase C's
server eager-record (C.1) and browser-side replay (C.4) consume.

- New `CaptureRef` interface: `captureDocId` + optional `staleness`,
  `state` (idle | running | error), `lastError`. Reserved as a string
  enum (per plan §Q-C4) so future error/idle states grow without
  another schema bump.
- `IndexDocument.captures?: Record<string, CaptureRef>` keyed by the
  same path used in `files`. Sidecar approach (per plan §Q-C1) is
  additive — no migration of existing `files` values.
- Bump `CURRENT_SCHEMA_VERSION` to 2.
- `migrateIndexDocument` now stages V0 → V1 → V2; V1-to-V2 is a
  version bump only (no structural change — captures is absent until a
  capture is recorded). Preserves any preexisting `captures` map.
- 6 new tests (V2 happy path, V1→V2 migration with identities preserved,
  V0→V2 fast-path, V2 no-op, sidecar preserved across migration,
  CaptureRef-all-fields type-shape check). 1 pre-existing test renamed
  + asserted as a forward migration rather than a v1 no-op.

Schema package: 47/47 tests, typecheck clean. Downstream consumer audit
shows the bump is invisible: `quarto-sync-client/src/client.ts:380`
runs `migrateIndexDocument(d)` without checking its return value; no
hub-client code imports the constant from this package.

Next on bd-kw93.1: sync-client wire-up, Rust IndexDocument mirror, WASM
`render_page_for_preview` signature widen.

Plan: claude-notes/plans/2026-05-13-q2-preview-phase-c.md §C.3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(sync-client): expose capture sidecar to consumers — bd-kw93.1

Phase C.3 sync-client wire-up. The IndexDocument's V2 captures sidecar
(landed in 2bdd6a5e) is now observable through the existing callback
shape — Phase C.4 (browser-side replay) will consume it from the SPA
without further plumbing.

- Re-export `CaptureRef` from `types.ts` so consumers can type-import
  the sidecar shape with one import (mirrors how `FileEntry` is
  surfaced).
- New `SyncClientCallbacks.onCapturesChange?: (captures: Record<string,
  CaptureRef>) => void`. Optional — existing consumers compile
  unchanged.
- `client.ts`: `getCapturesFromIndex` helper + `lastCaptures` tracking
  + `notifyCapturesIfChanged` diffing (JSON-equality, matching the
  identities pattern). Fired on initial connect, on every index doc
  change, and on createNewProject. Reset to `{}` on disconnect.
- `createNewProject` now writes `version: CURRENT_SCHEMA_VERSION`
  (currently 2) into the new IndexDocument literal instead of a
  hardcoded 1, so freshly-created projects are V2 from birth rather
  than relying on migrateIndexDocument to bump them on next load.
- 2 new tests (empty-sidecar fire, populated-sidecar fire) modelled on
  the existing identity tests.

sync-client tests: 68 → 70 passing; typecheck clean. Plan §C.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(quarto-hub): Rust IndexDocument capture sidecar mirror — bd-kw93.1

Phase C.3 Rust half. Adds a reader/writer for the same V2 capture sidecar
introduced in 2bdd6a5e (TS schema) so the server side can write
EngineCaptures from the file-watcher loop and the SPA can pick them up
through the existing samod sync — no extra channel.

Schema mirror:
- `CaptureState` enum (Idle | Running | Error) — matches the TS string
  union; serialized as "idle" / "running" / "error" so the SPA can
  read it without a discriminant.
- `CaptureRef` struct with `capture_doc_id` required + `staleness` /
  `state` / `last_error` optional. Optional fields are omitted from the
  automerge map when None, matching the TS shape and keeping forward-
  compat with future fields.
- `CAPTURES_KEY = "captures"` on the ROOT map; nested per-path entries
  each have their own ObjType::Map. Field names live in a private
  `capture_field` module keyed to the TS interface.

`IndexDocument` methods:
- `get_all_captures()`, `get_capture(path)`, `has_capture(path)` —
  return empty/None on V0/V1 docs that never had the key.
- `set_capture(path, &CaptureRef)` — creates the captures map lazily
  (so V0/V1 docs migrate on first server write without an explicit
  migration step). Delete-then-recreate the entry to avoid leaving
  stale optional fields from a previous CaptureRef.
- `remove_capture(path)` — no-op when absent.

Tests: 7 new (initial empty, set+get roundtrip, all-fields-roundtrip
through a fresh DocHandle, overwrite-preserves-no-stale-fields, remove,
get_all returns multiple entries, files/captures independence). The last
test pins the v1 limitation called out in plan §Risks #3: removing a
file does NOT cascade-delete its capture sidecar; that's a caller
responsibility tracked separately.

quarto-hub: 186/186 tests pass; `RUSTFLAGS="-D warnings" cargo build -p
quarto-hub` clean. Plan §C.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sync beads: close bd-kw93.1 (Phase C.3 schema complete)

C.3 sidecar schema is end-to-end: TS (2bdd6a5e), sync-client wire-up
(6a0af0e2), Rust mirror (804b8338). cargo xtask verify --skip-hub-build
all 12 steps green.

Also: bd-kw93.3 (C.4) description updated to absorb the WASM signature
widening originally listed under C.3 — doing it without the dispatch
logic (EngineRegistry::with_replay) would have been a half-finished
parameter, and C.4's test plan already exercises the dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* plan(q2-preview): Phase C Progress section, C.3 checked off

Top-level checklist tracks each sub-task. Records the WASM-widening
scope move from C.3 to C.4 (covered in bd-kw93.1's close reason and
bd-kw93.3's updated description, now also visible on the plan
itself).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(quarto-core): preview_record sub-pipeline for engine capture — bd-kw93.2

Phase C.1 first slice. Lands the engine-only sub-pipeline + capture
extraction that the q2 preview server will call from its file-watcher
loop to populate the IndexDocument capture sidecar.

`crates/quarto-core/src/engine/preview_record.rs`:
- New `record_capture(path, project, runtime, engine_registry) -> Result<Option<EngineCapture>>` entry point.
- Internal `CaptureCollector` observer wraps Arc<Mutex<Option<EngineCapture>>>, deserializes the ENGINE_CAPTURE_KIND aux payload that EngineExecutionStage already emits today (bd-45yw landed the emission seam).
- Sub-pipeline built by truncating `build_html_pipeline_stages_with_options` at the `engine-execution` stage — keeps the server-side record path in sync with the in-browser replay path automatically when new pre-engine stages are added.
- Optional engine_registry override is the test seam (passthrough engine registered under a non-"markdown" name to bypass the EngineExecutionStage short-circuit and trigger capture emission).

Returns `Ok(None)` when no capture is recorded — happens whenever the
resolved engine is `markdown` (its short-circuit at engine_execution.rs:191
skips both the execute() call and the aux event). Caller treats `None`
as "no capture needed" and omits the sidecar entry.

Tests (4 new, all passing):
- prose-only doc → None (default markdown engine path).
- doc wi…
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.

3 participants