Skip to content

Commit 164d33d

Browse files
cscheidclaude
andcommitted
docs(bd-bu5y34fp): context note for merging bd-ky14a (PR #235) onto main
PR #235 is 235 commits behind origin/main and CONFLICTING. The merge is not mechanical: #260's multi-engine reconciliation loop re-introduced the sequential-FileId + remap_file_ids workaround that bd-ky14a removes, the 61 snapshot conflicts require regeneration (main renamed pool keys sourceInfoPool->p / attrS->a), and #260's apply_node_edit.rs touches the primary-file-id assumption. This note records the full context and the open decisions, to iterate on before starting the merge. Strand: bd-bu5y34fp (discovered-from bd-ky14a) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent eb72633 commit 164d33d

1 file changed

Lines changed: 288 additions & 0 deletions

File tree

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# Merging bd-ky14a (PR #235, hash-based FileIds) onto current `main`
2+
3+
**Status:** context-gathering / awaiting go-ahead — do NOT start the merge yet
4+
**Braid:** bd-bu5y34fp (`discovered-from` bd-ky14a)
5+
**Implementation strand:** bd-ky14a
6+
**PR:** [#235](https://github.com/quarto-dev/q2/pull/235)
7+
**Branch:** `feature/bd-ky14a-pampa-hash-fileids` (this branch)
8+
**Author of this note:** investigation on 2026-06-15
9+
10+
## Why this document exists
11+
12+
PR #235 has been open since before #231 was superseded. #231#260
13+
(`feat(provenance): targeted paragraph/header inline editing`) is now
14+
**merged** (2026-06-10), so #235 is unblocked in the scheduling sense.
15+
But it is **not** a clean fast-forward. cscheid asked for a written
16+
context dump before any merge work begins, because the merge turned out
17+
to be genuinely complex — the work that landed in #260 re-introduced, in
18+
a new form, exactly the pattern bd-ky14a set out to remove.
19+
20+
**This note describes the situation. It is not a plan to execute yet.**
21+
22+
## What bd-ky14a does (recap)
23+
24+
pampa's `ASTContext` historically gave the primary document `FileId(0)`
25+
(sequential), while `quarto_yaml` keyed files by `hash(filename)`. The
26+
two schemes collide: the same `FileId` means different files depending
27+
on which parser produced it. bd-ky14a makes pampa adopt
28+
`quarto_yaml::file_id_for_filename(name)` so a `FileId` is globally
29+
meaningful, and **removes** the `quarto_ast_reconcile::remap_file_ids`
30+
workarounds in `include_expansion.rs` and `engine_execution.rs` plus the
31+
parallel-`SourceContext` lockstep.
32+
33+
The PR is a **single commit** (`eb726334`) on top of merge-base
34+
`65f13faf`. The user-visible effect: FileIds in the JSON wire format's
35+
`sourceInfoPool.d` go from `0,1,2…` to 64-bit hashes.
36+
37+
## Branch topology
38+
39+
```
40+
65f13faf (merge-base: "bd-ky14a: clarify TDD intent in test strategy")
41+
42+
├─ eb726334 feature/bd-ky14a-pampa-hash-fileids ← PR #235 (1 commit)
43+
44+
└─ …235 commits… origin/main (af21ccfc at time of writing)
45+
```
46+
47+
`origin/main` has moved **235 commits** past the merge-base. A trial
48+
`git merge origin/main` into the feature branch produces:
49+
50+
- **1 real source-code conflict:** `crates/quarto-core/src/stage/stages/engine_execution.rs`
51+
- **61 snapshot conflicts:** `crates/pampa/snapshots/json/*.snap`
52+
- Several files auto-merged but **need semantic review** (see §"Auto-merged but suspect").
53+
54+
`gh pr view 235` reports `mergeable: CONFLICTING`.
55+
56+
## The three things that make this complex
57+
58+
### 1. `engine_execution.rs` — architectural conflict (the real one)
59+
60+
This is the crux. When bd-ky14a was written, `EngineExecutionStage`
61+
ran a **single** engine pass over a `doc_ast` and used hash-based
62+
FileIds to register the engine's intermediate `<stem>.<engine>.rmarkdown`
63+
file directly — no remap, because the intermediate's filename hashes to
64+
its own `FileId` natively.
65+
66+
Since then, the provenance / multi-engine work (bd-5yff4, et al.) on
67+
`main` **rewrote the stage into a multi-engine loop**:
68+
69+
```rust
70+
// origin/main
71+
let mut merged_context = ast_context;
72+
73+
for (run_index, (engine, engine_config)) in to_run.into_iter().enumerate() {
74+
75+
// Register this engine's intermediate as a NEW SEQUENTIAL slot:
76+
let new_slot = quarto_source_map::FileId(merged_context.filenames.len());
77+
78+
// Remap the executed AST's FileId(0) up into this engine's slot:
79+
quarto_ast_reconcile::remap_file_ids(&mut executed_ast,
80+
&|id| quarto_source_map::FileId(id.0 + new_slot.0));
81+
let (reconciled_ast, plan) = quarto_ast_reconcile::reconcile(ast, executed_ast);
82+
ast = reconciled_ast;
83+
84+
}
85+
```
86+
87+
That is **the same `FileId(0) + remap` workaround bd-ky14a deletes**, now
88+
generalized to one sequential slot per engine and tracking a parallel
89+
`merged_context.filenames: Vec<…>` whose `.len()` is the next FileId.
90+
91+
**The merge cannot keep both sides.** The hash-based design has to be
92+
re-expressed inside the new multi-engine loop. The good news is that the
93+
hash scheme makes the loop *simpler*, not harder:
94+
95+
- Each engine's intermediate name already includes the engine
96+
(`<stem>.<engine>.rmarkdown`), so `file_id_for_filename(intermediate_name)`
97+
is distinct per engine **natively** — no `new_slot` counter, no
98+
`remap_file_ids`, no `filenames.len()` bookkeeping.
99+
- pampa on this branch reads the intermediate with `file_id: None`, which
100+
now derives `FileId(hash(intermediate_name))` automatically, so
101+
`executed_ast` already carries the right FileId before `reconcile`.
102+
- `reconcile` already tolerates mixed-provenance FileIds (its keep/replace
103+
decision is by content), so dropping the remap is safe.
104+
105+
But this is a **re-implementation of a conflicted hunk against a changed
106+
control-flow shape**, not a textual merge. It needs care and its own
107+
tests (the existing bd-ky14a contract tests assume the single-engine
108+
shape — see §"Tests").
109+
110+
**Open design question:** does anything else still depend on
111+
`merged_context.filenames` as a dense, sequential, slot-indexed `Vec`
112+
(e.g. a downstream consumer that indexes `filenames[file_id.0]`)? Under
113+
hash FileIds, `file_id.0` is a 64-bit hash, not a dense index, so any
114+
such consumer breaks. This needs a grep audit before re-implementing.
115+
116+
### 2. Semantic entanglement with #260`apply_node_edit.rs`
117+
118+
#260 added `crates/pampa/src/apply_node_edit.rs` (the targeted
119+
inline-edit path used by q2-preview / hub-client). It contains:
120+
121+
```rust
122+
// v1 assumption: single-file document → FileId(0).
123+
let Some(path) = lookup_block(&a_u, &target_si, FileId(0)) else { … };
124+
```
125+
126+
A literal `FileId(0)` as "the primary file" is exactly the invariant
127+
bd-ky14a removes. **However**, the risk is smaller than it first looks:
128+
129+
- `lookup_block(ast, target, _file_id)` **ignores** the file-id argument
130+
(it's `_file_id`). It matches on full `block.source_info() == target`
131+
equality, where `target_si` comes from the frontend-serialized
132+
`SourceInfo` (the resolved value, including its own `d`/file_id). So the
133+
`FileId(0)` literal is effectively dead — it does not constrain the match.
134+
- Correctness therefore depends on the **frontend and the AST agreeing on
135+
the FileId scheme**, not on the literal. After the merge both sides
136+
produce hash FileIds, so exact-match still holds.
137+
138+
Two things still need an explicit audit before declaring this safe:
139+
140+
- **`decode_compact_source_info`** reads `d` as
141+
`data.as_u64().unwrap_or(0) as usize`. `FileId(pub usize)` is **32-bit
142+
on `wasm32`** (the hub-client target) and 64-bit natively.
143+
`file_id_for_filename` likewise does `hasher.finish() as usize`, so the
144+
**same** truncation happens on both producer and consumer *within* wasm
145+
— consistent there. The danger is any path where a FileId computed
146+
**natively** (64-bit) is compared against one computed on **wasm**
147+
(32-bit truncation of the same hash). Confirm no such cross-target
148+
comparison exists (snapshots are native-only, so snapshot values will
149+
show the full 64-bit hash — expected).
150+
- The dead `FileId(0)` argument and its "single-file document" comment
151+
should be cleaned up (or made real) so the next reader isn't misled.
152+
153+
### 3. The 61 snapshot conflicts are NOT mechanical
154+
155+
The PR description frames the snapshot churn as a pure `"d": 0 → "d": <hash>`
156+
substitution. That **was** true against the merge-base, but `main` has
157+
since changed the JSON writer's *format*, so the feature branch's
158+
snapshots are now **doubly stale**:
159+
160+
Comparing the two conflict sides in e.g. `math-with-attr.snap`:
161+
162+
| Aspect | feature branch (HEAD) | origin/main |
163+
|---|---|---|
164+
| pool key | `"sourceInfoPool"` | `"p"` (renamed/compacted) |
165+
| attribute key | `"attrS"` | `"a"` (renamed) |
166+
| pool layout | one ordering | different dedup/ordering, different `id`/`s` indices |
167+
| `source:` header | `crates/pampa/tests/test.rs` | `crates/pampa/tests/integration/test.rs` (bd-xvdop test move) |
168+
| `d` value | `0` | `0` |
169+
170+
So you **cannot** take `main`'s snapshot and swap in the hash, and you
171+
cannot take the feature branch's snapshot at all (wrong keys, wrong
172+
layout, wrong source path). The only correct path is:
173+
174+
> Get the code merged and correct first, then **regenerate** all JSON
175+
> snapshots with `cargo insta` and review the diff to confirm the only
176+
> semantic change is `d: <small int> → d: <hash>` on top of `main`'s
177+
> current format.
178+
179+
This also means the "62 snapshots reviewed" checkbox in the PR body is
180+
**stale** and must be re-done after regeneration.
181+
182+
## Auto-merged but suspect (review before trusting)
183+
184+
These merged without conflict markers, but `main` changed them
185+
substantially, so the three-way merge may be *clean-but-wrong*:
186+
187+
- **`crates/pampa/src/readers/json.rs`**`main` rewrote this **+1041
188+
lines** (the provenance JSON reader/writer, incl. the `sourceInfoPool``p`
189+
and `attrS``a` renames). The feature branch's 24-line change touches
190+
how `d` (file_id) is read/written. Auto-merge may have silently dropped
191+
or mis-placed the feature change. **Read the merged result by hand.**
192+
- **`crates/quarto-core/src/stage/stages/include_expansion.rs`**`main`
193+
changed it (+7/−7) since base; the feature branch removes the
194+
remap/parallel-SourceContext workaround here. Confirm the removal still
195+
lands correctly on `main`'s version.
196+
- **`crates/wasm-quarto-hub-client/src/lib.rs`** — feature branch +17;
197+
verify against `main`'s current shape (this is the WASM entry surface
198+
q2-preview/hub-client consume).
199+
- **`crates/pampa/src/readers/qmd.rs`** — both sides changed (`main` +17,
200+
feature +11). Verify the `file_id` parameter plumbing survived.
201+
202+
### Files the feature branch changes that `main` did NOT touch (should apply cleanly)
203+
204+
`git diff --stat 65f13faf..origin/main` shows `main` did **not** modify
205+
these, so the feature branch's edits should graft on without conflict —
206+
but still build-check them:
207+
208+
- `crates/pampa/src/pandoc/ast_context.rs` (+127, the core of the change)
209+
- `crates/quarto-source-map/src/context.rs` (+34, new
210+
`add_file_with_id` / `add_file_with_id_and_info`)
211+
- `crates/quarto-core/src/transforms/attribution_render.rs` (+147/−…)
212+
- `crates/quarto-lsp-core/src/document.rs`, `parse_document.rs`,
213+
`pipeline.rs`, `perf-harness`
214+
215+
## Tests
216+
217+
bd-ky14a shipped 5 contract tests (TDD anchors):
218+
219+
1. `pampa::pandoc::ast_context::tests::bd_ky14a_with_filename_uses_quarto_yaml_file_id`
220+
2. `pampa::pandoc::ast_context::tests::bd_ky14a_source_context_indexed_by_hash_file_id`
221+
3. `quarto_core::bd_ky14a_file_id_contract::bd_ky14a_pampa_qmd_read_uses_quarto_yaml_file_id`
222+
4. `quarto_core::bd_ky14a_file_id_contract::bd_ky14a_sub_document_file_id_is_hash_based`
223+
5. `quarto_core::bd_ky14a_file_id_contract::bd_ky14a_fresh_source_context_renders_pampa_source_info`
224+
225+
Note that test #3#5 live in `crates/quarto-core/tests/bd_ky14a_file_id_contract.rs`
226+
as a **top-level `tests/*.rs` file**, which now violates the
227+
integration-test-layout rule (`.claude/rules/integration-tests.md`,
228+
landed via bd-xvdop while this branch was open). On merge it should move
229+
to `crates/quarto-core/tests/integration/bd_ky14a_file_id_contract.rs`
230+
and be registered in `main.rs`.
231+
232+
**A new test is needed** for the multi-engine FileId behavior (§1): two
233+
engines on one document must each get a distinct hash FileId with no
234+
collision and no remap. The existing contract tests predate the
235+
multi-engine loop and won't exercise it.
236+
237+
**A new test is wanted** for the §2 interaction: an `apply_node_edit`
238+
round-trip on a document whose primary FileId is a hash (not 0), proving
239+
the targeted-edit path still locates the destination block.
240+
241+
## Suggested merge strategy (for when the go-ahead comes)
242+
243+
1. Branch a working branch off `feature/bd-ky14a-pampa-hash-fileids`
244+
(keep the PR branch pristine until the merge is proven).
245+
2. `git merge origin/main`. Expect the conflicts above.
246+
3. Resolve `engine_execution.rs` by **re-implementing** the hash-based
247+
intermediate registration inside `main`'s multi-engine loop (drop
248+
`new_slot`/`remap_file_ids`/`filenames.len()` slot allocation). Do the
249+
`merged_context.filenames` consumer-audit grep first (§1 open question).
250+
4. For the 61 snapshot conflicts: `git checkout --theirs` is **also
251+
wrong** (wrong `d`). Instead resolve them to *anything that compiles*,
252+
then **regenerate** with `cargo insta accept` after the code is correct,
253+
and review the diff (§3).
254+
5. Hand-review the four "auto-merged but suspect" files (§"Auto-merged").
255+
6. Address the `apply_node_edit` audit (§2) and clean up the dead
256+
`FileId(0)` arg.
257+
7. Move the contract test file under `tests/integration/` (§Tests).
258+
8. Add the two new tests (multi-engine FileId; apply_node_edit hash
259+
round-trip).
260+
9. Full gate: `cargo build --workspace``cargo nextest run --workspace`
261+
`cargo xtask verify` (full, **not** `--skip-hub-build` — this touches
262+
`quarto-core`/`pampa`/`quarto-source-map` which the WASM leg depends on).
263+
10. End-to-end: `cargo run --bin q2 -- render <fixture>.qmd` and inspect
264+
the JSON pool; exercise a q2-preview targeted edit against the hash
265+
FileId (the §2 path) in a real browser session.
266+
267+
## Decisions needed from cscheid before starting
268+
269+
1. **Merge vs. rebase.** A single-commit branch could be `git rebase
270+
origin/main`'d instead of merged, giving a linear history and one
271+
clean commit to review. The conflict content is identical either way;
272+
the question is the history shape you want on the PR.
273+
2. **Scope of the `apply_node_edit` cleanup** (§2): minimal audit + comment
274+
fix, or fold the "primary file id" into a real helper now?
275+
3. **Whether to also tackle the lingering `FileId(0)` fallbacks** in
276+
`pampa/src/pandoc/location.rs`, `treesitter_utils/*`,
277+
`comrak-to-pandoc` (these are internal "no known file" sentinels, not
278+
primary-file assumptions — probably out of scope, but worth a yes/no).
279+
280+
## Appendix: reproduction
281+
282+
```bash
283+
git fetch origin
284+
git checkout -B trial origin/feature/bd-ky14a-pampa-hash-fileids
285+
git merge --no-commit --no-ff origin/main # observe conflicts
286+
git diff --name-only --diff-filter=U # 1 code + 61 snapshots
287+
git merge --abort
288+
```

0 commit comments

Comments
 (0)