Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
3da737c
docs(plans): provenance-epic revisions to plans 3-8
gordonwoodhull May 20, 2026
3edf500
docs(plans): tighten Plan 3 scope + propagate to plans 6/7/7a
gordonwoodhull May 20, 2026
6afc210
docs(plan-3): incorporate review decisions on hashing, drive modes, f…
gordonwoodhull May 21, 2026
4d8683d
docs(plan-3): reuse existing test helpers; pin decisions; long-lived …
gordonwoodhull May 21, 2026
51e2f40
plan-3 phase 1: meta-hash + divergence localization
gordonwoodhull May 21, 2026
a822169
plan-3 phase 2: idempotence test crate scaffolding
gordonwoodhull May 21, 2026
a44d562
plan-3 phase 3: 11 carry-forward fixtures
gordonwoodhull May 21, 2026
bc33a28
chore: refresh lockfiles after npm install + wasm build
gordonwoodhull May 21, 2026
47e7fd7
plan-3 phase 4a: 9 gap-closure doc fixtures + 1 in queue
gordonwoodhull May 21, 2026
f8e9c51
plan-3 phase 4b: include-in-header + resource-image fixtures
gordonwoodhull May 21, 2026
95b8644
plan-3 phase 4c: website-chrome, website-links, website-listing
gordonwoodhull May 21, 2026
70f2ff7
plan-3 phase 4d: attribution fixture
gordonwoodhull May 21, 2026
ef76747
plan-3 phase 6: idempotence-contract.md + cross-links
gordonwoodhull May 21, 2026
98c585f
plan-3 phase 7: final verification + queue state recorded
gordonwoodhull May 21, 2026
6dfb3e5
plan-3: check off the per-fixture coverage-gaps inventory
gordonwoodhull May 21, 2026
1abbafc
bd-rz2we: split vfs_root into write-root + url-root in ResourceResolv…
gordonwoodhull May 21, 2026
2d050d9
docs(plans): Plan 4 implementation-ready + cross-plan `from` rename
gordonwoodhull May 21, 2026
1fac357
docs(plans-4-and-5): annotate bd-3odjm as Plan-5-owned baseline failure
gordonwoodhull May 21, 2026
7dedebc
docs(plans): propagate SmallVec macro to plans 5 & 6 code samples
gordonwoodhull May 21, 2026
431be7e
docs(plans): bump SmallVec capacity to 2 and fold in research
gordonwoodhull May 21, 2026
afe67b0
docs(plans): correct SmallVec cap=2 memory delta (~40 bytes, not 16)
gordonwoodhull May 22, 2026
5542912
docs(plan-5): review pass — checklist, TS shape, scope cleanups
gordonwoodhull May 22, 2026
61d4af2
docs(plan-4): consolidate file-id walkers + close open questions
gordonwoodhull May 22, 2026
5465cb0
Merge feature/provenance-plan-5: review pass on Plan 5 wire format
gordonwoodhull May 22, 2026
9fecf91
plan-4: implement SourceInfo Generated + Anchor types
gordonwoodhull May 22, 2026
d51db4e
docs(plan-4): record implementation surprises
gordonwoodhull May 22, 2026
7fa41b5
docs(plan-5): review-2 pass — phase reorder, strict readers, TS rename
gordonwoodhull May 22, 2026
765adb0
docs(plan-5): sharpen Phase 0 + carry Plan-4 implementation learnings
gordonwoodhull May 22, 2026
0d7d1e9
docs(plan-5): resolve open questions from pre-impl audit
gordonwoodhull May 22, 2026
d044b97
fix(pampa): close bd-3odjm with code-3 dual-shape + code-4 readers (P…
gordonwoodhull May 22, 2026
eef41fa
docs(plan-6): review pass — close open questions, expand scope, fix P…
gordonwoodhull May 22, 2026
4d2ff4b
feat(pampa): emit Generated as JSON wire code 4 (Plan 5 Phases 3+4, a…
gordonwoodhull May 22, 2026
0b4f84a
docs(plans 6-8): post-review followups — cleanup open questions, cros…
gordonwoodhull May 22, 2026
254290e
feat(preview-renderer): consume code-4 Generated wire format (Plan 5 …
gordonwoodhull May 22, 2026
638a8d8
Merge review/provenance-plan-6: Plan-6 review pass
gordonwoodhull May 22, 2026
fe8c22d
plan-6 phase 0: add Inline/Block::source_info_mut accessors
gordonwoodhull May 22, 2026
d4ec690
plan-6 audit: enumerate sites + document AttrSourceInfo invariant
gordonwoodhull May 22, 2026
951222a
plan-6: shortcode stamper + dispatch funnel + error/literal call sites
gordonwoodhull May 22, 2026
163c01c
plan-6: synthesizer transforms emit Generated provenance
gordonwoodhull May 22, 2026
e8df75c
docs(plans 9, 10): research plans for ValueSource threading + Dispatc…
gordonwoodhull May 22, 2026
e8119f8
plan-6 tests: per-transform shape + shortcode + Lua enrichment
gordonwoodhull May 22, 2026
02b401a
docs(plan-7): rewrite — decompose API, settle review findings, add im…
gordonwoodhull May 24, 2026
91e3edd
plan-6: verify pass + WASM Cargo.lock update + plan checklist closed
gordonwoodhull May 22, 2026
8aeda2a
Merge review/provenance-plan-7: Plan-7 review pass + Plans 9/10 resea…
gordonwoodhull May 24, 2026
3262d6c
docs(plans 7, 7a, 10): post-merge follow-ups from Plan-7 wrap-up review
gordonwoodhull May 24, 2026
a22ce41
plan-7 phase 1: foundation primitives (preimage_in, atomicity registr…
gordonwoodhull May 25, 2026
9a473fe
plan-7 phase 2+3a: writer internals — soft-drop, Transparent/Omit, mu…
gordonwoodhull May 25, 2026
66181cf
docs(plan-7): fold codebase facts into Phase 2 + Phase 4 sections
gordonwoodhull May 25, 2026
4f815f0
ci(e2e): drop path filter so hub-client e2e runs on every PR (bd-izh3)
gordonwoodhull May 25, 2026
a0a4c7c
plan-7 phases 4-6: WASM bridge takes baseline AST; lift read-only guard
gordonwoodhull May 25, 2026
fceb862
docs(hub-client/changelog): plan-7 phases 4-6 entry
gordonwoodhull May 25, 2026
20f4b0f
plan-7 phase 7: SPA setAst wired; FNV-1a echo-prevention; DiagnosticS…
gordonwoodhull May 25, 2026
c72640a
plan-7 phase 8 (subset) + 9: WASM wrapper test, smoke, follow-up beads
gordonwoodhull May 25, 2026
4ee51e4
docs(changelog): update plan-7 phases 4-6 commit hash after rebase
gordonwoodhull May 25, 2026
6afa861
docs(plans 7, 9): note Plan 7 shipped; Phase-5 tests now unblocked
gordonwoodhull May 25, 2026
b879896
docs(plan-7b): write Plan 7 test-o-rama consolidation plan
gordonwoodhull May 25, 2026
e0aacdc
docs(provenance): contract doc for adding new Generated kinds
gordonwoodhull May 25, 2026
da44e04
docs(plan-7c): closure gaps from Plan-7 implementation session
gordonwoodhull May 25, 2026
bdcfdc5
fix(pampa/writers/incremental): recurse into non-atomic Generated wra…
gordonwoodhull May 25, 2026
47c4c57
docs(changelog): note q2-preview sectionize-wrapper edit fix (bdcfdc53)
gordonwoodhull May 25, 2026
b9f64b5
fix(pampa/writers/incremental): descend wrappers when deriving target…
gordonwoodhull May 25, 2026
2bf9266
fix(pampa/writers/incremental): preserve YAML frontmatter when blocks…
gordonwoodhull May 25, 2026
8f1d33d
refactor(pampa/writers/incremental): name the transparent-wrapper pat…
gordonwoodhull May 25, 2026
5f2bbab
fix(hub-client/ReactPreview): surface soft-drop warnings immediately …
gordonwoodhull May 25, 2026
3f96b39
docs(changelog): note soft-drop diagnostic surfacing fix (5f2bbab0)
gordonwoodhull May 25, 2026
e584428
refactor(pampa/writers/incremental): make CoarsenedEntry self-contain…
gordonwoodhull May 26, 2026
de2b2f6
fix(quarto-error-reporting): gracefully degrade ariadne source contex…
gordonwoodhull May 26, 2026
c9ac251
plan-7 review pass: extract writer contract to design doc, settle ope…
gordonwoodhull May 24, 2026
12170cb
plan-7: settle the last three open questions
gordonwoodhull May 24, 2026
ab41e8d
plan-7 review pass: settle open items, split into two sessions, add h…
gordonwoodhull May 24, 2026
8ef8cf5
docs(designs): cross-link provenance + incremental-writer contracts
gordonwoodhull May 25, 2026
3545fc0
docs(designs+plans): reconcile incremental-writer docs after rebase
gordonwoodhull May 27, 2026
760dacd
docs(plan-7d): research plan for algebraic soundness of coarsen/incre…
gordonwoodhull May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions .github/workflows/hub-client-e2e.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
name: Hub-Client E2E Tests

on:
push:
branches: [main]
paths:
- 'hub-client/**'
- '.github/workflows/hub-client-e2e.yml'
pull_request:
paths:
- 'hub-client/**'
- '.github/workflows/hub-client-e2e.yml'
workflow_dispatch:
inputs:
recreate-all-snapshots:
description: 'Delete and recreate ALL visual regression baselines'
type: boolean
default: false
push:
branches:
- main
pull_request:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
e2e-tests:
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ proc-macro2 = { version = "1.0.106", features = ["span-locations"] }
schemars = "1.2.1"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
smallvec = { version = "1.13", features = ["serde"] }
serde_yaml = "0.9"
thiserror = "2.0"
toml = "0.9.11"
Expand Down
563 changes: 563 additions & 0 deletions claude-notes/designs/incremental-writer-contract.md

Large diffs are not rendered by default.

399 changes: 399 additions & 0 deletions claude-notes/designs/provenance-contract.md

Large diffs are not rendered by default.

218 changes: 218 additions & 0 deletions claude-notes/designs/transparent-wrappers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Transparent wrappers — descending past synthesized block containers

**Status:** Active (introduced 2026-05-25 alongside Plan 7c Phase 8).
**Types:** `pampa::pandoc::Block`, `quarto_source_map::SourceInfo`.
**Reference impl:**
[`crates/pampa/src/writers/incremental.rs`](../../crates/pampa/src/writers/incremental.rs)
(`first_in_user_tree`, `is_transparent_wrapper`,
`derive_target_file_id`, `first_target_anchored_start_in`).
**Plans:**
[Plan 7](../plans/2026-05-04-q2-preview-plan-7-incremental-writer.md)
(writer) ·
[Plan 7c](../plans/2026-05-25-q2-preview-plan-7c-closure-gaps.md)
(Phase 8 — target_file_id descent) ·
[Plan 8](../plans/2026-05-04-q2-preview-plan-8-include-roundtrip.md)
(IncludeExpansion — *not* a transparent wrapper) ·
[Plan 9](../plans/2026-05-22-provenance-plan-9-valuesource-threading.md)
(`title_source_info`) ·
[Plan 10](../plans/2026-05-22-provenance-plan-10-dispatch-anchor.md)
(Lua-emitted wrappers).

## Summary

The post-render AST that q2-preview hands the React iframe is **not
flat.** The render pipeline wraps the user's blocks in synthesized
containers — most notably a single top-level `Div` from
`SectionizeTransform` — that group content by heading level for
sidebar / cross-reference / outline construction. These wrappers
carry `SourceInfo::Generated` with no `Invocation` anchor: they're
structurally part of the AST but have **no source bytes of their own**
in the user's qmd.

A *transparent wrapper* is the name for this shape. Code that asks
"where do the user's source bytes live?" must descend through
transparent wrappers, not read `blocks[0]` directly.

Three writer bugs landed on this rake before the pattern was named
(commits `bdcfdc53`, `b9f64b56`, `2bf92664`): the writer
soft-dropped the wrapper instead of recursing, derived the wrong
file id, and silently deleted the YAML frontmatter. All three were
the same mistake — `blocks[0]` is not necessarily a real user
block.

## Definition

A `Block` is a *transparent wrapper* with respect to a
`target_file_id` when **all three** hold:

1. Its `SourceInfo` is `Generated` with no `Invocation` anchor.
It has no source token of its own; its bytes are synthesized.
2. It is recognised by `block_block_children` (i.e. it's a `Div`,
`BlockQuote`, `Figure`, or `NoteDefinitionFencedBlock` — the
block-container kinds today's synthesizers emit).
3. At least one descendant has real
`preimage_in(target_file_id).is_some()` — there's actual user
content under it.

Condition (3) is what makes the predicate *structural* rather than
opt-in: a Lua filter that wraps existing user paragraphs in a
`Div.callout` produces a Generated Div whose children still carry
their original preimage → it's transparent → the visual editor sees
through it → user edits inside the wrapped content round-trip
cleanly. A filter that constructs a fresh Div from metadata has no
source-bearing children → it's atomic → editor treats it as a unit.
The filter author doesn't have to declare anything; the AST shape
declares it for them.

## Known transparent wrappers today

Produced by `pampa::pandoc::sugar::SectionizeTransform` and friends:

- **sectionize** Div — groups blocks by heading depth (`By::sectionize()`).
- **footnotes-container** Div — collects all footnote definitions.
- **appendix-container** Div — collects appendix-tagged content.

Plus, by structural construction, any Lua-emitted block-container
that meets the three conditions above (Plan 10).

**Not** transparent wrappers:

- `IncludeExpansion` CustomNode (Plan 8) — its `SourceInfo` is
`Original`, anchored to the include-token bytes in the parent qmd.
Descent stops at it; that's correct behaviour.
- Atomic CustomNodes like `CrossrefResolvedRef` — `SourceInfo`
is `Original` pointing at the `@ref` token.
- The synthesized title-block Header (`By::title_block()`) —
`is_atomic_kind` is `true` for `title-block`. Editor treats the
resolved title as read-only; the user's source-side knob is the
YAML `title:` key. (Not block-container shape either.)

## Sibling primitive on the emission side

`first_in_user_tree` (below) is the *traversal* primitive — how a
caller descends past transparent wrappers when looking up source
positions. The *emission* primitive is `CoarsenedEntry::Transparent`
in the incremental writer: same wrapper shape, but the question is
"how do I emit bytes through this wrapper?" rather than "where do
the user's source bytes live?"

Both rely on the same descent rule (skip the wrapper, look at the
children) and the same invariant (a `Generated` block-container
with no Invocation anchor and source-bearing children is
transparent). They diverge in what they do with the descent:
traversal stops at the first match; emission walks all children
and concatenates their bytes.

See [`incremental-writer-contract.md`](./incremental-writer-contract.md)
for the writer-side contract — in particular the rule that every
`CoarsenedEntry` variant must be self-contained, which is what
makes child entries safe to inline through a `Transparent`.

## Reference primitive: `first_in_user_tree`

```rust
fn first_in_user_tree<T>(
blocks: &[Block],
extract: &impl Fn(&Block) -> Option<T>,
) -> Option<T>
```

Walks `blocks` depth-first. On each block, applies `extract`; if
`Some`, returns it. If `None`, descends through
`block_block_children` and tries again. This is how we see through
transparent wrappers — a wrapper has no source position of its own
(extract returns `None` for it), so the walker looks inside.

The two consumers today are one-liners:

```rust
fn derive_target_file_id(blocks: &[Block]) -> FileId {
first_in_user_tree(blocks, &|b| b.source_info().root_file_id())
.unwrap_or(FileId(0))
}

fn first_target_anchored_start_in(blocks: &[Block], target: FileId) -> Option<usize> {
first_in_user_tree(blocks, &|b| {
b.source_info().preimage_in(target).map(|r| r.start)
})
}
```

A `visit_user_blocks(blocks, &mut visit)` sibling (visiting all user
blocks in document order, transparent wrappers skipped) is the
natural extension for callers that need every block, not just the
first; add it the moment a second caller wants it.

## When to use which

| Need | Tool |
|---|---|
| Find the first block where some property holds | `first_in_user_tree` |
| Visit all user blocks in document order | (add `visit_user_blocks` when needed) |
| Ask "is *this specific block* a transparent wrapper?" | `is_transparent_wrapper` |
| Get the document's editing-file id | `derive_target_file_id` |
| Find where the YAML frontmatter region ends | `first_target_anchored_start_in` |

`is_transparent_wrapper` is intentionally a small predicate — used
when a caller needs to make an *explicit* decision (e.g. a future
Q-3-44 diagnostic that hints "your filter walked into a sectionize
wrapper; you probably meant to walk its children"). Routine
source-position lookups should use the walkers, not the predicate.

## Where the code lives, and when to promote it

The primitives live in
`crates/pampa/src/writers/incremental.rs` next to
`block_block_children`. That's the right home today — the writer
is the only consumer.

Promote to `quarto-pandoc-types` (or a new
`quarto-pandoc-types::traversal` module) **the moment a second
crate needs them.** Plan 9's `DocumentProfile` extractor (when it
gains a "first H1" fallback), Plan 10's filter-output classifier,
and the project-replay engine's cell walker are the candidates.
Don't promote pre-emptively — premature generalisation has its own
debt.

## Adding a new synthesizer

If you're writing a new transform that wraps user content in a Div
(or other block container):

1. Emit `SourceInfo::generated(By::<your-kind>())` on the wrapper.
No `Invocation` anchor (because there's no source token).
2. Preserve the children's existing source_info — don't restamp
them with the wrapper's `By`. The whole point is that the
children stay editable.
3. Your wrapper is automatically transparent; nothing else to do.
4. If your `By::<your-kind>()` should *also* be considered
`is_atomic_kind()` (the resolved children are read-only, like
shortcode resolutions), add it to the atomic-kind set in
`crates/quarto-source-map/src/source_info.rs` — separate
concept, separate decision.

## Anti-patterns

- `ast.blocks[0]` for source-position questions (file id, start
offset, "the first user block"). Use `first_in_user_tree`.
- `ast.blocks.iter()` flatly for "every user block" enumeration
when the document might be wrapped. Use a descending visitor.
- Declaring a transparent wrapper via a `By::kind` registry. The
predicate is structural; don't add an opt-in mechanism that the
shape already encodes.
- Asking "is this Generated and atomic-kind?" when what you mean
is "should I descend?" — `is_atomic_kind` and transparency are
orthogonal. Shortcode resolutions are atomic *and* have
Invocation anchors (descent is meaningful but the resolved
content is read-only). Sectionize Divs are *neither* atomic
*nor* invocation-anchored. Mixing the two predicates produces
subtle bugs.

## History

| Date | Commit | What |
|---|---|---|
| 2026-05-25 | `bdcfdc53` | `coarsen` recurses Transparent into non-atomic Generated wrappers (the first bug — empty qmd) |
| 2026-05-25 | `b9f64b56` | `derive_target_file_id` descends; Plan 7c Phase 8 closed |
| 2026-05-25 | `2bf92664` | `emit_metadata_prefix` descends; YAML frontmatter preserved |
| 2026-05-25 | (this doc) | Pattern named, primitives centralized |
Loading