docs(framing): SS5b revision 16 --- the producer rule was self-defeating, and R7 recurs on a docs-only diff

Answers review of 15. Framing only. Three items reverse a rule 15
introduced, and one retracts a mutation that was not a defect.

**THE PRODUCER RULE CONTRADICTED PROACTIVE CANCELLATION.** The daemon
cancels BEFORE emitting the replacement frame, so the frontend needs
only to clear its local latch when that frame arrives, and send
nothing. Revision 15 asked it to emit a cancellation tail or retain the
latch: the tail is redundant --- the daemon would receive a release for
a gesture it has already settled, which is the duplicate release the
latch exists to prevent --- and RETAINING IS ACTIVELY HARMFUL, because
it manufactures a `Drag` under the NEW generation with no accepted
`Down`. That is the exact orphan the section exists to prevent,
produced by the rule meant to prevent it.

Ordering is what makes the simple rule safe: cancel, then emit. The
frame's arrival IS the cancellation signal; no second channel is
needed. Witnessed as `Down` -> key advances -> replacement frame ->
motion and physical `Up` produce no new drag and no duplicate release.

**THE LATCH HAD ONE TRIGGER AND NEEDED FIVE.** Cancellation runs on
every loss of gesture authority: generation advance, `Absent`, panel or
buffer identity change, geometry-epoch change EVEN AT AN UNCHANGED CELL
TOTAL, and detach. And an ordinary accepted `Up` must clear the latch,
or a later invalidation finds a gesture it believes live and
synthesises a duplicate release for a button already up --- the replay
lane's D1/D2 orphan race, arriving from the daemon's side.

**G9b's MUTATION WAS A VALID IMPLEMENTATION, NOT A DEFECT.** Keying the
dedupe by `(mapping_generation, coord)` preserves same-generation
suppression and naturally admits the first motion under a new
generation. Requiring it to fail would have forbidden a correct design.
Replaced with two real defects: compare only the cell and never key or
reset by generation (the first post-change motion is eaten), and reset
on every same-generation repaint (pixel-rate traffic returns).

**"PROJECTED CELL IDENTITY" CONTRADICTED THE STYLING CONTROL** in the
same section. The wire `Cell` derives `PartialEq` over `glyph`, STYLE
and `attachment` (`pmacs-protocol/src/cell.rs:153`), so an identity
keyed on cell equality moves on a pure recolour --- while the stable
controls rule style out. Terminal identity is now glyph and row
TOPOLOGY plus the view anchor, excluding face, style and cursor, with a
same-glyph/different-style control: the row that catches an
implementation reaching for `Cell` equality because it is right there.

P2s: zero-generation rows added in BOTH directions as independent legs
(a valid `PresentMapped` with generation zero must be rejected
atomically; a zero-generation `PanelPointerMapped` must be refused);
G7 split into outbound mapped-frame and inbound mapped-pointer legs,
since its old mutation only withheld the frame; G2's grid rows/columns
and fold-map-content/`fold_projection`-policy composites split; and
SS20 now names journey steps 5 and 8 while stating neither grade
changes --- an auditor scanning for grade movement alone would
otherwise conclude this slice touches no journey.

**AND R7 RECURRED, ON A DIFF THAT IS ENTIRELY DOCUMENTATION.** The
first `--protocol` run of this tree failed the `gpu` step on
`managed_retry_survives_transients_and_uses_the_successful_stream`,
with all three required fragments verified from the durable log
(`20260815T072601Z`). Recorded as R7's FIFTH occurrence.

It carries the strongest tree exclusion the row has had: occurrences 1
and 4 argued "unrelated lane", while this branch cannot be related at
all --- no Rust, no wire surface, no `pmacs-gpu` file. The line moved
to `attach.rs:1728` from `:1680`, which the row already treats as
occurrence-specific rather than a fragment. Isolated rerun green, and
the full gate green on the re-run (271/271 in the `gpu` step) --- which
per this file's rerun rule establishes INTERMITTENCE ONLY, though here
there is no tree change to exonerate.

What five occurrences across three flavors and five unrelated lanes now
support is that the failure is NOT LANE-CORRELATED. That is evidence
about where the cause is not. The retirement condition is unchanged.

Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260815T073556Z.

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-15 09:44:01 +02:00
parent fa980ef1ba
commit 137c7fc736
No known key found for this signature in database
3 changed files with 153 additions and 19 deletions

View File

@ -281,7 +281,7 @@ from #171 and #215.
**`githubsucks/panel-mapping-generation` is the authoritative tip.**
Recover with `git fetch githubsucks && git checkout
panel-mapping-generation`.
- **No PR yet. Checkpoint: framing revision 15 (§5b) AWAITING
- **No PR yet. Checkpoint: framing revision 16 (§5b) AWAITING
APPROVAL; NO IMPLEMENTATION.**
- **PROTOCOL-BEARING — v25, and it runs alone.**
`ADVERTISED_PROTOCOL_VERSION` stays pinned at **20**.
@ -327,10 +327,21 @@ from #171 and #215.
otherwise the first tick advances the key and the next queued tick is
refused, so the panel scrolls once and dies. Child-reported terminal
wheels keep the check, because SGR carries row and column.
- **Cancellation is PROACTIVE**, triggered by the key advancing, with a
daemon-side accepted-gesture latch. Reactive cancellation loses a
race: a replacement frame landing before the physical `Up` makes the
producer suppress the very event that would cancel.
- **Cancellation is PROACTIVE** — the daemon cancels, THEN emits the
replacement frame, and the frontend **clears its latch on receiving
that frame and sends nothing**. Revision 15 asked the producer to
emit a tail or retain the latch; retaining it manufactures a `Drag`
under the new generation with no accepted `Down`.
- **The latch dies on EVERY loss of gesture authority**: generation
advance, `Absent`, panel/buffer identity change, geometry-epoch
change **even at an unchanged cell total**, and detach — **and on an
ordinary accepted `Up`**, or a later invalidation synthesises a
duplicate release.
- **Terminal identity EXCLUDES style.** Wire `Cell` equality covers
`glyph`, `style` and `attachment`
(`pmacs-protocol/src/cell.rs:153`), so keying on it would move the
revision on a recolour and contradict the stable control. It is
glyph/row topology plus the view anchor.
- **The terminal key is NOT `Screen`'s generation** — that advances
from 39 sites including style, bell, tab-stops and cursor-only
motion, none of which change what a coordinate denotes.

View File

@ -1772,7 +1772,35 @@ cannot preserve the old panel-focused attach leak.
## 5b. The cell-mapping generation — a protocol slice (Q#BP-R3)
**Status: revision 15 — AWAITING APPROVAL. Nothing implemented.**
**Status: revision 16 — AWAITING APPROVAL. Nothing implemented.**
Revision 16 answers review of 15; three of its items reverse a rule 15
introduced:
- **The producer rule contradicted proactive cancellation.** Since the
daemon cancels *before* emitting the replacement frame, the frontend
simply **clears its latch on receiving that frame and sends
nothing**. 15's "emit a tail or retain the latch" was redundant in
the first case and **actively harmful** in the second — retaining it
manufactures a `Drag` under the new generation with no accepted
`Down`, the exact orphan the section exists to prevent.
- **The latch had one trigger and needed five.** Cancellation runs on
every loss of gesture authority: generation advance, `Absent`, panel
or buffer identity change, geometry-epoch change **even at an
unchanged cell total**, and detach. **An ordinary accepted `Up` must
clear the latch**, or a later invalidation synthesises a duplicate
release.
- **G9b's mutation was a valid implementation.** Keying dedupe by
`(mapping_generation, coord)` is a correct design, so requiring it to
fail would have forbidden a good one. Replaced with two real defects.
- **"Projected cell identity" contradicted the styling control** — wire
`Cell` equality includes `style`. Terminal identity is now glyph and
row topology plus the view anchor, **excluding face, style and
cursor**, with a same-glyph/different-style control.
- Zero-generation rows added **in both directions**; G7 split into
outbound and inbound legs; G2's grid and fold composites split;
journey steps **5** and **8** named in §20.
**Previously, revision 15 — SUPERSEDED.**
Revision 15 answers review of 14, and four of its items reverse
something 14 asserted:
@ -1866,12 +1894,25 @@ recoloured a character or rang the bell.
**Define a dedicated terminal mapping revision** over the things that
actually decide the inverse:
- **projected cell identity** — the grid the panel paints, including
size;
- **glyph and row TOPOLOGY** — which glyphs occupy which cells, and
which rows the projection holds;
- **retained-row identity** — scrollback rows entering or leaving the
projection;
- **the per-view scroll anchor**.
**Explicitly EXCLUDING face, style and cursor**, and revision 15's
"projected cell identity" got this wrong. The wire `Cell` derives
`PartialEq` over `glyph`, **`style`** and `attachment`
(`pmacs-protocol/src/cell.rs:153`), so an identity keyed on cell
equality **moves on a pure recolour** — which contradicts the very
stable control that rules style out, in the same section. The two
statements could not both hold.
**Control row: same glyphs, different style** — the child repaints the
projection in a new colour and the revision **does not move**, so a
drag survives it. That is the row that catches an implementation
reaching for `Cell` equality because it is right there.
**Stable controls are required, not optional**: style, title, bell,
tab-stop and cursor-only operations each get a row asserting the
revision **does not move**. Those are precisely the events the
@ -2031,18 +2072,56 @@ stays armed and the child keeps holding its button. **The cancelling
event never arrives.**
So the trigger is the authoritative key advancing while a gesture is
accepted, and the producer may not simply forget:
accepted:
1. **Daemon, on the key advancing with an accepted gesture live** —
1. **Daemon, on losing gesture authority with a gesture accepted** —
this runs whether or not any further event arrives:
- **document:** clear an empty selection, clear the click chain,
apply no cursor move;
- **terminal:** deliver the child's **release**, at the last
coordinate known valid, with the button and encoding the accepted
`Down` used.
2. **Producer:** either **emit a cancellation tail before clearing**,
or **retain the latch** until the release has been sent. It may not
clear first and drop the `Up`, which is what revision 14 permitted.
2. **Producer: clear the local latch on receiving the replacement
frame, and send nothing.** The daemon has already cancelled, ahead
of emitting that frame, so the gesture is over before the frontend
hears about it.
**Revision 15 asked the producer to emit a cancellation tail or retain
the latch. Both are wrong, and the second is actively harmful.** A tail
is redundant — the daemon cancelled first and would receive a release
for a gesture it has already settled, which is the duplicate release
the latch exists to prevent. **Retaining the latch is worse: it
manufactures a `Drag` under the NEW generation with no accepted
`Down`** — precisely the orphan this whole section is about, created by
the rule meant to avoid it.
**Ordering is what makes the simple rule safe.** Cancel, then emit. The
frontend's arrival of the replacement frame *is* the cancellation
signal, so no second channel is needed.
**Witness:** `Down` → the key advances → the replacement frame lands →
subsequent motion and the physical `Up` produce **no new drag and no
duplicate release**.
##### The latch's full lifecycle
**Cancellation runs on EVERY loss of gesture authority, not only a
mapping-generation advance.** Revision 15 named one trigger and left
the rest to be inferred:
| trigger | why it ends the gesture |
|---|---|
| **mapping generation advances** | the cells mean different bytes |
| **`Absent`** | the band the gesture belongs to is gone |
| **panel or buffer identity change** | the successor never saw the press |
| **geometry-epoch change** | a new declaration, **even when the cell total is unchanged** — the frontend re-declared, so the grid it hit-tested against is not the one in force |
| **detach** | there is no frontend left to finish the gesture |
**And an ordinary accepted `Up` MUST clear the latch.** Otherwise a
later invalidation finds a gesture it believes is still live and
**synthesises a duplicate release** for a button already up. That is
the same orphan race the replay lane's D1/D2 producer resets expose,
arriving from the daemon's side instead of the frontend's.
**The daemon needs an ACCEPTED-GESTURE LATCH** to do any of this, and
revision 14 assumed state that does not exist. Per frontend it records:
@ -2102,19 +2181,33 @@ added beside it.
| # | row | mutation |
|---|---|---|
| G1 | a **foreign** edit before the next render → the old generation is refused | never advance on content change → G1 passes a stale hit |
| G2 | every **changing** entry moves the key, one row each — and the composites are **split**: `view_top`; `view_left`; grid size; folds; **wrap** and **gutter geometry** separately; buffer content; terminal **projected content** and **scrollback movement** separately | omit that one entry from the key |
| G2 | every **changing** entry moves the key, one row each — and the composites are **split**: `view_top`; `view_left`; grid **rows** and **columns** separately; **fold-map content** and the owning frontend's **`fold_projection` policy** separately; **wrap** and **gutter geometry** separately; buffer content; terminal **projected content** and **scrollback movement** separately | omit that one entry from the key |
| G3 | every **stable** entry leaves it unchanged, one row each — focus; styling; selection-only; absorbed cursor motion; and the terminal controls **style, title, bell, tab-stop, cursor-only** | include that one entry → drags cancel on a repaint |
| G4 | a **selection repaint** preserves the generation and an in-flight drag **continues** | as G3 |
| G5a | the key advancing with an accepted gesture live **raises cancellation** — the daemon acts without waiting for another event | make cancellation reactive to a refused event → nothing happens when the frame lands before the physical `Up` |
| G5b | a stale `Up` with **no accepted `Down`** is **inert** | synthesise a release anyway → an unpressed button is released |
| G5c | cancellation **does not reclaim** a controller another frontend has since taken | reclaim it → a stale gesture steals a live one's terminal |
| G6 | **v24 positive control** — a legacy session drives a panel gesture end to end | gate ≤ v24 off → old peers lose the panel |
| G7 | **v25 positive control** — a mapped session drives one end to end | withhold `PresentMapped` from a v25 peer → the mapped path never runs and every refusal row still passes |
| G7a | **v25 positive control, OUTBOUND** — a v25 peer receives `PresentMapped`, carrying a live generation | withhold `PresentMapped` from a v25 peer → the mapped frame never reaches it |
| G7b | **v25 positive control, INBOUND** — a `PanelPointerMapped` with the current generation is **accepted and takes effect** | ignore the mapped variant inbound → every refusal row still passes while nothing works |
| G8a | a **bare `PanelPointer` from a ≥ v25 session is REFUSED** | accept it → the bypass returns inbound |
| G8b | a **legacy `Present` at a ≥ v25 frontend is REJECTED** | paint it → the frontend hit-tests a band it cannot map |
| G9a | **daemon emission**: identical cells across a generation change are still **emitted** | dedupe daemon-side across the change → the second gesture is suppressed |
| G9b | **frontend motion dedupe** is unchanged within one generation, and re-arms across one | fold the generation into the motion dedupe → pixel-rate traffic returns, or the first post-change motion is eaten |
| G9b | **frontend motion dedupe** suppresses a repeat within one generation and **re-arms across one** | (a) compare **only the cell**, never keying or resetting by generation → the first post-change motion is **eaten**; (b) reset on **every same-generation repaint** → repeated same-cell motion **returns** as pixel-rate traffic |
**G9b's mutation in revision 15 was a valid implementation, not a
defect.** Keying the dedupe by `(mapping_generation, coord)` preserves
same-generation suppression *and* naturally admits the first motion
under a new generation — it is one correct way to satisfy the row, so
requiring it to fail would have forbidden a good design. The two
mutations above are actual defects, one in each direction.
| G10 | an **invalid** mapped frame retains the previous frame **and** its generation, atomically | update one without the other → a valid generation names a frame never shown |
| G10a | a **structurally valid `PresentMapped` carrying generation ZERO is REJECTED**, atomically — previous frame and generation both retained | accept zero → a sender that never initialised the field disables the check for the whole session |
| G10b | a **`PanelPointerMapped` carrying generation ZERO is REFUSED** before mutation | accept zero → the inbound half of the same opt-out |
G10a and G10b are **independent rows in opposite directions**, not one
row seen twice: zero can arrive from either peer, and G10's atomicity
row exercises neither.
| G11 | **generation exhaustion fails CLOSED, and does not leave a zombie band**: the daemon publishes **`Absent`**, clears input authority, **cancels any accepted gesture**, and **latches** exhaustion for the session | fail open → the hole returns at the boundary; refuse input only → a stale panel stays painted and permanently inert, with no signal to the user; omit the latch → the next frame resurrects it |
G2 and G3 are enumerated per entry deliberately: one row asserting "the
@ -2152,8 +2245,11 @@ with no witness in the slice that introduces it.
### Coherence impact (`COHERENCE.md` §20)
- **Journey steps: none change grade.** This hardens a step that
already works rather than opening one.
- **Journey steps touched: 5** (document-panel editing and selection)
and **8** (terminal-panel interaction). **Neither changes grade** —
this hardens steps that already work rather than opening one.
Naming them matters even so: a reader auditing §20 by grade movement
alone would conclude this slice touches no journey at all.
- **Interaction islands: none added.** It **hardens an existing panel
island** — the same gestures, refused when their mapping is stale.
- **Config registry: no entry.** Nothing here is tunable; a generation

View File

@ -496,12 +496,39 @@ Stage 4; the lane touches no `pmacs-gpu` code at all.
| **selector** | `-p pmacs-gpu attach::tests::managed_retry_survives_transients_and_uses_the_successful_stream` |
| **job / flavor** | local (Linux), `cargo test --workspace --features crdt --no-fail-fast`, i.e. under full-sweep load |
| **required fragments** | `transient sequence must attach` + `Handshake(Io(` + `BrokenPipe` (or `code: 32`) |
| **status** | **FOURTH OCCURRENCE 2026-08-13 — causal status still UNRESOLVED. A NEW candidate mechanism is introduced by the observing lane and is NOT excluded (see below)** |
| **status** | **FIFTH OCCURRENCE 2026-08-15 — causal status still UNRESOLVED.** The fifth carries the strongest tree exclusion this row has had: a **documentation-only diff** |
| **what IS established** | **three** occurrences at `pmacs-gpu/src/attach.rs:1680`, the second and third with all three fragments **verified** rather than inferred; the test drives a scripted transient-then-success sequence over a real socket pair. **The added GPU test is not the mechanism** — see the third-occurrence control below |
| **what is NOT** | whether the broken pipe is the *fixture's* writer closing early or a real retry-path defect. **This row is not a claim that it is harmless** |
| **rerun evidence** | occurrence 1: 6 isolated runs green, plus a full `--workspace --features crdt` sweep green (113 targets). Occurrence 2: **30 green on the observing branch** (15 isolated selector, 15 full `-p pmacs-gpu`) **plus a 15-run merge-base control, also green**. Occurrence 3: 5 isolated selector runs green, 10 full `-p pmacs-gpu` runs green **with** the added test, and **1 failure in 10 with the added test `#[ignore]`d** — the first rerun in this row's history that reproduced anything. Per the rerun rule the green runs establish intermittence only; the red control run is what carries the exclusion |
| **retirement** | hardening that removes the named mechanism plus a discriminating witness — or a diagnosis showing the fixture, not the code, closes the pipe |
**Fifth occurrence — panel cell-mapping generation (§5b) framing,
2026-08-15, local (Linux).** The `scripts/gate` **`gpu` step** again,
the same flavor as occurrence 2, inside a `--protocol` run
(log `20260815T072601Z-2230169`).
* **All three fragments verified** from the durable log, not a filtered
stream: `transient sequence must attach: Attach(Handshake(Io(Os {
code: 32, kind: BrokenPipe, message: "Broken pipe" })))`.
* **The line moved and that is not a fragment.** It is
`pmacs-gpu/src/attach.rs:1728` here against `:1680` in the earlier
occurrences — `attach.rs` has changed since, and this row's
convention already treats a `:LINE` suffix as occurrence-specific.
* **The tree exclusion is the strongest available in this row's
history: the branch's entire diff is DOCUMENTATION.** No Rust, no
wire surface, no `pmacs-gpu` file. Occurrences 1 and 4 argued
"unrelated lane"; this one cannot be related at all.
* **Rerun: isolated selector green** (`1 passed`, 0.01 s). Per this
file's rerun rule that establishes **intermittence only** and does
not exonerate the tree — though here there is no tree change to
exonerate.
**What five occurrences now support, stated carefully:** the failure is
**not lane-correlated**. It has appeared under three flavors across
five unrelated lanes, once on a diff that touches no code whatsoever.
That is evidence about *where the cause is not*, and still says nothing
about what it is. **The retirement condition is unchanged.**
**Not attributed to the observing lane**, and in neither case is the
reasoning merely "my diff looks unrelated": long-lines Stage 4 added no
wire surface, no protocol version change, and touched no file in