Merge pull request #217 from levineuwirth/tree-primitive-framing

feat(listview): the tree primitive — optional depth/id, primitive-owned folding
This commit is contained in:
Levi Neuwirth 2026-08-05 22:15:20 +00:00 committed by GitHub
commit db1bbe94a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1486 additions and 47 deletions

View File

@ -107,7 +107,7 @@ remain open to them.
| 11 | Config layering + provenance | **Partial (foundation only)** | Typed registry is right; 5 settings live in it; no value provenance |
| 12 | Profiles | **Missing** | One hardcoded default keymap; not a named concept |
| 13 | Package lifecycle UX | **Resolution without lifecycle** | Mature resolver/lockfile; init-only install; no uninstall/disable/search |
| 14 | Workbench primitives | **Partial (best trajectory)** | Listview is a real primitive but only 3 call sites, all LSP panels; buffer-list and search re-implement it; **the bottom panel is COMPLETE — both frontends, and Stage 3 flipped the adopter default so omission means the panel**. **Tree is still ✗ and is now the arc's successor** |
| 14 | Workbench primitives | **Partial (best trajectory)** | Listview is a real primitive but only **4** call sites, all LSP panels (`*lsp*` added post-audit by #204); buffer-list and search re-implement it; **the bottom panel is COMPLETE — both frontends, and Stage 3 flipped the adopter default so omission means the panel**. **Tree is implemented (◐) with the LSP outline as its one adopter; the remaining consumers, including dired's `i`, have not adopted** |
| 15 | Contextual affordances | **Weak** | Right-click menu only; code actions apply first-blindly; no git integration at all |
| 16 | Semantic frontend | **Strong** | v6..=v21 schema support; production attach remains v20 during the dark panel slice; degradation practiced |
| 17 | Distribution | **Partial** | **v1.1.0 ships prebuilt Linux/macOS binaries on tag** (#211) with checksums and a stated glibc floor. No channels, in-place update, rollback, signing, or package-manager distribution |
@ -1282,10 +1282,23 @@ Primitive-by-primitive against the list above:
buffer-local keymap idiom (RET/SPC visit, n/p, g refresh, q quit)
that is inspectable and rebindable (§6's counter-example). **But its
adoption is narrower than this document claimed, and the correction
matters more than the grade.** Measured at `ad41cf1`: there are
matters more than the grade.** Measured at `ad41cf1`: there were
exactly **three** `pmacs.listview.open` call sites, **all three in
`builtin/runtime/lsp.lua`** — `*references*` (`:2056`), `*outline*`
(`:2102`) and `*lsp-help*` (`:2513`). The three other `listview`
(`:2102`) and `*lsp-help*` (`:2513`).
**Updated: there are now FOUR.** Journey Stage 1b-2 (**#204**) added
`*lsp*` via `lsp.status` — the audited claim above changed and that PR
did not update it, so this correction rides the tree-primitive framing
that found it (§25). All four remain in `lsp.lua`; per §25 the
symbols are authoritative and the `ad41cf1` line numbers have drifted.
**`*lsp*` is the only one of the four with a working refresh** — it is
the only one supplying `on_refresh`. `g` is bound on all four
unconditionally by `bind_local_keymap`, so the other three carry a
**dead refresh binding**: bound, dispatched, silently does nothing.
The three other `listview`
mentions under `builtin/` are comments in `compile.lua` and
`dired.lua` citing "the listview idiom", which is a *pattern being
copied*, not the primitive being used.
@ -1354,15 +1367,27 @@ Primitive-by-primitive against the list above:
(§9).
- **Help view** △ — exists twice (§5); needs unification, not
invention.
- **Tree** ✗ — none. The named future consumers (project files, symbol
hierarchy, package dependency graph, worker trees, git status) will
each need it; building it once *before* dired's directory view and
the workers tree harden their own conventions is exactly this
section's point. Dired Stage 1 (merged #165) landed **without** inventing
one: its listing is flat (Emacs parity), and the recursive
in-buffer case — `i` insert-subdirectory — is a named deferral in
`docs/dired-framing.md` §13, which is where a shared tree primitive
would land.
- **Tree** ◐ — **implemented, one consumer.** `listview` carries
optional `depth` and `id` on rows, primitive-owned collapse state, and
selection re-seated by id rather than by line; `TAB` toggles. Absent
`depth`/`id`, a row behaves exactly as before, which is what leaves
the flat consumers untouched (pinned by byte-identity coverage).
**The LSP outline is the only adopter**: it previously flattened a
genuine `DocumentSymbol` tree into indented strings, and now supplies
structure while keeping its rendered text.
The organising fact, worth carrying: **folding is local projection
state, not a refresh protocol.** Collapse only hides rows and never
changes a surviving row's depth, so the primitive re-renders from its
own array without calling the consumer — which is why the outline
works at all, having no `on_refresh`.
**Still one consumer, hence ◐ not ✓.** The named future consumers
(project files, package dependency graph, worker trees, git status)
have not adopted, and dired's recursive `i` insert-subdirectory — the
second real constraint source — remains the deferral in
`docs/dired-framing.md` §13. Dired Stage 1 (merged #165) landed
**without** inventing its own, which is what kept this possible.
- **Structured table / inspector / diff view** ✗ — none. (`describe.*`
tables are the inspector's data model without a view; the
wire-declared `ResourceOffer` family was reserved for diff/blame
@ -1669,9 +1694,19 @@ and §18's floor ride on this.
**State: the bottom panel is DONE (§14) — both frontends, Stage 1 #155
through Stage 2B-3, and Stage 3 flipped the adopter default so omitting
`display` means the panel. Arc 7 is complete.** Remaining elsewhere: the tree primitive (build it before dired
and the worker tree invent two), table/inspector/diff, help unification.
Wiring plus one modest model piece (the tree model).
`display` means the panel. Arc 7 is complete.**
**The tree primitive is IMPLEMENTED too (§14, ◐)** — `listview` carries
optional `depth`/`id`, primitive-owned collapse, and selection re-seated
by id. It was built before dired's recursive view and the worker tree
could invent their own, which was this priority's stated reason for
doing it early.
**What remains is ADOPTION, not construction.** The LSP outline is the
only consumer; dired's `i` insert-subdirectory is the next real
constraint source, and DAP's variables view is why the primitive was
worth building first. Also remaining: table / inspector / diff, and help
unification.
### Priority 6: Productize configuration

View File

@ -114,21 +114,148 @@ end
-- `M-x buffer.undo` did too, and no rebinding can remove that. The
-- primitive lifts the rope lock, writes, discards the history and
-- re-asserts the lock, all inside one registry borrow.
-- Tree support (docs/tree-primitive-framing.md, Q#TR1-TR4).
--
-- A row MAY carry `depth` (0-based, structural) and `id` (a STRING or
-- NUMBER, consumer-supplied, compared by value). Both optional: a row
-- without them behaves exactly as before, which is what keeps the
-- three flat consumers byte-identical.
--
-- `text` stays CONSUMER-RENDERED (Q#TR4). The primitive owns structure,
-- not presentation -- collapse only ever HIDES rows and never changes a
-- surviving row's depth, so pre-rendered indentation remains correct
-- and the primitive never has to re-format anything.
--
-- Descendants are a CONTIGUOUS RUN of following rows with greater
-- depth. That holds because consumers emit parents before children in
-- document order (the LSP outline's `Symbol` ordering guarantees it);
-- a consumer that emits depth out of order gets nonsense, which is why
-- `has_children` reads only the NEXT row rather than scanning.
local function has_children(rows, i)
local d = rows[i].depth
if not d then return false end
local nxt = rows[i + 1]
return nxt ~= nil and (nxt.depth or 0) > d
end
-- Is `rows[i]` hidden because some ANCESTOR is collapsed?
--
-- Walks backwards to shallower rows, which is the ancestor chain under
-- the contiguous-run invariant above. Stops at depth 0: a root has no
-- ancestor to hide it.
local function hidden_by_ancestor(p, rows, i)
local d = rows[i].depth
if not d or d == 0 then return false end
local want = d - 1
for j = i - 1, 1, -1 do
local dj = rows[j].depth or 0
if dj <= want then
if rows[j].id ~= nil and p.collapsed[rows[j].id] then return true end
want = dj - 1
if want < 0 then return false end
end
end
return false
end
-- Ids must be usable, unique table keys, and none of the three checks
-- below is fussiness about types.
--
-- SCALAR. Selection compares ids with `==`, which honours `__eq`;
-- collapse state stores them as TABLE KEYS, and Lua indexes tables by
-- raw identity, consulting no metamethod. A table id would therefore
-- satisfy one and quietly fail the other: after a refresh minted fresh
-- id tables, selection would be restored and the fold would be lost.
--
-- Equality-aware collapse lookup is the alternative, and it is worse
-- here: `hidden_by_ancestor` runs per row and would turn a linear
-- render quadratic to support a key type no consumer has wanted.
--
-- NOT NaN. `0/0` passes a `type(x) == "number"` test and then *errors*
-- at `p.collapsed[row.id]` with "table index is NaN" — the one scalar
-- Lua accepts as a number and refuses as a key. Caught here so the
-- report names the row, rather than surfacing on whichever later TAB
-- happens to reach it.
--
-- UNIQUE. Every lookup here resolves an id to the FIRST row bearing
-- it, so duplicates do not merely collide: selecting the second such
-- row toggles the first and re-seats the cursor onto it. An id that
-- does not identify a node is not an id, and the contract's word for
-- itself is identity.
--
-- All three are enforced where rows enter, so a bad id is a named
-- error at the call site instead of a lost fold or a stray jump later.
local function check_ids(rows)
local seen = {}
for i, row in ipairs(rows) do
local id, k = row.id, type(row.id)
if id ~= nil then
if k ~= "string" and k ~= "number" then
error(string.format(
"listview: row %d has a %s id; ids must be a string or number "
.. "(collapse state keys a table by identity, so a %s id would "
.. "lose its fold across a refresh)", i, k, k))
end
if id ~= id then
error(string.format(
"listview: row %d has a NaN id; NaN is a number but not a "
.. "usable table key, and collapse state would raise "
.. "\"table index is NaN\" on the first fold", i))
end
if seen[id] then
error(string.format(
"listview: rows %d and %d share the id %q; ids must be unique "
.. "(every lookup resolves to the first match, so selecting "
.. "the later row would toggle and re-seat the earlier one)",
seen[id], i, tostring(id)))
end
seen[id] = i
end
end
return rows
end
local function render(p, rows)
local lines = { p.header }
p.visible = 0
p.line_to_item = {}
for _, row in ipairs(rows) do
lines[#lines + 1] = row.text
p.line_to_item[#lines - 1] = row.item
p.line_to_row = {}
for i, row in ipairs(rows) do
if not hidden_by_ancestor(p, rows, i) then
lines[#lines + 1] = row.text
-- SPARSE BY CONSTRUCTION: `item` is optional, and a display-only
-- row (a grouping header in a tree, say) supplies none, so this
-- key is simply absent for it. Nothing may take `#` of this
-- table; `visible` below is the row count.
p.line_to_item[#lines - 1] = row.item
p.line_to_row[#lines - 1] = row
p.visible = #lines - 1
end
end
pmacs.buffer.set_generated_contents(p.buffer, table.concat(lines, "\n"))
end
-- The data line currently showing `id`, or nil. Selection is re-seated
-- BY ID rather than by line (Q#TR3): a collapse or expand inserts or
-- removes rows above the cursor, so a line-keyed restore lands on an
-- unrelated node.
local function line_of_id(p, id)
if id == nil then return nil end
for line, row in pairs(p.line_to_row) do
if row.id ~= nil and row.id == id then return line end
end
return nil
end
-- Re-seat the cursor on data line `line` (1-based, clamped).
-- `switch_active_buffer` zeroes the window cursor, so a fresh switch
-- puts us on the header; walk down from there.
local function seat_cursor(p, line)
local count = #p.line_to_item
-- `p.visible`, NOT `#p.line_to_item`: that map is sparse whenever a
-- row omits the optional `item`, and `#` on a sparse table is not
-- the row count. Reading it there left a tree of display-only rows
-- with the cursor stranded on the header, where TAB finds no node.
local count = p.visible or 0
if count == 0 then return end
local target = math.max(1, math.min(line or 1, count))
for _ = 1, target do
@ -146,6 +273,7 @@ local function bind_local_keymap(buf)
bind("<down>", "cursor.down")
bind("p", "cursor.up")
bind("<up>", "cursor.up")
bind("TAB", "listview.toggle")
bind("g", "listview.refresh")
bind("q", "listview.quit")
end
@ -186,7 +314,8 @@ local function ensure_panel(name)
end
local buf = pmacs.buffer.create(actual)
p = { requested_name = name, buffer = buf, line_to_item = {} }
p = { requested_name = name, buffer = buf, line_to_item = {},
line_to_row = {}, collapsed = {}, rows = {}, visible = 0 }
panels[#panels + 1] = p
-- Read-only (Q#P3): every non-bypass edit is rejected, with a NAMED
-- error. Kept beside the rope lock, not replaced by it: the layering
@ -219,7 +348,12 @@ function pmacs.listview.open(spec)
if active and not panel_for_buffer(active) then
p.prev = active
end
render(p, spec.rows or {})
-- Keep the row array: collapse re-renders from it WITHOUT calling the
-- consumer, which is what lets a panel with no `on_refresh` still
-- expand and collapse (the outline has none -- framing §1.5a).
p.rows = check_ids(spec.rows or {})
p.collapsed = {}
render(p, p.rows)
-- Bottom-panel arc (Q#BP11b): the placement opt-in. `seat_cursor` and
-- `listview.refresh` are active-window-only, so an interactive panel
-- MUST take `select = true` or it would silently seat the wrong
@ -261,7 +395,12 @@ pmacs.command.define {
local p = active_panel()
if not (p and p.on_refresh) then return end
local saved = pmacs.editor.cursor_line()
local rows = p.on_refresh() or {}
-- Q#TR3: remember the NODE, not the line. A refresh that changes
-- the row set moves every line; the id survives it.
local saved_row = p.line_to_row[saved]
local saved_id = saved_row and saved_row.id
local rows = check_ids(p.on_refresh() or {})
p.rows = rows
render(p, rows)
-- `set_generated_contents` has already refreshed this window's
-- TextView. Re-seat through the editor primitives instead of
@ -270,7 +409,63 @@ pmacs.command.define {
pmacs.editor.clear_selection()
pmacs.editor.set_view_top(0)
pmacs.editor.move_to_line(0)
seat_cursor(p, saved)
seat_cursor(p, line_of_id(p, saved_id) or saved)
end,
}
-- TAB toggles the node under the cursor. A leaf is a no-op with a
-- status, never a silent nothing -- the outline's `g` is already a
-- dead binding that responds to nothing (framing §1.3a) and this
-- primitive should not add a second one.
pmacs.command.define {
name = "listview.toggle",
description = "Collapse or expand the tree node under the cursor.",
fn = function()
local p = active_panel()
if not p then return end
-- A FLAT panel must keep its pre-tree TAB behaviour exactly.
--
-- `bind_local_keymap` binds TAB for every listview, so this command
-- now intercepts a key that previously fell through to the global
-- `buffer.tab` and was refused by the Q#P3 read-only intercept.
-- Emitting a listview status instead would be a behaviour change
-- for the three flat consumers -- invisible to a byte-identity test,
-- which sees the buffer and not the status line or the dispatch
-- path. So a panel with no tree rows at all delegates.
local is_tree = false
for _, r in ipairs(p.rows) do
if r.id ~= nil then is_tree = true break end
end
if not is_tree then
pmacs.command.invoke("buffer.tab")
return
end
local line = pmacs.editor.cursor_line()
local row = p.line_to_row[line]
if not (row and row.id ~= nil) then
pmacs.editor.set_status("listview: no node here")
return
end
-- `has_children` reads the FULL row array, not the rendered subset:
-- a collapsed node's children are absent from `line_to_row` by
-- construction, so asking the rendered view whether it has any
-- would answer "no" for every collapsed node and make expanding
-- impossible.
local idx
for i, r in ipairs(p.rows) do
if r.id ~= nil and r.id == row.id then idx = i break end
end
if not (idx and has_children(p.rows, idx)) then
pmacs.editor.set_status("listview: no children")
return
end
p.collapsed[row.id] = not p.collapsed[row.id] or nil
render(p, p.rows)
pmacs.editor.clear_selection()
pmacs.editor.set_view_top(0)
pmacs.editor.move_to_line(0)
seat_cursor(p, line_of_id(p, row.id) or line)
end,
}

View File

@ -2480,15 +2480,28 @@ function pmacs.lsp.document_symbols()
for _, sym in ipairs(syms) do
local tag = SYMBOL_KIND_TAGS[sym.kind] or "symbol"
rows[#rows + 1] = {
-- Tree primitive (docs/tree-primitive-framing.md): `depth` is
-- STRUCTURAL and `text` stays rendered here (Q#TR4). Collapse
-- only hides rows and never changes a surviving row's depth, so
-- the indentation below remains correct without the primitive
-- re-formatting anything.
--
-- `id` is line:col (Q#TR3) — unique per document and stable
-- across a re-render. The `::` parent chain was rejected: it
-- collides on overloads and same-named siblings, which is
-- exactly where a stale expansion would reattach to the wrong
-- node.
text = string.format(
"%s%s [%s]", string.rep(" ", sym.depth or 0), sym.name, tag),
item = sym,
depth = sym.depth or 0,
id = string.format("%d:%d", sym.line, sym.col),
}
end
pmacs.listview.open {
name = "*outline*",
header = string.format(
"%d symbol%s RET visit n/p move q quit",
"%d symbol%s RET visit TAB fold n/p move q quit",
#syms, (#syms == 1 and "" or "s")),
rows = rows,
on_visit = function(sym)

View File

@ -19,10 +19,21 @@ until review caught it — the #171 defect recurring. Its block is below.
**Updated later the same day.** #215 **merged** (`main` @ `12f2970`) and
that lane is **rewritten, not removed** — rule 4 removes a lane when its
ARC is done, and Stage 2 is the arc. Stage 2 (**hardening**) is now in
flight on `ci-signal-hardening`, with its own lane block, its own
checkpoint table, and a lane written **before** the PR was opened rather
than after review asked for it.
ARC is done, and Stage 2 is the arc. Stage 2 (**hardening**) ran on
`ci-signal-hardening`, with its own lane block, its own checkpoint
table, and a lane written **before** the PR was opened rather than
after review asked for it. **It merged as #216 on 2026-08-05**, which
completes the arc: R2 and R4 are retired with discriminating
witnesses.
**The lane is kept, not yet removed, and that is a deferral rather than
a judgement.** Rule 4 would remove it now — but its residue must be
re-homed first, or removing it loses the state: **R1** is referred to
the async-runtime lane (Q#MCI3) and **R3** is an unresolved possible
product defect owned by the process-signal / PTY lane, neither of which
has a block here yet. Retiring this arc and opening those two is an
**absorption pass**, and doing it inside an unrelated feature PR is how
a ledger acquires edits nobody reviewed.
**Updated 2026-08-04.** Four PRs landed since: the CI CRDT coverage
lane #209, Distribution Stage 1 #211 (released as **v1.1.0**), the
@ -169,7 +180,128 @@ form. All four steps ran clean. **The two-argument form still does not
work** for a remote-only branch (`fatal: invalid reference`), which is
why every lane below spells out the `-b` form.
## macOS CI signal integrity — STAGE 1 MERGED (#215); STAGE 2 IN REVIEW
## Tree primitive (P5) — PR #217 OPEN, awaiting review
**PR #217** — https://github.com/levineuwirth/pmacs/pull/217. It was
held through four framing rounds plus a review round; the hold is over
and the work is with the user. Full gate suite green at the tip,
including `PMACS_REQUIRE_GPU=1 -p pmacs-gpu` 221/0.
- **Branch `tree-primitive-framing`**, base `githubsucks/main` @
`12f2970`. **Pushed and in sync** — held means no PR is open, not
that the work is stranded locally, so it is portable per the
handoff's rule. `githubsucks/tree-primitive-framing` is the
authoritative tip; `d92f0ad` was it as of the line above, and any
edit to this lane advances past it, which is why the ref and not a
pinned SHA is the thing to trust. Recover with `git fetch githubsucks
&& git checkout tree-primitive-framing`. Framing
`docs/tree-primitive-framing.md` **revision 5** — approved after four
review rounds, with Q#TR1TR4 decided.
- **The substantive arc** (not an exhaustive log — read `git log
12f2970..githubsucks/tree-primitive-framing` for that): `61b1062`
framing through `5186bfd` rev 4, which carried the `COHERENCE.md` §14
call-site correction; `49a42ec` the primitive; `8f64c3b`
byte-identity coverage plus the verification record; `38e94dc` this
lane, §14's ✗ → ◐ and the handoff facts; `c59de95` the review round —
moving-selection witness, flat-panel TAB delegation, §20's build →
adopt correction.
### What it ships
`listview` gains **optional** `depth` and `id` on rows; absent, a row
behaves exactly as before, which is what leaves the flat consumers
untouched. Collapse state is **primitive-owned**, keyed by
consumer-supplied id. Selection is re-seated **by id, not by line**.
`TAB` toggles; a leaf reports rather than silently doing nothing.
**The observation that made it cheap:** collapse only ever *hides* rows
and never changes a surviving row's depth, and consumers emit parents
before children, so descendants are a **contiguous run**. Folding is
therefore **local projection state, not a refresh protocol** — the
primitive re-renders from its own array without calling the consumer,
which is why the anchor consumer works at all: **the outline has no
`on_refresh`**.
The LSP outline adopts, supplying `depth` and `id = line:col`; its
`text` stays consumer-rendered per Q#TR4.
### Verification
| gate | result |
|---|---|
| luajit sweep | **3453 / 0** (= `main` 3450 + 3 listview tests) |
| crdt sweep, isolated ×2 | **3722 / 0** (= `main` 3718 + 4 tests) |
| `listview_acceptance` | 22 / 22 |
| `m4_acceptance` | 150 / 150 (basedpyright skipped) |
| `--lib` | 1896 / 0 |
| fmt · diff-check · clippy ±crdt | pass |
Both behavioural claims are **bite-verified**: disabling the ancestor
filter fails the fold test; an unconditional gutter fails the
byte-identity test.
**One unclassified occurrence** — the first crdt sweep reported 7
failures whose signatures were destroyed before being read. It is
recorded in the framing's §6a, **not** as a row in
`docs/ci-red-signatures.md`, because it has no normalized signature to
match. Two non-causal hypotheses are recorded there; neither is testable
now.
### Recovery, once pushed
```sh
git fetch githubsucks
git worktree add ../pmacs-tree \
-b tree-primitive-framing \
githubsucks/tree-primitive-framing
```
### Not in scope
dired's `i` insert-subdirectory (the second consumer, its own stage);
the other four §14 consumers; DAP's variables view; giving the outline a
refresh, which is LSP request-lifecycle work.
## Leaked daemons from `gpu_invocation_acceptance` — NEEDS A LANE
**Found 2026-08-05 while cleaning up after the tree-primitive work. No
branch, no framing.**
- **42 orphaned `pmacs --daemon` processes** were resident on the
development machine, **the oldest 3 days 23 hours old**. All had been
**reparented to systemd** (`ppid=1`) and all had **deleted sockets**,
so nothing could ever reach or reap them.
- **Source: `tests/gpu_invocation_acceptance.rs`** — the one-command
tests, whose daemons carry `--socket <tempdir>/one-command.sock`. The
tempdir is cleaned up; the daemon is not.
- **Rate measured, not estimated: 3 per sweep.** A single isolated
`--features luajit,crdt` sweep leaked exactly three. 42 is what
several days of sweeps accumulate to.
- **This predates the tree work** — the oldest is four days old — so it
is a standing leak, not something a current lane introduced.
**Why it belongs to the reap-ledger family.** This is precisely the
shape that lane exists for: a process that outlives its supervisor with
nothing left watching it. The ledger arms only for `spec.group`, and
these are daemons spawned by a test harness rather than by compile mode,
so **nothing in the existing ledger covers them**.
**Why it matters beyond tidiness.** Dozens of resident daemons were
present during every local sweep run this week, including the one that
produced the unclassified failure recorded in the **tree-primitive lane
above** (and, in full, in that lane's framing §6a). That
makes them a **rival explanation** to the shared-target-dir mechanism
for that occurrence, and neither can be tested against it now — the
signatures were not captured. A leak that quietly changes the
environment of every subsequent test run is a measurement problem as
well as a resource one.
**First questions for whoever takes it:** does the test harness fail to
reap, or does the daemon fail to exit when its socket disappears? Those
have different fixes, and the second would be a product defect rather
than a test one.
## macOS CI signal integrity — STAGES 1 AND 2 MERGED (#215, #216)
**This file requires a lane for every open PR** (see the #171/#174 note
above: an open PR is exactly the volatile work this file records, and
@ -180,16 +312,18 @@ which is where the same defect stops recurring.
Framing `docs/macos-ci-signal-integrity-framing.md` **revision 3**,
already approved. Stage 1 is acceptance 15; Stage 2 is acceptance 69.
### Stage 2 — hardening, IN REVIEW
### Stage 2 — hardening, MERGED as #216
- **Branch `ci-signal-hardening`**, worktree `../pmacs-ci-signals`, base
`githubsucks/main` @ `12f2970` (the #215 merge). Opened from an
**isolated worktree** because the shared checkout was on another
lane's branch with clean-but-foreign state; never switch it.
- **PR: <https://github.com/levineuwirth/pmacs/pull/216>**. This block
was written *with* the work, before the PR existed, so the row below
was filled in rather than invented. **Checkpoints, newest last**,
because a lane that records only one head goes stale on the next push:
- **PR: <https://github.com/levineuwirth/pmacs/pull/216>, MERGED**
2026-08-05 as `2657568`, all 14 checks green at head `8ab20b5`. This
block was written *with* the work, before the PR existed, so the row
below was filled in rather than invented. **Checkpoints, newest
last**, because a lane that records only one head goes stale on the
next push:
| head | CI run | result |
|---|---|---|

View File

@ -124,6 +124,36 @@ commands, read `docs/active-work.md` immediately after this file.
- **A capability fallback must strip the QUIT ACTION too**, not just
the side parameters — a quit action stranded on a document window
makes a later `q` try to restore a presentation that never happened.
- **The tree primitive ships — `listview` gained depth, collapse and
identity** (P5, §14's last missing workbench primitive; implemented,
PR held). Rows carry **optional** `depth` and `id`; absent, a row
behaves exactly as before, which is what leaves the flat consumers
untouched. The LSP outline is the one adopter. Durable facts:
- **Folding is local projection state, not a refresh protocol.**
Collapse only *hides* rows and never changes a surviving row's
depth, and consumers emit parents before children, so descendants
are a **contiguous run**. The primitive therefore re-renders from
its own array **without calling the consumer** — which is the only
reason the anchor consumer works, because **the outline has no
`on_refresh` at all**. A design requiring the consumer to re-supply
rows on every fold would have fitted no existing consumer.
- **Identity is consumer-supplied and compared by equality; the
primitive never derives one.** `item` is opaque by design. The
outline uses `line:col`, because the `::` parent chain collides on
overloads and same-named siblings — exactly where a stale expansion
would reattach to the wrong node. **Selection is re-seated by id,
not by line**, since a fold inserts or removes rows above the
cursor.
- **`has_children` must read the FULL row array, not the rendered
subset.** A collapsed node's children are absent from the rendered
map by construction, so asking the view would answer "no" for every
collapsed node and make expanding impossible — a self-sealing bug
that looks like fold working and unfold silently not.
- **A bite that passes validates the pair, not the test.** The first
byte-identity injection used `row.depth or 0`; flat rows have no
depth, so it changed nothing and the test "passed" against a
regression the flat path is immune to. Ask which defect you
injected before believing a green bite.
- **pmacs is installable without cloning — Distribution Stage 1, #211,
released as v1.1.0.** A `v*` tag builds `pmacs` and `pmacs-gpu` on
pinned `ubuntu-22.04` / `macos-15` and publishes a GitHub Release with
@ -228,7 +258,7 @@ anchor, so every item is startable.
| 2 | Workspace + location | Missing; model gap | The long-lead arc. Start before a fifth subsystem grows its own root convention — four have already diverged (§7) |
| 3 | Extension ownership | Missing; prerequisite-shaped | **`pmacs.hook.remove` does not exist.** That one bug-sized gap blocks §13's disable/uninstall, §10's trust classes, and package-scoped cancellation |
| 4 | **Discovery** | **Stage 1 MERGED (#207)** | Stage 2 candidates, in rough dependency order: richer M-x rows (**protocol change** — `MinibufferPrompt.candidates` is `Vec<String>`; `CompletionPopupRow` already proves the pattern), `Command` gaining title/category/aliases/flags/arg-schema (~147 definition sites), predicate evaluation, help-layer unification, and the help-prefix decision |
| 5 | Workbench convergence | Partial; **Arc 7 COMPLETE** (Stage 3 merged, #213) | The bottom panel is finished on both frontends and the adopter default is flipped. **The tree primitive is now the arc's successor**`COHERENCE.md` §14 grades Tree ✗, and DAP's variables view is its next would-be inventor. Build it *before* dired's `i` and the worker tree invent two |
| 5 | Workbench convergence | Partial; **Arc 7 COMPLETE** (#213) and **the tree primitive is implemented** (PR held) | The bottom panel is finished on both frontends and the adopter default is flipped. The tree primitive has landed on a held PR: §14's Tree moves ✗ → ◐ with the LSP outline as its one adopter. **Next: adoption** — dired's `i`, then DAP's variables view, which is why it was built first |
| 6 | Config productization | Foundation only | Value provenance, then layering, then adoption migration (**table-valued settings are the hard prerequisite** — `ConfigValue` is four scalars) |
| 7 | Package lifecycle | Not started | Correctly sequenced after P3 |
| 8 | **Distribution** | **Stage 1 SHIPPED (v1.1.0, #211)** | Binaries on tag, checksums, machine-checked glibc floor. **Journey step 1 now works and the "invisible until this exists" blocker is lifted.** Next is a *decision* about channels / update / signing, not a queued plan |
@ -317,18 +347,34 @@ someone forgot.
`gpu_invocation_acceptance` tests fail on a missing `pmacs-gpu`
binary. `cargo build --workspace --no-default-features --features
luajit,crdt` is the invocation that produces both binaries.
- **A shared `CARGO_TARGET_DIR` makes a local sweep unattributable.**
- **A shared `CARGO_TARGET_DIR` makes concurrent sweeps unattributable.**
Every worktree on this machine resolves to the same target directory,
so `target/debug/pmacs` is a **shared mutable file**: a concurrent
`cargo test --workspace` in another worktree, at default (non-`crdt`)
features, overwrites the binary that a running `crdt` sweep is
spawning. That produced seven failures in three real-daemon suites
during the Stage 2 hardening sweep while the baseline was clean, and
the failure text named its own cause — *"start the daemon built with
the `crdt` feature"*. Confirmed with `pgrep` and discriminated by
re-running from the same tree with a **dedicated**
`CARGO_TARGET_DIR`. **Check for other live worktrees before believing
a sweep failure, and give the sweep its own target directory.**
so `target/debug/pmacs` is a **shared mutable file**: a
`cargo test --workspace` at default features in one worktree
overwrites the binary a running `crdt` sweep is spawning, and every
real-daemon suite then starts the wrong one. **Established once, on
2026-08-05, in the Stage 2 hardening sweep** — seven failures across
three real-daemon suites against a clean baseline, the failure text
naming its own cause (*"start the daemon built with the `crdt`
feature"*), `pgrep` confirming the rival build, and a re-run from the
same tree with a **dedicated** `CARGO_TARGET_DIR` giving 41/41. The
reciprocal case was seen from the other side the same day.
**Give a second worktree its own target dir**, check for live
worktrees before believing a sweep failure, and treat any red from a
sweep that overlapped another build as unattributable rather than as
evidence. *(This mechanism does NOT retroactively explain the tree
lane's unclassified occurrence — that one's signatures were destroyed
before being read, so it has no captured text to match against this
one's, and it keeps two non-causal hypotheses. A mechanism
established in one occurrence is not evidence about a different
occurrence that was never characterized.)*
- **A local sweep leaks daemons, and they accumulate across days.**
`gpu_invocation_acceptance`'s one-command tests leave ~3 orphaned
`pmacs --daemon` processes per sweep, reparented to systemd with
deleted sockets; 42 were resident at one point, the oldest four days
old. They are a rival explanation for any load-sensitive local
failure, so **check `pgrep -f "pmacs --daemon"` before trusting a
local red**. Lane recorded in `docs/active-work.md`.
- **A local sweep is blind to whichever feature configuration it does
not build.** Stage 3's census and every verification sweep ran
`--features luajit` WITHOUT `crdt`, so no crdt-gated suite was
@ -2033,7 +2079,20 @@ before trusting them:
`m6_8_supervisor_reaps_all_children_across_cycles`) are timing-based;
`editor::composition_overhead_under_ten_percent` is a render-ratio
microbenchmark that fails ~1/3 even isolated single-threaded (already
`cfg!(macos)`-disabled). Vterm Stage 3's merge CI saw one macOS timeout in
`cfg!(macos)`-disabled). *(Local measurements, 2026-08-05, 16-core
Linux, tree-primitive branch, which could not reach it — its diff
versus `main` touches no `src/`, no crate, no manifest. **Two reds,
both inside the full `--features crdt` lib run**, at `dispatch
overhead` 30.7% and 10.3%; two full-suite runs at the same tips were
green; and **10/10 green run alone**, ratios spanning -2.3% to
+1.5%. So the in-suite/isolated split has now been seen twice, and
the second red cleared the 10% budget by 0.3 points — the threshold
is marginal, not comfortably clear. Recorded as measurements, NOT a
cause: ten isolated greens establish that the ratio sits nowhere near
the threshold when alone, not that in-suite contention is what pushed
it over, and two reds against two greens in-suite is intermittence
rather than a mechanism. Not a `ci-red-signatures.md` row either —
that registry judges red **CI** runs, and these were local.)* Vterm Stage 3's merge CI saw one macOS timeout in
`real_tui_terminal_smoke_restores_host_after_output_input_resize_scroll_copy_and_bell`;
the complete failed-job rerun passed. The required-GPU gate also failed once
in `headless_diag_face_recolors_band_counter_despite_unchanged_text`, then

View File

@ -0,0 +1,594 @@
# Framing — the tree primitive
**Revision 5.** Status: **IMPLEMENTED and GATED; PR HELD for review.**
Q#TR1TR4 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<Symbol>`, 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 = <any> }, ... },
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 `<any>`
**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 `<any>` 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#TR1TR3 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 34 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).

View File

@ -739,6 +739,201 @@ fn s1_11_a_disambiguated_panel_still_answers_ret_g_and_q() {
/// raw-switch and capability-fallback listview loops**. `s1_12` pins the
/// second by keeping its panels in document windows; this pins the
/// first.
/// Tree primitive — a panel with `depth` + `id` collapses and expands,
/// and BOTH the collapse state and the selection survive a re-render.
///
/// Acceptance 2 and 3. The re-render is what the primitive controls;
/// `g` refresh is deliberately out of scope because the anchor consumer
/// (the outline) has no `on_refresh` at all — see the framing's §1.5a.
#[test]
fn tr_1_collapse_hides_descendants_and_survives_re_render() {
let mut s = editor();
exec(
&s,
r#"pmacs.listview.open {
name = "*tree*",
header = "tree TAB fold",
rows = {
{ text = "root", item = "root", depth = 0, id = "a" },
{ text = " kid1", item = "kid1", depth = 1, id = "b" },
{ text = " kid2", item = "kid2", depth = 1, id = "c" },
{ text = "tail", item = "tail", depth = 0, id = "d" },
},
}"#,
);
let body = |s: &EditorState| active_text(s);
assert!(
body(&s).contains("kid1"),
"children visible before collapse"
);
// Cursor opens on the first data row (root); TAB collapses it.
press(&mut s, KeyCode::Tab);
let collapsed = body(&s);
assert!(
!collapsed.contains("kid1"),
"descendants hidden: {collapsed}"
);
assert!(
!collapsed.contains("kid2"),
"ALL descendants hidden: {collapsed}"
);
assert!(
collapsed.contains("root") && collapsed.contains("tail"),
"the node itself and its SIBLING survive — collapse hides \
descendants, not the following run: {collapsed}"
);
// Selection is re-seated by ID, so the cursor is still on `root`.
let on_root: String = eval(
&s,
"return pmacs.describe.buffer(pmacs.window.buffer()).name",
);
assert_eq!(on_root, "*tree*");
press(&mut s, KeyCode::Tab);
assert!(
body(&s).contains("kid1") && body(&s).contains("kid2"),
"TAB again expands"
);
}
/// Selection survives a re-render that MOVES the selected node.
///
/// `tr_1` is not sufficient for this and was vacuous as a selection
/// test: it toggles the ROOT, which occupies line 1 before and after the
/// collapse, so the old line-based re-seating would pass it unchanged.
/// A selection test has to move the node.
///
/// Here `on_refresh` inserts a child ABOVE the selected sibling, so the
/// sibling's line shifts. Re-seating by line would land on the inserted
/// row; re-seating by id stays on the sibling.
#[test]
fn tr_4_selection_follows_the_node_when_rows_are_inserted_above_it() {
let mut s = editor();
exec(
&s,
r#"_G.EXTRA = false
pmacs.listview.open {
name = "*tree*", header = "tree",
rows = {
{ text = "root", item = "root", depth = 0, id = "a" },
{ text = " kid", item = "kid", depth = 1, id = "b" },
{ text = "sibling", item = "sibling", depth = 0, id = "z" },
},
on_refresh = function()
if _G.EXTRA then
return {
{ text = "root", item = "root", depth = 0, id = "a" },
{ text = " kid", item = "kid", depth = 1, id = "b" },
{ text = " kid2", item = "kid2", depth = 1, id = "c" },
{ text = "sibling", item = "sib", depth = 0, id = "z" },
}
end
return {
{ text = "root", item = "root", depth = 0, id = "a" },
{ text = " kid", item = "kid", depth = 1, id = "b" },
{ text = "sibling", item = "sib", depth = 0, id = "z" },
}
end,
}"#,
);
// Select `sibling` — data line 3.
press(&mut s, KeyCode::Char('n'));
press(&mut s, KeyCode::Char('n'));
let line_before: i64 = eval(&s, "return pmacs.editor.cursor_line()");
let text_at = |s: &EditorState| -> String {
let body = active_text(s);
let line: i64 = eval(s, "return pmacs.editor.cursor_line()");
body.lines()
.nth(usize::try_from(line).expect("line fits"))
.unwrap_or_default()
.to_string()
};
assert_eq!(text_at(&s), "sibling", "premise: sibling is selected");
// Refresh inserts `kid2` ABOVE sibling, so its line moves.
exec(&s, "_G.EXTRA = true");
press(&mut s, KeyCode::Char('g'));
let line_after: i64 = eval(&s, "return pmacs.editor.cursor_line()");
// Substantive claim first, so a regression reports as what it is.
// Under line-based re-seating the cursor stays on line 3, which now
// holds the INSERTED row.
assert_eq!(
text_at(&s),
"sibling",
"selection follows the NODE, not the line"
);
// …and the fixture really did move it, so the assertion above is not
// satisfied by the node happening to stay put (which is exactly how
// `tr_1` is vacuous as a selection test).
assert_ne!(
line_before, line_after,
"fixture: the insert must move the selected node"
);
}
/// A leaf reports rather than silently doing nothing.
///
/// The outline's `g` is already a dead binding — bound, dispatched, no
/// feedback (framing §1.3a). This primitive must not add a second one.
#[test]
fn tr_2_toggling_a_leaf_reports_instead_of_silently_doing_nothing() {
let mut s = editor();
exec(
&s,
r#"pmacs.listview.open {
name = "*tree*", header = "tree",
rows = { { text = "leaf", item = "leaf", depth = 0, id = "only" } },
}"#,
);
press(&mut s, KeyCode::Tab);
assert!(
status(&s).contains("no children"),
"a leaf toggle says so; got: {}",
status(&s)
);
}
/// Rows WITHOUT `depth`/`id` behave exactly as before — the property
/// that keeps the three flat consumers unaffected (acceptance 5).
#[test]
fn tr_3_a_flat_panel_is_untouched_by_the_tree_extension() {
let mut s = editor();
exec(
&s,
r#"pmacs.listview.open {
name = "*flat*", header = "flat",
rows = { { text = "one", item = 1 }, { text = "two", item = 2 } },
}"#,
);
let before = active_text(&s);
let status_before = status(&s);
press(&mut s, KeyCode::Tab);
assert_eq!(
active_text(&s),
before,
"TAB on a depthless panel changes nothing"
);
// Byte-identity of the BUFFER is not enough: TAB is bound for every
// listview, so the tree command intercepts a key that previously
// fell through to `buffer.tab` and the read-only intercept. A
// listview-specific status here would be a behaviour change the
// flat consumers never had, and invisible to a buffer comparison.
assert!(
!status(&s).contains("no node here") && !status(&s).contains("no children"),
"a flat panel must not gain tree feedback; status was {:?} (was {:?})",
status(&s),
status_before
);
assert!(
before.contains("one") && before.contains("two"),
"both rows render: {before}"
);
}
#[test]
fn s3_1_q_walks_the_side_presentation_chain_back_to_the_document() {
let mut s = editor();
@ -880,6 +1075,127 @@ fn s1_14_no_bypass_write_or_name_keyed_identity_remains() {
);
}
/// A tree row need not carry `item` — `on_visit` is optional, so a
/// display-only node (a grouping header) is a legitimate row. The
/// cursor must still seat on it.
///
/// This bit: `line_to_item` is SPARSE when rows omit `item`, and
/// `seat_cursor` took `#` of it. For an all-display-only tree that
/// length is 0, so the cursor never left the header — where TAB finds
/// no row and answers "no node here", making the tree unfoldable.
#[test]
fn tr_5_a_tree_of_display_only_rows_is_still_navigable_and_foldable() {
let mut s = editor();
exec(
&s,
r#"pmacs.listview.open {
name = "*tr5*",
header = "display-only TAB fold",
rows = {
{ text = "root", depth = 0, id = "r" },
{ text = " kid", depth = 1, id = "rk" },
},
}"#,
);
// Seated on a data row, not stranded on the header.
let line: i64 = eval(&s, "return pmacs.editor.cursor_line()");
assert_eq!(
line, 1,
"cursor seats on the first data row despite no `item`"
);
press(&mut s, KeyCode::Tab);
assert!(
!status(&s).contains("no node here"),
"TAB found the node: {}",
status(&s)
);
assert!(
!active_text(&s).contains("kid"),
"and folded it: {:?}",
active_text(&s)
);
}
/// Q#TR3's contract is that ids compare by value. Collapse state keys
/// a Lua table, and table indexing consults no `__eq`, so a non-scalar
/// id would compare equal for selection and unequal for folding: a
/// refresh would restore the cursor and silently lose the fold. The
/// contract is narrowed to scalars and enforced where rows enter,
/// rather than left to surface as a lost fold much later.
#[test]
fn tr_6_a_non_scalar_id_is_rejected_where_rows_enter() {
let s = editor();
let err: String = eval(
&s,
r#"local ok, e = pcall(function()
pmacs.listview.open {
name = "*tr6*",
header = "h",
rows = { { text = "a", depth = 0, id = {} } },
}
end)
return tostring(e)"#,
);
assert!(
err.contains("ids must be a string or number"),
"rejected where rows enter, with a reason: {err}"
);
}
/// A NaN id passes `type(x) == "number"` and then errors at
/// `p.collapsed[row.id]` with "table index is NaN" — the one scalar
/// Lua accepts as a number and refuses as a table key. It must be
/// caught where rows enter, naming the row, rather than surfacing on
/// whichever later TAB happens to reach it.
#[test]
fn tr_7_a_nan_id_is_rejected_rather_than_erroring_on_the_first_fold() {
let s = editor();
let err: String = eval(
&s,
r#"local ok, e = pcall(function()
pmacs.listview.open {
name = "*tr7*",
header = "h",
rows = { { text = "a", depth = 0, id = 0 / 0 } },
}
end)
return tostring(e)"#,
);
assert!(
err.contains("NaN id"),
"named at entry, not as a table-index error later: {err}"
);
}
/// Duplicate ids do not merely collide — every lookup resolves an id to
/// the FIRST row bearing it, so selecting the second toggles the first
/// and re-seats the cursor onto it. An id that does not identify a node
/// is not an id.
#[test]
fn tr_8_duplicate_ids_are_rejected_because_lookup_takes_the_first_match() {
let s = editor();
let err: String = eval(
&s,
r#"local ok, e = pcall(function()
pmacs.listview.open {
name = "*tr8*",
header = "h",
rows = {
{ text = "first", depth = 0, id = "same" },
{ text = "second", depth = 0, id = "same" },
},
}
end)
return tostring(e)"#,
);
assert!(
err.contains("share the id") && err.contains("rows 1 and 2"),
"both offending rows named: {err}"
);
}
// Isolated bootstrap storage roots (see the module docs): an
// integration test is compiled without `cfg(test)`, so a raw
// `EditorState::new()` would read the developer's real `init.lua` and

View File

@ -8025,6 +8025,99 @@ fn open_against_fake(path: &std::path::Path) -> pmacs::editor::EditorState {
/// hierarchical documentSymbol response ("Outer" class > "inner"
/// method): open, depth-indented rows, RET jump-ring visit to the
/// symbol's selectionRange, M-, back to the outline row, q restore.
/// Tree primitive, acceptance 5 — the FLAT listview consumers render
/// **byte-identically** after the depth/collapse extension.
///
/// This exists because the weaker claim was not true. `listview_
/// acceptance` says in its own header that the references panel "needs
/// a live LSP and is validated manually / via the m4 harness", so it
/// does not exercise `*references*` at all; and the hover test asserts
/// content *presence*, not exact output. Neither would notice a flat
/// consumer silently gaining an indent column — which is precisely the
/// regression a tree extension can introduce.
///
/// So the assertion is on the **exact rendered bytes**, through the
/// real entry points, against the fake language server.
#[test]
fn flat_listview_consumers_render_byte_identically_after_the_tree_extension() {
let dir = tempfile::tempdir().expect("tempdir");
let a_path = dir.path().join("r.rs");
std::fs::write(&a_path, b"fn main() {}\n").expect("write r");
let mut state = open_against_fake(&a_path);
let body = |state: &pmacs::editor::EditorState| -> String {
state
.lua_host
.lua()
.load("local b = pmacs.window.buffer() return b:slice(0, b:len())")
.eval()
.expect("panel text")
};
// --- *references* (on_visit, no depth) ---
state
.lua_host
.lua()
.load("pmacs.lsp.find_references()")
.exec()
.expect("invoke find_references");
assert!(
pump_lua_flag(
&mut state,
"pmacs.describe.buffer(pmacs.window.buffer()).name == '*references*'",
5,
),
"the references panel opened"
);
let refs = body(&state);
let (header, rows) = refs.split_once('\n').expect("header then rows");
assert_eq!(
header, "1 reference RET visit n/p move q quit",
"the header is unchanged — no fold affordance is advertised on a \
flat panel"
);
// EXACT: the row is the location string and nothing else. An added
// indent column, tree gutter or fold marker would all fail here.
assert_eq!(
rows,
format!("{}:12:3", a_path.display()),
"the flat references row renders verbatim"
);
// --- *lsp* (on_refresh, no depth) ---
state
.lua_host
.lua()
.load("pmacs.command.invoke('lsp.status')")
.exec()
.expect("invoke lsp.status");
let status_body = body(&state);
let (status_header, status_rows) = status_body.split_once('\n').expect("header then rows");
assert_eq!(
status_header, "LSP status g refresh q quit",
"the one panel WITH refresh keeps its exact header"
);
// `*lsp*` formats its OWN indentation — two spaces on detail lines —
// so "starts with a space" is not a violation here. What must hold
// is that the primitive reproduces the consumer's text EXACTLY: a
// prefix added by render would shift this line and break the match.
//
// Matched as a whole line rather than a substring, because a
// substring would still be found inside a further-indented version
// of itself. Volatile parts (pid, elapsed) are deliberately not
// included.
assert!(
status_rows
.lines()
.any(|l| l == " capabilities: sync, hover, completion, definition, diagnostics"),
"the consumer's own two-space indentation survives verbatim; got:\n{status_rows}"
);
assert!(
status_rows.lines().any(|l| l == "Servers:"),
"an unindented row stays unindented; got:\n{status_rows}"
);
}
#[test]
#[allow(
clippy::too_many_lines,