Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
02448fb
docs(plans): provenance-epic revisions to plans 3-8
gordonwoodhull May 20, 2026
af1d9f2
docs(plans): tighten Plan 3 scope + propagate to plans 6/7/7a
gordonwoodhull May 20, 2026
8a5936d
docs(plan-3): incorporate review decisions on hashing, drive modes, f…
gordonwoodhull May 21, 2026
ed71449
docs(plan-3): reuse existing test helpers; pin decisions; long-lived …
gordonwoodhull May 21, 2026
e9b6aec
plan-3 phase 1: meta-hash + divergence localization
gordonwoodhull May 21, 2026
89dfe14
plan-3 phase 2: idempotence test crate scaffolding
gordonwoodhull May 21, 2026
9195398
plan-3 phase 3: 11 carry-forward fixtures
gordonwoodhull May 21, 2026
c794e2f
chore: refresh lockfiles after npm install + wasm build
gordonwoodhull May 21, 2026
06e366f
plan-3 phase 4a: 9 gap-closure doc fixtures + 1 in queue
gordonwoodhull May 21, 2026
fe8588e
plan-3 phase 4b: include-in-header + resource-image fixtures
gordonwoodhull May 21, 2026
8ec17db
plan-3 phase 4c: website-chrome, website-links, website-listing
gordonwoodhull May 21, 2026
3eae623
plan-3 phase 4d: attribution fixture
gordonwoodhull May 21, 2026
5538808
plan-3 phase 6: idempotence-contract.md + cross-links
gordonwoodhull May 21, 2026
4d5b21b
plan-3 phase 7: final verification + queue state recorded
gordonwoodhull May 21, 2026
82c988d
plan-3: check off the per-fixture coverage-gaps inventory
gordonwoodhull May 21, 2026
d6294d5
bd-rz2we: split vfs_root into write-root + url-root in ResourceResolv…
gordonwoodhull May 21, 2026
5f89260
docs(plans): Plan 4 implementation-ready + cross-plan `from` rename
gordonwoodhull May 21, 2026
73f018c
docs(plans-4-and-5): annotate bd-3odjm as Plan-5-owned baseline failure
gordonwoodhull May 21, 2026
dc6b3e8
docs(plans): propagate SmallVec macro to plans 5 & 6 code samples
gordonwoodhull May 21, 2026
bbac753
docs(plans): bump SmallVec capacity to 2 and fold in research
gordonwoodhull May 21, 2026
2659021
docs(plans): correct SmallVec cap=2 memory delta (~40 bytes, not 16)
gordonwoodhull May 22, 2026
7a36799
docs(plan-4): consolidate file-id walkers + close open questions
gordonwoodhull May 22, 2026
b21607e
docs(plan-5): review pass — checklist, TS shape, scope cleanups
gordonwoodhull May 22, 2026
6dcc33d
plan-4: implement SourceInfo Generated + Anchor types
gordonwoodhull May 22, 2026
cdd6009
docs(plan-4): record implementation surprises
gordonwoodhull May 22, 2026
5eb2669
docs(plan-5): review-2 pass — phase reorder, strict readers, TS rename
gordonwoodhull May 22, 2026
b16b446
docs(plan-5): sharpen Phase 0 + carry Plan-4 implementation learnings
gordonwoodhull May 22, 2026
1868490
docs(plan-5): resolve open questions from pre-impl audit
gordonwoodhull May 22, 2026
13b31ee
fix(pampa): close bd-3odjm with code-3 dual-shape + code-4 readers (P…
gordonwoodhull May 22, 2026
653ac80
feat(pampa): emit Generated as JSON wire code 4 (Plan 5 Phases 3+4, a…
gordonwoodhull May 22, 2026
0f9412a
feat(preview-renderer): consume code-4 Generated wire format (Plan 5 …
gordonwoodhull May 22, 2026
2010f78
docs(plan-6): review pass — close open questions, expand scope, fix P…
gordonwoodhull May 22, 2026
6c3afe2
docs(plans 6-8): post-review followups — cleanup open questions, cros…
gordonwoodhull May 22, 2026
6e4f387
plan-6 phase 0: add Inline/Block::source_info_mut accessors
gordonwoodhull May 22, 2026
0b19137
plan-6 audit: enumerate sites + document AttrSourceInfo invariant
gordonwoodhull May 22, 2026
4fbc43d
plan-6: shortcode stamper + dispatch funnel + error/literal call sites
gordonwoodhull May 22, 2026
696c3fc
plan-6: synthesizer transforms emit Generated provenance
gordonwoodhull May 22, 2026
da55d44
plan-6 tests: per-transform shape + shortcode + Lua enrichment
gordonwoodhull May 22, 2026
9c64c8a
plan-6: verify pass + WASM Cargo.lock update + plan checklist closed
gordonwoodhull May 22, 2026
5706f1a
docs(plans 9, 10): research plans for ValueSource threading + Dispatc…
gordonwoodhull May 22, 2026
b154cbf
docs(plan-7): rewrite — decompose API, settle review findings, add im…
gordonwoodhull May 24, 2026
633aebf
docs(plans 7, 7a, 10): post-merge follow-ups from Plan-7 wrap-up review
gordonwoodhull May 24, 2026
3cdabdb
plan-7 phase 1: foundation primitives (preimage_in, atomicity registr…
gordonwoodhull May 25, 2026
c3936a8
plan-7 phase 2+3a: writer internals — soft-drop, Transparent/Omit, mu…
gordonwoodhull May 25, 2026
2b9b553
docs(plan-7): fold codebase facts into Phase 2 + Phase 4 sections
gordonwoodhull May 25, 2026
4dd3277
ci(e2e): drop path filter so hub-client e2e runs on every PR (bd-izh3)
gordonwoodhull May 25, 2026
9639b09
plan-7 phases 4-6: WASM bridge takes baseline AST; lift read-only guard
gordonwoodhull May 25, 2026
62f69ca
docs(hub-client/changelog): plan-7 phases 4-6 entry
gordonwoodhull May 25, 2026
92c9ce5
plan-7 phase 7: SPA setAst wired; FNV-1a echo-prevention; DiagnosticS…
gordonwoodhull May 25, 2026
919714a
plan-7 phase 8 (subset) + 9: WASM wrapper test, smoke, follow-up beads
gordonwoodhull May 25, 2026
8f27bbd
docs(changelog): update plan-7 phases 4-6 commit hash after rebase
gordonwoodhull May 25, 2026
3e87fbc
docs(plans 7, 9): note Plan 7 shipped; Phase-5 tests now unblocked
gordonwoodhull May 25, 2026
f65eb59
docs(plan-7b): write Plan 7 test-o-rama consolidation plan
gordonwoodhull May 25, 2026
c9f3034
docs(provenance): contract doc for adding new Generated kinds
gordonwoodhull May 25, 2026
2fd48a1
docs(plan-7c): closure gaps from Plan-7 implementation session
gordonwoodhull May 25, 2026
d522a67
fix(pampa/writers/incremental): recurse into non-atomic Generated wra…
gordonwoodhull May 25, 2026
4c4f409
docs(changelog): note q2-preview sectionize-wrapper edit fix (bdcfdc53)
gordonwoodhull May 25, 2026
6d55187
fix(pampa/writers/incremental): descend wrappers when deriving target…
gordonwoodhull May 25, 2026
e514340
fix(pampa/writers/incremental): preserve YAML frontmatter when blocks…
gordonwoodhull May 25, 2026
1906985
refactor(pampa/writers/incremental): name the transparent-wrapper pat…
gordonwoodhull May 25, 2026
60b70b9
fix(hub-client/ReactPreview): surface soft-drop warnings immediately …
gordonwoodhull May 25, 2026
2ec1358
docs(changelog): note soft-drop diagnostic surfacing fix (5f2bbab0)
gordonwoodhull May 25, 2026
b0bba3e
refactor(pampa/writers/incremental): make CoarsenedEntry self-contain…
gordonwoodhull May 26, 2026
b8ff244
fix(quarto-error-reporting): gracefully degrade ariadne source contex…
gordonwoodhull May 26, 2026
8482c44
plan-7 review pass: extract writer contract to design doc, settle ope…
gordonwoodhull May 24, 2026
d96e0d1
plan-7: settle the last three open questions
gordonwoodhull May 24, 2026
fec9df3
plan-7 review pass: settle open items, split into two sessions, add h…
gordonwoodhull May 24, 2026
2cdc29e
docs(designs): cross-link provenance + incremental-writer contracts
gordonwoodhull May 25, 2026
9a32db0
docs(designs+plans): reconcile incremental-writer docs after rebase
gordonwoodhull May 27, 2026
2cdcca5
docs(plan-7d): research plan for algebraic soundness of coarsen/incre…
gordonwoodhull May 27, 2026
8c14674
docs(plans, design): plan_user_writes + 7d/7e/7f split
gordonwoodhull May 29, 2026
af56f6f
docs(plan-7f, provenance): production-residue fixes + new By:: kinds
gordonwoodhull May 30, 2026
6769f5b
docs(plans): second-order consequences round
gordonwoodhull May 30, 2026
ed58da5
docs(plan-7f): reader rejects bare s:, no user_edit fallback
gordonwoodhull May 30, 2026
3463c03
docs(plan-7f, provenance): strict-reader consequences
gordonwoodhull May 30, 2026
af5fbed
docs(plan-7f, provenance): rename to read_completing_source_info + By…
gordonwoodhull May 30, 2026
8a36d57
docs(design): add Completeness; rename to authored content; (C1/C2/R/D)
gordonwoodhull May 30, 2026
a0b7aa2
docs(plan-7d): require dispatch-coverage instrumentation in Phase 4
gordonwoodhull May 30, 2026
8822e1f
docs(plan-7d): Phase 4 testing strategy intro
gordonwoodhull May 30, 2026
be6dd1e
docs(plan-7f): record findings for four open research items
gordonwoodhull May 30, 2026
9665f00
docs(plan-7f): apply audit fixes — drop unknown magic, drop reconcile…
gordonwoodhull May 30, 2026
6fa032d
docs(plan-7f): integrate cross-code audits + adopt -D deprecated as P…
gordonwoodhull Jun 1, 2026
e21bb8c
test(quarto-core): pre-move idempotence test into integration/ layout…
gordonwoodhull Jun 1, 2026
c9dcc6f
docs(plan-7f): correct Plan 7b coordination note in Phase 8
gordonwoodhull Jun 1, 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
155 changes: 155 additions & 0 deletions claude-notes/designs/incremental-writer-contract.md

Large diffs are not rendered by default.

418 changes: 418 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
Loading