docs(framing): revision 20 --- §2a re-measured at 0ec13b3

Revision 18's §2a was measured at `72da24a`, now 167 commits back, with
#242 and #243 landing on exactly the pointer paths 1b builds on. Every
1b anchor is stale again, which §2a's own history predicted.

All four load-bearing claims were re-verified and all four SURVIVE.
Quantization still happens BEFORE routing (:3254 quantize, :3262 return
on zero, :3270 panel routing) --- checked first, because an inversion
would have changed B1's design. `Elsewhere` still collapses four wheel
targets. The panel branch still tests `PanelCell` only, so B6's premise
holds. `code_scroll_residual` is still the caret-follow residual, not a
wheel accumulator.

Three base facts were wrong:

  main.rs is 22,502 lines, not 21,435
  PROTOCOL_VERSION is 25, not 24 --- #242 took v25
  the panel-replay premise is stale

That third one matters most. §2a said a panel wheel "does nothing today,
on either axis". #243 landed the replay and it is in this base: the
dispatcher is split into `classify_panel_pointer` and
`apply_panel_pointer`, and the vertical axis replays for real. THE
PREREQUISITE IS DISCHARGED. What does not lapse is the witness --- 1b
still owes an end-to-end panel-wheel EFFECT witness on both axes, and
#243 discharges only the vertical half.

Citation policy: 45 live citations were re-measured and renumbered; a
citation that records what an EARLIER revision cited is left exactly as
written, and its block is labelled "citations are HISTORICAL
(72da24a)". Renumbering those would falsify sentences whose whole point
is that an earlier citation was wrong --- revision 15's test-fixture
finding is the clearest case.

Verified after the sweep: 79 §2a citations, zero out of range, and the
seventeen load-bearing anchors each resolve to the construct they name.

No ruling changes. Re-measurement, three corrections, one discharge.
This commit is contained in:
Levi Neuwirth 2026-08-31 21:40:14 +02:00
parent bd595bd148
commit 12b37742a7
No known key found for this signature in database
1 changed files with 152 additions and 75 deletions

View File

@ -1,6 +1,52 @@
# GUI arc, Stage 1 — input foundation (framing) # GUI arc, Stage 1 — input foundation (framing)
**Status: revision 19 — RECONCILES TWO LINEAGES THAT BOTH NUMBERED **Status: revision 20 — §2a RE-MEASURED at `0ec13b3`, and three base
facts corrected.** Revision 18's §2a was measured at `72da24a`. That
base is now **167 commits back**, and #242 and #243 landed on exactly
the pointer paths 1b builds on, so every 1b anchor is stale again —
which §2a's own history predicted would happen.
**All four load-bearing claims were re-verified and all four SURVIVE:**
1. **Quantization still happens BEFORE routing.** `:3254` quantizes,
`:3262` returns on zero, `:3270` routes to the panel. CORRECTION 5's
central point is intact — this was checked first, because an
inversion would have changed B1's design;
2. **`Elsewhere` still collapses four wheel targets** — its doc at
`:2108` still reads *"Not the band: the document, the terminal, the
minimap, or the chrome."*;
3. **the wheel's panel branch still tests `PanelCell` only** (`:3270`),
so minimap pixels still fall through to `scroll_by_lines`. **B6's
premise survives**;
4. **`code_scroll_residual` is still not a wheel accumulator** — it is
the caret-follow pixel residual (`:1664`), cleared at `:6218` and
`:8433`. Reusing it would still be a defect.
**Three base facts were wrong, and are corrected in place:**
- `pmacs-gpu/src/main.rs` is **22,502 lines**, not 21,435;
- **`PROTOCOL_VERSION = 25`**, not 24 — v25 was taken by #242's
cell-mapping generation;
- **the panel-replay premise is stale.** §2a said a panel wheel "does
nothing today, on either axis". **#243 landed the replay and it is in
this base**: the dispatcher is split into `classify_panel_pointer`
(`src/editor.rs:2985`) and `apply_panel_pointer` (`:3088`), and the
vertical axis replays for real at `:3277`–`:3278`. **The prerequisite
is DISCHARGED.** What does not lapse is the witness: 1b still owes an
end-to-end panel-wheel **effect** witness on **both** axes, of which
#243 discharges only the vertical half.
**Citation policy, applied throughout:** a citation that makes a claim
about the tree **as it is** was re-measured and renumbered — 45 of
them. A citation that records **what an earlier revision cited** is
left exactly as written and its block is labelled *"citations are
HISTORICAL (`72da24a`)"*. Renumbering those would have falsified
sentences whose whole point is that an earlier citation was wrong.
**No ruling changes in revision 20.** It is a re-measurement, three
corrections, and a discharge.
**Previously, revision 19 — RECONCILES TWO LINEAGES THAT BOTH NUMBERED
THEMSELVES 13.** This document was advanced independently by two THEMSELVES 13.** This document was advanced independently by two
branches, and both called their change "revision 13" — the second branches, and both called their change "revision 13" — the second
block at `revision 13` below is the other lineage's, left exactly as it block at `revision 13` below is the other lineage's, left exactly as it
@ -35,7 +81,7 @@ window-targeted `view_left` path and B3's wrap-pins-to-zero.
**Nothing in revision 19 changes a ruling.** It is a reconciliation and **Nothing in revision 19 changes a ruling.** It is a reconciliation and
a record. a record.
**Previously, revision 18 — APPROVED (2026-08-14).** §2a is the 1b ground **Previously, revision 18 — APPROVED (2026-08-14); its citations are HISTORICAL (`72da24a`).** §2a is the 1b ground
truth and its rulings are settled: **Q#S1-11 (B)**, the five-clause truth and its rulings are settled: **Q#S1-11 (B)**, the five-clause
lifetime contract with witnesses L1–L8, B1's six-target enumeration lifetime contract with witnesses L1–L8, B1's six-target enumeration
with discriminators **R1–R5**, and B3's exact saturated bound. Revision with discriminators **R1–R5**, and B3's exact saturated bound. Revision
@ -70,7 +116,7 @@ survived from 14 and 15 untouched. That is the failure mode §6 names,
arriving once per round, and it is why the R- and L-rows specify their arriving once per round, and it is why the R- and L-rows specify their
*drivers* and *setups* rather than only their assertions. *drivers* and *setups* rather than only their assertions.
**Previously, revision 17 — SUPERSEDED.** Two additions and one **Previously, revision 17 — SUPERSEDED; its citations are HISTORICAL (`72da24a`).** Two additions and one
correction: correction:
- **B1 gains the two separations its crossing witness could not - **B1 gains the two separations its crossing witness could not
@ -94,7 +140,7 @@ correction:
binaries **sequentially**, so the two steps never differed in binaries **sequentially**, so the two steps never differed in
concurrency and no experiment reversed anything. concurrency and no experiment reversed anything.
**Previously, revision 16 — SUPERSEDED.** Four changes, three of them **Previously, revision 16 — SUPERSEDED; its citations are HISTORICAL (`72da24a`).** Four changes, three of them
corrections to 15's own reasoning: corrections to 15's own reasoning:
- **The panel-replay lane becomes a HARD PREREQUISITE in §3's - **The panel-replay lane becomes a HARD PREREQUISITE in §3's
@ -119,7 +165,11 @@ corrections to 15's own reasoning:
already says is false and unachievable; mutations now name the rows already says is false and unachievable; mutations now name the rows
they must **bite** and the **legitimate dependents**. they must **bite** and the **legitimate dependents**.
**Previously, revision 15 — SUPERSEDED.** Answered review of 14 and **Previously, revision 15 — SUPERSEDED. Every citation and every
present-tense claim in this block is HISTORICAL, measured at
`72da24a`**; see revision 20 for the live sites, and note in particular
that "replays nothing" was true then and is **not** true on `0ec13b3`.
Answered review of 14 and
**changed two B-row contracts**, which 14 wrongly denied. **changed two B-row contracts**, which 14 wrongly denied.
- **B1 is now fully ruled**, not half-ruled. Revision 14 left two - **B1 is now fully ruled**, not half-ruled. Revision 14 left two
@ -149,11 +199,11 @@ B1 gains a per-target enumeration it did not have, and B3 gains B7's
exact upper bound in place of "content bounds". B2, B4, B5, B6 and B7 exact upper bound in place of "content bounds". B2, B4, B5, B6 and B7
are unchanged. are unchanged.
**Previously, revision 14 — SUPERSEDED.** Ruled Q#S1-11 (B) with the **Previously, revision 14 — SUPERSEDED; its citations are HISTORICAL (`72da24a`).** Ruled Q#S1-11 (B) with the
five-clause lifetime contract, and gave B3 B7's exact saturated bound. five-clause lifetime contract, and gave B3 B7's exact saturated bound.
Both stand. Both stand.
**Previously, revision 13 — SUPERSEDED.** §2a's ground-truth **Previously, revision 13 — SUPERSEDED; its citations are HISTORICAL (`72da24a`).** §2a's ground-truth
re-measurement for Stage 1b at the post-#240 tip. Every 1b anchor was re-measurement for Stage 1b at the post-#240 tip. Every 1b anchor was
stale, as expected; what was not was that **three B-rows describe a stale, as expected; what was not was that **three B-rows describe a
field as empty when it is occupied**, and that **B7 re-opens a question field as empty when it is occupied**, and that **B7 re-opens a question
@ -290,27 +340,32 @@ produces nothing.
§2 above was measured for **1a**, at `4f77491`. 1a (#239) has since §2 above was measured for **1a**, at `4f77491`. 1a (#239) has since
merged and #240 landed on top, so **every coordinate the 1b table cites merged and #240 landed on top, so **every coordinate the 1b table cites
is stale** — `pmacs-gpu/src/main.rs` is now **21,435 lines** and 1-pre's is stale** — `pmacs-gpu/src/main.rs` is now **22,502 lines** and 1-pre's
router extraction moved the wheel path wholesale. Renumbering alone router extraction moved the wheel path wholesale. Renumbering alone
would have been routine. It is not what the measurement found. would have been routine. It is not what the measurement found.
**`PROTOCOL_VERSION = 24`** now (1a's `TextInput`). 1b remains **`PROTOCOL_VERSION = 25`** now (`pmacs-protocol/src/message.rs:2002`)
non-protocol-bearing. — **not 24**, which is what this line said until revision 20: v25 was
taken by the panel cell-mapping generation (#242). 1b remains
non-protocol-bearing either way.
### The anchors, re-measured ### The anchors, re-measured
| the table cites | holds what now | the real site | **Left column: HISTORICAL — what §1b's table cited, as measured at
`72da24a`. Right column: LIVE, re-measured at `0ec13b3`.**
| the table cited (historical, `72da24a`) | held what then | the real site NOW (`0ec13b3`) |
|---|---|---| |---|---|---|
| `main.rs:2061` "minimap is `Elsewhere`" | `PanelCell(CellCoord)` | `enum PointerSurface` `:2057`; **`Elsewhere` `:2067`** | | `main.rs:2061` "minimap is `Elsewhere`" | `PanelCell(CellCoord)` | `enum PointerSurface` `:2099`; **`Elsewhere` `:2109`** |
| `main.rs:3337` wheel reads `pointer_pos` | an `attach_client` line | `apply_wheel` `:3090` and `:3113` | | `main.rs:3337` wheel reads `pointer_pos` | an `attach_client` line | `apply_wheel` `:3270` and `:3293` |
| `main.rs:3373` falls to `scroll_by_lines` | a bare `}` | `apply_wheel` `:3126`; `scroll_by_lines` `:8002` | | `main.rs:3373` falls to `scroll_by_lines` | a bare `}` | `apply_wheel` `:3306`; `scroll_by_lines` `:8417` |
| `editor.rs:3052` `dispatch_mouse` | — | **`:3207`** | | `editor.rs:3052` `dispatch_mouse` | — | **`:3920`** |
| `editor.rs:3203` scroll arms | a doc-comment line | `ScrollUp` `:3358`, `ScrollDown` `:3362` | | `editor.rs:3203` scroll arms | a doc-comment line | `ScrollUp` `:4296`, `ScrollDown` `:4299` |
**B6's premise survives the move intact**, and is now stated by the code **B6's premise survives the move intact**, and is now stated by the code
itself: `Elsewhere`'s doc comment reads *"Not the band: the document, itself: `Elsewhere`'s doc comment reads *"Not the band: the document,
the terminal, the minimap, or the chrome."* The wheel's panel branch the terminal, the minimap, or the chrome."* The wheel's panel branch
(`:3090`) tests `PanelCell` only, so minimap pixels still fall through (`:3270`) tests `PanelCell` only, so minimap pixels still fall through
to `scroll_by_lines`. to `scroll_by_lines`.
**B1's defect is visible in four lines.** `apply_wheel` rounds to whole **B1's defect is visible in four lines.** `apply_wheel` rounds to whole
@ -321,11 +376,11 @@ let lines = match delta { LineDelta(_, y) => (-y * WHEEL_LINES_PER_TICK).round()
if lines == 0 { return; } if lines == 0 { return; }
``` ```
`:3075`–`:3084`. The `_` is the **x** delta, discarded at the same site `:3254`–`:3262`. The `_` is the **x** delta, discarded at the same site
— so **B1 and B2's witnesses are the same four lines**, and a residual — so **B1 and B2's witnesses are the same four lines**, and a residual
accumulator is what both need. **There is no wheel accumulator today.** accumulator is what both need. **There is no wheel accumulator today.**
`code_scroll_residual` (`:1622`) is *not* it: that is the caret-follow `code_scroll_residual` (`:1664`) is *not* it: that is the caret-follow
pixel residual, buffer-scoped, cleared at `:5928`. **Reusing it would pixel residual, buffer-scoped, cleared at `:6218`. **Reusing it would
be a defect**, not a shortcut. be a defect**, not a shortcut.
### CORRECTION 5 — B1's "surface" is not enumerated, and the classifier cannot enumerate it ### CORRECTION 5 — B1's "surface" is not enumerated, and the classifier cannot enumerate it
@ -334,20 +389,20 @@ B1 says "residual per **axis and surface**" and the table leaves
"surface" undefined. Two facts make that a hole rather than a detail. "surface" undefined. Two facts make that a hole rather than a detail.
**Quantization happens BEFORE routing.** The rounding and the **Quantization happens BEFORE routing.** The rounding and the
`lines == 0` return are at `:3074`–`:3084`; the panel branch is `:3090` `lines == 0` return are at `:3254`–`:3262`; the panel branch is `:3270`
and the terminal branch `:3112`. So a sub-tick delta bound for the and the terminal branch `:3292`. So a sub-tick delta bound for the
panel or the terminal is **discarded before anything knows where it was panel or the terminal is **discarded before anything knows where it was
going**. An accumulator added after the routing decision would fix the going**. An accumulator added after the routing decision would fix the
document and leave the wire targets exactly as broken as they are now. document and leave the wire targets exactly as broken as they are now.
**And `PointerSurface` cannot name the surfaces B1 needs.** **And `PointerSurface` cannot name the surfaces B1 needs.**
`classify_pointer_surface` (`:7192`) resolves panel geometry only; `classify_pointer_surface` (`:7577`) resolves panel geometry only;
`Elsewhere` (`:2067`) is *"the document, the terminal, the minimap, or `Elsewhere` (`:2109`) is *"the document, the terminal, the minimap, or
the chrome"* — four wheel targets under one name, three of which B1 and the chrome"* — four wheel targets under one name, three of which B1 and
B6 must distinguish. **B1 needs a wheel-target enumeration; it does not B6 must distinguish. **B1 needs a wheel-target enumeration; it does not
get one for free from the existing classifier.** get one for free from the existing classifier.**
Note also a live consequence of `:3090` matching `PanelCell(_)` alone: Note also a live consequence of `:3270` matching `PanelCell(_)` alone:
**a wheel over the panel DIVIDER or the band's BACKGROUND scrolls the **a wheel over the panel DIVIDER or the band's BACKGROUND scrolls the
document today**, though the enum's own doc says the band "still owns document today**, though the enum's own doc says the band "still owns
the pixel." the pixel."
@ -358,11 +413,11 @@ they differ 1b closes the gap:
| wheel target | classified | vertical today | vertical RULED | residual owner | horizontal RULED | | wheel target | classified | vertical today | vertical RULED | residual owner | horizontal RULED |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Panel cell | `PanelCell` | emits (`:3102`), **replay missing** | emit **both axes**; replay is a **prerequisite lane** | per panel | emit `ScrollLeft`/`Right` | | Panel cell | `PanelCell` | emits (`:3282`); **vertical replay LANDED in #243** (`src/editor.rs:3277`) | emit **both axes**; the horizontal leg is B1–B3's | per panel | emit `ScrollLeft`/`Right` |
| Panel divider / background | `PanelDivider` / `PanelBackground` | **scrolls the document** | **consume both axes** — the band owns the pixel | **none — discards** | consumed, no emit | | Panel divider / background | `PanelDivider` / `PanelBackground` | **scrolls the document** | **consume both axes** — the band owns the pixel | **none — discards** | consumed, no emit |
| Terminal | `Elsewhere` + `terminal.is_some()` (`:3112`) | emits (`:3122`) | emit **both axes** | per terminal | emit `ScrollLeft`/`Right`; **inert when not reporting** | | Terminal | `Elsewhere` + `terminal.is_some()` (`:3292`) | emits (`:3302`) | emit **both axes** | per terminal | emit `ScrollLeft`/`Right`; **inert when not reporting** |
| Minimap | `Elsewhere` | document `scroll_by_lines` | document viewport, **own residual** (B6) | **its own** | inert | | Minimap | `Elsewhere` | document `scroll_by_lines` | document viewport, **own residual** (B6) | **its own** | inert |
| Document | `Elsewhere` | `scroll_by_lines` (`:3126`) | unchanged | document | `code_scroll_left` (B3) | | Document | `Elsewhere` | `scroll_by_lines` (`:3306`) | unchanged | document | `code_scroll_left` (B3) |
| Chrome | `Elsewhere` | document | **shares the document's**, deliberately | **the document's** | shares the document's | | Chrome | `Elsewhere` | document | **shares the document's**, deliberately | **the document's** | shares the document's |
**Divider and background consume both axes, and BANK NOTHING.** Falling **Divider and background consume both axes, and BANK NOTHING.** Falling
@ -416,7 +471,7 @@ minimap do not get replaced — their state OUTLIVES the buffer.** A
panel or terminal residual can be keyed to a surface that goes away; the panel or terminal residual can be keyed to a surface that goes away; the
document's and the minimap's live in the long-lived GPU `State`, which document's and the minimap's live in the long-lived GPU `State`, which
survives a buffer switch. **Their reset is manual, and there is a list survives a buffer switch. **Their reset is manual, and there is a list
to be forgotten from.** Buffer replacement (`pmacs-gpu/src/main.rs:5922` to be forgotten from.** Buffer replacement (`pmacs-gpu/src/main.rs:6218`
onward) zeroes `scroll_top`, `code_scroll_residual` and onward) zeroes `scroll_top`, `code_scroll_residual` and
`code_scroll_left` **one explicit line at a time**, each with its own `code_scroll_left` **one explicit line at a time**, each with its own
comment — including `code_scroll_left`, added later precisely because a comment — including `code_scroll_left`, added later precisely because a
@ -463,8 +518,8 @@ needs no protocol bump** — 1b stays non-protocol-bearing.
**The terminal answer follows from the encoder.** `sgr_mouse_report` **The terminal answer follows from the encoder.** `sgr_mouse_report`
(`src/terminal/input.rs`) already encodes `ScrollLeft` as **66** and (`src/terminal/input.rs`) already encodes `ScrollLeft` as **66** and
`ScrollRight` as **67** (`:126`–`:127`), so the terminal handles both `ScrollRight` as **67** (`:130`–`:131`), so the terminal handles both
axes the moment they are emitted. Its guard (`:111`) returns `None` axes the moment they are emitted. Its guard (`:120`) returns `None`
unless `mouse_sgr` is on and tracking is not `Off`, so a unless `mouse_sgr` is on and tracking is not `Off`, so a
**non-reporting terminal is horizontally inert** — which matches the **non-reporting terminal is horizontally inert** — which matches the
TUI, where nothing consumes a horizontal tick either. No new arm is TUI, where nothing consumes a horizontal tick either. No new arm is
@ -474,22 +529,33 @@ needed.
**Revision 14 cited `src/daemon.rs:6683` as the panel's `ScrollUp` **Revision 14 cited `src/daemon.rs:6683` as the panel's `ScrollUp`
handler. That was wrong: it is a test fixture**, inside `#[cfg(test)] handler. That was wrong: it is a test fixture**, inside `#[cfg(test)]
mod tests` (opened at `:3740`). Citing a fixture as production is mod tests` (opened at `:6813`). Citing a fixture as production is
precisely the error B1's enumeration exists to prevent, and it hid a precisely the error B1's enumeration exists to prevent, and it hid a
real defect. real defect.
The production path is `dispatch_semantic_panel_pointer` **As measured at `72da24a`**, the production path was
(`src/editor.rs:2674`). It validates the coord against the panel grid, `dispatch_semantic_panel_pointer` (`src/editor.rs:2674` *at that base*).
resolves the side window, focuses when the gesture activates — and It validated the coord against the panel grid, resolved the side
**returns `true` without replaying anything.** Its own doc says so: window, focused when the gesture activated — and **returned `true`
*"**Replay is out of scope in Stage 2B-2.** Driving selection, listview without replaying anything.** Its own doc said so: *"**Replay is out of
rows, or child SGR reporting is **parent acceptance 48**, which needs scope in Stage 2B-2.** Driving selection, listview rows, or child SGR
the GPU band and lands in **Stage 2B-3**."* reporting is **parent acceptance 48**, which needs the GPU band and
lands in **Stage 2B-3**."*
So **a panel wheel does nothing today, on either axis** — the frontend **THAT IS NO LONGER THE BASE. #243 landed the replay, and revision 20
emits, the daemon validates and drops. That is a **pre-existing re-measures it.** On `0ec13b3` the dispatcher is split into
violation of an already-ruled contract**, uncovered by this `classify_panel_pointer` (`src/editor.rs:2985`) and
measurement, not created by 1b. `apply_panel_pointer` (`:3088`), and the effect path replays a **panel
wheel's vertical axis** for real: `PKind::ScrollUp =>
self.scroll_window(side, -SCROLL_LINES)` and its `ScrollDown` twin
(`:3277`–`:3278`).
**So "a panel wheel does nothing today, on either axis" is FALSE on this
base, and the sentence is corrected rather than deleted** — it was true
when written and is the reason the prerequisite lane existed. What
remains is narrower and was already assigned here: **the horizontal
document-panel leg is B1–B3's**, per the "Panel-replay consequence"
paragraph in §1b.
**Ruling: 1b does not absorb it, and 1b does not ship before it.** The **Ruling: 1b does not absorb it, and 1b does not ship before it.** The
replay is repaired in a **prerequisite lane** carrying parent replay is repaired in a **prerequisite lane** carrying parent
@ -548,13 +614,13 @@ Each of these reads as an empty field in the table and is not one. The
contracts are unaffected; the **implementation shape** is. contracts are unaffected; the **implementation shape** is.
- **B3 — "no horizontal scroll to clamp".** The GPU has a horizontal - **B3 — "no horizontal scroll to clamp".** The GPU has a horizontal
origin: **`code_scroll_left`** (`:1639`), a pixel offset *snapped to origin: **`code_scroll_left`** (`:1681`), a pixel offset *snapped to
the column grid*, moved by its own `horizontal_follow` (`:7702`). the column grid*, moved by its own `horizontal_follow` (`:8117`).
What is missing is a **wheel-driven** horizontal scroll. B3 adds a What is missing is a **wheel-driven** horizontal scroll. B3 adds a
second writer to an existing field, which is a different job from second writer to an existing field, which is a different job from
introducing one. introducing one.
- **B5 — "no I-beam".** True as stated, but the cursor already has an - **B5 — "no I-beam".** True as stated, but the cursor already has an
owner: **`apply_panel_cursor_icon`** (`:7328`) sets `RowResize` over owner: **`apply_panel_cursor_icon`** (`:7743`) sets `RowResize` over
the divider and **`CursorIcon::Default` everywhere else**. An I-beam the divider and **`CursorIcon::Default` everywhere else**. An I-beam
written as a separate site would be **clobbered by that else branch**. written as a separate site would be **clobbered by that else branch**.
B5 must extend this function, not join it. B5 must extend this function, not join it.
@ -570,7 +636,7 @@ contracts are unaffected; the **implementation shape** is.
This is the finding that needs a ruling rather than a renumber. This is the finding that needs a ruling rather than a renumber.
The TUI horizontal origin is **`window.view_left`** (`src/window.rs:386`), The TUI horizontal origin is **`window.view_left`** (`src/window.rs:386`),
and `horizontal_follow` (`src/editor.rs:4495`) already **pins it to 0 and `horizontal_follow` (`src/editor.rs:5233`) already **pins it to 0
under wrap** — so *B7's wrap clause is implemented today*, for the under wrap** — so *B7's wrap clause is implemented today*, for the
caret-follow path. Both frontends share the arithmetic caret-follow path. Both frontends share the arithmetic
(`pmacs_protocol::scroll::follow_left`, `scroll.rs:134`), deliberately. (`pmacs_protocol::scroll::follow_left`, `scroll.rs:134`), deliberately.
@ -591,18 +657,18 @@ scroll for exactly this reason:
> would otherwise snap `view_top` straight back … so the user's > would otherwise snap `view_top` straight back … so the user's
> mouse-wheel scroll would feel stuck after one notch. > mouse-wheel scroll would feel stuck after one notch.
(That citation is itself stale: `scroll_window` is **`src/editor.rs:3845`**, (That citation is itself stale: `scroll_window` is **`src/editor.rs:4583`**,
not `:3628`.) not `:3628`.)
So a wheel-driven `view_left` that does **not** carry the cursor is So a wheel-driven `view_left` that does **not** carry the cursor is
snapped back, and horizontal wheel scrolling "feels stuck after one snapped back, and horizontal wheel scrolling "feels stuck after one
notch" — the identical bug, one axis over, on **both** frontends, since notch" — the identical bug, one axis over, on **both** frontends, since
the GPU's `horizontal_follow` (`:7702`) has the same shape. the GPU's `horizontal_follow` (`:8117`) has the same shape.
**And it happens on the next PAINT, not the next caret event.** **And it happens on the next PAINT, not the next caret event.**
`prepare_window_cursor_visible` (`src/editor.rs:4518`) calls `prepare_window_cursor_visible` (`src/editor.rs:5256`) calls
`horizontal_follow` unconditionally as its **first** act (`:4539`), and `horizontal_follow` unconditionally as its **first** act (`:5277`), and
`paint_frame` (`:4747`) calls it every frame (`:4852`, and `:2572` for `paint_frame` (`:5485`) calls it every frame (`:5590`, and `:2572` for
the panel). Revision 13 said "the next caret event"; that was wrong and the panel). Revision 13 said "the next caret event"; that was wrong and
understated the exposure — the origin is overwritten by a redraw with understated the exposure — the origin is overwritten by a redraw with
no input at all. no input at all.
@ -616,17 +682,17 @@ viewport that is overwritten on the next frame.
**(A) is not viable in 1b, and the vertical precedent does not reach **(A) is not viable in 1b, and the vertical precedent does not reach
it.** `scroll_window` carries point because it is **TUI-side**, where it.** `scroll_window` carries point because it is **TUI-side**, where
the editor owns the cursor directly. The GPU has no such power: the editor owns the cursor directly. The GPU has no such power:
`OwnCursor` (`pmacs-gpu/src/main.rs:2337`) is *"pmacs-gpu's own cursor `OwnCursor` (`pmacs-gpu/src/main.rs:1774`) is *"pmacs-gpu's own cursor
position, **mirrored** from `CursorByte`"* — a read-only reflection of position, **mirrored** from `CursorByte`"* — a read-only reflection of
daemon state. The only wire operation that positions it is `Pointer`, daemon state. The only wire operation that positions it is `Pointer`,
and `dispatch_pointer` (`src/editor.rs:3638`) sets `active_frontend`, and `dispatch_pointer` (`src/editor.rs:4376`) sets `active_frontend`,
calls `break_command_chain`, and — by its own comment — *"Every calls `break_command_chain`, and — by its own comment — *"Every
`PointerKind` moves point or changes the selection."* Carrying point `PointerKind` moves point or changes the selection."* Carrying point
from a wheel would mean **a new wire operation**, which contradicts from a wheel would mean **a new wire operation**, which contradicts
1b's non-protocol scope outright. 1b's non-protocol scope outright.
Note also that **GPU vertical scrolling already does not carry point**: Note also that **GPU vertical scrolling already does not carry point**:
`apply_wheel` ends at `send_viewport` (`:3129`). (A) would therefore not `apply_wheel` ends at `send_viewport` (`:3309`). (A) would therefore not
even be internally consistent — it would make the horizontal axis carry even be internally consistent — it would make the horizontal axis carry
point on a frontend where the vertical axis does not. point on a frontend where the vertical axis does not.
@ -655,7 +721,7 @@ undefined, which is the part that decides whether the feature works.
*changing*, never by elapsed time or by the follow running. *changing*, never by elapsed time or by the follow running.
5. **Wrap and buffer replacement clear it and pin the origin to zero.** 5. **Wrap and buffer replacement clear it and pin the origin to zero.**
This is the existing rule (`horizontal_follow`'s wrap branch, and This is the existing rule (`horizontal_follow`'s wrap branch, and
the GPU's at `:7703`); authority must not survive either. the GPU's at `:8089`); authority must not survive either.
##### What B7 and B3 must witness ##### What B7 and B3 must witness
@ -664,12 +730,12 @@ a follow that runs inside a frame.
**The two frontends need DIFFERENT preservation drivers, and this is **The two frontends need DIFFERENT preservation drivers, and this is
where revision 14 was wrong.** It said "the next paint" without where revision 14 was wrong.** It said "the next paint" without
qualification. **That is TUI-only.** The GPU's `render()` (`:9881`) goes qualification. **That is TUI-only.** The GPU's `render()` (`:10296`) goes
straight to `render_to_view` and **never calls `horizontal_follow`**; straight to `render_to_view` and **never calls `horizontal_follow`**;
the follow reaches it only through `ensure_caret_painted` (`:7674`), the follow reaches it only through `ensure_caret_painted` (`:8071`),
whose callers are cursor paths (`:5607`, `:5659`, `:5776`, `:6320`) and whose callers are cursor paths (`:5897`, `:5949`, `:6066`, `:6610`) and
geometry paths — `resize` (`:9851`), `apply_font_facts` (`:9729`), geometry paths — `resize` (`:10266`), `apply_font_facts` (`:10144`),
`reflow_dynamic_code_geometry` (`:9672`). **A GPU wheel-then-paint row `reflow_dynamic_code_geometry` (`:10087`). **A GPU wheel-then-paint row
would stay green with the overwrite mutation restored**, which is a would stay green with the overwrite mutation restored**, which is a
vacuous witness of exactly the kind this framing keeps producing. vacuous witness of exactly the kind this framing keeps producing.
@ -764,7 +830,7 @@ the same bound on the same rule, and B7's exactness exists because the
loose version *blanks the viewport*. loose version *blanks the viewport*.
**B3 takes B7's rule verbatim, in the GPU's column-grid units.** The **B3 takes B7's rule verbatim, in the GPU's column-grid units.** The
GPU already reckons in that grid — `horizontal_follow` (`:7702`) GPU already reckons in that grid — `horizontal_follow` (`:8117`)
derives `cols = (width / advance).floor()` and `left_col = derives `cols = (width / advance).floor()` and `left_col =
(code_scroll_left / advance).round()`, then re-multiplies to snap the (code_scroll_left / advance).round()`, then re-multiplies to snap the
offset back onto the grid, deliberately, so both frontends put the same offset back onto the grid, deliberately, so both frontends put the same
@ -797,20 +863,31 @@ B7's revision found and B3 currently has no row to catch.
**1c is NOT protocol-bearing** under Q#S1-8's ruling. Protocol slices **1c is NOT protocol-bearing** under Q#S1-8's ruling. Protocol slices
are serialized. are serialized.
**The panel-replay lane is a HARD PREREQUISITE of 1b**, not a parallel **The panel-replay lane WAS a hard prerequisite of 1b. It is
track. §2a found that `dispatch_semantic_panel_pointer` validates and DISCHARGED.** §2a found, at `72da24a`, that
focuses but **replays nothing**, so a panel wheel is dead on both axes `dispatch_semantic_panel_pointer` validated and focused but **replayed
today — a pre-existing gap in **parent acceptance 48** nothing**, so a panel wheel was dead on both axes — a pre-existing gap
(`docs/bottom-panel-framing.md:1719`), which owns it. 1b's B1 makes the in **parent acceptance 48** (`docs/bottom-panel-framing.md:1719`), which
panel a first-class wheel target with its own residual, and **the owned it.
defect is exactly "frontend emits, receiver discards"**, so a 1b that
witnessed emission alone would repeat the blind spot that hid it.
**Order: the replay lane merges, 1b rebases onto that merge commit, **#243 repaired it, and it is in this base.** `0ec13b3` contains the
then 1b lands an end-to-end panel-wheel EFFECT witness on both axes.** split dispatcher (`src/editor.rs:2985`, `:3088`) and the vertical
1b's base panel-wheel replay (`:3277`–`:3278`). **1b has no outstanding
moves off `72da24a` accordingly; the rest of §2a is measured on the GPU prerequisite**, and the rebase this revision was written on is the one
and TUI sides and is unaffected. the order below called for.
**What does NOT lapse is the witness.** The reason for demanding it
stands unchanged: 1b's B1 makes the panel a first-class wheel target
with its own residual, and **the original defect was exactly "frontend
emits, receiver discards"** — so a 1b that witnessed emission alone
would repeat the blind spot that hid it. **1b still owes an end-to-end
panel-wheel EFFECT witness on BOTH axes**, and #243 discharges only the
vertical half of what that witness must show.
**Order, as it now stands: the prerequisite is met; 1b implements
B1–B7 on this base and lands the both-axis effect witness.** 1b's base
is **`0ec13b3`**, off `72da24a` as the earlier order required; the rest
of §2a is measured on the GPU and TUI sides and is unaffected.
## 4. Q#S1-8 — RULED: (A), preserve pre-window readiness ## 4. Q#S1-8 — RULED: (A), preserve pre-window readiness