feat(panel): the authority-loss matrix --- four transitions gain their effect

Task 19. SS5b wired Absent and left the other four transitions armed,
inert while nothing consumed the latch and defects the moment
cancellation gained an effect.

Three are visible in the producer, where the declaration is built: the
side WINDOW replaced, its BUFFER replaced, and the GEOMETRY epoch moved.
The last needed a retained geometry_epoch on PanelPresentation, because
nothing else the producer holds moves with a same-size geometry change
--- not the panel epoch, not the identity, and on a legacy peer not a
mapping key either --- so the transition was invisible and a live gesture
survived a grid it no longer belonged to. DETACH cancels in the
dispatcher, before any teardown, because it is the transition with no
later opportunity of any kind.

The release follows the RECORDED domain, so a buffer replacement pays
the child the gesture was pressed on rather than whatever occupies the
panel now.

G5b(a)-(d), G5m and G5j, each reading the child's byte stream or the
document's selection rather than the latch. The mutations discriminate
exactly: dropping the window half of the identity check fails only the
buffer leg, dropping the buffer half fails only the window leg, dropping
the geometry check fails only that leg, and dropping detach's cancel
fails only detach.

G5m survives all four, and correctly --- two coincident causes take the
same latch, so one release, and removing either cause still leaves one.

G5j's two legs differ and the row proves it: an empty selection is
cleared without moving point, while a real dragged region survives
anchor-and-cursor exact. Clearing every selection fails the second leg,
which is the mutation that matters --- ending a gesture is not a reason
to discard what the user selected.

Machine note: a foreign java build was running at 213% CPU during this
work, and the wall-clock budget row composition_overhead_under_ten_percent
redded twice under it, green in isolation both times. Functional rows
are unaffected; the gate should wait for a quiet machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-21 11:06:44 +02:00
parent 70d531cb7d
commit e2b080f6ae
No known key found for this signature in database
3 changed files with 431 additions and 4 deletions

View File

@ -357,8 +357,30 @@ from #171 and #215 — the correction the 1b lane missed, honoured here.
the real per-frontend frame loop; the unit rows call
`render_frame` directly and never enter it. Recorded rather than
treated as covered by its neighbours.
- **REMAINING, in order:** task 19's four stranding transitions, Q5's
acceptance-shaped row, then the full head-exact gate and the PR.
- **LANDED: the authority-loss matrix (task 19).** §5b wired `Absent`
and left the other four transitions armed — inert while nothing
consumed the latch, defects the moment cancellation gained an
effect. Three are visible in the producer where the declaration is
built (**window replacement**, **buffer replacement**, and a
**geometry-epoch change including at an unchanged size**, which
needed a retained `geometry_epoch` because nothing else the
producer holds moves with it); **detach** cancels in the dispatcher
before any teardown.
- **Witnessed: G5b(a)–(d), G5m, G5j**, each reading the CHILD'S
STREAM or the document's selection rather than the latch. The
mutations discriminate exactly: dropping the window half of the
identity check fails only the buffer leg, dropping the buffer
half fails only the window leg, dropping the geometry check fails
only that leg, and dropping detach's cancel fails only detach.
- **G5m survives all four**, correctly: two coincident causes take
the same latch, so one release, and removing either cause still
leaves one.
- **G5j has two legs and they differ**: an empty selection is
cleared without moving point, a REAL dragged region survives
anchor-and-cursor exact. Clearing every selection fails the
second.
- **REMAINING:** Q5's acceptance-shaped row, then the full head-exact
gate and the PR.
- **Two test seams added for this:** an opt-in child-input tap
(`start_send_tap_for_test`) and a drag-state read
(`view_is_dragging_for_test`). Nothing else exposes what the child

View File

@ -2995,8 +2995,15 @@ fn handle_dispatcher_event(
}
}
DispatcherEvent::SessionDetached { frontend_id } => {
// Before ANY teardown: the next line drops the state that
// holds an owed release, and detach has no later chance.
// Parent 48 G5b/G5i — DETACH is the fifth authority loss,
// and the only one with no later chance at all. End the live
// gesture, then pay it, and do BOTH before any teardown: the
// next line drops the state holding the record, and
// `detach_frontend_input` below releases the terminal
// controller the release still needs.
if let Some(state) = semantic_states.get_mut(&frontend_id) {
let _ = state.cancel_accepted_gesture();
}
drain_pending_release(editor, semantic_states, frontend_id);
render_states.remove(&frontend_id);
semantic_states.remove(&frontend_id);
@ -8793,6 +8800,369 @@ mod tests {
);
}
// -----------------------------------------------------------------
// G5b — the common authority-loss matrix.
//
// A live gesture belongs to the presentation it was pressed on.
// Five transitions end that presentation; §5b wired `Absent` and
// left the other four armed, inert while nothing consumed the
// latch. They became defects the moment cancellation gained an
// effect, and each has its own cause even though they share one
// consequence.
//
// Every row reads the CHILD'S STREAM: the release is the effect,
// and a latch that empties without one is the exact failure §5b's
// round four named.
// -----------------------------------------------------------------
/// Render one frame, which is where the producer notices an
/// authority loss, and re-declare so a later gesture can be sent.
fn render_and_redeclare(
editor: &crate::editor::EditorState,
states: &mut HashMap<FrontendId, crate::semantic_render::SemanticRenderState>,
fid: FrontendId,
) {
let sem = states.get_mut(&fid).expect("projection");
let _ = sem.render_frame(editor);
}
/// G5b(a) — the side WINDOW is replaced: a close/reopen of the same
/// buffer takes a fresh identity, and the old gesture ends.
#[test]
fn g5b_a_a_window_replacement_cancels_and_pays() {
let fid = FrontendId(820);
let (mut editor, mut states, mut render, panel, buffer_id, epochs) =
terminal_panel_session(fid, true);
let cell = pmacs_protocol::CellCoord::new(1, 2);
let press = pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left);
let none = pmacs_protocol::Modifiers::default();
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
cell,
press,
none,
);
assert_eq!(
child_stream(&editor),
vec![SGR_PRESS_1_2.to_vec()],
"fixture: the press reached the child"
);
// Close and reopen the SAME buffer: a different window, so a
// fresh panel identity.
{
let mut core = editor.core.borrow_mut();
core.active_frontend = fid;
core.focus_window(fid, panel);
assert!(core.close_active(), "fixture: the side window closes");
}
editor.reconcile_panel_layout(fid);
{
let mut core = editor.core.borrow_mut();
let mut request = crate::editor_core::DisplayRequest::new(buffer_id);
request.side = Some(crate::window::Side::Bottom);
request.height = Some(4);
core.display_buffer(fid, &request)
.expect("reopen the panel");
}
editor.reconcile_panel_layout(fid);
assert_ne!(
editor.core.borrow().side_window_for(fid).expect("reopened"),
panel,
"fixture: the successor really is a different window"
);
render_and_redeclare(&editor, &mut states, fid);
assert!(
!states[&fid].has_accepted_gesture(),
"the gesture must END: it belongs to a presentation that no \
longer exists"
);
assert!(
states[&fid].has_pending_release()
|| child_stream(&editor) == vec![SGR_RELEASE_1_2.to_vec()],
"and its release must be owed or already paid, never dropped"
);
}
/// G5b(b) — the side window's BUFFER is replaced.
///
/// The release still goes to the buffer the gesture was pressed on,
/// because the domain is recorded --- delivering it to whatever is on
/// screen now would tell the wrong child to lift a button.
#[test]
fn g5b_b_a_buffer_replacement_cancels_and_pays_the_old_child() {
let fid = FrontendId(821);
let (mut editor, mut states, mut render, panel, buffer_id, epochs) =
terminal_panel_session(fid, true);
let cell = pmacs_protocol::CellCoord::new(1, 2);
let press = pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left);
let none = pmacs_protocol::Modifiers::default();
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
cell,
press,
none,
);
let _ = child_stream(&editor);
// Same window, different buffer.
{
let mut core = editor.core.borrow_mut();
let replacement = core.registry.borrow_mut().create("*replacement*");
let view = {
let registry = core.registry.clone();
let registry = registry.borrow();
crate::text_view::TextView::new(registry.get(replacement).expect("replacement"))
};
let window = core.windows.get_mut(&panel).expect("panel window");
window.buffer_id = replacement;
window.text_view = view;
}
render_and_redeclare(&editor, &mut states, fid);
assert!(!states[&fid].has_accepted_gesture(), "the gesture must END");
assert!(
states[&fid].has_pending_release(),
"with its release owed to the buffer it was PRESSED on, not \
to whatever occupies the panel now"
);
}
/// G5b(c) — a geometry change at an UNCHANGED cell size.
///
/// Nothing else the producer holds moves: not the panel epoch, not
/// the identity, and on a legacy peer not a mapping key either. The
/// retained geometry epoch is the only thing that can see it.
#[test]
fn g5b_c_a_same_size_geometry_change_cancels_and_pays() {
let fid = FrontendId(822);
let (mut editor, mut states, mut render, _panel, buffer_id, epochs) =
terminal_panel_session(fid, true);
let cell = pmacs_protocol::CellCoord::new(1, 2);
let press = pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left);
let none = pmacs_protocol::Modifiers::default();
let size_before = editor.core.borrow().panel_grid_size(fid).expect("grid");
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
cell,
press,
none,
);
let _ = child_stream(&editor);
// A new geometry epoch at the SAME total size --- a font or scale
// change that leaves `CellSize` identical.
editor.accept_semantic_frame_geometry(fid, 2, CellSize::new(24, 80));
render_and_redeclare(&editor, &mut states, fid);
assert_eq!(
editor.core.borrow().panel_grid_size(fid).expect("grid"),
size_before,
"fixture: the SIZE must be unchanged, or this row is testing \
a resize instead"
);
assert!(
!states[&fid].has_accepted_gesture(),
"the gesture must END: the same cells now sit on a different \
grid, and the epochs the frontend echoes have moved under it"
);
assert!(
states[&fid].has_pending_release(),
"and its release is owed"
);
}
/// G5b(d) — DETACH ends a live gesture and pays it before teardown.
///
/// Q4 proved detach pays a release that was ALREADY owed. This one
/// starts from a live gesture, which is the case with no later
/// opportunity of any kind.
#[test]
fn g5b_d_detach_cancels_a_live_gesture_and_pays_it() {
let fid = FrontendId(823);
let (mut editor, mut states, mut render, _panel, buffer_id, epochs) =
terminal_panel_session(fid, true);
let cell = pmacs_protocol::CellCoord::new(1, 2);
let press = pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left);
let none = pmacs_protocol::Modifiers::default();
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
cell,
press,
none,
);
assert!(
states[&fid].has_accepted_gesture(),
"fixture: the gesture is LIVE, not merely owed"
);
let _ = child_stream(&editor);
detach_session(&mut editor, &mut states, &mut render, fid);
assert_eq!(
child_stream(&editor),
vec![SGR_RELEASE_1_2.to_vec()],
"detach must end the gesture AND pay it --- the frontend is \
gone, so a release not sent here is never sent"
);
}
/// G5m — coincident causes cancel ONCE.
///
/// Identity and geometry both change in one transition. Taking the
/// shared latch is what makes this one release rather than two.
#[test]
fn g5m_coincident_causes_emit_one_release() {
let fid = FrontendId(824);
let (mut editor, mut states, mut render, panel, buffer_id, epochs) =
terminal_panel_session(fid, true);
let cell = pmacs_protocol::CellCoord::new(1, 2);
let press = pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left);
let none = pmacs_protocol::Modifiers::default();
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
cell,
press,
none,
);
let _ = child_stream(&editor);
// BOTH at once: a replaced buffer and a moved geometry epoch.
{
let mut core = editor.core.borrow_mut();
let replacement = core.registry.borrow_mut().create("*both*");
let view = {
let registry = core.registry.clone();
let registry = registry.borrow();
crate::text_view::TextView::new(registry.get(replacement).expect("replacement"))
};
let window = core.windows.get_mut(&panel).expect("panel window");
window.buffer_id = replacement;
window.text_view = view;
}
editor.accept_semantic_frame_geometry(fid, 2, CellSize::new(24, 80));
render_and_redeclare(&editor, &mut states, fid);
assert_eq!(
states[&fid].panel_gesture_cancellations(),
1,
"TWO causes, ONE cancellation: they take the same latch, and \
counting per cause would emit a duplicate release"
);
}
/// G5j — document cancellation has two legs, and they differ.
///
/// A press that never dragged leaves an ACTIVE BUT EMPTY selection
/// whose stale anchor would capture the next shift-motion, so it is
/// cleared. A press that DID drag leaves a real region the user
/// selected, and cancelling the gesture must not take it away.
#[test]
fn g5j_document_cancellation_clears_only_an_empty_selection() {
for dragged in [false, true] {
let fid = FrontendId(if dragged { 826 } else { 825 });
let (mut editor, mut states, mut render, _document, panel, epochs) =
panel_session_at(LEGACY_PANEL_VERSION, fid);
let buffer_id = editor.core.borrow().windows[&panel].buffer_id;
foreign_edit(&editor, buffer_id, b"alpha beta gamma\ndelta\n");
let press = pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left);
let drag = pmacs_protocol::MouseKind::Drag(pmacs_protocol::MouseButton::Left);
let none = pmacs_protocol::Modifiers::default();
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
pmacs_protocol::CellCoord::new(0, 0),
press,
none,
);
if dragged {
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
pmacs_protocol::CellCoord::new(0, 5),
drag,
none,
);
}
let before = {
let core = editor.core.borrow();
let window = &core.windows[&panel];
(window.selection, window.cursor)
};
assert!(
before.0.is_some(),
"fixture: a selection is anchored either way"
);
editor.accept_semantic_frame_geometry(fid, 2, CellSize::new(24, 80));
render_and_redeclare(&editor, &mut states, fid);
drain_pending_release(&mut editor, &mut states, fid);
let after = {
let core = editor.core.borrow();
let window = &core.windows[&panel];
(window.selection, window.cursor)
};
if dragged {
assert_eq!(
after, before,
"a REAL region survives cancellation, anchor and \
cursor exactly --- the user selected it, and ending \
the gesture is not a reason to discard it"
);
} else {
assert!(
after.0.is_none(),
"an EMPTY selection is cleared, or its stale anchor \
captures the next shift-motion"
);
assert_eq!(after.1, before.1, "and clearing it does not move point");
}
}
}
/// P12 — a panel WIDER THAN 512 COLUMNS still routes pointer input.
///
/// A panel deliberately does not inherit the terminal's per-axis PTY

View File

@ -532,6 +532,15 @@ struct PanelPresentation {
window_id: WindowId,
buffer_id: BufferId,
panel_epoch: u64,
/// The geometry epoch this presentation was shipped under.
///
/// Retained for parent 48's authority-loss matrix: a geometry
/// change at an UNCHANGED `CellSize` moves nothing else the
/// producer holds — not the panel epoch, not the identity, and on a
/// legacy peer not a mapping key either — so without this the
/// transition is invisible and a live gesture survives a grid it no
/// longer belongs to.
geometry_epoch: u64,
}
/// One [`SemanticRenderState::diag_line_cache`] entry: the line-start
@ -1720,6 +1729,31 @@ impl SemanticRenderState {
return;
};
let identity = (projection.window_id, projection.buffer_id);
// Parent 48 G5b — AUTHORITY LOSS. A live gesture belongs to the
// presentation it was pressed on. Three of the matrix's five
// transitions are visible right here, and each is a separate
// cause with one shared consequence:
//
// * the side WINDOW was replaced (panel-epoch change),
// * its BUFFER was replaced,
// * the GEOMETRY epoch moved, including at an unchanged size.
//
// §5b left these armed deliberately — inert while nothing
// consumed the latch — and they became defects the moment this
// lane gave cancellation an effect. Taking the latch parks the
// release; the drain pays it.
//
// G5m: coincident causes take the latch ONCE. `cancel_accepted_gesture`
// is idempotent on an empty latch, so a transition that trips
// two of these conditions still emits one release.
if let Some(presentation) = self.panel_presentation
&& ((presentation.window_id, presentation.buffer_id) != identity
|| presentation.geometry_epoch != geometry.geometry_epoch)
{
self.cancel_accepted_gesture();
}
let panel_epoch = match self.panel_presentation {
Some(presentation) if (presentation.window_id, presentation.buffer_id) == identity => {
Some(presentation.panel_epoch)
@ -1803,6 +1837,7 @@ impl SemanticRenderState {
window_id: projection.window_id,
buffer_id: projection.buffer_id,
panel_epoch,
geometry_epoch: geometry.geometry_epoch,
});
self.last_panel_payload = Some(payload.clone());
out.push(InstanceMessage::PanelFrame(payload));