docs(framing): Stage 1 revision 15 --- B1 fully ruled, and three defects in 14

Answers review of revision 14. Still framing only; no code. Revision
15 DOES change two B-row contracts, B1 and B3; 14 claimed none changed
and that was false.

**I CITED A TEST FIXTURE AS A PRODUCTION HANDLER.** Revision 14 named
`src/daemon.rs:6683` as the panel's `ScrollUp` arm. It is inside
`#[cfg(test)] mod tests`, opened at `:3740`. The wrong citation hid a
real defect: the production path,
`dispatch_semantic_panel_pointer` (`src/editor.rs:2674`), validates the
coord, resolves the side window, focuses when the gesture activates,
and returns WITHOUT REPLAYING ANYTHING. Its own doc says replay is
parent acceptance 48 and lands in Stage 2B-3. So a panel wheel is dead
today on BOTH axes --- a pre-existing violation of an already-ruled
contract, which is why panel inertness was never an available answer.

Ruled: 1b does NOT absorb the replay. 1b owns the frontend half ---
per-panel residual, both axes emitted --- and replay is repaired in a
prerequisite lane. It is already scoped to 2B-3, it is not input work
(selection, listview rows, child SGR reporting, and it needs the GPU
band), and the defect predates this slice. 1b's panel rows therefore
witness EMISSION ONLY and must say so rather than implying a scroll the
user cannot yet see.

**B1 IS NOW RULED, NOT HALF-RULED.** Revision 14 left two cells reading
"must be ruled", which is a question wearing a table's clothes. The
terminal answer was derivable all along: the SGR encoder already
carries `ScrollLeft` as 66 and `ScrollRight` as 67
(`src/terminal/input.rs:126`), and its guard returns `None` unless
reporting is on --- so a non-reporting terminal is horizontally inert,
matching the TUI, and no new arm is needed. Divider and background now
CONSUME both axes, because falling through to the document contradicts
`PanelBackground`'s own doc that the band owns the pixel. Chrome shares
the document's scrolling and residual, stated so it is a choice rather
than a leak.

**THE GPU PRESERVATION WITNESS WAS VACUOUS.** "The next paint" is
TUI-only. The GPU's `render()` (`:9881`) goes straight to
`render_to_view` and never calls `horizontal_follow`; the follow
reaches it only via `ensure_caret_painted`, whose callers are cursor
paths and geometry paths (`resize`, `apply_font_facts`,
`reflow_dynamic_code_geometry`). A GPU wheel-then-paint row would have
stayed green with the overwrite mutation restored. Preservation is now
specified per frontend: TUI drives a real paint, GPU drives a real
same-cursor geometry re-follow.

Release must move the cursor OUTSIDE the manual viewport. Inside it
`follow_left` returns the same origin, so the row would pass whether or
not release happened.

**FIVE CLAUSES HAD THREE WITNESS SHAPES; THEY NOW HAVE EIGHT.** 14 left
unconstrained: point and selection staying put, clamp-absorbed motion
not arming authority, geometry/content changes re-clamping while
PRESERVING authority, and wrap/buffer replacement clearing the LATCH
rather than merely zeroing the origin. The existing wrap-origin rows
cannot see a stale latch surviving wrap then truncate. Six mutations,
each failing its own rows.

Clause 3's direction was backwards: a WIDER viewport lowers the maximum
origin, so widening re-clamps downward. 14 said "narrower".

Ledger: three false claims removed. The 1b entry arrived in the SECOND
commit, not the first --- recorded rather than quietly fixed, because a
lane claiming compliance it did not achieve is what the #171/#215
correction exists to catch. The entry no longer says all six targets
have horizontal answers. And the arc block said five slices remain, 1a
is next, and v23; it is four, 1b, and v24.

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

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 09:57:01 +02:00
parent e94ce85be1
commit a330658feb
No known key found for this signature in database
1 changed files with 170 additions and 57 deletions

View File

@ -1,24 +1,46 @@
# GUI arc, Stage 1 — input foundation (framing)
**Status: revision 14 — AWAITING APPROVAL.** Revision 14 answers review
of revision 13. **Q#S1-11 is RULED (B), viewport only**, with the full
five-clause lifetime contract and its witnesses; (A) was not viable
because the GPU cursor is a daemon-owned mirror and carrying point
would need a wire operation 1b is not allowed. Two further holes review
named are closed: **B1's "surface" is now enumerated** as a
wheel-target table with a residual owner and a horizontal answer per
target, and **B3 takes B7's exact saturated upper bound** in the GPU's
column grid instead of "content bounds". Revision 13's claim that the
snap-back lands on the next *caret event* is **corrected**: it lands on
the next **paint**.
**Status: revision 15 — AWAITING APPROVAL.** Revision 15 answers review
of 14 and **does change two B-row contracts**, which 14 wrongly denied.
- **B1 is now fully ruled**, not half-ruled. Revision 14 left two
"must be ruled" cells in the wheel-target table, which is a question
wearing a table's clothes. Every cell is normative now, and the two
gaps are closed from measurement: the **terminal** emits both axes
(the SGR encoder already carries codes 66/67, and a non-reporting
terminal is inert, matching the TUI), and the **panel** emits both
axes while its missing **replay** is repaired in a **prerequisite
lane** rather than absorbed here. Divider, background and chrome are
ruled too.
- **Revision 14 cited a TEST FIXTURE as the panel's production
handler** (`src/daemon.rs:6683`, inside `#[cfg(test)]`). The real
path validates and focuses and **replays nothing** — a pre-existing
violation of an already-ruled contract, which the wrong citation hid.
- **The GPU preservation witness 14 specified was vacuous.** "The next
paint" is TUI-only; the GPU's `render()` never calls
`horizontal_follow`. The lifetime rows are respecified per frontend.
- **Five clauses now have eight witnesses**, not three. 14 left point
and selection, clamp-absorbed motion, re-clamping, and the authority
latch under wrap unconstrained.
- **Clause 3's direction was backwards** in 14: a *wider* viewport
lowers the maximum origin.
**What is normative here, stated plainly:** §2a **changes B1 and B3**.
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
are unchanged.
**Previously, revision 14 — SUPERSEDED.** Ruled Q#S1-11 (B) with the
five-clause lifetime contract, and gave B3 B7's exact saturated bound.
Both stand.
**Previously, revision 13 — SUPERSEDED.** §2a's ground-truth
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
field as empty when it is occupied**, and that **B7 re-opens a question
another framing deliberately deferred**. **No B-row contract is changed
by either revision** — 13 and 14 add ground truth, a ruling, and
witnesses.
another framing deliberately deferred**. Its claim that the snap-back
lands on the next *caret event* was corrected by 14 to the next
**paint**, and by 15 to *per-frontend* drivers.
**Status: revision 13 — AWAITING APPROVAL with bottom-panel §5a
revision 16.** Revision 13 changes no previously ruled outcome. It
@ -49,9 +71,9 @@ Protocol slices stay serialized; one was inserted in front.
the tip after 1-pre; it was originally taken at `a994f37`. **§2a is
measured at `72da24a`** (2026-08-13), the tip after 1a and #240, and it
is the base for **1b only**; it carries Q#S1-11's ruling. Sections
other than §2/§2a were written
against `a994f37` and their *rulings* are unaffected by 1-pre, which
changed no behaviour — but **any line number outside §2 and §2a
other than §2/§2a were written against `a994f37` and their *rulings*
are unaffected by 1-pre, which changed no behaviour — but **any line
number outside §2 and §2a
predates 1-pre and should be re-checked before it is relied on.** The
1b table's own anchors are superseded by §2a wholesale; read §2a first.
@ -207,26 +229,84 @@ Note also a live consequence of `:3090` matching `PanelCell(_)` alone:
document today**, though the enum's own doc says the band "still owns
the pixel."
The enumeration B1 must carry, measured:
The enumeration B1 carries. **Every cell is normative** — "vertical
today" is the measurement, "vertical RULED" is the contract, and where
they differ 1b closes the gap:
| wheel target | classified today | vertical wheel today | residual owner | horizontal |
|---|---|---|---|---|
| Panel cell | `PanelCell` | wire `ScrollUp`/`Down` (`:3102`) | per panel | **must be ruled** — wire has the kinds, daemon has no arm |
| Panel divider / background | `PanelDivider` / `PanelBackground` | **falls through to the document** | — | inert |
| Terminal | `Elsewhere` + `terminal.is_some()` (`:3112`) | wire terminal `ScrollUp`/`Down` (`:3122`) | per terminal | **must be ruled** — same gap |
| Minimap | `Elsewhere` | document `scroll_by_lines` | **its own** (B6) | inert |
| Document | `Elsewhere` | `scroll_by_lines` (`:3126`) | document | `code_scroll_left` (B3) |
| Chrome | `Elsewhere` | document | none | inert |
| 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 divider / background | `PanelDivider` / `PanelBackground` | **scrolls the document** | **consume both axes** — the band owns the pixel | per panel | consumed, no emit |
| Terminal | `Elsewhere` + `terminal.is_some()` (`:3112`) | emits (`:3122`) | 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 |
| Document | `Elsewhere` | `scroll_by_lines` (`:3126`) | unchanged | document | `code_scroll_left` (B3) |
| Chrome | `Elsewhere` | document | **shares the document's**, deliberately | **the document's** | shares the document's |
**Divider and background consume both axes.** Falling through to the
document contradicts `PanelBackground`'s own doc — *"the band still owns
the pixel"* — and is a measurement, never a decision. Consuming is not
"inert": the residual is the panel's, so a gesture that crosses from
the band's background onto a cell does not jump.
**Chrome shares the document's scrolling and the document's residual.**
That is today's behaviour, and making it normative is what keeps 1b
from having to change both frontends' hit testing for no user-visible
gain. It is written down so it is a choice rather than a leak.
**The horizontal gap is not a wire gap.** `MouseKind::ScrollLeft` and
`ScrollRight` already exist (`pmacs-protocol/src/message.rs:245`,
`:247`) and round-trip (`src/protocol.rs:720`), so **emitting them
needs no protocol bump** — 1b stays non-protocol-bearing either way.
What is missing is a *handler*: the terminal path has arms for
`ScrollUp`/`ScrollDown` only (`src/editor.rs:3576`, `:3579`), and the
panel path only `ScrollUp` (`src/daemon.rs:6683`). So for each wire
target 1b must choose **emit-and-handle** or **explicitly inert**, and
say which. Silence here is what lets a horizontal tick vanish.
needs no protocol bump** — 1b stays non-protocol-bearing.
**The terminal answer follows from the encoder.** `sgr_mouse_report`
(`src/terminal/input.rs`) already encodes `ScrollLeft` as **66** and
`ScrollRight` as **67** (`:126`–`:127`), so the terminal handles both
axes the moment they are emitted. Its guard (`:111`) returns `None`
unless `mouse_sgr` is on and tracking is not `Off`, so a
**non-reporting terminal is horizontally inert** — which matches the
TUI, where nothing consumes a horizontal tick either. No new arm is
needed.
#### The panel replay gap — a pre-existing defect, repaired in a prerequisite lane
**Revision 14 cited `src/daemon.rs:6683` as the panel's `ScrollUp`
handler. That was wrong: it is a test fixture**, inside `#[cfg(test)]
mod tests` (opened at `:3740`). Citing a fixture as production is
precisely the error B1's enumeration exists to prevent, and it hid a
real defect.
The production path is `dispatch_semantic_panel_pointer`
(`src/editor.rs:2674`). It validates the coord against the panel grid,
resolves the side window, focuses when the gesture activates — and
**returns `true` without replaying anything.** Its own doc says so:
*"**Replay is out of scope in Stage 2B-2.** Driving selection, listview
rows, or child SGR 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
emits, the daemon validates and drops. That is a **pre-existing
violation of an already-ruled contract**, uncovered by this
measurement, not created by 1b.
**Ruling: 1b does not absorb it.** 1b owns the frontend half — per-panel
residual, both axes emitted — and the replay is repaired in a
**prerequisite lane** carrying parent acceptance 48. Three reasons:
1. **It is already scoped elsewhere.** Stage 2B-3 owns replay by an
existing ruling. Absorbing it would overrule that from an input
slice.
2. **It is not input work.** Replay drives selection, listview rows and
child SGR reporting, and needs the GPU band — none of which is
pointer-and-scroll.
3. **The defect predates 1b** and is not horizontal-specific: vertical
panel scrolling is equally dead today. A fix belongs where the
contract lives, not bolted to the slice that happened to find it.
**Panel inertness is therefore NOT an option and is not claimed.** The
panel's contract is "emit both axes with its own residual"; what is
deferred is the daemon replaying them, and **1b's panel rows witness
emission only, which they must say explicitly** rather than implying a
scroll the user cannot yet see.
**Without this table the rows are satisfiable by an implementation that
is wrong**: one global accumulator passes every per-surface row that
@ -336,7 +416,12 @@ undefined, which is the part that decides whether the feature works.
fully absorbed by the clamp arms nothing.
3. **Preserved by** repaint, a same-cursor follow, resize, and vertical
wheel. **Clamped by** geometry and content changes — clamped, not
released: a narrower window must not silently discard the gesture.
released. **A WIDER viewport lowers the maximum origin**
(`widest − viewport`), so widening re-clamps downward; the gesture is
preserved at the new bound rather than discarded, and authority
survives the clamp. (Revision 14 said "a narrower window", which had
the direction backwards — narrowing *raises* the ceiling and needs no
clamp at all.)
4. **A genuine cursor-position change releases it**, and normal follow
resumes on that same event. Release is driven by the cursor
*changing*, never by elapsed time or by the follow running.
@ -346,28 +431,55 @@ undefined, which is the part that decides whether the feature works.
##### What B7 and B3 must witness
Rows against the **real call sites**, on **both** frontends — a unit
test of a helper cannot see a follow that runs inside `paint_frame`:
Rows against the **real call sites**. A unit test of a helper cannot see
a follow that runs inside a frame.
- **Preservation:** wheel sideways, then drive a real paint with the
cursor unchanged; the origin holds.
- **Release:** wheel sideways, then a genuine cursor-position change;
the origin follows the cursor on that event.
- **Cross-axis, TUI only:** wheel sideways, then wheel **vertically**.
Vertical wheel carries point in the TUI (`scroll_window`), so this
path *does* move the cursor and would release a naive
authority-on-any-cursor-write implementation. Clause 3 says the
horizontal origin survives it.
**The two frontends need DIFFERENT preservation drivers, and this is
where revision 14 was wrong.** It said "the next paint" without
qualification. **That is TUI-only.** The GPU's `render()` (`:9881`) goes
straight to `render_to_view` and **never calls `horizontal_follow`**;
the follow reaches it only through `ensure_caret_painted` (`:7674`),
whose callers are cursor paths (`:5607`, `:5659`, `:5776`, `:6320`) and
geometry paths — `resize` (`:9851`), `apply_font_facts` (`:9729`),
`reflow_dynamic_code_geometry` (`:9672`). **A GPU wheel-then-paint row
would stay green with the overwrite mutation restored**, which is a
vacuous witness of exactly the kind this framing keeps producing.
Two mutations, and both must fail their own rows:
| # | witness | driver |
|---|---|---|
| L1 | preservation, TUI | wheel sideways → **a real paint** (`paint_frame` → `prepare_window_cursor_visible`) |
| L2 | preservation, GPU | wheel sideways → **a real same-cursor geometry re-follow** (`resize` / `apply_font_facts`), cursor unchanged |
| L3 | release | wheel sideways → a genuine cursor-position change **to a column OUTSIDE the manual viewport** |
| L4 | cross-axis, **TUI only** | wheel sideways → wheel **vertically**; vertical wheel carries point in the TUI (`scroll_window`), so a naive authority-on-any-cursor-write releases here. Clause 3 says the origin survives |
| L5 | point and selection unmoved | wheel sideways on both frontends → point and selection byte-identical (clause 1) |
| L6 | clamp-absorbed motion does not arm | at the bound already, wheel further → origin unchanged **and authority NOT armed**, so the next follow moves normally (clause 2's "effective") |
| L7 | re-clamp preserves authority | wheel sideways → **widen** the viewport → origin re-clamped to the new maximum, authority still held, next same-cursor follow does not overwrite (clause 3) |
| L8 | wrap and buffer replacement clear the LATCH | wheel sideways → toggle to `Wrap` (and separately, replace the buffer) → origin zero **and authority cleared**, verified by a following `truncate` toggle where the caret rule governs again (clause 5) |
**L3 must move the cursor OUTSIDE the manual viewport.** Inside it,
`follow_left` returns the same origin, so a release row would pass
whether or not release happened — the "authority never releases"
mutation would survive it.
**L8 is not covered by the existing wrap-origin rows.** Those assert the
origin is zeroed; they cannot see a **stale latch** surviving the wrap,
which surfaces only on the return to `truncate` when the caret rule
should have resumed and does not.
Mutations, each failing its own rows and no others:
| mutation | must fail |
|---|---|
| follow ignores manual authority (always overwrites) | preservation |
| manual authority never releases | release |
| follow ignores manual authority (always overwrites) | L1, L2 |
| manual authority never releases | L3 |
| authority armed by *any* wheel event, effective or not | L6 |
| re-clamp releases authority instead of preserving it | L7 |
| wrap/replacement zeroes the origin but leaves the latch set | L8 |
| the wheel path writes point or selection | L5 |
Clause 5 is already implemented for the caret path; the mutation for it
is the existing wrap-guard removal named in the B7 row.
Clause 5's *origin* half is already implemented for the caret path; the
existing wrap-guard removal named in the B7 row remains its mutation.
**L8 covers the half that is new — the latch.**
### CORRECTION 6 — B3's right bound is vaguer than B7's, for the same bound
@ -599,14 +711,15 @@ The crate has **exactly one** executable `event_loop.exit()`, in
### 1b — pointer and scroll
> **Read §2a first.** Every line number in this table was measured
> before 1-pre and is stale. §2a re-measures them at `72da24a`, records
> three rows whose "nothing exists yet" is wrong (B3, B4, B5),
> enumerates B1's wheel targets, gives B3 B7's exact upper bound, and
> rules **Q#S1-11 (B, viewport only)** with the lifetime contract and
> witnesses B7 and B3 both depend on. The contracts below are
> unchanged, but **none of B1, B3 or B7 is implementable from this
> table alone.**
> **Read §2a first, and note that it CHANGES two of these rows.** Every
> line number below was measured before 1-pre and is stale. §2a
> re-measures them at `72da24a`; records three rows whose "nothing
> exists yet" is wrong (B3, B4, B5); **replaces B1's undefined
> "surface" with a normative six-target enumeration**; **replaces B3's
> "content bounds" with B7's exact saturated upper bound**; and rules
> **Q#S1-11 (B, viewport only)** with a five-clause lifetime contract
> and eight witnesses. B2, B4, B5, B6 and B7 are unchanged. **None of
> B1, B3 or B7 is implementable from this table alone.**
| # | Contract | Witness | Mutation |
|---|---|---|---|