Commit Graph

4 Commits

Author SHA1 Message Date
Levi Neuwirth 758b985c35
feat(protocol): the mapped panel family --- v25 wire shapes and their pins
SS5b's first implementation commit: the two appended variants, the
version constants, and the pins that hold them in place. No gating, no
key, no replay --- those are the next commits, and the variants are
REFUSED everywhere until their gate lands.

**APPENDED AT THE TRUE END, confirmed by the discriminants.**
`PanelPointer` is 15, `TextInput` 16, `PanelPointerMapped` **17**;
`Present` 0, `Absent` 1, `PresentMapped` **2**. "Beside `Present`" would
have been adjacent insertion, which shifts every discriminant below and
silently re-interprets an older peer's bytes. `mapping_generation` is a
`u64`, last within each variant, documented invalid at zero --- the
value a default-constructed sender produces, so accepting it would let
a peer opt out of the check by sending nothing.

**THE COMPILER NAMED EVERY SEAM.** Four non-exhaustive matches:
`semantic_render`'s declaration accessor now sees through both
families, and the three routing sites REFUSE the mapped variant rather
than unwrapping it to legacy meaning. Refusal is the correct default at
an intermediate commit, not a placeholder --- until the frontend can
prove it negotiated v25 it IS a `<= v24` peer for gating purposes, and
painting first would ship a window in which the band is hit-tested with
no mapping identity at all.

**Five mutations, each biting its own rows:**

  insert `PanelPointerMapped` before `TextInput`
      -> the TextInput pin and the mapped pin. `PanelPointer`'s v23 pin
         correctly SURVIVES: its discriminant did not move, which is the
         "only the pin whose discriminant moved fails" behaviour G0a
         specifies
  insert `PresentMapped` before `Absent`
      -> the Absent pin and the mapped-frame pin
  swap `geometry_epoch` / `panel_epoch`
      -> the exact-bytes assertion, while the round-trip stays green.
         That is the blind spot G0b exists for, and it is why every
         adjacent same-typed field carries a distinct value
  bump the wire version without extending the supported set
      -> both new tripwires and 1a's v6 ladder
  move `ADVERTISED_PROTOCOL_VERSION` to 25
      -> the baseline pin

**Version fallout, enumerated rather than discovered one gate at a
time.** Four acceptance-suite tripwires (`bottom_panel_stage2b_gpu`,
`discovery_stage2` x2, `vterm_stage3`, `statusline_segments`) each say
"a wire bump must be a conscious edit here" and each worked. Rather
than fix them one run at a time I grepped the tree for version
assertions and updated all four in one pass.

Review folded five further corrections, two of which fix reasoning of
mine that was wrong:

  - I claimed reversing `frame` and `mapping_generation` "fails to
    compile" because they are different types. **False for NAMED
    variant fields** --- the initializer uses names, so reordering the
    declarations compiles and shifts postcard's positional bytes
    silently. The pin is the only thing catching that.
  - Ladder loops now track `PROTOCOL_VERSION` while TRIPWIRES stay
    literal. I had flattened both to `25`. A tripwire is literal so a
    bump is a conscious edit; a ladder must move, or the next bump
    silently stops testing the top rung. G14b is unaffected ---
    `PANEL_MAPPING_MIN_VERSION` stays literal, because there the
    arithmetic is exactly the hazard.
  - `assert!(24 < MIN)` was a compile-time tautology holding for every
    value above 24. Replaced with the literal equality plus
    `assert_ne!` against `TEXT_INPUT_MIN_VERSION`: the mapped family
    must not share v24's gate, or it is admitted on sessions that
    negotiated only `TextInput`.
  - Statusline support loop reaches `PROTOCOL_VERSION`; public protocol
    history records v25.

**CI-red observations are in the LANE LEDGER, not the registry**, and
that is deliberate: `ci-red-signatures.md` here ends at U9 while the
unmerged replay branch already added a U10, so a row from this branch
would duplicate an id or invent one blind --- which this file's own
history records going wrong, two branches' entries merging "without a
conflict, producing duplicate ids across four sites". R7 twice and the
composition budget once, fragments verified, owed to the registry by
whichever branch merges second.

Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260815T103555Z. Four runs were needed; three were lost to those
two signatures, not to this diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-20 17:55:21 +02:00
Levi Neuwirth 431d844322 feat(bottom-panel): split the GPU document bottom into three boundaries
Bottom-panel Stage 2B-3, part 2 of 3: the pixel substrate for the band.

`text_area_bottom` was three boundaries wearing one name — its own doc
comment called it "the single source for every bottom-of-text
computation" — and once a band can be installed they must diverge:

  status_band_top          = max(0, height - status_band_height)
  geometry_capacity_bottom = max(0, status_band_top - divider_height)
  document_text_bottom     = max(0, status_band_top - installed_band)

The census is 29 matches: 20 production call sites, 1 definition, 8 test
sites. All 20 were read in their enclosing function and classified
individually — 8 status-owned, 12 document-owned. A blanket rewrite that
subtracted the band from all of them would move the status chrome with
the document and pass an "everything moved" assertion, which is why the
classification is per site and the criterion asserts both directions.

The three easiest to get wrong keep their named symptoms: document
completion placement is document-owned (status-owned would overlap the
band), minibuffer candidate clipping is status-owned (the minibuffer is
global bufferless chrome anchored to the band, and clipping it at the
document boundary would cut it off), and edge scrolling is document-owned
(left on the old bottom it would auto-scroll from inside the panel).

`geometry_capacity_bottom` reserves the divider even while the panel is
absent. That asymmetry is what breaks the first-open cycle: the daemon
sizes a panel from the capacity it was told about, so a capacity that
ignored the divider would grant a first panel that does not fit once the
divider appears beside it. The document loses no pixels until a `Present`
frame is really on screen.

`PanelBandInset` is a newtype, not an `f32`, because three boundaries here
take a pixel height and only one takes this one.

Alongside it, the band's own machinery: `PanelBand` with ONE derivation of
"is a panel on screen" (`presented()` — retained valid frame, matching
geometry epoch, latch clear), the frontend-owned epoch state machine with
its fail-closed exhaustion latch, the `Absent`-is-authoritative receipt
path, `panel_cell_capacity` (no per-axis cap — a panel may legitimately be
wider than a PTY — plus the daemon's virtual status row), the stable
normal-face probe for column count, and the divider strip whose paint rect
IS its hit rect.

`TerminalPaintPlan::build_grid` factors the shared cell planner so a panel
and a terminal cannot disagree about a wide-continuation pair; terminal
selection spans stay outside it rather than being faked as empty inside.

`PANEL_MIN_VERSION` moves into `pmacs-protocol` so the GPU frontend aliases
one definition instead of restating 21.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-29 18:30:57 -04:00
Levi Neuwirth 9b364adc26 fix(panel): review round 1 — buffer_id, the transport ratchet, shared bounds
P1 — `PanelPointer` was missing the approved `buffer_id`. Q#BP16 gives it
and `panel_epoch` different jobs and neither subsumes the other:
`buffer_id` catches an A->B buffer replacement, `panel_epoch` catches
close/hide/reopen of the SAME persistent buffer, which a buffer id alone
cannot see. Added in the framing's field order, with a pin asserting each
field independently reaches the wire.

P1 — added parent criterion 39's transport-safety ratchet. It builds the
maximum legal panel payload, asserts the fixture actually spends the whole
aggregate glyph budget (otherwise the ratchet measures something smaller
than the worst case), asserts one byte more is rejected, and pins the
encoded `InstanceMessage::PanelFrame` below `MAX_FRAME_BYTES`. Shaped
`1 x MAX_PANEL_VISIBLE_CELLS` deliberately: no per-axis cap makes that a
legal panel geometry a terminal cannot express, so it is the worst case
the terminal's own ratchet never measured. Bitten by tripling the glyph
budget — 30,342,696 bytes against the 16 MiB cap.

P2 — the shared bounds were duplicated literals. `MAX_TERMINAL_GRAPHEME_BYTES`
now aliases `MAX_WIRE_GRID_GRAPHEME_BYTES`, so the terminal screen's
truncation (`src/terminal/screen.rs:697`, `:777`) and the validator cannot
drift. Two more had the same defect and are aliased too:
`MAX_TERMINAL_VISIBLE_CELLS` and `MAX_TERMINAL_FRAME_GLYPH_BYTES`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 17:31:04 -04:00
Levi Neuwirth 640c5cd0d2 feat(panel): bottom-panel Stage 2B — the v21 protocol layer
Adds the four wire shapes Q#BP9 names, bumps the protocol to v21, and
factors the cell-grid validator so a panel frame shares the terminal's
rules without inheriting its PTY caps.

- `InstanceMessage::PanelFrame(PanelFramePayload)`, appended after
  `InitialTargetResult`; `Absent` is an explicit authoritative state, not
  silence, because the receiver retains its last valid frame.
- `FrontendEvent::{FrontendCellGeometry, PanelResizeRows, PanelPointer}`,
  appended after `TerminalPointer`. Geometry is valid without a side
  window — gating it on panel presence would deadlock the first open,
  since the daemon needs columns before it can paint a first frame.
- `pmacs-protocol/src/wire_grid.rs` holds the shared rules: checked area,
  visible-cell bound, cell count, cursor bounds, glyph legality,
  wide-continuation topology, the aggregate glyph budget, and the
  attachment rejection. The 512 per-axis caps, metadata, selection spans,
  and the at_bottom/scroll_offset coupling stay terminal-only.
- The attachment rejection is deliberately shared despite its
  terminal-side wording: panels render no attachments either, so sharing
  it fails closed for both.

Both byte pins were falsified by revert: moving `PanelFrame` ahead of
`InitialTargetResult` shifts it 27 -> 28 and fails; moving the three
events ahead of `TerminalPointer` shifts it 12 -> 15 and fails.

The factoring changed no terminal acceptance — all 17 terminal tests pass
unchanged. It did surface a pre-existing coverage gap: those tests pin
the row cap but never the column cap, so widening `max_cols` to u32::MAX
left them green. `a_panel_wider_than_512_columns_is_legal_while_a_terminal_is_not`
now covers that direction.

The daemon projection, the epoch state machine, and the GPU band are
later slices of this stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR
2026-07-26 16:52:54 -04:00