{
  "summary": "High-confidence root cause: the purge path violates ratatui's single-writer/double-buffer contract. `picker::event_loop` draws `ConfirmPurge` through ratatui, but on Enter it calls `cli::cmd_endpoint_purge` while the picker still owns the alternate screen and raw terminal (`crates/spt/src/picker/mod.rs:116-174`, specifically `Outcome::Purge` at 165-173). The purge core always writes directly to the same physical terminal through stderr—success at `crates/spt/src/cli.rs:12825` (`eprintln!(\"PURGED:{id}\")`), and every refusal/warning/error through the same function at 12711-12744, 12774-12785, 12798-12820. Ratatui does not know those writes happened. Its `Terminal::flush` diffs only its previous and current logical buffers; equal cells, including equal blank cells, are omitted. The next PickExisting draw therefore does not repaint cells whose desired contents are unchanged, even though the out-of-band stderr write may have overwritten or scrolled those physical cells. Because the confirm footer is on the terminal's last row (`view.rs:383-409`), the preceding draw commonly leaves the physical cursor on that row; `eprintln!` then terminates with a newline at the bottom and can scroll the entire alternate screen, maximizing the desynchronization. This exactly predicts stale confirmation glyphs and fragments in whitespace after purge.\n\nFalsifiable reproduction: create/retain one offline local endpoint; run bare `spt endpoint run`; select the Local category (the model opens on PickExisting/Project when endpoints exist: `model.rs:879-935`), highlight the offline local row, press `x`, then Enter. `x` transitions through `handle_pick_key` (`mod.rs:294-309`) to `PickerModel::enter_confirm_purge` (`model.rs:1163-1181`); Enter yields `Outcome::Purge` through `handle_key` (`mod.rs:258-266`) and `purge_outcome` (`model.rs:1184-1190`); the core prints `PURGED:<id>` to stderr before the loop redraws the list. Observe shifted/stale confirm text in otherwise blank list cells. Control: repeat `x`, then Esc instead of Enter. Esc performs only the model back-transition and no out-of-band print; cleanup should be correct. A second control is to suppress the purge core's stderr or clear after it; artifacts should disappear. A purge race that becomes online between model gate and dispatch exercises the same defect via the core's error `eprintln!`.\n\nRanked causes: (1) HIGH—inline `cmd_endpoint_purge` stderr desynchronizes ratatui's logical buffers from the physical alternate screen. It is deterministic because success always prints. Prediction: clearing only after the core returns removes the artifact; Esc-only never triggers it. (2) MEDIUM/secondary—`setup_terminal` enters the alternate screen and constructs `Terminal` without `terminal.clear()` (`mod.rs:95-105`). Ratatui's own CrosstermBackend example clears after entering alternate screen, and the first logical diff omits default blank cells. This can retain pre-existing alternate-buffer cells on terminals that preserve alternate-screen contents, but does not explain the purge-specific correlation as strongly as cause 1. Prediction: prefill the physical backend before the first draw; stale cells survive unless setup clears. (3) LOW/refuted for ordinary transitions—`Wrap { trim: true }` in `render_confirm_purge` (`view.rs:384-407`) trims wrapped prose, not terminal cells. The picker renders a full frame, ratatui resets the next buffer, and `Buffer::diff` emits changed nonblank-to-blank cells. There is no picker-side `trim_end`, blank-line filter, clear-line suppression, cursor math, or per-cell `skip` assignment. Thus ConfirmPurge→PickExisting via Esc should erase old glyphs normally. The missing spaces occur only where ratatui believes the physical cell already equals the desired blank.\n\nMinimal fix: immediately after `cmd_endpoint_purge` returns and after updating `model`/`model.screen`, call `terminal.clear()?` before the loop's next `draw`. Ordering is load-bearing: clear AFTER all purge-core stderr writes, never before. Ratatui `Terminal::clear` clears the physical fullscreen viewport and resets the previous/back buffer, forcing the next draw to reconstruct the screen from a known blank baseline. A defensive `terminal.clear()?` in `setup_terminal` is also reasonable for first-frame correctness, but it is not a substitute for the post-purge clear. A larger, cleaner alternative is to split `cmd_endpoint_purge` into a silent typed core plus CLI presenter so the picker converts diagnostics to `model.flash`; that removes the second terminal writer but is not the minimal repair.\n\nDeterministic regression seam: the existing `view::tests::rendered` helper (`view.rs:664-680`) creates a fresh TestBackend for each isolated frame, so it cannot observe double-buffer/physical-screen desynchronization; current purge tests cover only gates/outcomes/removal (`model.rs:2733-2808`), and there is no stateful purge render test. Add a stateful TestBackend test: (a) draw PickExisting; (b) transition to and draw ConfirmPurge; (c) simulate the purge core's bottom-row newline with `Backend::append_lines(1)` on `terminal.backend_mut()` (or a tiny shared-screen test backend); (d) remove the row, set the flash and PickExisting; (e) execute the production post-inline-output invalidation/clear seam; (f) draw again; (g) compare the physical TestBackend buffer with the same final model rendered into a fresh cleared backend. Without the clear, the buffers differ because the physical screen scrolled while ratatui's previous logical buffer did not; with the clear they match. To ensure the test guards production rather than a test-only recipe, extract the small post-purge transition/invalidation operation into a generic `B: Backend` helper used by `event_loop`, or make the event loop backend/event source injectable.\n\nThe picker does NOT share the broker PTY/rc rendering path. Picker input and rendering are local crossterm events → pure `PickerModel` transitions → `view::render` → ratatui `Terminal<CrosstermBackend<Stdout>>` (`mod.rs:29-38, 93-119`). The picker restores raw/alternate-screen state before terminal outcomes are dispatched (`mod.rs:79-82`); only afterward can an Attach outcome enter the rc pump. In contrast, rc receives `AttachRecord::Output`, base64-decodes it, and writes bytes verbatim to stdout (`crates/spt/src/rc.rs:1965-1993`); a cold rc attach may receive the broker's synthesized `ScreenGrid::render_repaint` (`crates/spt-daemon/src/broker.rs:812-826, 1028-1053`; `crates/spt-term/src/screen.rs:1-15`). ADR-0031 explicitly scopes ScreenGrid to broker cold-attach repaint and leaves live bytes raw. Therefore picker purge artifacts and broker/rc whitespace loss may look alike but do not share a project render/diff/filter implementation; their only common endpoint is the user's terminal emulator.",
  "files": [
    {
      "path": "crates/spt/src/picker/mod.rs",
      "description": "Primary defect site. `setup_terminal` lines 95-105 enters alternate screen without an explicit clear; `event_loop` lines 116-174 draws via ratatui, then runs the stderr-printing purge core inline at 165-173 while the TUI remains active; key routing is at 183-309; terminal restoration happens only after the loop at 79-82 and 108-111."
    },
    {
      "path": "crates/spt/src/cli.rs",
      "description": "`cmd_endpoint_purge` lines 12707-12827 is the shared purge core and an out-of-band terminal writer. It emits refusal/warning/error diagnostics throughout and unconditionally emits `PURGED:{id}` on success at line 12825."
    },
    {
      "path": "crates/spt/src/picker/view.rs",
      "description": "Pure full-frame render path. Dispatcher at 89-103; PickExisting at 286-374; ConfirmPurge at 377-409. `Wrap { trim: true }` is prose wrapping, not physical blank suppression. Existing test helper at 664-680 renders one fresh frame, so it misses stateful diff desynchronization."
    },
    {
      "path": "crates/spt/src/picker/model.rs",
      "description": "Pure action path and existing coverage. Picker defaults at 879-935; purge gate/outcome/removal at 1155-1201; unit coverage at 2733-2808 proves semantic transitions but not terminal cleanup."
    },
    {
      "path": "crates/spt/src/rc.rs",
      "description": "Separate attach renderer. `pump` receives Output records and writes decoded bytes verbatim to stdout at 1965-1993; it is not used by the picker while the picker is active."
    },
    {
      "path": "crates/spt-daemon/src/broker.rs",
      "description": "Separate broker cold-attach path. `OutputLog::repaint_initial` at 812-826 and `become_controller` initial batch around 1028-1053 use `ScreenGrid::render_repaint`; no picker dependency."
    },
    {
      "path": "crates/spt-term/src/screen.rs",
      "description": "Broker-side VT grid documented at lines 1-15 and exposed through `ScreenGrid::render_repaint`; scoped to cold PTY attach, not picker rendering."
    },
    {
      "path": "docs/adr/0031-server-side-screen-grid-render-repaint.md",
      "description": "Architecture decision confirming ScreenGrid is the server-side current-screen model only for cold attach; live frames remain raw. Supports the finding that picker and rc/broker do not share a render/diff path."
    },
    {
      "path": "docs/NEXT-MILESTONE-BUG-TRIAGE.md",
      "description": "Historical D-#7 report at lines 79-82 and diagnosis at 120-128 concern PTY/rc raw-stream and marker artifacts, not ratatui picker cleanup; visually similar symptom, distinct implementation path."
    }
  ],
  "architecture": "Picker flow: crossterm key event → `handle_key`/`PickerModel` mutation → `view::render` fills ratatui's current Buffer → ratatui diffs previous/current buffers → CrosstermBackend emits absolute cursor moves and changed cell symbols to local stdout. The intended invariant is one writer owns the physical screen between draws. Purge breaks that invariant by running a CLI presenter which writes stderr while ratatui still considers its previous Buffer authoritative. rc/broker flow is disjoint: broker PTY bytes update `spt_term::ScreenGrid`; a cold attach replaces retained raw history with one ANSI repaint; attach protocol forwards Output records; rc decodes and writes bytes directly. No picker buffer, ratatui diff, or picker blank suppression participates in rc output."
}