# Framing — the tree primitive **Revision 5.** Status: **IMPLEMENTED and GATED; PR HELD for review.** Q#TR1–TR4 decided at approval. Both sweeps green and both behavioural claims bite-verified — see §6a. Scouted against `githubsucks/main` @ `12f2970`. Carries a correction to `COHERENCE.md` §14 (revision 4, below). **Revision 4 → 5** records the four decisions and makes acceptance final. **The decision that made the others cheap:** collapse only ever *hides* rows — it never changes a surviving row's depth. Combined with §1.1's document order (**parents before children**), a node's descendants are a **contiguous run of following rows with greater depth**. So collapse is **filtering an existing array**, not re-deriving one. The primitive therefore never calls the consumer to re-render a collapse, and pre-rendered indentation stays correct — which is why Q#TR4 resolves toward the consumer keeping `text`. | question | decision | |---|---| | **Q#TR1** | **Extend `listview`.** A separate `treeview` would either duplicate ~200 lines of panel discipline (Q#GB18 handle identity, Q#GB13 `<2>` disambiguation, the read-only intercept, `prev` capture, quit chain, generated-buffer writes) or require *extracting* them from a shipped primitive first — the riskier change. Extending is backward-compatible by construction: absent `depth`/`id` give today's behaviour exactly, which the three flat consumers already produce. | | **Q#TR2** | **Primitive-owned collapse state**, keyed by row id, held in the panel record. Consumer-owned would make every consumer reimplement refresh survival. | | **Q#TR3** | **Consumer-supplied `row.id`**, a **string or number**, compared by value; the primitive never derives one. *(Review narrowed this from "opaque": collapse state keys a table, and Lua table indexing ignores `__eq`, so an opaque id could restore selection while silently losing its fold. `check_ids` enforces it where rows enter — and with it that ids are **unique** and **not NaN**, since every lookup takes the first match (so a duplicate makes selecting the later row toggle the earlier) and `0/0` is the one scalar Lua counts as a number and refuses as a table key.)* The outline uses **`line:col`** — unique per document and stable across re-render, where the `::` parent chain collides on overloads. | | **Q#TR4** | **Consumer keeps pre-rendered `text`**; `depth` is structural only. Also sidesteps the conflict with dired's fixed-width `_layout` column contract when it adopts. | **Acceptance 5 is decided too: byte-identity coverage is written**, including the fake-LSP harness work for `*references*`. **Revision 3 → 4**: - **"No `g`" was literally false**, in revisions 2 and 3 both. `bind_local_keymap` binds `g → listview.refresh` on **every** panel unconditionally (`listview.lua:147`). What three of the four lack is an `on_refresh`. The §1.3a table now separates **`g` bound**, **refresh advertised** and **refresh functional**, because those are three different facts and I had been collapsing them into one. Consequence worth noting on its own: the outline has a **dead refresh binding** — `g` is dispatched and silently does nothing, with no status message. - **`COHERENCE.md` §14 is corrected in this branch**, not deferred. §25 requires an audited claim to be updated by the PR that changes it; **#204 changed it and missed it**, so the correction rides the framing that found it. The `ad41cf1` audit fact is retained as history, with the current count of four and `*lsp*` named as the post-audit addition. The §0 scorecard row moves with the body, since it carried the same "3 call sites". **Revision 2 → 3**, three further review findings, all verified in source: - **`*references*` has no `on_refresh`** — revision 2 claimed it had refresh, twice. The consumer with refresh is **`*lsp*`** (§1.5a). The error came from reading `listview.lua`'s **module-docstring example**, which illustrates the API using `*references*` with `g refresh` in the header. An example is not a consumer. *The refresh-scoping conclusion is unaffected — it depended on the OUTLINE lacking refresh, which holds.* - **The branch plan still said "references or buffer-list"** — the same `*buffer-list*` error acceptance 5 had already been corrected for, surviving one section further down. - **"Byte-identically, pinned by their existing suites" was unsupported.** `listview_acceptance` states in its own header that `*references*` needs a live LSP and is not exercised there, and the m4 hover test asserts content *presence*, not byte-exact output. Acceptance 5 now poses that as a decision — write the byte-identity test, or weaken the claim — with a leaning and its cost. **Found while verifying those: §14's "exactly three call sites" is stale — there are FOUR** (§1.3a), and the fourth (`*lsp*`, added by #204) is the only one with refresh. **Revision 1 → 2**, all from review, all verified against source before applying: - **Refresh was unscoped for the anchor consumer.** Two criteria rested on `g` refresh; the outline has no `g`, no `on_refresh`, and `listview.refresh` no-ops without one. Refresh is now **out of scope**, with the LSP re-request question it actually raises stated (§1.5a). - **Acceptance 1 decided Q#TR4 while calling it open** — "no `string.rep` in `lsp.lua`" commits to primitive-owned indentation. It is representation-neutral now. - **Q#TR1 misread §14.** A tree is not the "second primitive" §14 warns against; §14 **explicitly lists one**. The warning is against bespoke per-consumer plumbing. Retradeoffed without prejudging. - **The regression criterion named the wrong consumers.** `*buffer-list*` and project search do **not** use listview — §14 says so and calls the older claim an error. The real siblings are `*references*` and `*lsp-help*`. - **Consumer accounting tightened**: five named future consumers, not six; **one** existing anchor plus **one** future constraint source, not "two that exist"; and "every input a tree needs" qualified, since stable identity is exactly what is missing. `COHERENCE.md` §14 grades **Tree ✗ — none**, and it is the last missing workbench primitive. §20's Priority 5 names it as what remains after the bottom panel, and its argument is specific: *"building it once before dired's directory view and the workers tree harden their own conventions is exactly this section's point."* **This document deliberately narrows that argument.** §14 names five future consumers — project files, symbol hierarchy, package dependency graph, worker trees, git status — and designing a shared primitive against five hypothetical ones is how you get a model that fits none. The scout found a narrower and firmer basis, and the distinction between its two halves matters: - **One EXISTING anchor consumer.** The LSP outline already ships a tree and fakes it (§1.1). It is the only consumer that exists today. - **One FUTURE constraint source.** dired's `i` insert-subdirectory is scoped and deliberately deferred (§1.2). It constrains the design; it does not validate it, because nothing has been built against it. Calling these "two consumers that exist" would overstate the evidence by exactly one. --- ## 0. Coherence impact (COHERENCE §20) - **Concern: §14 Coherent Workbench Primitives.** Tree is the one remaining ✗ in its inventory. This closes it for the **one consumer that exists** (the LSP outline) and gives the rest an adoption path. - **Journey steps touched:** none directly. Step 6 (LSP) gains a real hierarchy view where it currently has indented text. - **Interaction islands (§6): NONE, unless evidence forces one.** The default is the ordinary **buffer-local keymap** idiom that listview, compile, dired and terminal already use — a tree's expand/collapse keys are buffer-local bindings on a generated buffer, not a new dispatch shadow. §6 grades islands "weak, and growing"; this stage must not add to that count. If some behaviour genuinely cannot be expressed as a buffer-local binding, that is a finding to report, not a licence to add an island. - **Config registry adoption:** none proposed. If a preference emerges (initial expansion depth, say), it enters the registry rather than becoming a hardcoded constant — but nothing yet requires one. - **Background-work attribution:** none. - **Enables:** DAP's variables view, which is inherently a tree (scopes → objects → fields) and would otherwise become the **third** bespoke implementation. --- ## 1. Ground truth (measured at `12f2970`) ### 1.1 The LSP outline is a shipped tree consumer, faking it **The hierarchy already exists and is already discarded.** `Symbol::push_hier` (`src/symbol.rs:110`) walks a genuine LSP `DocumentSymbol` tree — it recurses on `item.get("children")` — and **flattens it** into `Vec`, preserving: - `depth: u32` — "Nesting depth in a hierarchical `DocumentSymbol` tree"; - `parent` — `containerName` for flat shapes, or the parent chain joined with `::` for hierarchical ones; - document order, **parents before children** (`SymbolResponse.symbols`). `lsp.lua` then re-renders that depth as *leading spaces inside the row text*: ```lua -- Arc 1b phase 2: a browsable *outline* panel. Symbols arrive -- FLAT with a `depth` field --- indent, don't recurse. text = string.format("%s%s [%s]", string.rep(" ", sym.depth or 0), sym.name, tag), ``` So the outline has **no collapse, no expand, no parent/child navigation** — indentation is a string. **Every input a tree needs to RENDER is already computed** — depth, parent, and order — and only the view throws it away. That is not the same as every input a tree needs: **stable node identity is missing** (§1.5, Q#TR3), and it is the one input no existing field supplies. **This is the anchor consumer.** It needs no new data plumbing, and its limitation is observable today rather than hypothetical. ### 1.2 dired declined to invent one, and its case is already scoped Dired Stage 1 landed a **flat** listing for Emacs parity and deferred the recursive case explicitly: `docs/dired-framing.md` §13 names **`i` insert-subdirectory (in-buffer recursive listing)** as deferred. §14 credits this directly: dired "landed **without** inventing one". That restraint is what keeps the door open — and it means this constraint source carries real requirements rather than a wishlist: a fixed-width column contract (`pmacs.dired._layout`), a frozen test fixture, and path-keyed entries. ### 1.3 The workers view is NOT a consumer yet `editor.list-workers` is `pmacs.window.switch_buffer(pmacs.workers.show())` — a **Rust-generated text buffer, raw-switched** into the active window. It is not a listview and has no rows. §14's "worker trees" is a future consumer, not a current one. *(Incidental, and worth a separate look: that raw switch is the same `switch_buffer` pattern that broke the outline under bottom-panel Stage 3. It is harmless while `*workers*` is not a panel, and inherits the hazard the moment it becomes one.)* ### 1.3a There are FOUR listview consumers now, not three §14 measured "exactly **three** `pmacs.listview.open` call sites, all three in `builtin/runtime/lsp.lua`" at `ad41cf1`. **There are four**, and the fourth matters here: | call site | panel | `on_visit` | `on_refresh` | `g` bound? | refresh advertised? | refresh FUNCTIONAL? | |---|---|---|---|---|---|---| | `lsp.lua:2442` | `*references*` | yes | no | **yes** | no | **no** | | `lsp.lua:2488` | `*outline*` | yes | no | **yes** | no | **no** | | `lsp.lua:2924` | `*lsp-help*` | no | no | **yes** | no | **no** | | `lsp.lua:3004` | `*lsp*` (`lsp.status`) | no | **yes** | **yes** | **yes** | **yes** | **`g` is bound on ALL FOUR.** `bind_local_keymap` binds `g → listview.refresh` for every panel unconditionally (`listview.lua:147`), so "no `g`" — which revisions 2 and 3 both said — is **literally false**. What three of them lack is an `on_refresh`, and `listview.refresh` returns immediately without one. **So `g` on the outline is a DEAD BINDING**: bound, dispatched, silently does nothing. That is a small UX wart in its own right — a key that responds to nothing, with no status message — and it is a separate observation from anything this framing proposes. Recorded, not fixed here. `*lsp*` arrived with Journey Stage 1b-2 (#204), after §14's audit. It is **the only listview consumer with refresh at all**, which is why §1.5a's scoping conclusion holds: refresh is a feature exactly one panel has, and it is not the anchor. §14's line numbers have also drifted (`:2056`/`:2102`/`:2513` against today's `:2442`/`:2488`/`:2924`). The count is the part that matters; this is recorded so the next reader does not inherit "three". ### 1.4 What listview's model would have to gain listview's contract today: ```lua pmacs.listview.open { name = "*references*", header = "12 references RET visit n/p move g refresh q quit", rows = { { text = "src/foo.rs:12:4", item = }, ... }, on_visit = function(item) ... end, on_refresh = function() return rows end, } ``` and `render` is: ```lua local lines = { p.header } p.line_to_item = {} for _, row in ipairs(rows) do lines[#lines + 1] = row.text p.line_to_item[#lines - 1] = row.item end pmacs.buffer.set_generated_contents(p.buffer, table.concat(lines, "\n")) ``` **A flat array plus a line→item map.** Depth appears nowhere; it is baked into `row.text` before listview ever sees it. `item` is `` — **opaque to listview by design**, which matters for §2's identity question. ### 1.5 Refresh restores a LINE, not a node — and that is the crux ```lua local saved = pmacs.editor.cursor_line() local rows = p.on_refresh() or {} render(p, rows) ... seat_cursor(p, saved) ``` `refresh` saves a **line number**, rebuilds the rows wholesale from a freshly-produced array, and re-seats by walking `move_down` that many times. Today this is a mild wrong-restore: if the new list has a different shape, the cursor lands on whatever row now occupies that line. For a flat list of roughly stable shape, tolerable. **Collapse breaks it outright.** Expanding a node inserts rows *above* the cursor, so a line-keyed restore lands somewhere unrelated. And collapse state itself must survive refresh, which requires recognising "the same node" across two independently-produced arrays — something neither `line_to_item` nor an opaque `item` can do. **This is why Q#TR3 exists and why it is not a detail.** It decides whether selection and expansion can survive a model update at all. ### 1.5a …but the ANCHOR CONSUMER HAS NO REFRESH AT ALL Revision 1 built two acceptance criteria on `g` refresh without checking that the outline supports it. **It does not:** - its header offers `RET visit n/p move q quit` — refresh is **not advertised** (`lsp.lua:2490`); - it supplies **no `on_refresh`**; - `listview.refresh` opens `if not (p and p.on_refresh) then return end` — **a no-op** for this panel (`listview.lua:262`); - and `g` **is** bound regardless (§1.3a), so the outline has a **dead refresh binding**, not an absent one. So "collapse state survives `g`" was unreachable for the only consumer that exists. **Refresh is therefore out of scope for this stage** unless someone first answers a question this framing does not: an outline refresh means **re-requesting `textDocument/documentSymbol`**, which is an async LSP round-trip with its own await, failure and staleness handling — and it raises who owns the resulting state when the response arrives against a buffer the user may have edited or left. That is a real feature — **`*lsp*` (`lsp.status`) has `g refresh` and an `on_refresh`** (`lsp.lua:3004`); the outline never gained one — and it is **not** a tree concern. Bundling it here would make the tree lane responsible for LSP request lifecycle. *(Revision 2 attributed refresh to `*references*` twice. It has neither: its header is `RET visit n/p move q quit` and it supplies only `on_visit` (`lsp.lua:2442`). The error came from reading `listview.lua`'s **module-docstring example**, which illustrates the API using `name = "*references*"` and a header containing `g refresh` — an example, not a consumer.)* **Consequence for acceptance:** the criteria are re-scoped to what the anchor consumer can actually exercise — collapse and selection surviving **re-render**, which the primitive controls — and refresh-survival is recorded as a follow-on for whoever gives the outline a refresh. ### 1.6 What is NOT established - **Nothing is implemented or measured.** Unlike the last two lanes there is no fallout to census: §14 grades Tree ✗, so there is no existing behaviour to preserve and no baseline to diff. **This framing argues a model rather than measuring one**, which is the shape that has historically needed the most review rounds here (Lean Stage 3b took six; the signal lane had three tolerance rules rejected in a row). Treat its claims as proposals. - **No consumer has asked for collapse.** The outline's limitation is inferred from its structure and its own "indent, don't recurse" comment, not from a user report. - **dired's `i` has not been re-scouted** against current `main`; §13's deferral is the only evidence that its requirements are as described. --- ## 2. Questions All four are genuinely open. The first two the review already flagged as open; the third is the one review added; the fourth follows from §1.4. - **Q#TR1 — extend `listview`, or add a separate `treeview`?** **Revision 1 framed this wrongly and the correction changes the tradeoff.** It claimed a separate treeview would be "exactly the second primitive §14 warns about". §14 does not warn against a tree — **it explicitly lists one**, alongside virtual list, in the reusable set it wants: *"editable text view, virtual list, **tree**, structured table, inspector…"* (`COHERENCE.md:1264`). A tree surface is a named goal, not a violation. What §14 actually warns against is **bespoke per-consumer plumbing** — each subsystem inventing its own UI vocabulary. A `treeview` that shares the existing buffer/panel disciplines (generated-buffer writes, Q#GB18 handle identity, panel placement, `q` quit-action) is not that; a tree hand-rolled inside `lsp.lua` would be. So the real tradeoff is narrower: - **Extending listview** touches three shipped call sites and the `line_to_item` contract, and risks making a working flat primitive worse for the consumers that do not need depth. - **A separate treeview** keeps the flat primitive untouched, but must *share* rather than *duplicate* the panel disciplines — and "shares them" is an implementation claim that has to be verified, not asserted. **No leaning recorded**; the scout found nothing that decides it. - **Q#TR2 — who owns collapse state?** Candidates: the primitive (keyed by node id), the consumer (passed in with the rows each render), or the buffer (as generated-buffer state). Consumer-owned keeps the primitive stateless and makes refresh the consumer's problem; primitive-owned centralises it and forces Q#TR3 to be answered first. - **Q#TR3 — what is a stable node identity across refresh?** *(Added at review.)* This determines whether **selection and expansion survive a model update** (§1.5). Constraints the scout established: - listview cannot derive one: `item` is `` and opaque. - The outline's data is nearly sufficient — the `::`-joined parent chain plus name — but **not unique**: overloads and same-named siblings collide. - dired's would be genuinely stable: the path. - So identity is almost certainly **consumer-supplied**, which makes it part of the primitive's public contract rather than an internal detail. That is a real API commitment and should be decided deliberately, not defaulted into. - **Q#TR4 — does the row still carry pre-rendered `text`?** Today the consumer formats indentation into the string. If the primitive owns depth it should probably own indentation too — but the outline also appends `[kind]` tags and dired has a fixed-width column contract, so "the primitive renders the row" may not survive contact with either. --- ## 3. Bets - **Bet 1 — the outline is a sufficient first consumer on its own.** Its data already carries depth and parent; adopting it requires no LSP-side change. *Falsified if adoption needs `Symbol` to change shape.* - **Bet 2 — identity must be consumer-supplied** (§1.5, Q#TR3). *Falsified if some derivable key proves both stable and unique across the two consumers.* - **Bet 3 — no interaction island is required.** Expand/collapse are buffer-local bindings on a generated buffer, exactly as `RET`/`n`/`p`/ `g`/`q` already are. *Falsified if some behaviour cannot be expressed that way — which would be a finding worth reporting, not a licence.* --- ## 4. Acceptance **Not final** — this framing argues a model, and the criteria cannot be fixed until Q#TR1–TR3 are decided. The shapes they will take: 1. The **LSP outline supplies structural `depth` and `id`** so the primitive can collapse and expand, and `Symbol` is unchanged. **Per Q#TR4 its `text` stays consumer-rendered** — the `string.rep` indentation remains in `lsp.lua`, because collapse hides rows without changing any surviving row's depth, so pre-rendered indentation is still correct. `id` is `line:col`. 2. **Collapse and expand work**, and **collapse state survives a re-render** — the primitive re-emitting the buffer from the same model. *(Not "survives `g` refresh": the anchor consumer has no refresh at all — §1.5a.)* 3. **Selection survives a re-render by node, not by line** (§1.5). 4. **No new interaction island**: every tree key is a buffer-local binding, and the dispatch-shadow count is unchanged. Asserted, not assumed. 5. **listview's OTHER consumers are unaffected** — `*references*`, `*lsp-help*` and `*lsp*` (§1.3a: four call sites, all in `lsp.lua`). **What existing suites actually pin, stated honestly.** `listview_acceptance` drives the substrate hermetically and says so in its own header: *"The references panel itself needs a live LSP and is validated manually / via the m4 harness"* — so **it does not exercise `*references*` at all**. The m4 hover test asserts content *presence*, not byte-exact output. **"Byte-identical, pinned by existing suites" was therefore unsupported** for both panels named. So this criterion needs a decision, not a wording tweak: - **either** byte-identity becomes a **new test requirement** this stage writes — capturing each panel's rendered buffer before and after and diffing it, which needs the m4 fake-LSP harness for `*references*`; - **or** the claim weakens to what is genuinely pinned today: the substrate behaviours `listview_acceptance` covers (open, navigate, visit, `q` restore, the read-only intercept, the round-trip gate, refresh) plus content-presence for hover. **DECIDED: write the byte-identity test**, including the fake-LSP harness work for `*references*`. A flat consumer silently gaining an indent column is exactly the regression this criterion exists to catch, and content-presence would not see it. **Revision 1 named `*buffer-list*` and project search here and was wrong** — §14 measured that they do **not** use listview and calls the earlier claim a documentation error (`COHERENCE.md:1286`). Repeating it would have re-introduced a mistake that document exists to correct. If the broader surfaces are ever in scope, their independent render paths and tests must be named explicitly. 6. **Q#GB18 identity holds**: a foreign buffer with the panel's name is never adopted, and the primitive is keyed by handle, not name. 7. The **generated-buffer write invariant** is preserved (`set_generated_contents`), including the rope lock and history rules the listview suite already pins. --- ## 5. Parked - **The other four §14 consumers** — project files, package dependency graph, worker trees, git status. They adopt later; designing for them now is the failure this framing avoids. - **DAP's variables view.** The reason the primitive is worth building before the debugger, and not part of it. - **dired's `i` insert-subdirectory.** The second consumer, and the right forcing function for the design — but it is its own stage with its own framing, and dired Stage 2b/3 are ahead of it. - **Making `*workers*` a listview/tree consumer** (§1.3), and the raw-switch hazard noted there. - **Tree rendering in the GPU frontend** beyond whatever the shared generated-buffer path already gives. - **Giving the outline a refresh** (§1.5a). **`*lsp*` has `g` and an `on_refresh`**; the outline never gained one. Adding it means re-requesting `textDocument/documentSymbol` with its own await, failure and staleness handling, and deciding who owns the result when it arrives against a buffer the user may have edited. **That is LSP request-lifecycle work, not tree work** — and only once it exists can "collapse survives refresh" be a criterion rather than an aspiration. --- ## 6. Gates The standing `CLAUDE.md` suite. The suites most likely to move are `listview_acceptance` and `m4_acceptance` (the LSP outline and hover panels are listview consumers — bottom-panel Stage 3 established that transitive relationship the hard way). **Sweep both feature configurations.** Stage 3 shipped a broken crdt-gated suite because every local sweep ran `--features luajit` without `crdt`; and `--no-fail-fast` is required, or a multi-suite break reports as one suite. --- ## 6a. Verification record, including one unclassified occurrence **The luajit sweep is 3453 / 0** and the count reconciles exactly: `main` is 3450 (Stage 3's 3449 sweep predated its own capability-fallback pin) plus this lane's three listview tests and one m4 test. **The crdt sweep is 3722 / 0**, likewise +4 on `main`'s 3718. ### An UNCLASSIFIED, UNCAPTURED local occurrence The **first** local crdt sweep of this branch reported **7 failures**. **It is recorded here as unclassified and it is deliberately NOT a row in `docs/ci-red-signatures.md`** — that registry keys on a normalized signature, and this occurrence has none to match, so a row would confer recognisability it cannot support. **The signatures were destroyed before they were read.** The sweep was piped through an aggregation that emitted only totals. That is the exact failure the registry exists to prevent, committed one lane after writing it — and it is why the cause cannot now be established rather than merely being unknown. **Re-runs, with what each does and does not support:** | run | isolated? | result | |---|---|---| | first | no — concurrent with another lane's build | **7 failed, signatures lost** | | second | no | 3722 / 0 | | A | **no** — the isolation guard printed "aborting" and did not abort | 3722 / 0 | | B | **yes** — verified idle | 3722 / 0 | | C | **yes** — verified idle | 3722 / 0 | Two genuinely isolated runs, both clean. **That supports repeatability under isolation. It does not establish what caused the original.** ### Two NON-CAUSAL hypotheses, neither testable now Both are mechanisms known to have been present. Neither is offered as an explanation, because the occurrence's signatures no longer exist to test either against: 1. **Shared `CARGO_TARGET_DIR`.** Another lane's worktree shared `/home/jeans/build/cargo-target`, so its `cargo test --workspace` overwrote `target/debug/pmacs` mid-sweep. That lane observed the reciprocal case independently, caught the concurrent build with `pgrep`, and its failing text named its own cause ("start the daemon built with the `crdt` feature"). 2. **Resident leaked daemons.** ~40 orphaned `pmacs --daemon` processes were present, some four days old (see the lane in `docs/active-work.md`). Isolated sweeps leak 3–4 each, so the population was growing throughout. **Having two plausible mechanisms and no way to discriminate is the result.** Reporting either as *the* cause would be the reasoning this project has rejected repeatedly: concluding something about an occurrence from something that was not about that occurrence. ## 7. Branch plan Q#TR1 is decided, so the listview-extension shape applies: 1. **Extend `listview`** — optional `depth` / `id` on rows, primitive-owned collapse state, ancestor-collapsed filtering in `render`, selection re-seated **by id**, and a toggle binding. Absent `depth`/`id` must behave exactly as today. 2. **Byte-identity proof for the flat consumers** (acceptance 5), landing *before* the outline adopts, so any regression in `*references*`, `*lsp-help*` or `*lsp*` is attributable to the primitive change rather than to adoption. 3. **The outline adopts** — supplies `depth` and `id = line:col`, keeps its rendered `text`. 4. **Lane, handoff and `COHERENCE.md` §14** updated; the §14 correction from revision 4 rides this PR per §25. dired's `i` is **not** attempted here (§5).