G5a is the one G5 row this slice owns: the mapping key advancing must
raise cancellation at the advance, not reactively when a later event is
refused. Reactive cancellation loses a race --- if the successor frame
reaches the frontend before the physical `Up`, the producer clears its
latch and the cancelling event never arrives.
Cancelling requires something to cancel, so the accepted-gesture latch
lands with it: `AcceptedPanelGesture` (button, coord, buffer, and
whether the press reached a child) in a per-frontend slot on
`SemanticRenderState`, armed from the daemon's accepted inbound arms
after every gate has passed.
Writing that code decides three things the framing's deferred rows later
assert about, so they are pinned here under SUBSTRATE names rather than
under G5c/G5d/G5g/G5p. Those IDs stay on `panel-pointer-replay` per
SS5b's split table --- each asserts something about a synthetic release
or a real drag continuation that does not exist on this base, and
claiming an ID in two branches is the merge hazard `active-work.md`
already records surviving a clean merge once.
Two consequences are recorded rather than fixed:
- Cancellations are COUNTED, not queued. The record queue is what
replay drains to deliver each release; landing it here would grow
one entry per cancelled drag with nothing ever draining it. A
saturating count is bounded and still separates a consume from a
cancellation.
- The other G5b transitions --- panel epoch, buffer replacement,
same-size geometry, detach --- leave the latch armed on this base.
Each strands a live gesture whose release can never be accepted.
That is inert while nothing consumes the latch, and becomes a defect
exactly when replay supplies effects, in the branch that owns the
row. `Absent` is wired anyway, because `publish_absent_panel`
clears input authority two lines later; leaving it out would be an
inconsistency inside one function rather than a clean deferral.
Five mutations, each biting only its named row:
- drop the advance trigger (reactive cancellation) -> G5a alone
- arm on every accepted pointer event -> the arming substrate alone
- an ordinary `Up` no longer consumes -> the arming substrate alone
- a consume counts as a cancellation -> the arming substrate alone
- one global latch via a shared slot -> the ownership substrate alone
Also repairs a fourth rustdoc split on this branch. Inserting
`AcceptedPanelGesture` at what read as a blank gap adopted
`SemanticRenderState`'s doc comment AND its
`#[allow(clippy::struct_excessive_bools)]`, silently un-suppressing a
lint on the struct that needed it. Same mechanism all four times; the
ledger now records the check as "look UP from the insertion point".
Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; `cargo test --lib` (1956 passed); the two
`bottom_panel_stage2b_*` acceptance suites (39 passed); `git diff
--check`. The full eleven-stage `--protocol` gate is reserved for slice
completion per the standing procedure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai