docs(framing): Q#BP-R3 overruled --- the lane blocks on a cell-mapping generation

Records the overrule and frames the slice (SS5b). No implementation;
the lane is PAUSED behind it.

**ALL THREE OF MY BOUNDS WERE WRONG**, and each is recorded because
each is why the trade looked cheap:

  "self-inflicted"       another frontend, or a background process, can
                         edit the same buffer --- the mapping moves with
                         `view_top` untouched and the clicking frontend
                         did nothing
  "bounded by 3 rows"    unbounded: ticks, paging, folds, edits and
                         reloads accumulate before the new frame is seen
  "one round trip"       until the frontend PRESENTS the replacement
                         frame; a slow or backed-up frontend widens the
                         window arbitrarily

With the narrowness gone there is nothing left to trade on.

**AND THE FIX IS A GENERATION, NOT A TOKEN.** A per-frame token would
invalidate a gesture on every repaint --- the exact mistake
`panel_epoch` exists to avoid, and it would break drags outright.
`mapping_generation` identifies the INVERSE MAPPING: it moves on
viewport, folds, wrap/gutter geometry and buffer content, and holds
across focus, styling, cursor motion and selection-only changes. The
stability half is load-bearing rather than an optimisation --- a drag
provokes selection repaints on every motion, and a generation that
moved with them would cancel the drag after one step.

SS5b frames the slice: appended variants (`PresentMapped`,
`PanelPointerMapped`) rather than widened structs, because postcard
encodes positionally and a shipped variant's field list is frozen;
bilateral gating, with neither side assuming the other's support and a
bare `PanelPointer` from a new-enough peer NOT silently upgraded; and a
drop-before-mutation check at the same rung of the ladder as the epoch
gates, one level finer. Dropping mid-drag is correct: if the mapping
changed, the cells being dragged across no longer mean what they meant.

This is where the hole always was --- `buffer_id` catches replacement,
`panel_epoch` catches close/reopen, `geometry_epoch` catches a
declaration race, and nothing catches "the text under that cell
changed".

Consequence: SS5b takes the next protocol version and GUI arc 1e's
`OpenTarget` moves to the one after. That edit is OWED BY THE 1b
BRANCH, which already revises `docs/gui-stage1-input-framing.md`
heavily; making it here would collide at the rebase 1b is scheduled
for, so it is recorded as required rather than made.

Gates: all nine green under `env -u TMPDIR`, log 20260814T155745Z.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-14 18:02:24 +02:00
parent 3dff79a0ec
commit 7e107bd137
No known key found for this signature in database
2 changed files with 171 additions and 4 deletions

View File

@ -281,8 +281,27 @@ from #171 and #215 — the correction the 1b lane missed, honoured here.
**`githubsucks/panel-pointer-replay` is the authoritative tip** (the
ref, not a SHA). Recover with
`git fetch githubsucks && git checkout panel-pointer-replay`.
- **No PR yet. Checkpoint: framing revision 12 (§5a) APPROVED
2026-08-14; IMPLEMENTATION BEGINS.** Nothing gates this lane now. Commit one was the ground-truth
- **No PR yet. Checkpoint: framing revision 13 AWAITING APPROVAL;
IMPLEMENTATION PAUSED.** §5a's replay contract is approved at
revision 12 and partly implemented, but **Q#BP-R3 was OVERRULED** and
the lane now **blocks on a protocol-bearing cell-mapping
generation**, framed in **§5b**.
- **THE BLOCKER, and why the earlier acceptance failed.** A
`PanelPointer` names a cell; nothing on the wire says which inverse
mapping the frontend saw, so the daemon inverts against whatever is
current. Revision 12 accepted that on three bounds and **all three
were wrong**: a **foreign** edit moves the mapping with `view_top`
untouched, the error is **unbounded** once ticks/folds/edits/reloads
accumulate, and the window lasts until the frontend **presents** the
new frame. §5b adds a **cell-mapping generation** — moves with the
inverse mapping, stable across focus/styling/cursor/selection-only
repaints so drags survive — as **appended** wire variants with
bilateral gating.
- **Chain: §5b (protocol) → panel replay → GUI arc 1b.** §5b takes the
next protocol version, so **1e's `OpenTarget` moves to the one
after**. **That edit is OWED BY THE 1b BRANCH**, which already
revises `docs/gui-stage1-input-framing.md` (`:1011`, `:1221` still
say v25); making it here would collide at 1b's scheduled rebase. Commit one was the ground-truth
re-measurement; 6 added the four replay edges; **7 answers review of
6; **8 answers review of 7** — R-c is target × gesture-ORIGIN
(terminals reject all chrome kinds and raw chrome coords fail the

View File

@ -7,7 +7,27 @@ protocol v20, 2026-07-24. Amended by the pre-implementation dependency
verification in §0.6: the folding dependency is cleared, and one geometry
caller-census error is corrected.**
**Revision 12 — 2026-08-14, APPROVED.** §5a is the acceptance-48
**Revision 13 — 2026-08-14, AWAITING APPROVAL. Q#BP-R3 is OVERRULED:
the lane BLOCKS on a protocol-bearing cell-mapping generation (new
§5b).** Revision 12 accepted current-state hit semantics on three
bounds, and all three were wrong — a **foreign** edit moves the mapping
with `view_top` untouched, the error is **unbounded** once ticks, folds,
edits or reloads accumulate, and the stale window lasts until the
frontend **presents** the replacement frame rather than one round trip.
With the narrowness gone there is nothing to trade on.
The follow-up is a **cell-mapping generation, not a per-frame token**:
it moves when the inverse mapping moves (viewport, folds, wrap/gutter
geometry, buffer content) and holds across focus, styling, cursor and
selection-only repaints, so ordinary drags survive. §5b frames it —
appended variants rather than widened structs, bilateral version
gating, and a drop-before-mutation check. **GUI arc 1e's `OpenTarget`
moves to the following protocol version.**
**Everything else in revision 12 stands**, including Q#BP-R1, Q#BP-R2
and the R-a…R-d edges.
**Previously, revision 12 — APPROVED.** §5a is the acceptance-48
ground truth and its rulings are settled: **Q#BP-R1** (a single click
selects a listview row only), **Q#BP-R2** (a terminal-chrome wheel is
consumed, daemon-side, before activation), the **R-a…R-d** replay
@ -2031,7 +2051,38 @@ follows acceptance 48's wording — it names row *selection* — and keeps
document navigation from becoming an incidental consequence of wiring
replay.
#### Q#BP-R3 — a panel cell has no frame-content provenance **RULED: current-state hit semantics, narrowly, with the token named as follow-up**
#### Q#BP-R3 — a panel cell has no frame-content provenance **RULED: BLOCK on a protocol-bearing mapping generation**
**Revision 12's answer — accept current-state semantics — is
OVERRULED, and all three of its bounds were wrong.** They are recorded
because each was the reason the trade looked cheap:
| the bound I claimed | why it does not hold |
|---|---|
| "self-inflicted — the same frontend must move the view and then click" | **another frontend, or a background process, can edit the same buffer.** The cell→byte mapping changes with `view_top` untouched, and the clicking frontend did nothing |
| "bounded by `SCROLL_LINES`" | **unbounded.** Multiple wheel ticks, paging, folds, edits or a reload can all land before the new frame is seen |
| "one round trip" | **until the frontend actually PRESENTS the replacement frame** — a slow or backed-up frontend widens the window arbitrarily |
With those gone there is no narrowness left to trade on, so **the lane
blocks on the wire fix** rather than shipping a mis-hit whose size and
cause are both unbounded.
**And the follow-up is NOT a per-frame token.** A token that changed on
every frame would invalidate a gesture on every repaint, which is the
same mistake `panel_epoch` deliberately avoids — it would break drags
outright. What is needed is a **CELL-MAPPING GENERATION**: an identity
of the *inverse mapping*, not of the frame.
- **It changes when the inverse mapping changes**: viewport (`view_top`
or grid size), folds, wrapping or gutter geometry, or buffer content.
- **It is stable across repaints that cannot move a cell's byte**:
focus, styling and theme, cursor movement, and selection-only
changes. That stability is what lets an ordinary drag survive the
repaints it provokes.
See **§5b** for the slice.
#### Superseded — revision 12's reasoning, kept for the record
**The hole.** `PanelPointer` carries `geometry_epoch`, `panel_epoch`,
`buffer_id` and a `coord` — **and nothing identifying the frame CONTENT
@ -2577,6 +2628,103 @@ R-d's orphan is distinct from R-b's: **R-b's arrives from a passive
panel, R-d's from a replaced or re-declared one**, and an
implementation can fix either alone.
## 5b. The cell-mapping generation — a protocol-bearing slice (Q#BP-R3)
**Status: framing, AWAITING APPROVAL. Nothing implemented.** This slice
**blocks** panel-pointer replay, which in turn blocks GUI arc 1b.
### 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. The
mapping can move for reasons the clicking frontend neither caused nor
can observe — **a foreign edit**, a fold, a reload — and the epochs do
not move with it, so every existing gate accepts the gesture.
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"**.
### The generation, and why not a token
**A per-frame token is the wrong object.** Panels repaint constantly —
focus, cursor blink, styling — and a token that moved with the frame
would invalidate a live drag on the next repaint. That is precisely why
`panel_epoch` is stable across ordinary frames, and the same reasoning
applies one level down.
**`mapping_generation` is an identity of the INVERSE MAPPING.**
| changes it | leaves it alone |
|---|---|
| `view_top` | focus gained or lost |
| panel grid size | styling, theme, face changes |
| fold state | cursor movement |
| wrap mode, gutter geometry | selection-only changes |
| **buffer content — any edit, from any source** | a re-emitted identical frame |
**The stability half is load-bearing, not an optimisation.** A drag
provokes selection repaints on every motion; if those moved the
generation, the drag would cancel itself after one step.
### Wire shape — appended variants, never widened
Postcard encodes enums **positionally**, so a variant's field list is
frozen once shipped. Both messages therefore gain **new variants
alongside the existing ones**:
- `PanelFramePayload::PresentMapped { .. }` beside `Present`, carrying
the frame plus its `mapping_generation`.
- `FrontendEvent::PanelPointerMapped { .. }` beside `PanelPointer`,
echoing the generation the frontend was displaying.
The existing variants stay **byte-frozen** and keep their current
meaning, and the frozen-byte pin moves to what is then the previous
final variant of each enum.
### Bilateral gating
Both directions gate on the negotiated version, and **neither side may
assume the other's support**:
- The daemon sends `PresentMapped` **only** to a peer whose negotiated
version reaches the new floor; every older peer keeps receiving
`Present`.
- The frontend sends `PanelPointerMapped` **only** when the session
negotiated it; otherwise it sends `PanelPointer` as today.
- **A daemon that receives the bare `PanelPointer` from a
new-enough peer must not silently upgrade it.** The gesture carries
no generation, so it is handled under the old semantics — the
version gate decides the shape, not a guess about intent.
### Enforcement
On `PanelPointerMapped`, the daemon compares the echoed generation with
the panel's current one and **drops the gesture before any mutation**
when they differ — the same position in the ladder as the epoch checks,
one level finer.
**Dropping mid-drag is correct.** If the mapping changed, the cells the
user is dragging across no longer mean what they meant when the drag
began, and continuing would select text they never pointed at.
### Consequence for the GUI arc
This slice takes the **next** protocol version, so **GUI arc 1e's
`OpenTarget` moves to the one after it**. The arc's serialization rule
is unchanged — protocol-bearing slices run alone — and this is simply a
new one inserted ahead of 1e.
**The edit belongs to the 1b branch, not to this one.**
`docs/gui-stage1-input-framing.md` says *"1e — `OpenTarget` (v25)"*
(`:1011`) and *"floor: v25, after v24, serialized"* (`:1221`), and that
document is heavily revised on `gui-stage1b-pointer-scroll`
(revisions 13–18). Changing it here would collide at the rebase 1b is
already scheduled for. **It is recorded as required rather than made**,
so the version bump lands with the branch that owns the file.
## 6. Deferred (named)
Left / right / top side windows; multiple slots per side; **rehoming a leaf