Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
703be1c
docs(plans): provenance-epic revisions to plans 3-8
gordonwoodhull May 20, 2026
c5d7b47
docs(plans): tighten Plan 3 scope + propagate to plans 6/7/7a
gordonwoodhull May 20, 2026
f89dec6
docs(plan-3): incorporate review decisions on hashing, drive modes, f…
gordonwoodhull May 21, 2026
bf078af
docs(plan-3): reuse existing test helpers; pin decisions; long-lived …
gordonwoodhull May 21, 2026
7738318
plan-3 phase 1: meta-hash + divergence localization
gordonwoodhull May 21, 2026
cfb5973
plan-3 phase 2: idempotence test crate scaffolding
gordonwoodhull May 21, 2026
2ce4d71
plan-3 phase 3: 11 carry-forward fixtures
gordonwoodhull May 21, 2026
5320a96
chore: refresh lockfiles after npm install + wasm build
gordonwoodhull May 21, 2026
d7521fc
plan-3 phase 4a: 9 gap-closure doc fixtures + 1 in queue
gordonwoodhull May 21, 2026
f08469c
plan-3 phase 4b: include-in-header + resource-image fixtures
gordonwoodhull May 21, 2026
399601e
plan-3 phase 4c: website-chrome, website-links, website-listing
gordonwoodhull May 21, 2026
c7c4cf0
plan-3 phase 4d: attribution fixture
gordonwoodhull May 21, 2026
79a7901
plan-3 phase 6: idempotence-contract.md + cross-links
gordonwoodhull May 21, 2026
f0fff5d
plan-3 phase 7: final verification + queue state recorded
gordonwoodhull May 21, 2026
037175c
plan-3: check off the per-fixture coverage-gaps inventory
gordonwoodhull May 21, 2026
98f455c
bd-rz2we: split vfs_root into write-root + url-root in ResourceResolv…
gordonwoodhull May 21, 2026
fa14398
docs(plans): Plan 4 implementation-ready + cross-plan `from` rename
gordonwoodhull May 21, 2026
09ea81a
docs(plans-4-and-5): annotate bd-3odjm as Plan-5-owned baseline failure
gordonwoodhull May 21, 2026
ca48d0c
docs(plans): propagate SmallVec macro to plans 5 & 6 code samples
gordonwoodhull May 21, 2026
6491cda
docs(plans): bump SmallVec capacity to 2 and fold in research
gordonwoodhull May 21, 2026
5fbf8b4
docs(plans): correct SmallVec cap=2 memory delta (~40 bytes, not 16)
gordonwoodhull May 22, 2026
b1e51cc
docs(plan-4): consolidate file-id walkers + close open questions
gordonwoodhull May 22, 2026
a1dd163
docs(plan-5): review pass — checklist, TS shape, scope cleanups
gordonwoodhull May 22, 2026
b01e90a
plan-4: implement SourceInfo Generated + Anchor types
gordonwoodhull May 22, 2026
d4497b5
docs(plan-4): record implementation surprises
gordonwoodhull May 22, 2026
13486d6
docs(plan-5): review-2 pass — phase reorder, strict readers, TS rename
gordonwoodhull May 22, 2026
72d751a
docs(plan-5): sharpen Phase 0 + carry Plan-4 implementation learnings
gordonwoodhull May 22, 2026
deda9fb
docs(plan-5): resolve open questions from pre-impl audit
gordonwoodhull May 22, 2026
05bff23
fix(pampa): close bd-3odjm with code-3 dual-shape + code-4 readers (P…
gordonwoodhull May 22, 2026
fd29177
feat(pampa): emit Generated as JSON wire code 4 (Plan 5 Phases 3+4, a…
gordonwoodhull May 22, 2026
03622eb
feat(preview-renderer): consume code-4 Generated wire format (Plan 5 …
gordonwoodhull May 22, 2026
4026877
docs(plan-6): review pass — close open questions, expand scope, fix P…
gordonwoodhull May 22, 2026
5dfe045
docs(plans 6-8): post-review followups — cleanup open questions, cros…
gordonwoodhull May 22, 2026
f5b40d5
plan-6 phase 0: add Inline/Block::source_info_mut accessors
gordonwoodhull May 22, 2026
e7a0367
plan-6 audit: enumerate sites + document AttrSourceInfo invariant
gordonwoodhull May 22, 2026
80429de
plan-6: shortcode stamper + dispatch funnel + error/literal call sites
gordonwoodhull May 22, 2026
e1e9f1d
plan-6: synthesizer transforms emit Generated provenance
gordonwoodhull May 22, 2026
5c7f9f9
plan-6 tests: per-transform shape + shortcode + Lua enrichment
gordonwoodhull May 22, 2026
2e44b01
plan-6: verify pass + WASM Cargo.lock update + plan checklist closed
gordonwoodhull May 22, 2026
f531ada
docs(plans 9, 10): research plans for ValueSource threading + Dispatc…
gordonwoodhull May 22, 2026
8312ce7
docs(plan-7): rewrite — decompose API, settle review findings, add im…
gordonwoodhull May 24, 2026
8743065
docs(plans 7, 7a, 10): post-merge follow-ups from Plan-7 wrap-up review
gordonwoodhull May 24, 2026
04d37b3
plan-7 phase 1: foundation primitives (preimage_in, atomicity registr…
gordonwoodhull May 25, 2026
a41eefd
plan-7 phase 2+3a: writer internals — soft-drop, Transparent/Omit, mu…
gordonwoodhull May 25, 2026
e60fa4e
docs(plan-7): fold codebase facts into Phase 2 + Phase 4 sections
gordonwoodhull May 25, 2026
8b9c34b
ci(e2e): drop path filter so hub-client e2e runs on every PR (bd-izh3)
gordonwoodhull May 25, 2026
cc582b8
plan-7 phases 4-6: WASM bridge takes baseline AST; lift read-only guard
gordonwoodhull May 25, 2026
53ba35e
docs(hub-client/changelog): plan-7 phases 4-6 entry
gordonwoodhull May 25, 2026
0667052
plan-7 phase 7: SPA setAst wired; FNV-1a echo-prevention; DiagnosticS…
gordonwoodhull May 25, 2026
2fe6f58
plan-7 phase 8 (subset) + 9: WASM wrapper test, smoke, follow-up beads
gordonwoodhull May 25, 2026
e2793ec
docs(changelog): update plan-7 phases 4-6 commit hash after rebase
gordonwoodhull May 25, 2026
55e4560
docs(plans 7, 9): note Plan 7 shipped; Phase-5 tests now unblocked
gordonwoodhull May 25, 2026
d317bf0
docs(plan-7b): write Plan 7 test-o-rama consolidation plan
gordonwoodhull May 25, 2026
6bbc6dc
docs(provenance): contract doc for adding new Generated kinds
gordonwoodhull May 25, 2026
b3e539f
docs(plan-7c): closure gaps from Plan-7 implementation session
gordonwoodhull May 25, 2026
4974864
fix(pampa/writers/incremental): recurse into non-atomic Generated wra…
gordonwoodhull May 25, 2026
699676f
docs(changelog): note q2-preview sectionize-wrapper edit fix (bdcfdc53)
gordonwoodhull May 25, 2026
d60119d
fix(pampa/writers/incremental): descend wrappers when deriving target…
gordonwoodhull May 25, 2026
fcbb55d
fix(pampa/writers/incremental): preserve YAML frontmatter when blocks…
gordonwoodhull May 25, 2026
76bd96f
refactor(pampa/writers/incremental): name the transparent-wrapper pat…
gordonwoodhull May 25, 2026
99104f3
fix(hub-client/ReactPreview): surface soft-drop warnings immediately …
gordonwoodhull May 25, 2026
2e14092
docs(changelog): note soft-drop diagnostic surfacing fix (5f2bbab0)
gordonwoodhull May 25, 2026
6e3134e
refactor(pampa/writers/incremental): make CoarsenedEntry self-contain…
gordonwoodhull May 26, 2026
b661b4e
fix(quarto-error-reporting): gracefully degrade ariadne source contex…
gordonwoodhull May 26, 2026
13983a8
plan-7 review pass: extract writer contract to design doc, settle ope…
gordonwoodhull May 24, 2026
b245abd
plan-7: settle the last three open questions
gordonwoodhull May 24, 2026
4de5310
plan-7 review pass: settle open items, split into two sessions, add h…
gordonwoodhull May 24, 2026
09c4d04
docs(designs): cross-link provenance + incremental-writer contracts
gordonwoodhull May 25, 2026
a6c812b
docs(designs+plans): reconcile incremental-writer docs after rebase
gordonwoodhull May 27, 2026
00d6b80
docs(plan-7d): research plan for algebraic soundness of coarsen/incre…
gordonwoodhull May 27, 2026
d0d3e3f
docs(plans, design): plan_user_writes + 7d/7e/7f split
gordonwoodhull May 29, 2026
d3e417d
docs(plan-7f, provenance): production-residue fixes + new By:: kinds
gordonwoodhull May 30, 2026
d129ec9
docs(plans): second-order consequences round
gordonwoodhull May 30, 2026
4baefd7
docs(plan-7f): reader rejects bare s:, no user_edit fallback
gordonwoodhull May 30, 2026
3e1e157
docs(plan-7f, provenance): strict-reader consequences
gordonwoodhull May 30, 2026
f3210a7
docs(plan-7f, provenance): rename to read_completing_source_info + By…
gordonwoodhull May 30, 2026
c457f44
docs(design): add Completeness; rename to authored content; (C1/C2/R/D)
gordonwoodhull May 30, 2026
aa2020d
docs(plan-7d): require dispatch-coverage instrumentation in Phase 4
gordonwoodhull May 30, 2026
1500c83
docs(plan-7d): Phase 4 testing strategy intro
gordonwoodhull May 30, 2026
6c9ab18
docs(plan-7f): record findings for four open research items
gordonwoodhull May 30, 2026
a2f0ab8
docs(plan-7f): apply audit fixes — drop unknown magic, drop reconcile…
gordonwoodhull May 30, 2026
3faffc4
docs(plan-7f): integrate cross-code audits + adopt -D deprecated as P…
gordonwoodhull Jun 1, 2026
b10c6be
test(quarto-core): pre-move idempotence test into integration/ layout…
gordonwoodhull Jun 1, 2026
df3480e
docs(plan-7f): correct Plan 7b coordination note in Phase 8
gordonwoodhull Jun 1, 2026
f212326
docs(provenance-contract): drop reconcile_synthesize + UNKNOWN_SOURCE…
gordonwoodhull Jun 1, 2026
a8ccc5b
feat(framework): preserve parent s: across child edits (Plan 7f Phase 2)
gordonwoodhull Jun 1, 2026
9d2ad2b
docs(hub-client): changelog for d336daa (Plan 7f Phase 2 — q2-debug f…
gordonwoodhull Jun 1, 2026
a3aa7c5
feat(framework): stampUserEdits walker at setLocalAst boundary (Plan …
gordonwoodhull Jun 1, 2026
c0bc077
feat(quarto-source-map): add By::unknown() non-atomic constructor (Pl…
gordonwoodhull Jun 1, 2026
471e4d2
feat(pampa): strict json::read + completing reader split (Plan 7f Pha…
gordonwoodhull Jun 1, 2026
81adbd7
feat(pampa,ts-packages): rename wire-format `attrS`→`a`, `sourceInfoP…
gordonwoodhull Jun 1, 2026
f342856
feat(pampa): reserved pool slot 0 = Generated{user_edit} (Plan 7f Pha…
gordonwoodhull Jun 1, 2026
025fa72
feat(quarto-source-map): test_scaffold + for_test + Phase-6.5 By cons…
gordonwoodhull Jun 1, 2026
190a305
test: swap SourceInfo::default() → for_test() in 4 crates' test scaff…
gordonwoodhull Jun 1, 2026
afc7f98
test: swap SourceInfo::default() → for_test() in pampa test code
gordonwoodhull Jun 1, 2026
bbee422
test: workspace-wide SourceInfo::default() → for_test() sweep
gordonwoodhull Jun 1, 2026
270555a
feat(quarto-pandoc-types,pampa): Phase 6.5 ConfigValue residue → By::…
gordonwoodhull Jun 1, 2026
7c7ca9e
feat(quarto-core): Phase 6.5 project_resources + navigation_href residue
gordonwoodhull Jun 1, 2026
f094124
refactor(quarto-yaml-validation): SchemaError::InvalidStructure::loca…
gordonwoodhull Jun 1, 2026
a05a5e0
refactor(quarto-pandoc-types,pampa): InlineAttr::new requires explici…
gordonwoodhull Jun 1, 2026
231feb0
docs(plan-7f): update CURRENT.md to reflect Phase 6 + 6.5 completion
gordonwoodhull Jun 1, 2026
a956fde
feat(quarto-source-map,pampa): Phase 6.5 pampa residue + By::citeproc()
gordonwoodhull Jun 1, 2026
494afb5
feat(quarto-source-map,quarto-core,quarto-navigation,quarto-analysis,…
gordonwoodhull Jun 1, 2026
88ef2ad
docs(plan-7f): mark all production residue addressed in Phase 6.5
gordonwoodhull Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
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
5 changes: 5 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.

417 changes: 417 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