diff --git a/docs/active-work.md b/docs/active-work.md index b4a90f6..288c709 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -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. diff --git a/docs/bottom-panel-framing.md b/docs/bottom-panel-framing.md index 6faaa04..1deee77 100644 --- a/docs/bottom-panel-framing.md +++ b/docs/bottom-panel-framing.md @@ -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 diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index cdc4818..4177b75 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -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