diff --git a/docs/active-work.md b/docs/active-work.md index 0b409fe..faf37d8 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -353,10 +353,13 @@ from #171 and #215 — the correction the 1b lane missed, honoured here. and with that assert compiled out the byte-order assertion catches the same defect — which is what a release build relies on. - - **Q5 is OWED.** The projection-seam drain needs a row that drives - 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. + - **Q5 is CLOSED.** The projection seam is extracted as + `project_semantic_frame`, which returns its messages **unwritten** + — so a caller holding them has by construction not sent the + successor frame, and a release already delivered at that moment + provably precedes it. The row bites its own drain and no other: + removing it fails Q5 while Q1–Q4 stay green on the other two + drain points. - **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 @@ -372,8 +375,7 @@ from #171 and #215 — the correction the 1b lane missed, honoured here. cancellation, which a mutation confirms it catches. The count is asserted in the row, because a loop that quietly stops covering a combination passes exactly as loudly as one that covers them all. - Each quadrant **drains - explicitly** and asserting the effect — the exact release bytes + Each quadrant **drains explicitly and asserts the effect** — the exact release bytes for a reporting terminal, the cleared empty selection for a document, and an empty slot afterwards. An earlier version stopped at `has_pending_release()` and would have passed while @@ -404,8 +406,15 @@ from #171 and #215 — the correction the 1b lane missed, honoured here. gone, so the completion has nothing left to clear and the gesture ending is the whole effect. Written into the row rather than left as a silently absent assertion. - - **REMAINING:** Q5's acceptance-shaped row, then the full head-exact - gate and the PR. + - **REMAINING: the full head-exact gate, then the PR.** The gate + wants a QUIET machine: a foreign C++/java build has been running at + load 114+ through this work, and the wall-clock rows + (`composition_overhead_under_ten_percent`, + `m6_2_pty_streaming_respects_byte_ceiling`, + `full_buffer_summary_flatten_scales_on_large_grammar_file`) redded + under it and were green in isolation every time. That is U6/U9/U10 + territory and running the gate into it would manufacture another + rotating-red incident. - **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 diff --git a/src/daemon.rs b/src/daemon.rs index 5df32d7..d7144fb 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1019,6 +1019,36 @@ fn panel_event_epochs_are_current( .is_some_and(|geometry| geometry.geometry_epoch == geometry_epoch) } +/// Project one SEMANTIC frontend's frame, paying any release that +/// projection itself raised before the caller can write the result +/// (parent 48, drain point three). +/// +/// **The seam exists because the cancellation happens too deep to pay +/// itself.** A mapping-generation advance cancels the live gesture +/// INSIDE `render_frame`, while the successor `PresentMapped` is still +/// being built, so "before that frame is produced" is not a place that +/// exists. This is the first place that is: projection has returned, +/// and none of what it returned has been written. +/// +/// Returning the messages UNWRITTEN is what makes the ordering +/// testable — a caller holding them has, by construction, not yet sent +/// the successor frame, so a release already delivered at that moment +/// provably precedes it. +/// +/// Grid sessions do not come here: they hold no panel and no gesture. +fn project_semantic_frame( + editor: &mut EditorState, + semantic_states: &mut HashMap, + fid: FrontendId, +) -> Vec { + let messages = semantic_states + .get_mut(&fid) + .map(|sem| sem.render_frame(editor)) + .unwrap_or_default(); + drain_pending_release(editor, semantic_states, fid); + messages +} + /// Deliver the release this frontend is owed, if any (parent 48). /// /// **Order is the ruling, not just the existence of a slot.** A @@ -1578,8 +1608,8 @@ fn dispatcher_loop( // out locally); it still receives `CursorByte` below // (semantic implies `crdt_replica`) and participates in // presence. A grid session takes the M5.2 cell path. - let messages = if let Some(sem) = semantic_states.get_mut(fid) { - sem.render_frame(editor) + let messages = if semantic_states.contains_key(fid) { + project_semantic_frame(editor, &mut semantic_states, *fid) } else { // T M10.9 — gather other-frontend presences for the // overlay paint. Reads `last_broadcast` (updated by @@ -1597,16 +1627,6 @@ fn dispatcher_loop( render_state.render_frame(editor, *fid, &terminal_snapshots, &other_presences) }; - // Parent 48 — THE PROJECTION SEAM. A mapping-generation - // advance cancels the live gesture INSIDE `render_frame`, - // while the successor `PresentMapped` is still being built, - // so "before that frame is produced" is not a place that - // exists. This is the first point that is: projection has - // returned and none of what it returned has been written. - // Draining here keeps the release ahead of the frame whose - // own new mapping is what required it. - drain_pending_release(editor, &mut semantic_states, *fid); - // Vterm Stage 3 — a semantic frontend showing a terminal has // no document cursor: the identity buffer is empty, so a // `CursorByte` would describe byte 0 of a buffer with no @@ -8740,6 +8760,69 @@ mod tests { // rather than unit shaped. Recorded as OWED rather than assumed // covered by its neighbours. + /// Q5 — a release raised by PROJECTION is paid before the frame + /// that raised it can be written. + /// + /// This is the third drain, and the only one a unit row could not + /// reach before: it sits inside the daemon's per-frontend frame + /// loop. `project_semantic_frame` is that seam, extracted --- it + /// returns the messages UNWRITTEN, so a caller holding them has by + /// construction not sent the successor frame yet, and a release + /// already delivered at that moment provably precedes it. + /// + /// Recorded as OWED through tasks 18 and 19; this closes it. + #[test] + fn q5_a_projection_raised_release_precedes_the_frame_that_raised_it() { + let fid = FrontendId(870); + let (mut editor, mut states, mut render, _panel, buffer_id, epochs) = + terminal_panel_session_at(fid, true, PROTOCOL_VERSION); + let cell = pmacs_protocol::CellCoord::new(1, 2); + + send_press( + &mut editor, + &mut states, + &mut render, + fid, + PROTOCOL_VERSION, + epochs, + buffer_id, + cell, + ); + assert_eq!( + child_stream(&editor), + vec![SGR_PRESS_1_2.to_vec()], + "fixture: the press reached the child" + ); + assert!( + states[&fid].has_accepted_gesture(), + "fixture: the gesture is live" + ); + + // The panel stops being presentable. `publish_absent_panel` + // cancels from INSIDE projection, which cannot deliver. + editor.hide_panel_for_test(fid); + + let messages = project_semantic_frame(&mut editor, &mut states, fid); + + assert_eq!( + child_stream(&editor), + vec![SGR_RELEASE_1_2.to_vec()], + "the release must already be delivered when projection hands \ + its messages back --- the caller has not written them, so \ + this is the release preceding the successor frame, not \ + merely both arriving" + ); + assert!( + !messages.is_empty(), + "fixture: projection really did produce the successor frame \ + whose own transition required that release" + ); + assert!( + !states[&fid].has_pending_release(), + "and nothing is left owed" + ); + } + /// Q6 — a SECOND PRESS with the first still live: the old release /// reaches the child before the new press. ///