docs,test: 1a review round 3 --- an overclaimed first, and a vacuous row

**"The first frontend→instance extension needing a gate in both
directions" was false**, and checking it took one grep: the v19 terminal
family and the v21 panel family each have an inbound variant AND an
outbound one, each gated. "First inbound-only extension" does not hold
either --- v7's `TripleDown` and v11's `Context` are inbound-only
`PointerKind` variants.

So the superlative is gone rather than weakened. The rustdoc now states
the PROPERTY and its consequence: the gate is producer-side and
receiver-side, and for an inbound variant the receiving half is the
load-bearing one, because withholding would otherwise be the peer's job
and a client built from this same crate can encode the discriminant
whatever it negotiated. The v19/v21 precedent is named so the shape does
not read as novel; what is unusual is only that this extension has no
outbound counterpart, so the receiver check is the whole of the daemon's
half.

**`multi_scalar_text_input_creates_no_typed_provenance` proved half of
what its name claimed.** It started from a fresh editor, where the
command chain is ALREADY empty, so asserting emptiness afterwards passed
whether or not `break_command_chain` ran --- the assertion could not
fail for the reason it existed.

Split in two. The record half keeps its name and its claim; the chain
half is a new row that PRIMES the chain first and then asserts it
cleared. Priming goes through 1a's own single-scalar path, because
`pmacs.command.invoke('buffer.self-insert')` cannot prime it: rotation
belongs to the dispatcher and invoking the command directly deliberately
never rotates --- the first attempt at this row failed on exactly that
precondition, which is a better outcome than passing on a chain that was
never live.

**M-1a-2 deletes `break_command_chain` from the multi-scalar branch and
fails the new row alone**; the other six stay green.

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-12 17:29:21 +02:00
parent 97da79f136
commit b2a273b1fd
No known key found for this signature in database
2 changed files with 51 additions and 17 deletions

View File

@ -1904,15 +1904,22 @@ pub enum ResourceBody {
/// `PanelPointer`, the final v23 `FrontendEvent` variant, so no /// `PanelPointer`, the final v23 `FrontendEvent` variant, so no
/// existing discriminant moves. /// existing discriminant moves.
/// ///
/// **This is the first FRONTEND→INSTANCE extension to need a gate in /// **The gate is producer-side AND receiver-side**, and for an inbound
/// both directions**, and the reason is that the direction of travel is /// variant the receiving half is the load-bearing one. An
/// reversed: for an instance→frontend variant the daemon simply /// instance→frontend variant is gated by the daemon simply not sending
/// withholds, but here the *frontend* must withhold below /// it, which is entirely within the daemon's control; an inbound variant
/// [`TEXT_INPUT_MIN_VERSION`] **and** the daemon must refuse what a peer /// cannot be, because the withholding would be the *peer's* job and a
/// below it nonetheless sends. A client built from this crate can encode /// client built from this same crate can encode the discriminant
/// the variant whatever it negotiated, so the producer gate alone would /// whatever it negotiated. So the daemon refuses `TextInput` from a
/// leave a v6–v23 session able to drive an edit through a variant its /// session below [`TEXT_INPUT_MIN_VERSION`] rather than trusting the
/// own session never declared. /// producer to withhold — otherwise a v6–v23 session could drive an edit
/// through a variant its own session never declared.
///
/// This is not a new shape: the v19 terminal and v21 panel families
/// already gate their own inbound events on the authenticated session's
/// negotiated version. What is unusual here is only that the extension
/// is **inbound-only** — there is no outbound counterpart to withhold,
/// so the receiver check is the whole of the daemon's half.
pub const PROTOCOL_VERSION: u32 = 24; pub const PROTOCOL_VERSION: u32 = 24;
/// Protocol version placed in the daemon's server-first [`Hello`]. /// Protocol version placed in the daemon's server-first [`Hello`].
@ -2096,10 +2103,9 @@ pub fn negotiated_session_version(frontend_offer: u32) -> u32 {
/// both. [`ADVERTISED_PROTOCOL_VERSION`] does not move. /// both. [`ADVERTISED_PROTOCOL_VERSION`] does not move.
/// ///
/// GUI arc Stage 1a: extended to `[6, ..., 24]` for /// GUI arc Stage 1a: extended to `[6, ..., 24]` for
/// [`FrontendEvent::TextInput`]. Additive, and gated in **both** /// [`FrontendEvent::TextInput`]. Additive, and gated producer-side AND
/// directions rather than only daemon-side — see [`PROTOCOL_VERSION`] /// receiver-side — see [`PROTOCOL_VERSION`] for why an inbound variant
/// for why an inbound frontend→instance variant needs the receiving /// cannot rely on the producer withholding. [`ADVERTISED_PROTOCOL_VERSION`] does not move: a v23
/// check too. [`ADVERTISED_PROTOCOL_VERSION`] does not move: a v23
/// frontend negotiates v23, never sends the variant, and keeps today's /// frontend negotiates v23, never sends the variant, and keeps today's
/// first-scalar behaviour. /// first-scalar behaviour.
pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[ pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[

View File

@ -127,10 +127,38 @@ fn multi_scalar_text_input_creates_no_typed_provenance() {
no_record, no_record,
"a multi-scalar commit must not forge a typed-edit record" "a multi-scalar commit must not forge a typed-edit record"
); );
let this_command: String = eval(&s, "return pmacs.editor.this_command() or ''"); }
assert_ne!(
this_command, "buffer.self-insert", /// §5 — a MULTI-scalar commit **breaks the command chain**, as a paste
"a multi-scalar commit is not a self-insert" /// does.
///
/// **The chain is PRIMED first, and that is what makes the row
/// discriminating.** Starting from a fresh editor the chain is already
/// empty, so an assertion that it is empty afterwards passes whether or
/// not `break_command_chain` is called — the first version of this row
/// did exactly that and would have survived deleting the call.
#[test]
fn multi_scalar_text_input_breaks_a_live_command_chain() {
let mut s = editor_with("");
// Prime it with 1a's OWN single-scalar path, which rotates to
// `buffer.self-insert`. A programmatic
// `pmacs.command.invoke('buffer.self-insert')` cannot prime it:
// rotation belongs to the dispatcher, and invoking the command
// directly deliberately never rotates or arms.
s.dispatch_text_input(FID, "x");
let primed: String = eval(&s, "return pmacs.editor.this_command() or ''");
assert_eq!(
primed, "buffer.self-insert",
"precondition: the chain is live before the commit"
);
s.dispatch_text_input(FID, "e\u{301}");
let after: Option<String> = eval(&s, "return pmacs.editor.this_command()");
assert_eq!(
after, None,
"a multi-scalar commit is not a command and must clear the chain"
); );
} }