From ca816a1917eabe3017b6d8762be58864d499a146 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Fri, 14 Aug 2026 18:37:14 +0200 Subject: [PATCH] docs(framing): the panel cell-mapping generation (v25) --- SS5b revision 14 Own branch, own slice, protocol-bearing, runs alone. Framing only; no implementation. Blocks `panel-pointer-replay`, which blocks GUI arc 1b. Answers review of revision 13. Every item below reverses or completes something 13 got wrong. **GATING IS REFUSAL, NOT FALLBACK.** Revision 13 said a bare `PanelPointer` from a new peer would be "handled under the old semantics". That is a BYPASS: it leaves the exact hole this slice exists to close, reachable by omitting a field. A >= v25 session sending the legacy event is REFUSED before mutation, and a >= v25 frontend REJECTS a legacy `Present` rather than painting a band it cannot safely hit-test. Only negotiated <= v24 keeps legacy semantics; `Absent` stays common to both families. **ONE AUTHORITATIVE PER-FRONTEND KEY**, used by projection AND inbound validation, advanced after any mapping mutation and BEFORE the next inbound pointer is handled --- whether or not anything has rendered. Comparing against the last EMITTED frame recreates the hole, because a mutation not yet painted has still changed the inverse mapping. **STALE TAILS TERMINATE; THEY DO NOT VANISH.** A blanket drop breaks liveness: a refused `Up` leaves an empty document selection armed with a stale anchor, and leaves a reporting terminal child HOLDING A BUTTON FOREVER. Cancellation is now a ruled outcome --- producer latch reset, daemon selection and click-chain cleanup, and the child's release delivered at the last coordinate known good. A cancelled gesture is explicitly not a replayed one: the release is for liveness, and no selection or scroll effect is applied from the stale event. Stale BEGINNINGS may still simply drop. **THE DOMAIN WAS INCOMPLETE.** `view_left` is added, because 1b makes horizontal scrolling real. "Cursor movement is stable" is now CONDITIONAL: a cursor move that triggers vertical or horizontal follow changes `view_top` or `view_left` and therefore does change the mapping. Terminal panels are ruled explicitly --- their coordinates are decided by the SCREEN, so output and scrollback movement change the generation while their buffer revision does not. **SS5b HAD NO ACCEPTANCE MATRIX.** G1-G11 now cover the foreign edit before render, every changing and stable domain entry ROW BY ROW, a selection repaint that must preserve the generation and let a drag continue, mid-gesture cancellation, v24 and v25 positive controls with both wrong-family refusals, identical cells across a generation change still emitting, atomic retention of frame and generation on an invalid frame, and fail-closed exhaustion. The per-entry enumeration is deliberate: one aggregate row cannot show WHICH input moved the key, and a key ignoring `view_left` passes every vertical-only row. **MAPPED MOTION KEEPS ITS COALESCING TAGS.** A new variant falling through to the lossless default would put pixel-rate `Move`/`Drag` on a bounded queue. **PINS ACCUMULATE.** Revision 13 said the pin "moves", which would delete coverage of the shape it protects. `PanelPointer` is retained; exact `TextInput` bytes are added as the previous-final `FrontendEvent`; the complete nested `PanelFrame(Absent)` bytes are added as the previous-final `PanelFramePayload`. Recorded honestly: I could find NO exact-bytes pin for `PanelPointer` anywhere in the tree, though `pmacs-protocol/src/message.rs:524` says one is in the tests. Either my search missed it or the doc overclaims; this slice resolves it either way, since it must add exact pins regardless. **AND THIS SLICE OWNS THE VERSION CORRECTION.** `docs/gui-stage1-input-framing.md` now says 1e's `OpenTarget` is **v26**, with the reason stated at the top. An expected rebase conflict on `gui-stage1b-pointer-scroll` is not grounds for leaving the canonical document false --- which is what I argued last round, and it was wrong. `ADVERTISED_PROTOCOL_VERSION` stays pinned at 20. Gates: all ELEVEN green under `env -u TMPDIR`, with `--protocol` (`build-crdt`, `sweep-crdt`), log 20260814T162843Z. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 60 ++++++++++ docs/bottom-panel-framing.md | 190 +++++++++++++++++++++++++++++++ docs/gui-stage1-input-framing.md | 11 +- 3 files changed, 259 insertions(+), 2 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 6460507..3434850 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -270,6 +270,65 @@ hazard in a shape that looks committed. **A documented error message that never appears is worse than no documentation**, because the reader waits for a signal that is not coming. +## Panel cell-mapping generation (v25) — ACTIVE, framing only + +**Written with the branch's FIRST commit**, per the standing correction +from #171 and #215. + +- **Branch `panel-mapping-generation`**, base `githubsucks/main` @ + **`72da24a`**, worktree + `/home/jeans/Repos/personal/pmacs-mapping-gen`. + **`githubsucks/panel-mapping-generation` is the authoritative tip.** + Recover with `git fetch githubsucks && git checkout + panel-mapping-generation`. +- **No PR yet. Checkpoint: framing revision 14 (§5b) AWAITING + APPROVAL; NO IMPLEMENTATION.** +- **PROTOCOL-BEARING — v25, and it runs alone.** + `ADVERTISED_PROTOCOL_VERSION` stays pinned at **20**. +- **Why it exists.** A `PanelPointer` names a cell and nothing on the + wire says which inverse mapping the frontend saw, so the daemon + inverts against whatever is current. `buffer_id` catches + replacement, `panel_epoch` close/reopen, `geometry_epoch` a + declaration race — **nothing catches "the text under that cell + changed"**. Q#BP-R3 first accepted this as narrow; that was + **overruled**, because a **foreign** edit moves the mapping with + `view_top` untouched, the error is unbounded, and the window lasts + until the frontend **presents** the new frame. +- **A generation, not a per-frame token.** A token moving with the + frame would cancel a drag on the next repaint — the mistake + `panel_epoch` is stable to avoid. The key moves with the inverse + mapping (`view_top`, **`view_left`**, grid size, folds, wrap/gutter + geometry, buffer content, terminal output/scrollback) and holds + across focus, styling, selection-only repaints and cursor motion + **that the follow rules absorb**. +- **One authoritative per-frontend key**, used by projection AND + inbound validation, advanced after any mapping mutation and + **before** the next inbound pointer — comparing against the last + emitted frame recreates the hole. +- **Gating is REFUSAL, not fallback.** A ≥ v25 session sending bare + `PanelPointer` is refused; a ≥ v25 frontend rejects legacy + `Present`. Only ≤ v24 sessions keep legacy semantics; `Absent` is + common. +- **Stale tails TERMINATE, they do not vanish.** A dropped `Up` leaves + an empty selection armed and a reporting child holding a button + forever, so cancellation is a ruled outcome: producer latch reset, + daemon selection/click-chain cleanup, and the child's release + delivered. +- **Pins ACCUMULATE**: keep `PanelPointer`, add exact `TextInput` + bytes (previous-final `FrontendEvent`) and the complete nested + `PanelFrame(Absent)` bytes (previous-final `PanelFramePayload`). + **No exact-bytes pin for `PanelPointer` could be found in the tree** + despite `pmacs-protocol/src/message.rs:524` claiming one; this slice + resolves that either way. +- **Owns the version correction.** `docs/gui-stage1-input-framing.md` + moves 1e's `OpenTarget` to **v26** here — an expected rebase + conflict on `gui-stage1b-pointer-scroll` is not grounds for leaving + the canonical document false. +- **Chain: this slice → `panel-pointer-replay` (rebases onto it) → + GUI arc 1b.** +- **Gates:** the four `bottom_panel_*` suites, the GUI 1a wire suite, + `PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`, and **`--protocol`**. + ## GPU launcher / probe SIGINT teardown — MERGED as #241 (`f8033bc`) - **MERGED 2026-08-20** at approved head `5089715`, merge commit @@ -722,6 +781,7 @@ from #171 and #215. bite, direct-test diagnosis, unaffected foreground success, mutation, an otherwise unchanged gate, a distinct `error` outcome, and a non-Linux-unix statement. +||||||| parent of 8a4711e (docs(framing): the panel cell-mapping generation (v25) --- SS5b revision 14) ## `scripts/gate` TMPDIR isolation — PR #240 OPEN diff --git a/docs/bottom-panel-framing.md b/docs/bottom-panel-framing.md index 35abd22..2e98924 100644 --- a/docs/bottom-panel-framing.md +++ b/docs/bottom-panel-framing.md @@ -1770,6 +1770,196 @@ cannot preserve the old panel-focused attach leak. preserving the Stage 1 unknown-value rollback assertions; the Stage 3 PR then runs the full gate suite. +## 5b. The cell-mapping generation — a protocol slice (Q#BP-R3) + +**Status: revision 14 — AWAITING APPROVAL. Nothing implemented.** This +slice **blocks** panel-pointer replay (`panel-pointer-replay`), which +blocks GUI arc 1b. It is protocol-bearing and runs alone. + +### What it fixes + +A `PanelPointer` names a **cell**; the daemon must invert that to a +**byte**. Nothing on the wire says which inverse mapping the frontend +was looking at, so the daemon inverts against whatever is current — and +the mapping can move for reasons the clicking frontend neither caused +nor can observe. + +This is the one hole in the ladder: `buffer_id` catches replacement, +`panel_epoch` catches close/reopen, `geometry_epoch` catches a +declaration race, and **nothing catches "the text under that cell +changed"**. + +**Revision 12 accepted this as narrow. It is not.** A **foreign** edit +moves the mapping with `view_top` untouched; ticks, paging, folds, +edits and reloads accumulate without bound; and the window lasts until +the frontend **presents** the replacement frame, which a backed-up +frontend widens arbitrarily. + +### The generation, and why not a token + +**A per-frame token is the wrong object.** Panels repaint constantly, +and a token that moved with the frame would invalidate a live drag on +the next repaint — the mistake `panel_epoch` is stable to avoid. + +**`mapping_generation` identifies the INVERSE MAPPING.** + +| changes it | leaves it alone | +|---|---| +| `view_top` | focus gained or lost | +| **`view_left`** — 1b makes horizontal scrolling real | styling, theme, face changes | +| panel grid size | selection-only changes | +| fold state | a re-emitted identical frame | +| wrap mode, gutter geometry | **cursor motion that moves nothing else** | +| **buffer content — any edit, from any source** | | +| **terminal output or scrollback movement** (terminal panels) | **terminal buffer revision** (see below) | + +**"Cursor movement is stable" is CONDITIONAL, and revision 13 stated it +flatly.** A cursor move that triggers vertical or horizontal follow +changes `view_top` or `view_left`, and therefore **does** change the +generation. The stable case is a cursor move the follow rules absorb. + +**Terminal panels are ruled explicitly.** What a coordinate denotes +there is decided by the terminal's **screen**, so output and scrollback +movement change the generation. Their **buffer revision** does not — a +terminal buffer's revision counter tracks something else, and keying on +it would both miss real changes and fire on non-changes. + +**The stability half is load-bearing, not an optimisation.** A drag +provokes selection repaints on every motion; a generation that moved +with them would cancel the drag after one step. + +### Ownership and refresh timing + +**One authoritative per-frontend mapping key**, owned by the daemon, +**used by both projection and inbound validation**. Not two derivations +that agree by inspection. + +- It **advances after any mapping mutation and BEFORE the next inbound + pointer is handled**, whether or not a frame has been rendered or + emitted since. +- **Comparing against the last EMITTED frame recreates the hole.** A + mutation that has not yet been painted still changes the inverse + mapping, and a gesture arriving in that gap must be refused. +- Projection stamps the frame with the same key it validates against, + so "what the frontend was shown" and "what the daemon checks" cannot + drift. + +### Wire shape — appended variants, never widened + +Postcard encodes enums **positionally**, so a shipped variant's field +list is frozen. Both messages gain **new variants**: + +- `PanelFramePayload::PresentMapped { .. }` beside `Present`. +- `FrontendEvent::PanelPointerMapped { .. }` beside `PanelPointer`. + +`Absent` is unchanged and **common to both families** — hiding a band +carries no mapping. + +### Bilateral gating — REFUSAL, not fallback + +Revision 13 said an unmapped event from a new peer is "handled under +the old semantics". **That is a bypass**: it leaves the exact hole the +slice exists to close, reachable by omitting a field. + +| negotiated | daemon sends | daemon accepts | frontend sends | frontend accepts | +|---|---|---|---|---| +| **≤ v24** | `Present` | `PanelPointer` | `PanelPointer` | `Present` | +| **≥ v25** | `PresentMapped` | `PanelPointerMapped` **only** | `PanelPointerMapped` | `PresentMapped` **only** | + +- A **≥ v25 session sending bare `PanelPointer` is REFUSED**, dropped + before any mutation, exactly as an out-of-epoch event is. +- A **≥ v25 frontend receiving legacy `Present` REJECTS it** rather + than painting a band it cannot safely hit-test. +- Only a negotiated **≤ v24** session retains legacy semantics. +- `Absent` is accepted from either family. + +### Enforcement, and the liveness it must not break + +On `PanelPointerMapped`, the daemon compares the echoed generation with +the authoritative key and refuses the gesture before any mutation when +they differ. + +**But a blanket drop breaks liveness, and revision 13's did.** A +refused **tail** is not the same as a refused **beginning**: + +- **Stale BEGINNINGS may simply drop.** A `Down` that never took effect + leaves nothing behind. +- **Stale TAILS must TERMINATE the gesture, not vanish.** A dropped + `Up` leaves an empty document selection armed with a stale anchor, + and leaves a **reporting terminal child holding a button forever**. + +**Cancellation is therefore ruled as a first-class outcome:** + +1. **Producer:** the frontend resets its gesture latch — + `pointer_held`, `last_pointer_cell`, `gesture_last_content_cell` — + on the same signal, so no further `Drag` is manufactured. +2. **Daemon, document:** the panel's selection is cleared if empty, the + click chain is cleared, and no cursor move is applied. +3. **Daemon, terminal:** the child receives its **release** — a + reporting child must not be left holding a button because the + mapping moved — and the controller claim is settled. + +**A cancelled gesture is not a replayed one**: the release is delivered +for liveness, at the last coordinate known to be valid, and no +selection or scroll effect is applied from the stale event. + +### Motion must stay coalesced + +`PanelPointerMapped` carrying `Move` or `Drag` **takes the same +tail-coalescing tags as `PanelPointer`** (`pmacs-gpu/src/attach.rs:374` +onward). A new variant that fell through to the lossless default would +put pixel-rate motion on a bounded queue — the failure the coalescing +tags exist to prevent. `Down`/`Up`/wheel/context stay lossless and +ordered, as today. + +### Pins ACCUMULATE + +**They are not moved.** Revision 13 said the pin "moves to the previous +final variant", which would delete coverage of the shape it was +protecting. + +| pin | covers | +|---|---| +| existing `FrontendEvent::PanelPointer` | retained, unchanged | +| **new**: exact `FrontendEvent::TextInput` bytes | the previous-final `FrontendEvent`, appended by 1a and never pinned | +| **new**: complete nested bytes of `InstanceMessage::PanelFrame(PanelFramePayload::Absent)` | the previous-final `PanelFramePayload` variant | + +**A note on what exists today.** The panel protocol suite round-trips +these shapes and asserts `Absent` differs from an empty `Present` +(`tests/bottom_panel_stage2b_protocol_acceptance.rs:196`), but I could +find **no exact-bytes pin** for `PanelPointer`, despite +`pmacs-protocol/src/message.rs:524` stating one is in the tests. Either +it is somewhere my search missed, or the doc overclaims — **this slice +resolves it either way**, since it must add exact-byte pins regardless. + +### Acceptance and mutation matrix + +| # | row | mutation | +|---|---|---| +| G1 | a **foreign** edit before the next render → the old generation is refused | never advance on content change → G1 passes a stale hit | +| G2 | every **changing** entry of the domain table moves the generation, one row each | omit that entry from the key | +| G3 | every **stable** entry leaves it unchanged, one row each | include that entry → drags cancel on repaint | +| G4 | a **selection repaint** preserves the generation and an in-flight drag **continues** | as G3 | +| G5 | a mid-gesture mapping change **cancels**: latch reset, empty selection cleared, click chain cleared, **child receives its release** | drop the stale tail silently → the child holds the button and the selection stays armed | +| G6 | **v24 positive control** — a legacy session drives a panel gesture end to end | gate ≤ v24 off → old peers lose the panel | +| G7 | **v25 positive control** — a mapped session drives one end to end | — | +| G8 | **wrong-family refusals, both directions** — bare `PanelPointer` from a v25 session is refused; legacy `Present` at a v25 frontend is rejected | accept either → the bypass returns | +| G9 | **identical cells, changed generation, still emitted** — motion dedupe is per cell, and a mapping change makes the same cell a different byte | dedupe across a generation change → the second gesture is suppressed | +| G10 | an **invalid** mapped frame retains the previous frame **and** its generation, atomically | update one without the other → a valid generation names a frame never shown | +| G11 | **generation exhaustion fails CLOSED** — refuse gestures rather than accept unchecked ones | fail open → the hole returns at the boundary | + +G2 and G3 are enumerated per entry deliberately: one row asserting "the +generation changed" cannot show **which** input moved it, and a key +that ignores `view_left` passes every row that only scrolls +vertically. + +### Consequence for the GUI arc + +This slice takes **v25**, so GUI arc **1e's `OpenTarget` moves to +v26** — corrected in `docs/gui-stage1-input-framing.md` **by this +slice**, because a canonical document that says v25 is false the moment +this lands. `ADVERTISED_PROTOCOL_VERSION` stays pinned at **20**. + ## 6. Deferred (named) Left / right / top side windows; multiple slots per side; **rehoming a leaf diff --git a/docs/gui-stage1-input-framing.md b/docs/gui-stage1-input-framing.md index fd1a647..5bed92f 100644 --- a/docs/gui-stage1-input-framing.md +++ b/docs/gui-stage1-input-framing.md @@ -11,6 +11,13 @@ review overturned; revision 11 retracts it and P2 is implemented as written** (§6). **Q#S1-8, Q#S1-9 and Q#S1-10 are RULED.** **1-pre is IMPLEMENTED**; 1a onward may begin from this document. +**v26, not v25 — corrected by the panel mapping-generation slice.** +That slice (`docs/bottom-panel-framing.md` §5b) takes **v25** for +`PanelFramePayload::PresentMapped` / `FrontendEvent::PanelPointerMapped`, +and it lands ahead of 1e because panel-pointer replay blocks 1b. +Protocol slices stay serialized; one was inserted in front. +`ADVERTISED_PROTOCOL_VERSION` remains pinned at **20**. + **Verification base:** §2 is **re-measured at `4f77491`** (2026-08-12), the tip after 1-pre; it was originally taken at `a994f37`. Sections other than §2 were written against `a994f37` and their *rulings* are @@ -350,7 +357,7 @@ says nothing about glyphs or hit tests staying at scale 1. | D5 | Overlay clears on **empty `Preedit`, `Ime::Disabled`, and focus loss** — all three | no overlay | clear on focus loss only → `Disabled` row leaves stale text | | D6 | Dead-key state owned here; 1a buffers nothing | dead keys dropped | buffer in 1a → D6 by construction | -### 1e — `OpenTarget` (v25) +### 1e — `OpenTarget` (v26) | # | Contract | Witness (fails today because) | Mutation | |---|---|---|---| @@ -560,7 +567,7 @@ and flushed to the socket**. **Bound: 250 ms.** | | **1a — `TextInput`** | **1e — `OpenTarget` + `OpenTargetResult`** | |---|---|---| -| **floor** | **v24** | **v25**, after v24, serialized | +| **floor** | **v24** | **v26**, after v25's mapping generation, serialized | | **encoding** | **appended variant**; never widen a field in place — postcard is positional | appended variants | | **byte pin** | frozen-byte fixture on the **previous final variant** | same | | **gate** | daemon accepts from `>= 24`; producer withholds below | `>= 25`; producer withholds below |