From c2eeb60dc75327a3cd405da1b93374aa51ba47c7 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sat, 25 Jul 2026 18:00:40 -0400 Subject: [PATCH] docs: bottom-panel Stage 2 framing (revision 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The re-framing `docs/bottom-panel-framing.md` rev 4 §2 requires before Stage 2 (the GPU panel band) is implemented. Re-scouted against canonical `main` @ `5aa9044`, protocol v20. It does not restate the parent's decisions; it records what the re-scout found. Every source anchor Stage 2 inherits had moved, but none of the parent's mechanical model was falsified. Two facts held and are load-bearing: protocol is still v20, so Q#BP9 resolves to **v21** with no reservation needed, and both byte pins (`InstanceMessage::InitialTargetResult`, `FrontendEvent::TerminalPointer`) are still their enums' final variants. Four findings: - **Q#BP2S1, new and open.** Stage 1 landed a daemon-side geometry epoch allocator (`declare_frame_geometry`), but Q#BP15a specifies a frontend-owned epoch echoed by every `Present`. The landed allocator also dedups on value and uses `saturating_add`, which is neither wrapping nor the fail-closed the framing asks for. Three resolutions are stated with a recommendation. - **The §1.3 census is essentially unrouted.** Stage 1 built the `primary_document_window` seam but it has one production caller; ~80 direct `.active` reads remain. This is Stage 2's bulk, not its tidy-up, and the stage plan sequences it first. - **The statusline active read is three sites, not one.** - **Four scout obligations are still open** and are named rather than papered over, including the GPU-side pixel formula inputs. Also carries the staged plan, draft acceptance criteria, the coherence impact per `COHERENCE.md` §20 (§14 is the section it serves), and four questions for the user. Co-Authored-By: Claude Opus 5 (1M context) --- docs/bottom-panel-stage2-framing.md | 326 ++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 docs/bottom-panel-stage2-framing.md diff --git a/docs/bottom-panel-stage2-framing.md b/docs/bottom-panel-stage2-framing.md new file mode 100644 index 0000000..19f9203 --- /dev/null +++ b/docs/bottom-panel-stage2-framing.md @@ -0,0 +1,326 @@ +# Bottom panel Stage 2 — the GPU panel band (framing) + +**Revision 1 — pre-implementation. Ground truth: canonical `main` @ +`5aa9044`, protocol v20, 2026-07-25.** + +Stage 1 (#155, merge `e745068`) gave pmacs window placement, window +parameters, TUI side windows, the divider, and the adopter `display` +opt-in. It deliberately set `FrontendView::panel_capable = false` for +every semantic session, so a GPU frontend silently falls back to the +non-side target. **Stage 2 flips that bit and earns the right to.** + +This document is the re-framing `docs/bottom-panel-framing.md` (rev 4) +§2 requires before Stage 2 is implemented. It does **not** restate the +parent's decisions. It records what the re-scout against current `main` +found: which anchors moved, which parent claims survived, which did +not, and the four questions the parent framing cannot answer without a +decision from the user. + +Read the parent framing's Q#BP8, Q#BP9, Q#BP14b, Q#BP15, Q#BP15a, +Q#BP16, and Q#BP17 alongside this. Those decisions stand except where +§4 below revises them. + +## 0. Why the re-scout was required + +The parent framing's Stage 2 sections were written against `main` @ +`0dd16a5` and last re-scouted at `47581f4`. Since then eleven PRs have +merged: #149/#150 (folding Stage 2), #152–#155 (through bottom-panel +Stage 1), #158–#166 (inline math, minimap, Lean 4 Stages 1–2, +COHERENCE.md, find-file, the dired framing and Stage 1, the GPU +terminal input fix). Every source anchor Stage 2 depends on has moved. + +Two things did **not** change, and both are load-bearing: + +- **Protocol is still v20.** `PROTOCOL_VERSION` is `20` + (`pmacs-protocol/src/message.rs:1568`); no intervening PR bumped it. + Q#BP9's conditional resolves: **Stage 2 is v21**. +- **Both byte pins are still the final variants.** + `InstanceMessage::InitialTargetResult` is last in its enum + (`message.rs:577` within the enum at `:569`), and + `FrontendEvent::TerminalPointer` is last in its own. Q#BP9's + append-plus-pin instruction applies verbatim, with no re-derivation. + +## 1. Anchor re-scout + +Every line reference Stage 2 inherits, re-verified. "Claim" is the +parent framing's assertion about that site; "verdict" is what the code +at `5aa9044` actually says. + +| Parent anchor | Now at | Claim | Verdict | +| --- | --- | --- | --- | +| `src/editor.rs:2833` `paint_frame` returns cursor separately | `src/editor.rs:3171` | cells alone lose the caret | **Holds.** Signature still returns `Option` | +| `src/editor.rs:2883-2935` cursor-visible prep | `src/editor.rs:3249+` | extract with the per-window body | **Holds**, but see §3.1 — Stage 1 inserted work *above* it | +| `src/editor.rs:2937-3040` per-window paint body | after `:3260` | origin-agnostic `Viewport<'a>`, extractable | **Holds** | +| `src/editor_core.rs:566` `fold_map_for_window` | `src/editor_core.rs:734` | gates on the **active** frontend | **Holds** — `:738` is `if !self.fold_projection_active()` | +| `src/window.rs:339` stale invariant comment | `src/window.rs:562` | "a semantic session never enters `paint_frame`" | **Holds, still stale.** Updating it remains a Stage 2 obligation | +| `src/statusline.rs:634` indirect `view.active` read | `src/statusline.rs:629`, `:644`, `:675` | one read to close | **Revised: three sites**, not one | +| `src/daemon.rs:3122-3130` grid-only `Mouse` | `src/daemon.rs:3123` | `Mouse` is contractually the grid path | **Holds** | +| `pmacs-gpu/src/attach.rs:420-429`, `:573-577` | `pmacs-gpu/src/attach.rs:577` | permanent `24×80` placeholder | **Holds**, single site now | + +Nothing in the parent's mechanical model was falsified by the +re-scout. The decisions in §4 come from what Stage 1 *added*, not from +anything Stage 2 got wrong. + +## 2. What Stage 1 already built for Stage 2 + +More than the parent framing anticipated, which shrinks Stage 2 and +changes one of its wire contracts. + +- **`DeclaredFrameGeometry { geometry_epoch: u64, total: CellSize }` + exists** (`src/window.rs:522-528`), stored as + `FrontendView::frame_geometry: Option<_>` (`:589`) with `None` + meaning **unknown** — exactly Q#BP15a's "unknown is first-class". +- **The declaration path exists.** + `EditorState::sync_frame_geometry` (`src/editor.rs:877-882`) calls + `EditorCore::declare_frame_geometry` then `reconcile_panel_layout`. + Two daemon sites already drive it, both gated on + `panel_capable_for` (`src/daemon.rs:1882-1883` at attach, + `:1972-1973` on resize). +- **`paint_frame` itself declares geometry** (`src/editor.rs:3187`), + before the statusline fan-out and before the long mutable core + borrow, with a comment naming Q#BP2b/Q#BP15a. +- **`StatuslineEvaluationTarget`** (`src/statusline.rs:212-226`) is + already a two-variant enum — `Grid { frontend_id }` and `Semantic { + frontend_id, declared_buffer }`. Q#BP8's "generalize the fan-out" + is an added variant, not a refactor of a concrete type. +- **`primary_document_window`** exists (`src/editor_core.rs:2830`). + +## 3. What the re-scout found + +### 3.1 The geometry epoch is allocated daemon-side; the wire contract says frontend-side + +This is the one genuine conflict between landed Stage 1 and framed +Stage 2, and it needs a decision before implementation. + +`declare_frame_geometry` (`src/editor_core.rs:3155-3172`) **allocates +the epoch itself**: + +```rust +let next = view + .frame_geometry + .map_or(1, |geometry| geometry.geometry_epoch.saturating_add(1)); +``` + +Q#BP15a specifies the opposite: `FrontendEvent::FrontendCellGeometry { +frontend_id, geometry_epoch, total }` carries a **frontend-owned** +declaration id, and `PanelResizeRows` / `PanelPointer` / every +`Present` echo it. Under the landed code the daemon would have to +either ignore the wire epoch (breaking the echo contract the GPU +validates against) or overwrite its own allocator for panel-capable +semantic frontends only (two allocation regimes for one field). + +Two further details of the landed allocator matter: + +- **It dedups on value.** The function returns early when `total` is + unchanged, so the epoch advances only on an actual size change. For + a grid frontend that is correct — cells are the unit, and an + unchanged grid means an old `PanelFrame` is still valid under the new + metrics. For a frontend that *owns* its epoch, the daemon cannot + dedup by value without discarding a declaration the frontend already + considers current. +- **`saturating_add` is neither wrapping nor fail-closed.** Q#BP15a + requires exhaustion to "fail closed rather than wrap". Saturation + pins the epoch at `u64::MAX`, after which two different geometries + share one id — the exact staleness confusion the epoch exists to + prevent. Unreachable in practice; wrong as a contract, and free to + fix. + +**Q#BP2S1 (new, needs a decision).** Three candidate resolutions: + +1. **Frontend-owned, as framed.** `FrontendCellGeometry` carries the + epoch; the daemon stores it verbatim for semantic panel-capable + frontends and rejects a lower-or-equal epoch carrying different + data. Grid/LOCAL keep the local allocator, which never collides + because those frontends never send the event. Cost: one field, two + provenances, documented. +2. **Daemon-owned, GPU echoes.** `FrontendCellGeometry` carries only + `total`; the daemon allocates and the GPU learns its current epoch + from the next `PanelFrame`. Simpler invariant, but it reintroduces a + first-open ordering problem — the GPU must send `PanelResizeRows` + and `PanelPointer` carrying an epoch it has not been told yet, so + the first gesture after a resize is unvalidatable and must be + dropped. +3. **Frontend-owned everywhere.** Grid/LOCAL synthesize an epoch at + their existing declaration sites and the allocator moves out of + `EditorCore` entirely. Most uniform; largest Stage 1 churn, and it + touches code #155 just stabilized. + +**Recommendation: option 1.** It preserves the parent framing's +validation chain intact, and the "two provenances" cost is one doc +comment on a field that already carries three. + +### 3.2 The §1.3 census is essentially unrouted + +The parent framing's §1.3 lists 23 transitive active-context reads that +must route through `primary_document_window` before a panel can hold +focus without corrupting the document mirror. Stage 1 created the seam +but routed almost nothing through it: `primary_document_window` has +**three** references in `src/`, one of which is its own definition and +one a doc-comment link. The single production caller is +`src/daemon.rs:1639`. + +For scale, `src/*.rs` still contains ~80 non-test direct `.active` +reads (excluding `active_frontend`, setters, and predicates), on top of +the `active_window*` / `active_buffer*` helper family at +`src/editor_core.rs:663-967`. + +This is not a defect in Stage 1 — with `panel_capable = false` for +every semantic session, no semantic frontend can hold a side window, so +the unrouted reads are unreachable from the GPU. It does mean **the +census is the bulk of Stage 2's work**, not a tidy-up at the end, and +the stage plan in §5 sequences it first. + +### 3.3 The statusline read is three sites, not one + +Q#BP8 says closing the indirect `view.active` read at +`src/statusline.rs:634` falls out of the target generalization. There +are three: `:629` and `:675` compute `active: window_id == +view.active`, and `:644` does `.get(&view.active)`. They are the same +concern, but a fix that closes one and leaves two is a live risk, and +the acceptance criterion should name all three. + +### 3.4 Scout obligations still open + +Stated plainly rather than papered over. These were not re-verified in +this pass and must be before the doc leaves revision 1: + +- `pmacs-protocol/src/terminal.rs`'s validator internals, which Q#BP15 + asks to factor into a shared parameterized wire-cell-grid validator + (the `MAX_TERMINAL_ROWS/COLS = 512` split). +- `pmacs-gpu/src/attach.rs`'s bounded outbox policy and its existing + tail-coalescing classes, which Q#BP15a asks to extend with two new + classes and Q#BP16 with two more. +- The GPU-side band renderer and where it clips against the status + band — Q#BP15a's pixel formula is stated but its inputs + (`status_band_height_px`, `TEXT_TOP_px`, `code_line_height_px`, + `resolved_monospace_advance_px`) were not located in this pass. +- Whether folding Stage 3 lands first. Both stages touch the semantic + projection, and the ledger's standing rule is that whichever is + framed second re-scouts the other's landed state. + +## 4. Revisions to the parent framing + +Only these. Everything else in Q#BP8/9/14b/15/15a/16/17 stands. + +- **Q#BP9 resolves to v21.** No reservation was taken; none was needed. +- **Q#BP15a's epoch ownership is reopened as Q#BP2S1** (§3.1). +- **Q#BP8's statusline criterion names three sites** (§3.3). +- **Q#BP17's stale comment is at `src/window.rs:562`**, and its text is + now embedded in a longer `fold_projection` doc block that also + explains the Stage 2/Stage 3 split — the edit is a paragraph rewrite, + not a one-line correction. + +## 5. What ships, in order + +Sequenced so each step is independently gateable and the census — the +riskiest part — lands before anything depends on it. + +1. **Route the census.** Every §1.3 read through + `primary_document_window`, with `panel_capable` still `false`. No + wire change, no behavior change for any existing frontend; pure + seam adoption, falsifiable by revert. +2. **Extract the per-window painter.** Lift `paint_frame`'s per-window + body plus the active-window cursor-visible preparation into a + function taking the fold map as a **parameter** (Q#BP17), leaving + `sync_frame_geometry` and the statusline fan-out where Stage 1 put + them. Grid rendering must be byte-identical. +3. **Protocol v21.** Append `InstanceMessage::PanelFrame` and + `FrontendEvent::{FrontendCellGeometry, PanelResizeRows, + PanelPointer}`, each with a byte pin on the current final variant. + Factor the shared cell-grid validator. Gated both directions. +4. **Daemon-side panel projection.** `PanelFrame` production, + presentation epochs, `Absent` authority, the third statusline + target, the focus-chrome pass (Q#BP14b). +5. **GPU band.** Geometry declaration, band paint, divider chrome, + document clip, pointer transport, and the `panel_capable = true` + flip for semantic sessions — the flip is last, and it is what makes + the whole stage observable. + +Steps 1 and 2 are reviewable without any protocol change and could ship +as a separate PR if the user prefers a smaller first review. **That is +one of the questions in §8.** + +## 6. Coherence impact (per `COHERENCE.md` §20) + +- **Journey steps touched:** none directly. Stage 2 does not add a + journey step; it removes a frontend-dependent *hole* in one. Today a + user who runs `pmacs --gpu` and triggers compile, grep, references, + or a terminal gets the non-side fallback — the panel silently becomes + a stolen window. Every journey step that ends in an output surface + behaves differently on GPU than on TUI, and Stage 2 is what closes + that. +- **Interaction islands added: none, and this is a reduction.** §6 + grades islands "weak, and growing by one island per modal feature". + The panel is the opposite move: `display = "panel"` is one adopted + policy across listview, compile, and terminal, and Stage 2 extends + the existing policy to a second frontend rather than adding a + parallel GPU-only surface. The focus-chrome routing table (Q#BP14b) + deliberately reuses the existing `SearchPrompt` / `MenuPrompt` / + `CompletionPopup` messages instead of minting panel-specific ones. +- **Config registry adoption:** inherited, not extended. Stage 1's + `window.panel-height` and `window.min-height` already live in the + registry; Stage 2 adds no new user-facing option. If the GPU needs a + band-specific preference, it enters the registry — no new + configuration mechanism. +- **Background-work attribution:** unchanged. Stage 2 introduces no + worker, task, or process. It does, however, make §9's "✓ mechanics / + ✗ visibility" gap materially cheaper to close on the GPU: a terminal + PTY appearing in no user-visible activity view (§9) is partly a + *placement* problem, and after Stage 2 both frontends have a place to + put one. +- **Section this serves:** `COHERENCE.md` §14, which already records + the panel primitive as landed for Stage 1 and names "Stage 2 (GPU + band) pending its own framing" as the open item. This is that doc. + +## 7. Acceptance criteria (draft) + +Numbered for review; each must be falsifiable by revert, per the +standing lesson that a guard with no production caller passes every +direct-call test. + +1. Every §1.3 census read resolves through `primary_document_window`; + asserted at the outermost user-reachable seam, not by direct call. +2. All three `src/statusline.rs` active reads route through the new + frontend-layout target. +3. Grid `paint_frame` output is byte-identical across the extraction. +4. A semantic frontend with `fold_projection = false` painting a panel + over a folded buffer shows **every** source line (Q#BP17), and the + panel path never calls `fold_map_for_window`. +5. v21 round-trips; v20 peers negotiate without the panel events; each + extended enum's previous final variant is byte-pinned. +6. `Absent` is emitted on both close and hide, and clears input + authority before any later event validates. +7. A `PanelPointer` failing any of Q#BP16's six checks mutates no view, + controller, selection, menu, or PTY. +8. A geometry epoch change makes an older `PanelFrame` non-painted and + non-hit-testable until a matching `Present` arrives. +9. A panel wider than 512 columns is legal; a panel exceeding the + shared wire budget fails closed to `Absent`, not to a partial frame. +10. Panel focus does not disturb the document mirror + (`BufferSnapshot`, `CursorByte`, `Viewport`). +11. Native popups clear on document→panel focus change and panel + popups clear on panel→document, per Q#BP14b's ordering. + +## 8. Questions for the user + +1. **Q#BP2S1 epoch ownership** (§3.1) — recommendation is option 1, + frontend-owned with the daemon storing verbatim. Confirm or pick + another. +2. **One PR or two?** Steps 1–2 (census + extraction) are protocol-free + and independently valuable; steps 3–5 are the wire and the band. + Splitting gives two small reviews instead of one very large one, at + the cost of a second round-trip. +3. **Ordering against folding Stage 3.** Both touch the semantic + projection. Framing bottom-panel Stage 2 first means folding Stage 3 + re-scouts against a landed band. Confirm that order. +4. **Scout obligations in §3.4** — should revision 2 close all four + before implementation, or is the GPU-side pixel formula (the largest + of them) allowed to be settled during implementation? + +## 9. Gates + +The standing suite, plus `bottom_panel_stage1_acceptance` and a new +`bottom_panel_stage2_acceptance`, the three vterm suites (the panel +hosts terminals), folding Stage 2's 48 (shared projection), and +`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`. Protocol round-trip and +byte-pin tests ride step 3.