docs(panel): Stage 2 framing rev 5 — the three-way slice of 2B

Rev 4 §9 scoped Stage 2B as a single PR: v21 protocol, daemon panel
projection, GPU band, and the negotiated `panel_capable` flip.
Implementation showed that to be roughly four thousand lines across
three crates with three different failure modes. Rev 5 splits it into
2B-1 (the wire layer), 2B-2 (the daemon projection and epoch machine),
and 2B-3 (the GPU band and the flip), on the rule that a slice ends
where the next thing to build has a different authority.

No decision changes. What changes is the allocation:

- §7.2 becomes three subsections, and criteria that span a boundary are
  named in every slice they touch with their half stated, rather than
  assigned wholesale to one. Parent 39 is the clearest case: its
  shared-validation and transport-budget halves are wire properties
  provable in 2B-1, while "the previous valid frame is retained" and "a
  duplicate does no work" need the epoch machine and are 2B-2. A2B-1
  splits the same way — grid exhaustion in 2B-2, the frontend latch in
  2B-3.
- §9 lists four serial PRs instead of two, each cut from `main`, and
  states that every slice runs the full gate set rather than the subset
  its own crate suggests.
- §6 records which slice pays the coherence debt. The journey claim
  belongs to 2B-3 alone: with `panel_capable = false`, a GPU user still
  gets the Stage 1 non-side fallback after 2A, 2B-1 and 2B-2 have all
  landed. Three quarters of this stage is preparation.

Two things recorded because they are easy to inherit silently:

- This revision is retroactive for slice 1. `bottom-panel-stage2b`
  already carried the v21 protocol layer, written before the revision
  existed, which inverts framing -> approval -> branch -> implement. The
  slicing was sound; taking it in code rather than in the document is
  how a stage's scope drifts without anyone deciding that it should.
- 2B-1 and 2B-2 ship dark. The bump advertises a capability whose only
  distinguishing feature is unreachable until 2B-3, so the arc must not
  stall between them. Safe for compatibility — appended variants,
  extended ladder, a v20 peer still negotiates 20 — but a stall should
  be visible as a decision, not inherited as a default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
This commit is contained in:
Levi Neuwirth 2026-07-26 20:02:19 -04:00
parent 9b364adc26
commit 56301eda02
1 changed files with 190 additions and 22 deletions

View File

@ -1,7 +1,10 @@
# Bottom panel Stage 2 — the GPU panel band (framing)
**Revision 4 — pre-implementation. Ground truth: canonical `main` @
`ccf29e3`, protocol v20, 2026-07-25.**
**Revision 5 — 2A merged, 2B in progress. Ground truth: canonical
`main` @ `42025e4`, protocol v20 on `main` and v21 on
`bottom-panel-stage2b`, 2026-07-26.** Revisions 14 were
pre-implementation; rev 5 records the three-way slice of Stage 2B
(§0.0, §7.2, §9) after its first slice was already built.
Stage 1 (#155, merge `e745068`) gave pmacs window placement, window
parameters, TUI side windows, the divider, and the adopter `display`
@ -26,7 +29,57 @@ geometries), Q#BP16 (pointer transport), Q#BP17 (fold projection), and
## 0. Revision history
### 0.0 Round 3 (rev 3 → rev 4) — 1 blocking, 1 high, 1 medium, all closed
### 0.0 Rev 4 → rev 5 — the three-way slice of 2B (not a review round)
This revision changes no decision. It splits one approved
implementation slice into three and reallocates the acceptance
criteria across them.
- **R5-1 — why.** Rev 4 §9 scoped 2B as a single PR: v21 protocol,
daemon panel projection, GPU band, and the negotiated
`panel_capable` flip. Implementation showed that to be roughly four
thousand lines spanning `pmacs-protocol`, `src/daemon.rs`, and
`pmacs-gpu` — three review surfaces with different failure modes, in
one diff. The same argument that produced 2A/2B applies again one
level down, and it is the argument this arc has already accepted
twice (Lean 4 stages 3a/3b and 4a/4b).
- **R5-2 — the boundary rule.** A slice ends where the next thing to
build has a different *authority*: the wire format, the daemon that
produces frames, and the frontend that paints them. Each slice is
independently reviewable against a subset of the parent criteria,
and each is additive — no slice makes a previously-passing assertion
fail.
**Criteria that span a boundary are named in every slice they touch,
with their half stated**, rather than assigned wholesale to one. The
clearest case is parent 39: its shared-validation and
transport-budget halves are wire properties provable in 2B-1, while
"the previous valid frame is retained" and "a duplicate does no
work" are receiver-state properties that need the epoch machine and
land in 2B-2.
- **R5-3 — this revision is retroactive for slice 1, and that is a
process defect worth recording.** `bottom-panel-stage2b` already
carries the v21 protocol layer (three commits, one review round
closed) written before this revision existed. The workflow is
framing → approval → branch → implement; slice 1 inverted it. The
slicing decision was sound, but it was taken in code and discovered
in the branch rather than proposed in the document, which is exactly
how a stage's scope drifts without anyone deciding that it should.
Rev 5 exists to put the decision back where it belongs before slices
2 and 3 are written.
- **R5-4 — two of the three slices ship dark, deliberately.** Nothing
in 2B-1 or 2B-2 is reachable by a user: `panel_capable` stays
`false` for every negotiated semantic session until 2B-3, so a v21
daemon and a v21 GPU frontend negotiate 21 and behave exactly as
they do at v20. This is the same posture 2A took ("seam adoption
that becomes load-bearing in 2B") and it carries the same
obligation: **the version bump advertises a capability whose only
distinguishing feature is unreachable until 2B-3 lands.** That is
safe for compatibility — the variants are appended, the ladder is
extended, and a v20 peer still negotiates 20 — but it means the arc
must not stall between 2B-1 and 2B-3. Recorded here so a stall is
visible as a decision rather than inherited as a default.
### 0.1 Round 3 (rev 3 → rev 4) — 1 blocking, 1 high, 1 medium, all closed
- **R3-1 (blocker).** Rev 3's three-boundary model was right but its
call-site table was wrong in five places, and each error was a real
@ -54,7 +107,7 @@ geometries), Q#BP16 (pointer transport), Q#BP17 (fold projection), and
`cell.attachment.is_some()` rejection. It is now classified — and
**shared**, with the reasoning pinned.
### 0.1 Round 2 (rev 2 → rev 3) — 1 blocking, 2 high, 1 medium, all closed
### 0.2 Round 2 (rev 2 → rev 3) — 1 blocking, 2 high, 1 medium, all closed
- **R2-1 (blocker).** Rev 2's "one document-bottom seam" conflated two
boundaries that must **diverge** once a panel exists. Several sites it
@ -81,7 +134,7 @@ geometries), Q#BP16 (pointer transport), Q#BP17 (fold projection), and
- Both §8 open items are decided (§5.3): `BASE_DIVIDER_HEIGHT = 4.0` at
scale 1.0, and `TEXT_TOP` stays unscaled.
### 0.2 Round 1 (rev 1 → rev 2) — 2 blocking, 3 high, 3 revision points, all closed
### 0.3 Round 1 (rev 1 → rev 2) — 2 blocking, 3 high, 3 revision points, all closed
- **R1-1 (blocker).** Rev 1 said all 23 census reads route through
`primary_document_window`. That contradicts Q#BP14, which routes only
@ -589,11 +642,20 @@ exactly once.
- **Section this serves:** `COHERENCE.md` §14, which records the panel
primitive as landed for Stage 1 and names "Stage 2 (GPU band)
pending its own framing" as the open item.
- **Which slice pays the coherence debt (rev 5).** The journey claim
above is Stage 2B-3's alone. 2A, 2B-1, and 2B-2 close **no** journey
divergence: with `panel_capable = false`, a GPU user still gets the
Stage 1 non-side fallback on steps 710 after all three land. Stated
explicitly so no slice's PR can claim the arc's coherence benefit
before the flip earns it — three quarters of this stage is
preparation, and only the last quarter is the improvement.
## 7. Acceptance
**Parent criteria 3755 remain authoritative and are not replaced.**
This section maps them to the two slices and adds only refinements.
This section maps them to the four slices — 2A, then 2B-1/2B-2/2B-3 —
and adds only refinements. A criterion that spans a slice boundary is
named in each slice it touches, with its half stated.
### 7.1 Stage 2A — classified census routing + painter extraction
@ -638,18 +700,80 @@ Refinements 2A adds:
scroll state**. Byte-identical cells alone would not catch a clamp
that silently moved to the wrong window.
### 7.2 Stage 2B — v21 protocol + daemon projection + GPU band
### 7.2 Stage 2B — v21 protocol, daemon projection, GPU band
Parent criteria that apply in full: **37, 38, 39, 40, 41, 45, 46, 47,
48, 49, 50, 51, 53, 54, 55**, plus re-assertion of **42, 43, 44, and
52** **through the actual negotiated capability flip** rather than
Stage 2B as a whole owns parent criteria **37, 38, 39, 40, 41, 45, 46,
47, 48, 49, 50, 51, 53, 54, 55**, plus re-assertion of **42, 43, 44,
and 52** **through the actual negotiated capability flip** rather than
through a test-only panel-capable semantic view. 52's 2B form is the
production one: a real semantic frontend with `fold_projection = false`
displaying a folded buffer in a panel shows every source line, and the
panel path never reaches `fold_map_for_window`.
Refinements 2B adds:
Per §0.0 R5-1 those land across three slices. Each slice's own gate run
is the standing suite plus §9's named acceptance suites; **only 2B-3
changes what a user sees.**
#### 7.2.1 Slice 2B-1 — the v21 wire layer
**Authority: `pmacs-protocol`.** The four wire shapes Q#BP9 names, the
version bump, and the shared cell-grid validator. No producer, no
consumer, no capability change.
- **37, in full.** `PanelFrame` round-trips including `panel_epoch` and
`geometry_epoch`, with independent byte pins on the previous final
`InstanceMessage::InitialTargetResult` and
`FrontendEvent::TerminalPointer` variants. **Both pins must be
falsified by revert**, not merely observed passing: a byte pin that
never saw the shift it exists to catch pins nothing.
- **39, the wire half only.** Shared cell/topology/glyph/area
validation; an area-bounded panel wider than 512 columns is accepted
while a terminal frame retains its 512-column PTY cap; the maximum
legal panel encoding stays below the transport limit. **The ratchet's
fixture must be shown to spend the whole aggregate glyph budget** —
otherwise it measures something smaller than the worst case and the
bound it proves is not the bound that matters. The worst case is
`1 × MAX_PANEL_VISIBLE_CELLS`, a legal panel geometry no terminal can
express, so the terminal's own ratchet has never covered it.
**39's receiver half — atomic rejection with retention of the
previous valid frame, and a duplicate doing no work — is 2B-2.**
- **The version ladder moves with the bump.** `PROTOCOL_VERSION`
becomes 21, `SUPPORTED_PROTOCOL_VERSIONS` accepts `6..=21` and
rejects 22, and any test whose *name* encodes the old number is
renamed. A ladder pin that passes across a bump was not pinning the
version.
- **Shared bounds are aliased, not duplicated.** Every constant the
terminal screen and the panel validator both enforce is one
definition with the other as an alias, so truncation and validation
cannot drift apart.
- **Not in this slice:** the daemon arm that drops panel events from a
grid session is exhaustiveness bookkeeping the bump forces, not
projection. It asserts only that a grid session's panel declaration
is dropped rather than trusted.
#### 7.2.2 Slice 2B-2 — the daemon panel projection and epoch machine
**Authority: `src/daemon.rs`.** Produces `PanelFrame`; derives the
grid; owns stale-event rejection. Exercised through a **test-only**
panel-capable semantic view — `panel_capable` stays `false` in
production negotiation until 2B-3.
- **38** (open → replace buffer → hidden by a tiny frame → reappear →
close, with authoritative `Absent` and a new epoch on
replacement/reappearance), **40** (first open at a non-80×24 frame
stays absent until real `FrontendCellGeometry` arrives, never
consulting the 24×80 attach placeholder), **49**, **50**, **51**,
**53**.
- **39's receiver half**, per §7.2.1.
- **41, the daemon half:** the daemon alone derives the grid; an older
retained frame neither paints nor accepts input after a new
`geometry_epoch` until a matching `Present` arrives; row-clamping
preserves the stored request; zero, non-finite, and non-positive
metric inputs fail closed to zero usable geometry. *The pixel→cell
formula and its call sites are 2B-3.*
- **42, 43, 44, 45, 52** in their projection form, through the
test-only panel-capable view. Their production re-assertion through
the real flip is 2B-3.
- **A2B-1.** The epoch state machine of §3.1 is pinned row by row,
including the lower-epoch-identical-data rejection and the
same-epoch-different-total rejection, and each row's
@ -660,7 +784,33 @@ Refinements 2B adds:
hides (a subsequent real resize must not paint a stale-geometry
panel), and a frontend that exhausts latches — a retained `Present`
whose epoch still matches cannot make the band reappear, and only a
fresh session clears the latch.
fresh session clears the latch. **A2B-1's grid-exhaustion half is
2B-2; its frontend-latch half needs a real frontend and is 2B-3.**
Both halves are named here so neither is lost at the seam.
#### 7.2.3 Slice 2B-3 — the GPU band and the capability flip
**Authority: `pmacs-gpu`, plus the negotiation rule.** This is the only
slice a user can observe, and the only one that closes the journey
divergence in §6.
- **46** (band + divider shrink the document text area by exactly their
pixel height; carets, hits, and scroll geometry respect the reduced
area), **47** (divider drag, `window.min-height`, `RowResize` hover,
and the stalled-writer tail-coalescing), **48** (`PanelPointer`
driving selection, terminal mouse reporting, and click-to-focus
without disturbing the document mirror), **54** (the
`--headless-probe` run: one real daemon, real PTY, real wgpu, through
a panel-hosted terminal), **55**.
- **41, the GPU half:** the pixel→cell conversion pinned at fractional
widths and heights, and geometry refresh on window resize, font
change, and scale change.
- **42, 43, 44, 45, 52 re-asserted through the production flip**, not
the test-only view. This is the point of the re-assertion: a
test-only panel-capable view can be constructed wrongly and agree
with itself, so the production negotiation path must carry the same
assertions.
- **A2B-1's frontend-latch half**, per §7.2.2.
- **A2B-2.** A font or scale change that leaves `CellSize` **identical**
still produces a new `geometry_epoch`, and the older `PanelFrame`
neither paints nor hit-tests until a matching `Present` arrives. This
@ -704,19 +854,37 @@ fixes. It belongs to a spacing-system change of its own.
## 9. Slices, branches, and gates
Per review round 1: **two serial implementation PRs**, each a named
slice under this framing so one-feature/one-branch/one-PR holds. **2A
lands before 2B branches** — not stacked.
Per review round 1 and §0.0 R5-1: **four serial implementation PRs**,
each a named slice under this framing so one-feature/one-branch/one-PR
holds. **Each slice lands before the next branches** — none are
stacked, and each is cut from `main`.
- **Stage 2A** — classified census routing + per-window painter
extraction. Branch `bottom-panel-stage2a`. No protocol change. The
three-boundary GPU split is **2B**, not 2A: it is only observable
once a band can be installed.
- **Stage 2B** — v21 protocol, daemon panel projection, GPU band, and
the negotiated `panel_capable` flip. Branch `bottom-panel-stage2b`,
cut from `main` after 2A merges. Repeats 2A's relevant census
- **Stage 2A — MERGED as #177** (`main` @ `0a3fcd1`). Classified census
routing + per-window painter extraction. Branch
`bottom-panel-stage2a`. No protocol change. The three-boundary GPU
split is **2B-3**, not 2A: it is only observable once a band can be
installed.
- **Stage 2B-1 — the v21 wire layer.** Branch `bottom-panel-stage2b`.
The four wire shapes, the version bump, the shared cell-grid
validator, and the version-ladder move. **No producer, no consumer,
no capability change** — `panel_capable` stays `false`.
- **Stage 2B-2 — the daemon panel projection and epoch machine.** Cut
from `main` after 2B-1 merges. Produces `PanelFrame` and owns
stale-event rejection, exercised through a **test-only**
panel-capable semantic view. Still no production flip.
- **Stage 2B-3 — the GPU band and the negotiated flip.** Cut from
`main` after 2B-2 merges. The three-boundary text-area split, the
divider, pointer routing, and `panel_capable = true` for a v21+
negotiated authenticated semantic session. **This is the slice that
changes what a user sees**, and it repeats 2A's and 2B-2's relevant
assertions through the real capability flip.
**Each slice runs the full gate set below, not a subset of it.** A
slice that touches only `pmacs-protocol` still runs the GPU and vterm
suites: the shared validator and the wire enums are exactly the kind of
change whose breakage surfaces in a consumer rather than at its own
definition.
Gates for both: the standing suite from `CLAUDE.md`, plus the **touched
acceptance suites named explicitly** — the standing rule is to run the
suites a change touches, and "standing suite" does not name them: