docs(framing): fold the mapping-generation closure audit into revision 16

Close the producer, receiver, protocol-family, and gesture-lifecycle
cross-products in one pass. Separate route witnesses from replay effects,
freeze both old boundaries and new variant fields, and record the bounded
review method for the remaining chain.
This commit is contained in:
Levi Neuwirth 2026-08-15 11:36:24 +02:00
parent 137c7fc736
commit 775a046ce4
No known key found for this signature in database
2 changed files with 357 additions and 86 deletions

View File

@ -281,8 +281,8 @@ from #171 and #215.
**`githubsucks/panel-mapping-generation` is the authoritative tip.**
Recover with `git fetch githubsucks && git checkout
panel-mapping-generation`.
- **No PR yet. Checkpoint: framing revision 16 (§5b) AWAITING
APPROVAL; NO IMPLEMENTATION.**
- **No PR yet. Checkpoint: framing revision 16 (§5b) APPROVED
2026-08-15; 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
@ -294,6 +294,10 @@ from #171 and #215.
**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.
- **Semantic seam only.** The TUI hit-tests current daemon state
directly, receives no `PanelFrame`, and carries no mapping token. Its
existing panel click/drag/wheel effects are a structural control
against moving the generation gate into shared replay.
- **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
@ -308,35 +312,61 @@ from #171 and #215.
- **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.
common. The reciprocal wrong-family cases are witnessed too: a v24
daemon refuses `PanelPointerMapped`, and a v24 frontend rejects
`PresentMapped`. Compiling the variant in the same crate never
overrides the negotiated family.
- **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.
daemon selection cleanup, and the child's release delivered.
Click-chain invalidation is a separate mapping-identity transition;
it also runs when no gesture is held.
- **Pins ACCUMULATE**: keep `PanelPointer`, add exact `TextInput`
bytes (previous-final `FrontendEvent`) and the complete nested
`PanelFrame(Absent)` bytes (previous-final `PanelFramePayload`).
`PanelFrame(Absent)` bytes (previous-final `PanelFramePayload`), then
add exact encode/decode pins for both new mapped variants with
distinct same-typed field values. Previous-final pins protect append
position; new-variant pins protect their own frozen field order.
**The `PanelPointer` pin DOES exist** at `src/protocol.rs:1975` —
revision 14 recorded it as missing, which was wrong: it lives in the
root crate's test module, not under `pmacs-protocol/` or `tests/`.
- **Appended means LAST**: `PresentMapped` after `Absent`,
`PanelPointerMapped` after `TextInput`. `mapping_generation` is a
`u64`, **zero invalid**, gated by `PANEL_MAPPING_MIN_VERSION = 25`.
- **Frontend generation is a session high-water mark.** Higher values
may skip, equal-generation style/selection repaints are valid, lower
values are rejected atomically, and `Absent` does not erase the
high-water mark. Detach does.
- **Coordinate-free wheels are EXEMPT** from the freshness check —
otherwise the first tick advances the key and the next queued tick is
refused, so the panel scrolls once and dies. Child-reported terminal
wheels keep the check, because SGR carries row and column.
- **Cancellation is PROACTIVE** — the daemon cancels, THEN emits the
replacement frame, and the frontend **clears its latch on receiving
that frame and sends nothing**. Revision 15 asked the producer to
emit a tail or retain the latch; retaining it manufactures a `Drag`
under the new generation with no accepted `Down`.
- **The latch dies on EVERY loss of gesture authority**: generation
advance, `Absent`, panel/buffer identity change, geometry-epoch
change **even at an unchanged cell total**, and detach — **and on an
ordinary accepted `Up`**, or a later invalidation synthesises a
duplicate release.
- **Cancellation is PROACTIVE**, at the transition that revokes
authority: mapped-generation advance before its frame, before
daemon-authored `Absent` or panel/buffer replacement, when a new
geometry declaration is accepted, and at detach. The last four apply
to both v24 legacy and v25 mapped gestures. It is taken exactly once;
no producer cancellation tail is sent.
- **Producer reset has cause-specific signals**, not one generic
replacement frame: accept the valid generation/identity frame;
accept `Absent`; locally advance the geometry declaration; or tear
down the frontend. Invalid frames and same-identity repaints retain
the latch.
- **The accepted-gesture latch is left-button-only and common to both
wire families.** It arms only after accepted `Down(Left)` and dies on
accepted `Up(Left)` or any authority loss. Move, wheel, context, and
right/middle presses do not arm it; otherwise the GPU's press-only
right path manufactures a later release. A terminal Down also fixes
child-reporting versus local-selection domain through Drag/Up, so a
later Shift or reporting-mode change cannot orphan or invent a child
release.
- **Click-chain state is independent and per frontend.** It survives
an ordinary `Up` by design, but every mapping identity change clears
or re-keys it even when no gesture is held. A completed click followed
by a foreign edit and same-cell click is single, not double; an
intervening click from another frontend does not erase the first
frontend's legitimate chain.
- **Terminal identity EXCLUDES style.** Wire `Cell` equality covers
`glyph`, `style` and `attachment`
(`pmacs-protocol/src/cell.rs:153`), so keying on it would move the
@ -345,10 +375,34 @@ from #171 and #215.
- **The terminal key is NOT `Screen`'s generation** — that advances
from 39 sites including style, bell, tab-stops and cursor-only
motion, none of which change what a coordinate denotes.
- **G5's EFFECTS are owed by the rebased replay lane**, not provable
here: `gesture_last_content_cell` and replay exist only on
`panel-pointer-replay`. G5a — that the key advancing raises
cancellation — stays in this slice.
- **G5 is now an explicit transition matrix.** It crosses every common
authority loss with v24 and v25, distinguishes each producer reset
signal from invalid/same-identity controls, pins ordinary-Up
exact-once and left-only arming, and tests click identity without a
live gesture. This slice owns only the new mapping-generation signal
and mapped-frame state; the rebased replay lane owns the common
invalidations and every document, terminal, complete producer-latch
and click-chain effect through real call sites. No dead
classification helper is added on a base without replay.
- **Routing and effects are separate controls.** This slice can prove
v24/v25 events reach the existing focus path, but selection,
terminal reporting, stable-generation drag continuation, two-tick
wheels and exhaustion cancellation remain explicit replay-lane rows;
no classification-only result is reported as end-to-end input.
- **Review-process correction — carry into the next handoff
absorption.** Revision 16 replaces finding-at-a-time review with one
closure pass for this chain and future framings: verify cited ground
truth; enumerate trigger × protocol family × target × lifecycle;
separate producer, route, receiver and effect; prove every witness's
discriminating setup and positive control; check that each proposed
mutation is actually defective; and assign every row to the branch
that can reach its real call site. Protocol work also checks all four
wrong-family quadrants, exact pins and version fallout. Findings are
returned as one batch. If three revisions do not close a framing,
stop serial review and perform/fold this complete audit rather than
opening another one-finding round. The overdue #239/#240 handoff
absorption must promote this rule; this feature branch does not edit
the deliberately lagging canonical handoff.
- **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

View File

@ -1772,23 +1772,49 @@ cannot preserve the old panel-focused attach leak.
## 5b. The cell-mapping generation — a protocol slice (Q#BP-R3)
**Status: revision 16 — AWAITING APPROVAL. Nothing implemented.**
**Status: revision 16 — APPROVED 2026-08-15. Nothing implemented.**
Revision 16 answers review of 15; three of its items reverse a rule 15
introduced:
- **The producer rule contradicted proactive cancellation.** Since the
daemon cancels *before* emitting the replacement frame, the frontend
simply **clears its latch on receiving that frame and sends
nothing**. 15's "emit a tail or retain the latch" was redundant in
the first case and **actively harmful** in the second — retaining it
manufactures a `Drag` under the new generation with no accepted
`Down`, the exact orphan the section exists to prevent.
- **The mapped-generation producer rule contradicted proactive
cancellation.** Since the daemon cancels *before* emitting that
successor frame, the frontend simply **clears its latch on accepting
the frame and sends nothing**. 15's "emit a tail or retain the
latch" was redundant in the first case and **actively harmful** in
the second — retaining it manufactures a `Drag` under the new
generation with no accepted `Down`, the exact orphan the section
exists to prevent. Other authority losses have their own reset
signals below.
- **The latch had one trigger and needed five.** Cancellation runs on
every loss of gesture authority: generation advance, `Absent`, panel
or buffer identity change, geometry-epoch change **even at an
unchanged cell total**, and detach. **An ordinary accepted `Up` must
clear the latch**, or a later invalidation synthesises a duplicate
release.
epoch or buffer identity change, geometry-epoch change **even at an
unchanged cell total**, and detach. The last four apply to the legacy
and mapped families alike; generation advance exists only in the
mapped family. **An ordinary accepted `Up(Left)` must clear the
latch**, or a later invalidation synthesises a duplicate release.
- **"The replacement frame" was not one producer signal.** A mapped
generation or panel/buffer replacement clears on atomic acceptance of
its valid frame; `Absent` clears when accepted; a local geometry
declaration and detach clear at their own transition, before any
replacement frame can be required. Invalid frame payloads and
same-identity repaints retain the latch.
- **Gesture liveness and click counting are different state.** The
accepted-gesture latch covers an accepted `Down(Left)` only and is
consumed by `Up(Left)` or loss of authority. `MouseClickState`
deliberately survives an ordinary `Up` for multi-click detection,
but its mapping identity must be cleared or re-keyed on every mapping
invalidation even when no gesture is held.
- **Routing controls had been written as effects.** This base can prove
legacy and mapped events reach the already-landed focus path; it
cannot prove selection, terminal reporting, drag continuation,
two-tick scrolling, or exhaustion cleanup before replay exists.
Those controls are split, and the effect halves are obligations of
the rebased replay lane rather than claims made by this slice.
- **Bilateral gating needed all four wrong-family legs.** The v25
refusal rows did not constrain a v24 session sending or accepting the
mapped variants. Same-crate peers can encode them regardless of what
they negotiated, so both inbound and outbound are now witnessed at
v24 and v25, with exact-family positive controls.
- **G9b's mutation was a valid implementation.** Keying dedupe by
`(mapping_generation, coord)` is a correct design, so requiring it to
fail would have forbidden a good one. Replaced with two real defects.
@ -1799,6 +1825,13 @@ introduced:
- Zero-generation rows added **in both directions**; G7 split into
outbound and inbound legs; G2's grid and fold composites split;
journey steps **5** and **8** named in §20.
- **The folded closure pass made the remaining implicit contracts
executable in one matrix:** authenticated-session family authority;
a frontend generation high-water mark surviving `Absent`; exact pins
for the new variants as well as their previous finals; left-only and
fixed-domain terminal gestures; exact-once coincident invalidations;
independent click identity; the semantic/TUI boundary; and explicit
ownership of every route versus effect row (G0–G15).
**Previously, revision 15 — SUPERSEDED.**
Revision 15 answers review of 14, and four of its items reverse
@ -1848,6 +1881,15 @@ This is the one hole in the ladder: `buffer_id` catches replacement,
declaration race, and **nothing catches "the text under that cell
changed"**.
The hole exists only across the **semantic frontend's asynchronous
projection/inbound seam**. The TUI hit-tests and dispatches against its
current daemon state in one process; it neither receives `PanelFrame`
nor echoes a mapping generation. This slice adds no TUI token, gate or
gesture state, and the existing direct TUI panel effects remain a
regression control. Applying the mapped-family check inside the shared
replay body instead of at the semantic-session boundary would break
that control.
**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
@ -1989,6 +2031,14 @@ future reader can see that nothing was reordered.
default-constructed or partially-initialised sender produces, and
accepting it would let a peer opt out of the check by sending
nothing. A live key starts at 1.
- It is **monotonic for the authenticated frontend session**. A
frontend accepts a valid generation above its retained high-water
mark (gaps are fine), and accepts an equal-generation repaint because
styling, focus and selection may legitimately change while the
mapping holds. It rejects a lower generation atomically. `Absent`
clears the presentation and pointer state but **retains the
high-water mark**; otherwise a delayed pre-`Absent` frame can roll
authority backward. Detach ends the session and its high-water mark.
- **`PANEL_MAPPING_MIN_VERSION = 25`**, beside the existing family
constants, and the gate reads that constant rather than a literal.
@ -2006,6 +2056,14 @@ slice exists to close, reachable by omitting a field.
| **≤ v24** | `Present` | `PanelPointer` | `PanelPointer` | `Present` |
| **≥ v25** | `PresentMapped` | `PanelPointerMapped` **only** | `PanelPointerMapped` | `PresentMapped` **only** |
Both choices use the **authenticated session's negotiated version**.
Inbound code never derives the family gate from the payload's claimed
`frontend_id`; like every inbound variant, that field is untrusted and
is validated separately. Outbound projection uses the destination
session's negotiated version, not the crate's compile-time ability to
encode either discriminant. The family gate runs before focus,
controller, click-chain, gesture-latch or document mutation.
- 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
@ -2045,6 +2103,13 @@ row and column into the child's input stream, so a stale one aims an
application action at a cell the user never pointed at — the same
hazard as a stale click, and it keeps the check.
The exemption skips **only generation equality**. It does not bypass
the authenticated family gate, the nonzero-generation requirement,
frontend/panel/geometry/buffer identity, coordinate bounds, or target
resolution. In particular, a zero-generation coordinate-free wheel is
still refused; zero means "no mapping identity was supplied", not "one
was supplied and has since gone stale".
**Witnesses: two ticks under ONE generation.** A document wheel and a
non-reporting terminal wheel must each scroll **twice** when the second
event carries the generation the first invalidated. Without a
@ -2071,20 +2136,37 @@ own `Up`**, and the daemon is never told anything — so the selection
stays armed and the child keeps holding its button. **The cancelling
event never arrives.**
So the trigger is the authoritative key advancing while a gesture is
accepted:
So the daemon cancels at the transition that revokes authority, not at
the next pointer event:
1. **Daemon, on losing gesture authority with a gesture accepted** —
this runs whether or not any further event arrives:
- **document:** clear an empty selection, clear the click chain,
apply no cursor move;
- **terminal:** deliver the child's **release**, at the last
coordinate known valid, with the button and encoding the accepted
`Down` used.
2. **Producer: clear the local latch on receiving the replacement
frame, and send nothing.** The daemon has already cancelled, ahead
of emitting that frame, so the gesture is over before the frontend
hears about it.
| authority loss | daemon cancellation point | protocol families |
|---|---|---|
| mapping generation advances | after the mapping mutation and **before** publishing the next `PresentMapped` | mapped, ≥ v25 only |
| `Absent` | **before** publishing `Absent` | legacy and mapped |
| panel epoch or buffer identity changes | before publishing the successor `Present`/`PresentMapped` | legacy and mapped |
| geometry epoch changes | when the new frontend geometry declaration is accepted, even when its rows and columns equal the old declaration | legacy and mapped |
| detach | in that frontend's teardown, before its pointer state is discarded | legacy and mapped |
If an accepted left-button gesture is live, cancellation is taken
**exactly once**:
- **document:** clear an empty selection, preserve an already-nonempty
region, and apply no cursor move;
- **terminal:** deliver the child's **release**, at the last coordinate
known valid, with the left button and encoding the accepted press
used.
The click chain is invalidated at the same authority transitions but is
**not conditional on a live gesture**; its lifetime is specified
separately below.
Authority losses may coincide. A changed-size geometry declaration, for
example, advances both `geometry_epoch` and the mapping generation;
buffer/panel replacement can also change the projected mapping. All
causes take the **same** accepted-gesture latch, so the first
cancellation settles it and later causes observe no live gesture.
Click-chain invalidation is likewise idempotent. Trigger ordering may
not create two releases or two selection cleanups.
**Revision 15 asked the producer to emit a cancellation tail or retain
the latch. Both are wrong, and the second is actively harmful.** A tail
@ -2095,13 +2177,31 @@ manufactures a `Drag` under the NEW generation with no accepted
`Down`** — precisely the orphan this whole section is about, created by
the rule meant to avoid it.
**Ordering is what makes the simple rule safe.** Cancel, then emit. The
frontend's arrival of the replacement frame *is* the cancellation
signal, so no second channel is needed.
**There is no single producer-side "replacement frame" transition.**
The producer clears its local held-gesture state at the first
authoritative signal available for the cause:
**Witness:** `Down` → the key advances → the replacement frame lands →
subsequent motion and the physical `Up` produce **no new drag and no
duplicate release**.
| cause | producer reset point |
|---|---|
| mapped generation changes | atomically accepting the valid successor `PresentMapped` |
| panel epoch or buffer identity changes | atomically accepting the valid successor `Present`/`PresentMapped` |
| `Absent` | accepting `Absent` |
| geometry epoch changes | locally, when `next_geometry_declaration` advances the epoch (`pmacs-gpu/src/main.rs:6847`), **before** sending the declaration |
| detach | frontend teardown |
An invalid frame and a repaint with the same generation and identities
**do not clear** the producer latch. A local geometry declaration is
different: advancing the local epoch has already revoked the old grid,
so the producer clears before the daemon can accept or refuse the
declaration. For the daemon-authored frame cases, ordering is cancel
then emit; the frame's arrival is the producer's signal, so no second
channel is needed. For geometry and detach, the local transition is
itself the signal and no replacement frame is required.
After any reset, subsequent motion and the physical `Up` produce **no
new drag and no duplicate release**. The producer never emits a
cancellation tail and never carries the held state into the successor
identity.
##### The latch's full lifecycle
@ -2113,7 +2213,7 @@ the rest to be inferred:
|---|---|
| **mapping generation advances** | the cells mean different bytes |
| **`Absent`** | the band the gesture belongs to is gone |
| **panel or buffer identity change** | the successor never saw the press |
| **panel epoch or buffer identity change** | the successor never saw the press |
| **geometry-epoch change** | a new declaration, **even when the cell total is unchanged** — the frontend re-declared, so the grid it hit-tested against is not the one in force |
| **detach** | there is no frontend left to finish the gesture |
@ -2124,14 +2224,40 @@ the same orphan race the replay lane's D1/D2 producer resets expose,
arriving from the daemon's side instead of the frontend's.
**The daemon needs an ACCEPTED-GESTURE LATCH** to do any of this, and
revision 14 assumed state that does not exist. Per frontend it records:
revision 14 assumed state that does not exist. The latch is common to
legacy `PanelPointer` and mapped `PanelPointerMapped`, because four of
the five authority losses are common. Per frontend it records:
- **whether a `Down` was ACCEPTED**, and for a terminal whether it
actually reached the child;
- **the last valid coordinate**, the **button**, and the **encoding**
the child was told — a release must match the press it terminates;
- whether a **`Down(Left)` was ACCEPTED**, and for a terminal whether
it actually reached the child;
- the last valid coordinate and the encoding the child was told — a
release must match the press it terminates;
- enough identity to know the gesture is still the one that began.
It arms only **after** `Down(Left)` is accepted, updates its last valid
coordinate on an accepted drag, and is taken by either an ordinary
accepted `Up(Left)` or one authority-loss transition. `Move`, wheel,
context, and right/middle press events never arm it. `MouseKind` has
right and middle `Down` variants (`pmacs-protocol/src/message.rs:220`),
and the GPU panel path currently emits `Down(Right)` from the
press-only `apply_right_press` route
(`pmacs-gpu/src/main.rs:2997`, `:3029`). Treating every `Down` as held
would therefore manufacture a delayed release at the next
invalidation. If right/middle release semantics are added, they need
their own complete producer/replay contract rather than silently
borrowing this left-drag latch.
For a terminal, the accepted `Down(Left)` also fixes the gesture's
**domain** until it ends: child-reported, or local terminal selection.
Shift precedence and the child's reporting mode decide the domain at
the beginning; later modifier or mode changes do not switch a live
gesture into a path that never saw its Down. Thus a child-reported
gesture sends Drag/Up using the recorded reporting contract and exact
release encoding even if reporting turns off or Shift is pressed, and
a locally-started gesture stays local even if Shift is released or
reporting turns on. This does not freeze wheel precedence: a wheel is
self-contained and never arms the gesture latch.
Two consequences fall out of the latch and are ruled here:
- **A stale `Up` with no accepted `Down` is INERT.** It terminates
@ -2142,10 +2268,53 @@ Two consequences fall out of the latch and are ruled here:
the claim is not taken back. Reclaiming would make a stale gesture
steal a live one's terminal.
Detach has an additional ordering constraint: if the departing
frontend still owns the controller, its child's matching release is
delivered **before** normal detach teardown releases that claim and
discards the gesture record. If another frontend owns it, the release
still settles the recorded gesture without changing that newer claim.
Clearing controller/gesture state first would make the teardown path
look clean while leaving the child button down.
**A cancelled gesture is not a replayed one**: the release is delivered
for liveness, and no selection or scroll effect is applied from the
stale event.
##### Click-chain identity is independent
`MouseClickState` (`src/editor.rs:352`) is **not** the
accepted-gesture latch. An ordinary
`Up(Left)` consumes the gesture latch, while click state intentionally
survives long enough to recognize a second click. Consequently, only
clearing click state from the cancellation path is insufficient: after
a completed click there may be no live gesture to cancel.
The click chain therefore carries, or is cleared against, the complete
mapping identity: frontend, target window and buffer, panel epoch,
geometry epoch, and — for mapped sessions — mapping generation. Every
authority transition in the table above invalidates it independently
of the gesture latch. A completed click followed by a foreign edit and
the successor mapped frame, then a `Down` at the same cell inside the
multi-click interval, is a **single** click under the new mapping, not
a double-click on unrelated text. The same rule holds for the common
legacy identity transitions even though v24 has no mapping generation.
This extends rather than replaces Q#M5 and the existing TUI event
lifetime: only an **unmodified `Down(Left)`** writes click state;
Shift-Down never enters the double-click chain. Drag, Move, wheel and
right/context gestures clear it, while ordinary `Up(Left)` and a
same-identity repaint preserve it. Mapping invalidation is an
additional clear, not permission to couple click state to every frame
or to the accepted-gesture latch.
Semantic click chains are **keyed per frontend**, then carry the target
identity above. Merely storing `frontend_id` inside one global slot
prevents a false cross-frontend double-click but lets B's click erase
A's legitimate chain. The adjacent `window_drag` comment already names
that exact defect and uses a `HashMap<FrontendId, ..>` because
concurrent frontend gestures are legal (`src/editor.rs:298`). Detach
removes only the departing frontend's entry.
### Motion must stay coalesced
`PanelPointerMapped` carrying `Move` or `Drag` **takes the same
@ -2166,6 +2335,8 @@ protecting.
| 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 |
| **new**: exact `FrontendEvent::PanelPointerMapped` bytes, encoded **and decoded**, with distinct values in every same-typed field | the new discriminant and its frozen field order; a same-type field swap still round-trips against itself, so round-trip alone is insufficient |
| **new**: complete nested bytes of `InstanceMessage::PanelFrame(PanelFramePayload::PresentMapped { .. })`, encoded **and decoded** | the new payload discriminant, nesting and `frame`-then-generation order |
**Correction: the `PanelPointer` pin DOES exist**, at
`src/protocol.rs:1975`
@ -2173,27 +2344,66 @@ protecting.
14 said it could not be found. It is in the **root crate's** test
module rather than under `pmacs-protocol/` or `tests/`, which is
exactly where the search did not look. `message.rs:524` was right and
the doubt was mine. The pin is **retained**, and the two new pins are
added beside it.
the doubt was mine. The pin is **retained**, and the four new pins are
added beside it. The previous-final pins catch an insertion on the
wrong side of the append boundary; the new-variant pins catch
reordering inside the variants themselves. Neither substitutes for the
other.
### Acceptance and mutation matrix
| # | row | mutation |
|---|---|---|
| G0a | the existing `PanelPointer` pin and the new exact previous-final `TextInput` and nested `PanelFrame(Absent)` pins all remain byte-identical | insert a wedge before each protected previous-final variant → only the pin whose discriminant moved fails |
| G0b | exact encode **and decode** bytes for `PanelPointerMapped` and nested `PresentMapped`, using unequal values for every adjacent same-typed field | reorder two `u64` fields or reverse `frame`/generation → a self-consistent round-trip stays green but the exact pin fails |
| 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 moves the key, one row each — and the composites are **split**: `view_top`; `view_left`; grid **rows** and **columns** separately; **fold-map content** and the owning frontend's **`fold_projection` policy** separately; **wrap** and **gutter geometry** separately; buffer content; terminal **projected content** and **scrollback movement** separately | omit that one entry from the key |
| G3 | every **stable** entry leaves it unchanged, one row each — focus; styling; selection-only; absorbed cursor motion; and the terminal controls **style, title, bell, tab-stop, cursor-only** | include that one entry → drags cancel on a repaint |
| G4 | a **selection repaint** preserves the generation and an in-flight drag **continues** | as G3 |
| G5a | the key advancing with an accepted gesture live **raises cancellation** — the daemon acts without waiting for another event | make cancellation reactive to a refused event → nothing happens when the frame lands before the physical `Up` |
| G5b | a stale `Up` with **no accepted `Down`** is **inert** | synthesise a release anyway → an unpressed button is released |
| G5c | cancellation **does not reclaim** a controller another frontend has since taken | reclaim it → a stale gesture steals a live one's terminal |
| G6 | **v24 positive control** — a legacy session drives a panel gesture end to end | gate ≤ v24 off → old peers lose the panel |
| G7a | **v25 positive control, OUTBOUND** — a v25 peer receives `PresentMapped`, carrying a live generation | withhold `PresentMapped` from a v25 peer → the mapped frame never reaches it |
| G7b | **v25 positive control, INBOUND** — a `PanelPointerMapped` with the current generation is **accepted and takes effect** | ignore the mapped variant inbound → every refusal row still passes while nothing works |
| G8a | a **bare `PanelPointer` from a ≥ v25 session is REFUSED** | accept it → the bypass returns inbound |
| G8b | a **legacy `Present` at a ≥ v25 frontend is REJECTED** | paint it → the frontend hit-tests a band it cannot map |
| G4a | a **selection repaint** preserves the generation | as G3 |
| G4b | after that repaint, an in-flight drag reaches real replay and **continues** | route only or clear the latch despite the stable generation → G4a stays green while the gesture dies |
| G5a | **mapped generation**: the key advancing with an accepted left gesture live raises cancellation before the successor frame, without waiting for another pointer event | make cancellation reactive to a refused event → nothing happens when the frame lands before the physical `Up` |
| G5b | **table-driven common authority-loss matrix**: `Absent`, panel-epoch change, buffer replacement, same-size geometry-epoch change, and detach each cancel a live left gesture; every entry has legacy-v24 and mapped-v25 legs, and the rebased effect matrix applies each to a document and a reporting terminal | omit that transition, arm the latch only for the mapped family, or cancel only one target kind → its named leg leaves the gesture live |
| G5c | an ordinary accepted `Down(Left)` → `Up(Left)` consumes the latch; a later authority loss emits **no second** document cleanup or child release, with v24 same-size geometry and v25 generation legs | leave the latch armed after `Up` → the later transition duplicates the release |
| G5d | a stale `Up` with **no accepted `Down`** is **inert** | synthesise a release anyway → an unpressed button is released |
| G5e | cancellation **does not reclaim** a controller another frontend has since taken | reclaim it → a stale gesture steals a live one's terminal |
| G5f | **producer reset matrix**: mapped generation and panel/buffer replacement clear only on atomic acceptance of the valid successor frame; `Absent` clears on acceptance; same-size geometry clears at local declaration; detach clears in teardown; structurally invalid, stale, zero-generation and wrong-family frames plus same-identity repaints retain | funnel every reset through frame arrival → geometry/detach stay armed; clear before validation or on every repaint → the negative controls lose a valid drag |
| G5g | only an accepted **`Down(Left)`** arms cancellation; table-driven Move, wheel, context and right/middle controls followed by an authority loss emit no synthetic release | arm on every `Down` or every accepted pointer event → the press-only right path or another non-gesture event manufactures a delayed release |
| G5h | **click-chain identity is independent**: complete a click, then independently drive `Absent`→Present, panel-epoch change, buffer replacement, same-size geometry change, and detach→reattach with no live gesture; a same-cell click inside the interval is single. A foreign content edit plus successor generation is the additional mapped-v25 leg | clear/re-key `MouseClickState` only while cancelling a held gesture, or omit one identity → unrelated text is treated as the second/third click |
| G5i | reporting-terminal detach delivers the recorded child release before dropping the old gesture/controller state, then leaves no controller claim owned by the detached frontend; a newer owner's claim remains untouched | tear down controller or gesture state before cancellation → no release bytes; reclaim/release unconditionally → the newer owner is disturbed |
| G5j | document cancellation has two legs: Down with no accepted Drag clears the empty selection; Down plus accepted Drag preserves the nonempty region's exact anchor/cursor while ending the gesture | clear every selection → the user's completed range disappears; preserve every selection → the empty armed anchor captures later motion |
| G5k | **terminal gesture-domain matrix**: child-reported Down then (a) reporting off or (b) Shift held before Drag/Up still yields child Drag/release in the recorded encoding and no local selection; local Down via (c) Shift or (d) reporting-off then the condition reverses still finishes locally with no child tail | re-evaluate Shift/reporting independently on every event → the child keeps a button down or receives an Up for a Down it never saw |
| G5l | **click-chain stability control**: complete a click, accept a same-generation style/selection repaint with every identity unchanged, then click the same cell inside the interval → it remains the second click | clear click state on every frame or couple it to the ordinary-Up gesture-latch clear → multi-click never survives a harmless repaint/release |
| G5m | **coincident invalidations cancel once**: changed-size geometry (geometry epoch + mapping generation) and panel/buffer replacement that also changes the mapping each produce exactly one document cleanup/child release | keep per-cause live flags or inspect without taking the shared latch → one transition emits duplicate tails |
| G5n | **click event-lifetime table**: unmodified left Down writes; Shift-Down does not; Drag, Move, wheel and right/context clear; ordinary left Up preserves | write on Shift-Down or omit one clear arm → Q#M5 or the successor click is misclassified; clear on Up → every double-click becomes two singles |
| G5o | **per-frontend click ownership**: A click, B click, then A same-cell click inside A's interval still produces A's double-click, while detaching B removes only B's chain | keep one global `Option<MouseClickState>` or clear the whole map on detach → B erases A's valid sequence |
| G5p | **per-frontend gesture ownership**: A and B hold gestures on distinct panels; B's panel-epoch change or detach cancels B exactly once while A's next valid Drag still applies | keep one global accepted-gesture latch or clear the whole map on one frontend transition → B's lifecycle cancels or erases A |
| G6a | **v24 positive control, OUTBOUND** — a v24 peer receives exactly legacy `Present`, never `PresentMapped` | send the mapped family or both → an old negotiated session receives a discriminant it did not declare |
| G6b | **v24 routing control, INBOUND** — a current legacy `Down(Left)` reaches the semantic dispatcher and performs the already-landed focus activation | gate v24 off or drop after decoding → the old peer's event is mapped but inert |
| G6c | **v24 effect control after replay rebase** — the same event performs its document/terminal replay effect | keep only the pre-existing focus path → G6b stays green while replay is dead |
| G7a | **v25 positive control, OUTBOUND** — a v25 peer receives exactly `PresentMapped`, carrying a live generation, never legacy `Present` | withhold `PresentMapped`, send legacy, or send both → the mapped frame contract is absent or ambiguous |
| G7b | **v25 routing control, INBOUND** — a current `PanelPointerMapped` carrying `Down(Left)` reaches the semantic dispatcher and performs the already-landed focus activation | ignore the mapped variant inbound → every refusal row still passes while nothing routes |
| G7c | **v25 effect control after replay rebase** — that current mapped event performs its document/terminal replay effect | stop after focus → G7b stays green while replay is dead |
| G8a | a **bare `PanelPointer` from a ≥ v25 session is REFUSED before mutation** | accept it → the bypass returns inbound |
| G8b | a **legacy `Present` at a ≥ v25 frontend is REJECTED**, atomically retaining its mapped frame, generation and pointer state | paint it or clear state first → the frontend hit-tests a band it cannot map or loses a valid gesture |
| G8c | a **`PanelPointerMapped` from a ≤ v24 session is REFUSED before mutation**, even though a peer compiled from this crate can encode it | accept it → inbound negotiation is treated as a sender convention rather than a gate |
| G8d | a **`PresentMapped` at a ≤ v24 frontend is REJECTED**, retaining the previous legacy frame atomically | paint it → outbound negotiation is treated as a sender convention rather than a gate |
| G8e | **authenticated-session authority**: in both inbound wrong-family cases, claiming another frontend whose session negotiated the desired family still refuses | gate by the payload's `frontend_id` lookup → the forged cross-session claim bypasses negotiation |
| G9a | **daemon emission**: identical cells across a generation change are still **emitted** | dedupe daemon-side across the change → the second gesture is suppressed |
| G9b | **frontend motion dedupe** suppresses a repeat within one generation and **re-arms across one** | (a) compare **only the cell**, never keying or resetting by generation → the first post-change motion is **eaten**; (b) reset on **every same-generation repaint** → repeated same-cell motion **returns** as pixel-rate traffic |
| G9c | **frontend acceptance**: a byte-identical `PanelFrame` with a higher generation still updates the high-water/current generation and resets pointer state atomically; it may report no visual redraw | return early on frame equality before applying generation → G9a passes because the message arrived, but the producer keeps echoing stale authority |
| 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 |
| G10a | a **structurally valid `PresentMapped` carrying generation ZERO is REJECTED**, atomically — previous frame and generation both retained | accept zero → a sender that never initialised the field disables the check for the whole session |
| G10b | a **`PanelPointerMapped` carrying generation ZERO is REFUSED** before mutation, including a coordinate-free document and local-terminal wheel | apply the wheel exemption before the nonzero check → the inbound opt-out returns through the exempt path |
| G10c | mapped-frame generation is nondecreasing: a higher generation may skip values, an equal-generation style/selection repaint is accepted, and a lower generation is rejected atomically; after `PresentMapped(9)` → `Absent`, a valid `PresentMapped(8)` is still rejected | require only nonzero, or clear the high-water mark on `Absent` → a delayed frame rolls the producer's authority backward |
| G11a | **generation exhaustion fails CLOSED, and does not leave a zombie band**: the daemon publishes `Absent`, clears input authority, and latches exhaustion for the session | fail open → the hole returns at the boundary; refuse input only → a stale panel stays painted and permanently inert; omit the latch → the next frame resurrects it |
| G11b | exhaustion with an accepted left gesture also performs the ordinary `Absent` cancellation effects **before** publishing it | publish `Absent` and discard authority without replay cancellation → G11a stays green while the gesture is orphaned |
| G12a | one mapped generation carrying **two document wheel ticks** applies both window-level scrolls even though the first changes the mapping | check every mapped coordinate indiscriminately → only the first tick applies |
| G12b | one mapped generation carrying **two non-reporting terminal wheel ticks** applies both local-scrollback moves even though the first changes the view anchor | remove the terminal coordinate-free exemption → only the first tick applies |
| G13a | mapped `Move` and `Drag` retain their respective tail-coalescing behaviour, including replacement by the latest coordinates/generation without crossing an intervening lossless event | let the new variant fall through to the default → pixel-rate traffic fills the bounded queue |
| G13b | mapped press, release and all wheel kinds remain lossless and ordered | coalesce every mapped pointer → multi-click, release or wheel ticks silently disappear |
| G14a | version tripwire: `PROTOCOL_VERSION == 25`, `SUPPORTED_PROTOCOL_VERSIONS` reaches it, `PANEL_MAPPING_MIN_VERSION == 25`, and `ADVERTISED_PROTOCOL_VERSION == 20` remains pinned | bump only the wire constant or move the advertised baseline → self-handshake fails or the permanent baseline drifts |
| G14b | the supported set accepts every version through `PROTOCOL_VERSION`, rejects `PROTOCOL_VERSION + 1`, and the literal boundary routes v24 legacy versus v25 mapped | end the set at 24 or express the boundary arithmetically against a moving ceiling → the current wire is unsupported or a later bump silently moves this feature |
| G15 | **TUI structural control** — the existing direct panel click/drag/wheel paths keep their effects with no generation token | move the generation requirement into shared replay instead of the authenticated semantic boundary → local TUI input is refused |
**G9b's mutation in revision 15 was a valid implementation, not a
defect.** Keying the dedupe by `(mapping_generation, coord)` preserves
@ -2201,26 +2411,32 @@ same-generation suppression *and* naturally admits the first motion
under a new generation — it is one correct way to satisfy the row, so
requiring it to fail would have forbidden a good design. The two
mutations above are actual defects, one in each direction.
| 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 |
| G10a | a **structurally valid `PresentMapped` carrying generation ZERO is REJECTED**, atomically — previous frame and generation both retained | accept zero → a sender that never initialised the field disables the check for the whole session |
| G10b | a **`PanelPointerMapped` carrying generation ZERO is REFUSED** before mutation | accept zero → the inbound half of the same opt-out |
G10a and G10b are **independent rows in opposite directions**, not one
row seen twice: zero can arrive from either peer, and G10's atomicity
row exercises neither.
| G11 | **generation exhaustion fails CLOSED, and does not leave a zombie band**: the daemon publishes **`Absent`**, clears input authority, **cancels any accepted gesture**, and **latches** exhaustion for the session | fail open → the hole returns at the boundary; refuse input only → a stale panel stays painted and permanently inert, with no signal to the user; omit the latch → the next frame resurrects it |
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.
The mutation column states each row's **minimum required bite**, not a
prediction that no legitimate dependent can fail. Alternatives `(a)`,
`(b)`, or distinct omitted table arms are separate mutations; one
mega-mutation cannot establish their individual coverage. Record the
actual bite graph only after each compiling mutant runs. A proposed
mutation that is itself a valid implementation is replaced, as G9b
was, rather than used to reject the implementation.
### What this slice can witness, and what the replay lane owes
**G5's EFFECTS cannot be proven on this branch, and revision 14 claimed
them anyway.** Cancellation's observable outcomes — the producer latch,
the panel's selection, the child's release — are all reached through
**panel replay, which does not exist at this base**.
**Gesture EFFECTS cannot be proven on this branch, and earlier
revisions claimed some anyway.** Cancellation's observable outcomes —
the complete producer latch, the panel's selection, the click chain and
the child's release — plus drag continuation, replay positive controls
and two-tick scrolling are all reached through **panel replay, which
does not exist at this base**.
`gesture_last_content_cell` and the document/terminal replay live on
`panel-pointer-replay` (`pmacs-gpu/src/main.rs:2143` there); the same
struct here (`:2124`) has no such field, and
@ -2231,17 +2447,18 @@ whoever runs the tests:
| owned by **this slice** | owed by the **rebased replay lane** |
|---|---|
| the wire shapes, positions and field order | G5's document effects — empty selection cleared, click chain cleared |
| the family gate, both directions (G6–G8) | G5's terminal effect — the child's release, with matching button and encoding |
| the authoritative key: what moves it, what does not (G1–G4) | G5b's inert stale `Up` |
| the wheel exemptions and their two-tick rows | G5c's controller non-reclaim |
| **G5a** — that the key advancing RAISES cancellation, observed as the daemon-side signal | the end-to-end effect witnesses for that signal |
| G9–G11 | — |
| wire shapes, positions, pins and field order (G0), plus the bidirectional family gate (G6a–b, G7a–b, G8) | document cancellation effect — empty selection cleared without moving point |
| authoritative-key changing/stable inputs (G1–G4a) and the **mapping-generation** authority-loss signal before frame emission (G5a) | real stable-generation drag continuation, G5a's effects, and the v24/v25 replay-effect controls (G4b, G6c, G7c) |
| mapped-frame atomicity and generation-aware producer state (the mapped-frame legs of G5f) | complete producer reset lifecycle, including the geometry, `Absent`, identity and detach paths whose fields are introduced by replay (G5f) |
| motion coalescing, dedupe, invalid-frame atomicity, zero rejection, exhaustion state, version fallout and the TUI structural control (G9–G11a, G13–G15) | every common legacy/mapped cancellation transition and real gesture/click lifecycle (G5b–e, G5g, G5i–p), exhaustion cancellation (G11b), and both two-tick wheel effects (G12) |
**G5a stays here on purpose.** The trigger is this slice's rule; only
its consequences need replay. Moving the whole row out would leave the
proactive-cancellation ruling — the one that fixes revision 14's race —
with no witness in the slice that introduces it.
Only the **new mapping-generation** transition stays here on purpose,
and its classification does **not** discharge the effect row. The
common invalidations do not get a dead classification helper on this
base; replay owns them where accepted gesture state exists. The rebased
lane must run the whole G5 matrix through real document and terminal
call sites. This is the same producer-emits/receiver-discards blind
spot that forced the replay prerequisite in the first place.
### Coherence impact (`COHERENCE.md` §20)