# Discovery Stage 2 — M-x rows stop being bare names **Status: framing pass, revision 3. Pre-implementation. Awaiting approval.** **Revision 3 fixes three things revision 2 asserted without checking the mechanism it was reasoning about**: a "frozen-shape" test that freezes nothing, a cache hazard that this architecture makes impossible, and a clipping rule that is unachievable at narrow enough widths. All three verified in the tree. **Revision 2 fixes two claims revision 1 made about compatibility and about the TUI, both wrong, both checkable.** An in-place field change cannot preserve v22 — postcard is not self-describing — and "both frontends render it" was false, because the grid TUI never reads that message at all. Verified in the tree, not reasoned about. --- ## 1. The gap, stated exactly `COHERENCE.md` §5 grades unified discoverability **Partial** after Stage 1 (#207), and names three things left. This lane takes one: > `Command` still has no title/category/flags, **M-x rows are still > bare names**, and the Rust help layer is still orphaned. **The descriptions already exist.** `Command.description` is a required field (`src/command.rs:69`), and `help.list-commands` already renders "every registered command **with its description**" (`builtin/runtime/help.lua:339`). A user who runs `M-x help` can read what everything does. **What they cannot do is see it at the moment of choosing.** `M-x` shows names alone — so the information exists, is already surfaced elsewhere, and is missing from the one place it would change a decision. That is §1.1's *substrate without surface* in its purest form, and it is felt every time the editor is used. ## 2. Ground truth Scouted: - **The wire asymmetry is a single field.** `InstanceMessage::MinibufferPrompt` carries `candidates: Vec` (`pmacs-protocol/src/message.rs:1113`). - **The rich pattern is already proven in a sibling variant.** `CompletionPopup` carries `rows: Vec` — `label`, `kind: u8`, `detail: Option` (`:1387`) — and both frontends already render it. *(Revision note: an earlier read of mine reported two bare-string sites. There is one. The second grep hit was `CompletionPopup`'s doc comment, which says "candidates" while the field is `rows`.)* - **`Command` needs no change for this lane.** `description` is already there and already required. Title/category/aliases — the lane's other Stage-2 candidate — would enrich these rows further and are **deliberately not** in scope: they are a ~175-site change and this lane can deliver the felt improvement without them. - **`ADVERTISED_PROTOCOL_VERSION` is pinned at 20** (`pmacs-protocol/src/message.rs:1767`) and **must not be edited**, per handoff §3/§5. - **The transport is postcard** (`pmacs-protocol/src/transport.rs:1`), which is **not self-describing**: enum variants encode by index and fields by position. **Changing a field's type in place is a wire break**, not a compatible evolution — a v22 peer would mis-decode the bytes rather than ignore them. - **`MinibufferPrompt` is sent to every peer negotiated `>= 12`** (`src/daemon.rs:1472`, "Q#MB1 — MinibufferPrompt gated at v12"). So the population that would break is every frontend from v12 to v22. - **The grid TUI never reads `MinibufferPrompt`.** `src/editor.rs` contains **zero** references to it; `paint_minibuffer` reads `core.minibuffer` directly and renders the selected candidate as an inline suffix, `format!(" [{cand}]")` (`src/editor.rs:5484`), with its own `ui.minibuffer.candidate` face. **The rich wire reaches `pmacs-gpu` only.** ## 3. The change **This is a protocol change: v22 → v23**, and it is **additive**, not an edit. ### 3.1 A new variant, because an in-place change cannot be compatible Revision 1 proposed changing `candidates` in place. **That breaks every frontend from v12 to v22**: postcard encodes fields positionally, so a v22 peer decoding a `Vec` where it expects `Vec` mis-reads the bytes — it does not skip them. And gating the changed variant at `>= 23` does not rescue it: the peer would then receive **no minibuffer message at all**, because there is only one variant to send. Compatibility means *sending the old shape*, which requires the old shape to still exist. So: - **`MinibufferPrompt` is retained, unchanged, for v12–v22.** Its encoding is frozen. - **`MinibufferPromptRows` is a NEW variant appended to the enum**, carrying `rows: Vec` and otherwise mirroring `MinibufferPrompt`'s fields. - **Appended, not inserted.** Variant indices are positional in postcard; inserting anywhere but the end renumbers every later variant and breaks everything at once. ### 3.2 Per-session selection, and the ordering that matters - **Selection is per peer, decided from its negotiated version**: `>= 23` receives `MinibufferPromptRows`; `12..=22` receives `MinibufferPrompt`. This mirrors the existing gates in `src/daemon.rs:1472`, which already suppress `MenuPrompt`, `MinibufferPrompt` and `LineNumbers` per peer. - **Exactly one of the two is sent to any given peer, ever.** Sending both to a v23 peer would double-render; sending neither is the bug gating alone would have caused. - **The selection is a producer gate**, named `peer_knows_minibuffer_rows`, alongside the existing `peer_knows_minibuffer_prompt` / `peer_knows_menu_prompt` / `peer_knows_completion_popup` (`src/daemon.rs:1410-1435`). One new gate in an established pattern, not a new mechanism. - **ONE per-peer minibuffer cache, not a per-variant key.** **Revision 2's rationale for a per-variant key was false**, and the architecture is why: `SemanticRenderState::for_peer(frontend_id, negotiated_protocol_version)` is created **per peer, with its version baked in, on attach** (`src/daemon.rs:2080`) and **removed on detach** (`:1591`). A cache therefore never spans two negotiated versions — the v23→v22 reconnect suppression I described **cannot occur**, because reconnecting creates a fresh state. The corresponding test is removed rather than written; a test for an impossible condition passes forever and teaches the next reader that the hazard is real. - **The close message must still use the same variant family as the open** — a `MinibufferPromptRows` session closed by a legacy clear is the mismatch that leaves a popup on screen forever. That one is independent of caching and stands. ### 3.3 What each frontend does - **`pmacs-gpu`** renders label + detail from the new variant. - **The grid TUI does not consume this message at all** and is addressed separately in §3.4. ### 3.4 The TUI presentation contract Revision 1 said "both frontends render label + detail". **The grid TUI does not read `MinibufferPrompt`** — it paints from `core.minibuffer` and renders the selected candidate as `format!(" [{cand}]")` (`src/editor.rs:5484`). The wire change reaches it not at all. *My vote: **an inline selected form, matching what is already there***: ``` M-x buffer.sa [buffer.save — Write the buffer to its file] ``` - **Source: local.** The TUI is in-process with the core, so it reads `Command.description` from the registry directly. **No wire involvement**, which is why this half of the lane is independent of the bump. - **Only the selected candidate**, as today. This is a formatting change to an existing suffix, not a new surface. - **Clipping, in three ordered steps.** The suffix is already written against `max = term_size.cols` with a running `written` count, and the prompt plus typed input consume that budget first — so the remaining width can be **too small even for the bare name**. Revision 2 said "the name must survive", which is not achievable at arbitrary widths and would have forced a partial name. The rule: 1. **If the remaining suffix width cannot fit the WHOLE name, omit the suffix entirely.** Never emit a partial name — `[buffer.sa…]` is worse than nothing, because it reads as a different command. 2. **Only once the whole name fits** is a description attempted. 3. **If the description does not fit whole, drop the description**, leaving today's `[name]`. No ellipsis stub. So the guarantee is *"never a partial name"*, which is achievable, rather than *"the name always survives"*, which is not. - **The `ui.minibuffer.candidate` face already exists** and continues to cover the suffix. **A multi-row TUI chooser is explicitly NOT this lane.** It would be a new interaction surface, a §6 island risk, and materially larger than the wire work — it is named here so that "make the TUI match the GPU" does not quietly become that. ### 3.5 Scheduling consequence, which is not incidental `PROTOCOL_VERSION` is a strict serialization point — two lanes bumping it collide, and this session recorded eight broken version assertions from a single bump. So: - **This lane holds the bump slot.** Git Stage 1 is deliberately no-wire and runs beside it without contention. - **Git Stage 2 (gutter markers) also needs a bump and must therefore wait for this to land.** That ordering should be explicit in the ledger rather than discovered when the two collide. ## 4. Coherence impact (§20) - **§5 unified discoverability — the direct target**, and the specific clause "M-x rows are still bare names". - **Journey step 4** ("understand the interface"): `COHERENCE.md` P4 says most of it "rides on" discovery. This improves the step without adding one. - **§16 semantic frontend:** a clean instance of the architecture — the instance states *what a candidate is*, each frontend decides how to draw it. Degradation is the established practice (Q#D2-4). - **Interaction islands (§6): none added.** No new key interception; this changes what an existing prompt carries. - **Config registry:** no new setting. Whether detail rendering is optional is Q#D2-3, and my vote is no setting at all. - **Background-work attribution (§9): untouched.** No new background work. ## 5. Open questions ### Q#D2-1 — reuse `CompletionPopupRow`, or a new type? Reuse is tempting and I think wrong. `CompletionPopupRow.kind` is an **LSP `CompletionItemKind` code (1..=25)** with a documented contract; an M-x command is not an LSP completion item and has no honest value for that field. Reusing it would mean either inventing a fake kind or declaring 0/unknown everywhere — a type whose invariant is "meaningless in half its uses". *My vote: **a new `MinibufferRow { label, detail: Option }`*** — no `kind`. If a category field is wanted later it arrives with `Command.category` (the other Stage-2 candidate), typed as what it actually is rather than borrowed from LSP. ### Q#D2-2 — which prompts get rows? `pmacs.minibuffer.read` serves many sources, not just M-x: file paths, buffer names, apropos substrings, settings. Only some have a natural `detail`. *My vote: **the field is `Option` per row and the daemon fills it where it has one.*** Commands get their description; a file-path prompt leaves it `None` and renders exactly as today. No source is obliged to invent a detail, and none is prevented from gaining one later. ### Q#D2-3 — is detail rendering configurable? *My vote: **no setting.*** §11 grades the registry "partial (foundation only)"; adding a speculative toggle for a feature nobody has yet asked to disable is how a registry becomes noise. If somebody wants it off, that is use evidence and a later one-line addition. ### Q#D2-4 — older frontends — **RESOLVED, in §3.1–3.2** No longer open, and the revision-1 answer was wrong. "Gate the richer form at `>= 23`" would have **removed the minibuffer entirely** from every v12–v22 peer, because there would have been only one variant to gate. Compatibility requires the legacy shape to still exist and still be sent — hence the additive `MinibufferPromptRows` variant, a per-peer `peer_knows_minibuffer_rows` producer gate, **one per-peer minibuffer cache**, and matched open/close families. *(Revision 2 said "per-variant cache keys" here. §3.2 corrected that in revision 3 — the render state is per peer with its version baked in, so a cache cannot span two versions — and this sentence was left stale.)* The `CompletionPopup` gate I proposed copying (`daemon-gated >= 15`) **is** the right precedent for *how to select per peer*; it is not a precedent for changing a live variant's shape, because that variant was new when it was gated. ### Q#D2-5 — does this tempt closed-set acceptance? **(a trap)** The discovery lane's own handoff note warns: **completion is assistance, not validation** — `resolve_accepted_value` returns the literal typed text when no candidate is selected, so closed-set acceptance is unbuilt Rust work. Richer rows make M-x *look* like a closed set, which invites someone to make acceptance reject unmatched input. **That is out of scope and would be a behaviour change**, not a rendering one. Stated here because the temptation arrives with the feature. ## 6. Verification - **A command's description reaches the GPU row**, asserted through the real prompt path rather than by constructing a message. - **A v22 peer still receives `MinibufferPrompt`, with its old encoding** — the case revision 1 would have broken. Asserted by negotiating v22 and observing the legacy variant arrive, **not** by observing "no error". - **A v23 peer receives `MinibufferPromptRows` and NOT the legacy variant** — the double-render guard. - **LITERAL POSTCARD BYTE FIXTURES for the legacy variant**, open and clear: `assert_eq!(encoded, LEGACY_BYTES)` against a constant. **Revision 2 proposed a round-trip and that freezes nothing.** A round-trip encodes and decodes with the *same* types, so adding a field to `MinibufferPrompt` leaves it passing — both sides simply learn the new shape, while every v12–v22 peer in the field breaks. The existing `minibuffer_prompt_round_trips_through_postcard` (`src/protocol.rs:2363`) is exactly that kind of test, and **there are no literal byte fixtures anywhere in the protocol tests today** — checked, not assumed. Only comparing against bytes captured *now* can fail when the encoding changes. Two fixtures: an open prompt with candidates and a selection, and a cleared band — the two shapes the existing semantic test already covers, so the corpus is not a new judgement call. - **No cross-version cache test.** Revision 2 required one; it asserts a condition this architecture makes impossible (§3.2), and a test that cannot fail passes forever while teaching the next reader that the hazard is real. What *is* asserted is the producer gate: a v22 peer and a v23 peer attached simultaneously each receive their own variant and only their own. - **Close matches open**: a `MinibufferPromptRows` session is closed by its own family, witnessed by the popup actually clearing. - **The TUI renders `name — description` for the selected candidate** (§3.4), from the local registry, with **no wire involvement**. - **TUI clipping is witnessed at THREE widths** (§3.4): wide enough for name + description; wide enough for the name only (description dropped, `[name]` as today); and **too narrow for even the whole name — the suffix vanishes entirely**. The last is the case revision 2's rule could not express, and the assertion is that no *prefix* of a name is ever emitted. - **A source with no detail renders exactly as before** — the file-path prompt is the witness (Q#D2-2). - **Typed-but-unmatched input is still accepted** (Q#D2-5) — the guard against this lane quietly becoming a validation change. - **The version-bump discipline**: `ADVERTISED_PROTOCOL_VERSION` unchanged at 20, and the tripwire assertions updated **knowingly**. Handoff §3 requires the strengthened two-configuration sweep for a `PROTOCOL_VERSION` change — `scripts/gate --protocol`, which exists precisely for this. **What this will not prove:** that `Command` carries title or category (not in scope), or that predicates are evaluated (Stage 3+). ## 7. Not in scope `Command` gaining title/category/aliases/flags/arg-schema — the ~175-site change, and the lane's next candidate. **A multi-row TUI chooser** (§3.4) — a new interaction surface and materially larger than this lane. **Changing `MinibufferPrompt`'s existing shape** — it is frozen for v12–v22. Predicate evaluation, which makes commands stop being invocable and needs its own decision at each call site. Help-layer unification (`src/help.rs` is still orphaned). The help prefix key — `C-h` is **not** free, since non-kitty terminals cannot disambiguate Ctrl+Backspace from Ctrl+H (both are byte 0x08). Closed-set acceptance (Q#D2-5).