docs(framing): revise panel replay contract after review

Make the three-state panel-pointer result a pre-effect disposition and
replace the competing latch matrices with one complete lifecycle table.
Accepted tails now require a live gesture, accepted release replay and
record-driven termination are mutually exclusive, and the witnesses
cover stale tails plus immediate and later duplicate completion.

Place projection-raised release delivery at the enforceable daemon
boundary after semantic rendering returns and before its messages are
written. Keep the arm invariant as a backstop behind pointer, detach,
and frame-order witnesses.

Split horizontal-wheel ground truth by terminal reporting/local and
document targets. Record the missing document-panel effect explicitly
under GUI Stage 1b B1--B3, update that framing's surface scope, and
synchronize the active-work checkpoint.
This commit is contained in:
Levi Neuwirth 2026-08-20 20:50:21 +02:00
parent cda078fa91
commit 1fc3af63de
No known key found for this signature in database
3 changed files with 174 additions and 111 deletions

View File

@ -281,13 +281,15 @@ 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 15 AWAITING APPROVAL
(revision 14 was reviewed and had four blocking gaps, all answered);
- **No PR yet. Checkpoint: §5a framing revision 16 and its GUI Stage
1b revision-13 ownership amendment AWAITING APPROVAL (revision 15
was reviewed, corrected and folded into this branch);
IMPLEMENTATION STILL PAUSED, now on approval rather than on a
blocker.** §5a's replay contract is approved at revision 12; revision
13 ruled Q#BP-R3 and blocked the lane on a protocol-bearing mapping
generation; **that block is DISCHARGED** — the slice merged as #242
(`47b5463`).
blocker.** §5a's **pre-merge** replay contract was approved at
revision 12; revisions 14–16 are the post-merge amendment now under
review. Revision 13 ruled Q#BP-R3 and blocked the lane on a
protocol-bearing mapping generation; **that block is DISCHARGED** —
the slice merged as #242 (`47b5463`).
- **MERGED main into this branch** at `b758c2e` rather than rebasing:
the lane's 12 commits include 10 framing revisions over the same
800–1000 line doc regions, so a rebase meant twelve rounds of
@ -314,14 +316,22 @@ from #171 and #215 — the correction the 1b lane missed, honoured here.
§5b and this lane gave `dispatch_semantic_panel_pointer`'s `bool`
different meanings — accepted-as-a-gesture versus consumed-here. A
mode-line press therefore **arms the latch** on this branch today.
**Q#BP-R4** rules a three-state `PanelPointerOutcome`, with an
asymmetric latch rule: arm only on `Accepted` + `Down(Left)`,
consume on any `Up(Left)` that was not `Refused`.
- **Revision 14 also carries** the rows §5b's split table assigned
here, a **pending-release slot** for the cancellation record §5b
leaves nowhere to wait, the **four transitions** that strand a live
gesture once effects attach, and a **re-measurement obligation**:
every production anchor in §5a has moved, evidenced in the revision.
**Q#BP-R4** rules a three-state `PanelPointerOutcome`, classified
**before** target effects. Only an `Accepted` `Down(Left)` arms;
left `Drag`/`Up` require a live record; an accepted `Up` performs
ordinary replay once, a consumed/chrome `Up` performs the recorded
completion once, and a refused `Up` preserves the record.
- **Revision 16 carries** the rows §5b's split table assigned here, a
**pending-release slot** for the cancellation record §5b leaves
nowhere to wait, and the **four transitions** that strand a live
gesture once effects attach. Drain order is executable: before the
next panel-pointer effect; before detach teardown; and, for a
projection-raised cancellation, after `render_frame` returns but
before any returned message is written. Ground truth is re-measured
at `2c0d3ff`. Document-panel horizontal scrolling is a named deferral
to GUI Stage 1b B1–B3; a horizontal tick whose terminal precedence
selects child reporting already emits SGR, and the local terminal
branch has no horizontal viewport effect.
- **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
@ -387,32 +397,26 @@ from #171 and #215 — the correction the 1b lane missed, honoured here.
**controller identity** catches the shared-path mutation, since
`apply_terminal_gesture` claims at `src/editor.rs:3571` before local
handling and activation alone claims nothing.
- **Why this lane exists.** `PanelPointer` **replays nothing**:
`dispatch_semantic_panel_pointer` (`src/editor.rs:2674`) validates,
focuses, returns. A panel wheel is dead on both axes and so is every
gesture past focus. **GUI arc 1b is BLOCKED on this lane and rebases
onto its merge commit.**
- **Why this lane exists, re-measured at `2c0d3ff`.** The branch now
replays document selection, terminal mouse reporting and vertical
wheels; the remaining acceptance-48 effect is **listview row
selection**. Q#BP-R4 and §5b's inherited rows still need
implementation: pre-effect disposition/latch ordering, fixed-domain
gesture tails, exact-once termination, cancellation effects and the
pending-release drains. **GUI arc 1b is BLOCKED on this lane and
rebases onto its merge commit.**
- **No new framing document.** Acceptance 48 is already ruled in
`docs/bottom-panel-framing.md`; §5a adds ground truth to it.
- **The measurement's headline: AC48 is HALF implemented**, and nothing
had written the halves down separately.
- **DONE:** click-to-focus and the terminal activation rule
(`src/editor.rs:2701` and its `activates`); the focused-only
auto-scroll clamp with passive `view_top` preserved (`:2568`–`:2571`, which
already cites parent 48); the coalescing rules — `Move`/`Drag`
tails coalesce, press/release/context/wheel lossless
(`pmacs-gpu/src/attach.rs:374`).
- **MISSING:** listview row selection, panel selection, terminal
mouse reporting, wheel replay.
- **The replay is mostly WIRING; both mechanisms exist.**
`apply_terminal_gesture` (`src/editor.rs:3525`) is *"the one terminal
pointer path, shared by both frontend kinds"* and already drives
child reporting, selection and scrollback — a panel terminal needs
the same call, with `side_window_for` + `TerminalViewKey` +
`panel_grid_size` (which the dispatcher already fetches).
`scroll_window` (`:3845`) is window-scoped, cursor carry included.
**A wheel-only bridge is the wrong shape** — the shared path takes
every kind at once.
- **Current clause split.** DONE: click/focus and terminal activation;
focused-only auto-scroll with passive `view_top` preserved; lossless
and coalesced event delivery; panel document selection; terminal
child reporting/local selection; vertical document and terminal
wheel effects. MISSING here: listview row selection and the
lifecycle/cancellation effects above. Horizontal wheel is split:
child-reporting terminal ticks already emit codes 66/67; the local
terminal branch is deliberately inert; document-panel `view_left`
is explicitly GUI Stage 1b B1–B3's effect, matching the production
comment in `src/editor.rs:2999`–`:3003`.
- **The scoping hazard.** `set_cursor_byte` (`src/editor_core.rs:1216`),
`begin_selection` (`:4691`) and `clear_selection` are
**active-window scoped**; used naively they would move the

View File

@ -1,5 +1,13 @@
# Bottom panel — framing (window placement + side windows)
**Revision 16 — 2026-08-20, AWAITING APPROVAL.** Revision 16 closes
review of 15: Q#BP-R4 is one pre-effect disposition/gesture-lifecycle
table; projection-raised releases drain after `render_frame` returns
and before its messages are written; horizontal-wheel ground truth is
split by target/domain and the missing document effect is explicitly
owned by GUI Stage 1b; superseded current-state text is removed from
this section and the active-work ledger.
**Revision 4 — pre-implementation, DRAFT after review round 3 plus landed-state
audit. Ground truth: canonical `main` @ `ddaa80d` (documentation landing #152;
runtime @ `0dd16a5`, GPU initial target / #148 after folding Stage 2 / #149),
@ -7,7 +15,7 @@ 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 13 — 2026-08-14, AWAITING APPROVAL. Q#BP-R3 is OVERRULED:
**Previously, revision 13 — 2026-08-14. Q#BP-R3 was 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
@ -1970,13 +1978,22 @@ cannot preserve the old panel-focused attach leak.
## 5a. Acceptance 48 — ground truth, RE-MEASURED at `2c0d3ff` (2026-08-20)
**Status: revision 15 — AWAITING APPROVAL.** Revision 13 ruled Q#BP-R3
**Status: revision 16 — AWAITING APPROVAL.** Revision 13 ruled Q#BP-R3
and **blocked** this lane on a protocol-bearing mapping generation.
That slice was framed as §5b, approved at revision 16, and **merged as
#242 (`47b5463`)**, so **revision 13's block is DISCHARGED** and its
ruling stands as history rather than as a gate. Revision 14 answered
what the lane inherits from a substrate that changed underneath it.
**Revision 16 closes review of 15.** It replaces the two competing
Q#BP-R4 matrices with one pre-effect disposition/lifecycle table;
names the projection drain at the only enforceable seam — after
`render_frame` returns and before its messages are written; splits
horizontal-wheel ground truth by target and gesture domain; records
GUI Stage 1b as the owner of document-panel horizontal scrolling; and
removes revision 14's now-false remeasurement obligation from the live
section and ledger.
**Revision 15 answers review of 14, and four of its five changes are
corrections rather than additions:**
@ -1995,7 +2012,8 @@ corrections rather than additions:**
- **The ground truth was knowingly false and is now RE-MEASURED**, at
`2c0d3ff`, rather than deferred to implementation. Two clauses the
old table called MISSING are DONE, and a third — the horizontal
wheel — turns out to be an unruled gap.
wheel — exposed an ownership gap that revision 16 now splits and
assigns explicitly.
- **"This lane MOVES the grade" was wrong.** Step 5/GPU is floored
`Partial` by 5(a) IME and step 8 is already `Works`; no journey cell
moves. The draft had read this section's own MISSING column as if it
@ -2024,19 +2042,24 @@ kept below as history; **this table is the current one**:
| move/drag tails coalesce; press/release/context/wheel lossless and ordered | **DONE** | `coalesce_kind` (`pmacs-gpu/src/attach.rs:338`) |
| **panel selection** | **DONE — was MISSING** | `replay_panel_document_gesture` (`src/editor.rs:2914`), called at `:2903`: `Down`/`Drag`/`Up(Left)`, shift-extend, double-click word select, right-press |
| **terminal mouse reporting** | **DONE — was MISSING** | `apply_terminal_gesture` called at `src/editor.rs:2899`, viewport `content_rows` |
| **wheel moves the panel's viewport** | **VERTICAL DONE — was MISSING** | `ScrollUp`/`ScrollDown` → `scroll_window` (`src/editor.rs:2925`) |
| **horizontal wheel** | **INERT, AND UNRULED** | `ScrollLeft`/`ScrollRight` fall into the catch-all no-op arm (`src/editor.rs:3004`) **with no comment and no ruling** — the old table's "dead on both axes" became half-true without anyone deciding the other half |
| **vertical wheel moves the panel's viewport** | **DONE — was MISSING** | document `ScrollUp`/`ScrollDown` → `scroll_window` (`src/editor.rs:2925`); reporting and local terminal paths share `apply_terminal_gesture` |
| **horizontal wheel, terminal precedence selects child reporting** | **RECEIVER DONE; PRODUCER AWAITS GUI 1b** | `apply_terminal_gesture` reaches `encode_mouse`, whose `ScrollLeft`/`ScrollRight` codes are 66/67 (`src/terminal/input.rs:130`–`:131`) |
| **horizontal wheel, terminal precedence selects local handling** | **RULED INERT** | Shift, reporting-off or a scrolled-back view can select the local branch; terminal scrollback is vertical and there is no local horizontal viewport, so the shared path deliberately has no `ScrollLeft`/`ScrollRight` arm |
| **horizontal wheel, document panel** | **NAMED DEFERRAL: GUI Stage 1b B1–B3** | `ScrollLeft`/`ScrollRight` are claimed and dropped at `src/editor.rs:3004`; the comment at `:2999`–`:3003` already assigns horizontal panel scrolling to 1b, whose framing now explicitly includes the panel-document surface |
| **listview row selection** | **MISSING** | — no listview handling on the replay path; Q#BP-R1 ruled the semantics (single click selects only), and nothing implements them |
| without disturbing the document mirror | **the constraint on all of the above** | — |
**Two findings the re-measurement produced**, neither of which a
line-number patch would have surfaced:
- **The horizontal wheel is an unruled gap, not a deferral.** It sits
in a catch-all arm beside `Move` and the non-left buttons, so it
reads as deliberate and is not. **Revision 14 does not rule it** —
it names it, because inventing a ruling inside a re-measurement is
how an unexamined default becomes a decision.
- **"Horizontal wheel" was not one verdict.** A tick for which terminal
precedence selects child reporting already sends the horizontal SGR
code; the local terminal branch has no horizontal viewport to move;
only a document panel lacks an effect. That last leg is a **named
deferral to GUI Stage 1b B1–B3**,
not an unexplained catch-all: 1b owns horizontal tick production,
sign, per-surface residuals and the window-targeted `view_left`
effect. The panel-replay lane remains 1b's prerequisite.
- **Two of the four "MISSING" effects are DONE on this branch.** The
section had been telling every reader that replay does nothing while
the branch it describes had implemented most of it.
@ -2694,7 +2717,7 @@ 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.
### Revision 14 — what §5b changed underneath this lane
### Revisions 14–16 — what §5b changed underneath this lane
#### Q#BP-R4 — what does `dispatch_semantic_panel_pointer`'s answer MEAN? **RULED: a three-state outcome, not a bool**
@ -2722,30 +2745,49 @@ fixed, re-entering by merge rather than by edit.
| **a three-state outcome** | **RULED** |
```rust
/// What the dispatcher did with a panel gesture.
/// How an authenticated panel event relates to the authoritative
/// panel surface, decided BEFORE any target effect.
pub enum PanelPointerOutcome {
/// Neither this panel's nor well-formed: no grid, out of grid, no
/// side window, or a buffer that is not the one shown there.
/// Not addressable as this panel: no grid, out of grid, no side
/// window, or a buffer that is not the one shown there.
Refused,
/// This panel's, and handled HERE deliberately --- the chrome
/// swallows of Q#BP-R2 and R-c. Not a content gesture.
/// This panel owns the cell/event, but it is deliberately not a
/// content gesture --- the chrome claims of Q#BP-R2 and R-c.
Consumed,
/// Reached the target as a content gesture.
/// A content gesture for the resolved target.
Accepted,
}
```
**The latch rule, and it is NOT "arm and consume on `Accepted`".** The
two halves are asymmetric, because a release ends a gesture wherever
the pointer happens to be:
**The outcome is a DISPOSITION, not proof that an effect already ran.**
It is decided before replay. The current function combines validation,
classification and mutation, but that ordering cannot implement §5b's
stale-tail rule: an `Up` or `Drag` with no accepted `Down` has already
reached the child/selection before the daemon consults the latch. The
implementation may split the function into classify/apply helpers or
use an equivalent single wrapper, but these two facts are fixed:
- **ARM** only on `Accepted` **and** `Down(Left)`. A chrome press
begins nothing.
- **TERMINATE** on any `Up(Left)` that was **not `Refused`** — so
`Accepted` **or** `Consumed`. A **`Refused`** release is different in
kind: out-of-grid or wrong-buffer means the daemon cannot tell the
event is even about this gesture, and §5b already pinned that it must
not consume.
1. the editor remains the **only** authority that derives
`Refused`/`Consumed`/`Accepted`; the daemon never re-derives chrome,
target kind or content bounds;
2. disposition and the live-gesture check both complete **before** a
left `Drag`/`Up` target effect.
**One transition table is authoritative:**
| event | `Refused` | `Consumed` | `Accepted` |
|---|---|---|---|
| `Down(Left)` | no effect; do not arm | no target effect; do not arm | apply the content press, then arm **from the effect result** so target/domain/encoding and `reached_child` describe what actually happened |
| `Drag(Left)` | no effect; retain any live record | no target effect; retain any live record | no live record → inert; live record → continue in the **recorded domain**, update its last valid content cell |
| `Up(Left)` | no effect; retain any live record for a later authoritative cancellation | no live record → inert; live record → deliver the **recorded completion** at its last valid content cell, then take it | no live record → inert; live record → apply the ordinary in-content completion **once**, then take it; do **not** also run recorded completion |
| every other kind | no effect; no latch change | no target effect; no latch change | apply its one-shot content effect; never arm or consume the left-gesture latch |
This is the required asymmetry. A chrome press begins nothing. A
chrome release can end a live gesture, but a refused release cannot be
known to concern it. An accepted release already performs the ordinary
target effect; a consumed release has not, so it terminates from the
record. A stale `Drag`/`Up` with no accepted `Down` is inert rather than
sending a child tail or changing selection.
**"TERMINATE", not "consume", and the distinction is the whole of
review finding 1.** An earlier draft of this ruling said consume, and
@ -2760,42 +2802,42 @@ anything. A latch that empties while the child stays pressed is
*exactly* the failure the draft claimed to prevent, now invisible
because the bookkeeping looks right.
**RULED: a non-`Refused` `Up(Left)` must produce the target's
completion effect, and the row asserts the EFFECT, not the latch.**
**RULED: a live gesture plus non-`Refused` `Up(Left)` must produce the
target's completion effect exactly once, and the row asserts the
EFFECT, not the latch.**
| target | required completion effect |
|---|---|
| terminal panel, child reporting on | the **release is reported to the child** — the same report an in-content `Up` produces, at the gesture's last content cell per R-c2 |
| terminal panel, child reporting on | the **release is reported to the child in the recorded encoding** — an accepted content `Up` uses its accepted cell; a consumed/chrome `Up` uses the gesture's last content cell per R-c2 |
| terminal panel, child reporting off | the **local terminal selection completes** — the selection the drag built is finalised, not abandoned |
| document panel | the document gesture completes at the last content cell; an empty selection is cleared **without moving point** (the effect §5b's split table already assigns here) |
**And exactly once.** A completion delivered here must not be delivered
again by a later cancellation of the same gesture, which is reachable
because the four stranding transitions below can fire afterwards.
**And exactly once.** An accepted `Up` must not replay normally and
then also run the record-driven completion. Any completion must take
the record, so a later cancellation of the same gesture — reachable
through the four stranding transitions below — cannot repeat it.
| # | mutation | must bite |
|---|---|---|
| P1 | chrome press returns `Accepted` | a chrome `Down(Left)` does not arm |
| P2 | `Refused` treated as `Accepted` | §5b's four `g5_substrate_a_refused_*` rows |
| P2 | `Refused` treated as `Accepted` | §5b's four `g5_substrate_a_refused_*` rows plus their no-target-effect assertions |
| P3 | chrome `Up` empties the latch but delivers **no** completion | the child-release / selection-completion row — **the latch-only assertion must NOT satisfy this** |
| P4 | `Refused` release delivers a completion | a refused release leaves both the latch and the child untouched |
| P5 | completion delivered, then a later cancellation delivers another | the no-duplicate-release row |
| P5 | an accepted content `Up` replays normally **and** invokes record-driven completion | the exact child stream contains one release, not two |
| P6 | completion delivered, then a later cancellation delivers another | the no-duplicate-release row |
| P7 | an accepted `Up` with no live record reaches the target | the stale-release row: no child bytes and no local/document selection mutation |
| P8 | an accepted `Drag` with no live record reaches the target | the orphan-drag row: no child bytes and no cursor/selection mutation |
**P3 is written against the earlier draft's own weakness.** A row that
asserts only `has_accepted_gesture() == false` passes while the child
receives nothing, so the row must read the child's reported bytes or
the terminal's selection state.
**That asymmetry is what earns the third state.** Under it, all three
outcomes are behaviourally distinct at the latch, so each is
falsifiable:
| # | mutation | must bite |
|---|---|---|
| P1 | chrome press returns `Accepted` | a chrome `Down(Left)` does not arm |
| P2 | `Refused` treated as `Accepted` | §5b's four `g5_substrate_a_refused_*` rows |
| P3 | `Consumed` treated as `Refused` for `Up` | a chrome `Up(Left)` consumes a live gesture |
| P4 | consume gated on `Accepted` alone | the same chrome-release row as P3, from the other side |
**That asymmetry is what earns the third state.** `Refused` preserves a
live gesture, `Consumed` owns the surface but runs only a recorded
completion, and `Accepted` reaches content. Each is behaviourally
distinct at the effect/lifecycle seam, and the single matrix above is
the witness contract.
**R-c2 does not discharge P3.** The producer normalizes a release that
lands on chrome back to the last content cell, so a conforming
@ -2869,9 +2911,15 @@ effect too late.
2. **before detach teardown**, so a frontend that goes away does not
take an owed release with it — detach is one of the four stranding
transitions below, and it is the one with no later opportunity;
3. **before the frontend's next frame is produced**, so a cancellation
raised during projection cannot be overtaken by the frame that
caused it.
3. **after semantic projection returns and before any message it
returned is filtered or written.** Mapping cancellation is
discovered *inside* `SemanticRenderState::render_frame`, while the
successor `PresentMapped` is being constructed, so "before that
frame is produced" is impossible. The enforceable daemon seam is
immediately after `sem.render_frame(editor)` returns
(`src/daemon.rs:1428`–`:1430`) and before `for msg in &messages`
(`:1591`). The pending termination drains there, so the successor
frame cannot overtake the release its new mapping required.
| # | mutation | must bite |
|---|---|---|
@ -2879,7 +2927,8 @@ effect too late.
| Q2 | park it but never drain | the same row, from the delivery side |
| Q3 | drain **after** `dispatch_semantic_panel_pointer` instead of before | the **old-release-before-new-press ordering row** — assert the child's byte stream carries the release ahead of the press, not merely that both arrive |
| Q4 | skip the drain on detach | the detach row: an owed release is delivered before teardown |
| Q5 | allow arming over a pending release | the invariant witness — kept, now as a **backstop** behind the ordering rows rather than as the guarantee |
| Q5 | enter the returned-message write loop before draining a projection-raised release | the daemon-order witness records the termination effect before the first successor-frame write |
| Q6 | allow arming over a pending release | the invariant witness — kept, now as a **backstop** behind the ordering rows rather than as the guarantee |
#### Four transitions strand a live gesture, and they become defects HERE
@ -2899,23 +2948,13 @@ motion. The daemon side has the same shape and needs the same
independently discriminating legs rather than one shared "resets
something" assertion.
#### The ground truth must be RE-MEASURED, and this is evidenced
#### Re-measurement closed in revision 16
This section's heading says `72da24a`. **Every production anchor in it
has moved**, measured at `b758c2e`:
| §5a claims | actually |
|---|---|
| `dispatch_semantic_panel_pointer` at `src/editor.rs:2674` | `:2809` |
| `apply_terminal_gesture` at `src/editor.rs:3525` | `:3875` |
| `scroll_window` at `src/editor.rs:3845` | `:4195` |
| `Absent` clears the latch at `pmacs-gpu/src/main.rs:6909` | `:7206` |
**The clause table itself is NOT assumed to have survived.** §5b
rewrote the dispatcher this lane measured, so each DONE/MISSING verdict
is owed a re-measurement before implementation, not a line-number
patch. A table that is right about *what* is missing and wrong about
*where* is the kind of record that gets trusted and then misleads.
The authoritative clause table is now the `2c0d3ff` table at the start
of §5a. The `72da24a` table is retained only under its explicit
**Superseded** heading. Revision 14's line-movement inventory did its
job by forcing a clause-by-clause scout; it is no longer a live
implementation obligation.
#### Coherence impact (`COHERENCE.md` §20)
@ -2926,11 +2965,12 @@ patch. A table that is right about *what* is missing and wrong about
inferred from this section's own MISSING column:
- **Step 5 / GPU is `Partial`, and stays `Partial`.** It is floored
by **5(a)** — no IME, no `set_ime_allowed`, so composed and CJK
input is impossible (`COHERENCE.md:462`). Completing 5(c) inside a
input is impossible (`COHERENCE.md:468`). Completing 5(c) inside a
panel cannot lift a cell held down by a different clause. That is
§2a's aggregation rule working as designed.
- **Step 8 is already `Works` on all three columns.** There is no
grade left to move; 8(b) is hardened, not opened.
- **Step 8 is already `Works` on all three columns**
(`COHERENCE.md:471`). There is no grade left to move; 8(b) is
hardened, not opened.
- The draft's error was reading §5a's MISSING column as if it were
the scorecard. **A section-local gap list is not a journey grade**,
and the one place that decides grades is `COHERENCE.md`.
@ -2941,7 +2981,7 @@ patch. A table that is right about *what* is missing and wrong about
- **Background work: none started**, and no existing attribution
changes.
#### What revision 14 does NOT do
#### What revisions 14–16 do NOT reopen
It does not re-open Q#BP-R1 (single click selects only), Q#BP-R2 (a
terminal panel's chrome wheel is consumed, not clamped), or R-a/R-b/

View File

@ -1,6 +1,13 @@
# GUI arc, Stage 1 — input foundation (framing)
**Status: revision 12 — APPROVED.** Revision 12 is §2's ground-truth
**Status: revision 13 — AWAITING APPROVAL with bottom-panel §5a
revision 16.** Revision 13 changes no previously ruled outcome. It
makes B1–B3's “per surface” scope explicit for the panel-document
surface, closing the ownership gap exposed by §5a's remeasurement:
panel replay is the prerequisite, while GUI 1b owns horizontal tick
production, residuals and the document-panel `view_left` effect.
**Previously, revision 12 — APPROVED.** Revision 12 is §2's ground-truth
re-measurement for Stage 1a and changes no ruling; it carries two
corrections to claims that were wrong at the original anchor too.
@ -308,9 +315,9 @@ The crate has **exactly one** executable `event_loop.exit()`, in
| # | Contract | Witness | Mutation |
|---|---|---|---|
| B1 | Residual per **axis and surface** | deltas discarded | share one accumulator → surface-switch jump |
| B2 | Wheel-right raises leftmost column; wheel-down raises top line | `x` discarded | invert a sign → that axis's row |
| B3 | Clamps at content bounds; never a negative origin | no horizontal scroll to clamp | remove clamp → **at-bounds row: origin goes negative and the view blanks** |
| B1 | Residual per **axis and surface** — primary document, panel document and minimap are distinct owners | deltas discarded | share one accumulator → primary↔panel or document↔minimap surface-switch jump |
| B2 | Wheel-right raises the leftmost column of the **document surface under the pointer**; wheel-down raises its top line. Primary-document horizontal state stays frontend-local; a panel-document tick reaches the daemon's window-targeted `view_left` path | `x` discarded; panel replay currently claims and drops document `ScrollLeft`/`ScrollRight` | invert a sign → that axis's row; omit the panel effect → primary moves while the panel remains inert |
| B3 | Each document surface clamps at its own content bounds; never a negative origin, and wrap pins horizontal origin to zero | no horizontal scroll to clamp | remove a clamp → **at-bounds row: that surface's origin goes negative and the view blanks**; reuse primary bounds for a panel → unequal-width surface row fails |
| B4 | Middle-click paste uses **PRIMARY on Linux** | no middle-click path | use `CLIPBOARD` → B4 only |
| B5 | I-beam over text content only | no I-beam | extend over the gutter → B5 only |
| B6 | Wheel over the minimap scrolls the **document viewport** with **its own residual accumulator**; click/drag remains scrub | **a FULL tick already scrolls today** — minimap pixels are `Elsewhere` (`main.rs:2061`) and the wheel falls through to `scroll_by_lines` (`main.rs:3373`). What fails is **fractional accumulation**, and **residual ownership distinct from the document's**: sub-tick minimap deltas are discarded, and a **surface-switch fractional witness** (part-tick over the minimap, then over the document) must not carry residue across | share the document's accumulator → the surface-switch fractional row jumps |
@ -322,6 +329,18 @@ blank**. The bound is *width − viewport*, saturating at zero for buffers
narrower than the viewport, and **the right-bound witness asserts the
final display column is still visible**.
**Panel-replay consequence, ruled after §5a's 2026-08-20
re-measurement.** “Horizontal panel wheel” has three receiver outcomes,
not one. When terminal precedence selects child reporting it already
forwards `ScrollLeft` and `ScrollRight` as SGR codes 66/67. The local
terminal branch — selected by Shift, reporting-off or a scrolled-back
view — has no horizontal viewport and remains inert. **B1–B3 own the missing
document-panel leg**: fractional production/residuals in the GPU, the
lossless `PanelPointer` tick, and the daemon's window-targeted
`view_left` effect and clamp. This is a named deferral from panel replay,
whose source comment already assigned it here; it does not make the
prerequisite lane implement half of 1b early.
**Why B6 changed — and revision 5's reason was wrong.** Scrubbing on
wheel is not *impossible*: the wheel handler already reads the cached
`state.pointer_pos` for surface routing (`main.rs:3337`), so an absolute