The Q6 rewrite spliced its replacement text ending with P12's heading
while the slice it replaced began with the same line, concatenating both
copies onto one line. Neither cargo fmt nor clippy reflows doc comments,
so nothing downstream noticed.
Swept the rest of the crate for the same shape; the only other hit is a
`file:///` URI inside a legitimate doc line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of ab8ddae. The entry drain could not close this: it
looks for an OWED release, and a gesture that is still LIVE owes nothing
yet. Arming was what cancelled it, and arming runs after the replacement
press has already reached the target --- so a second press with the first
never released put `old press, new press, old release` on the wire. Two
presses outstanding, then a release arriving for the wrong one.
The Down arm now ends the live gesture and drains it before applying the
replacement, so the child sees `old press, old release, new press`.
The invariant moved to where it is relied on. arm_accepted_gesture now
asserts that neither a live gesture nor an owed release remains, at the
point of ARMING rather than inside cancellation --- arming is what the
ordering protects, and checking during cancellation cannot see the case
where nothing has been cancelled yet. The defensive cancel stays for
release builds, because parking late is recoverable and overwriting is
not.
Q6 was rewritten, because the old one never sent a second press while
the first was live and so could not observe any of this; its final
assertion also ran after a further cancellation. It now expects the
exact bytes `release(1,2), press(2,4)` in that order.
Both layers are witnessed separately. Reverting the ordering trips the
new debug assertion at the point of arming; reverting it AND compiling
that assertion out --- which is what a release build does --- fails the
byte-order assertion instead, with the child receiving only the new
press.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Task 18. Cancellation now PARKS the record instead of returning it into
a context that drops it: two of the three cancellation sites --- the
mapping-generation advance and publish_absent_panel --- run inside frame
production, where no target effect can happen, so the gesture ended with
the child still holding its button.
A SLOT, not a queue. The latch holds at most one gesture per frontend,
so at most one release can be owed, and the bound is structural rather
than a cap someone chose. Overwriting a still-parked release is a
debug_assert rather than a silent loss --- a backstop, since the ordering
is what actually prevents it.
Three drains, each against a specific way the release would otherwise
arrive too late or not at all: before any subsequent panel-pointer
effect, before detach teardown (the next statement in that arm drops the
state holding the record), and at the projection seam between
render_frame returning and its messages being written.
Q1, Q2, Q3, Q4 and Q6, each biting its named mutation with the run
counts checked. The mutations discriminate: dropping the park fails all
three delivery rows; removing the effect-drain spares Q4, which has its
own; removing the detach drain hits Q4 alone. Q3 asserts ORDER rather
than arrival, and the mutation that keeps the drain but moves it after
the press effect fails exactly that assertion, with the child's stream
coming back press-then-release.
Q5 IS OWED, AND RECORDED AS SUCH. The projection-seam drain needs a row
that drives the real per-frontend frame loop; these rows call
render_frame directly and never enter it. The seam still matters --- a
cancellation with no following panel event and no detach would let the
successor frame overtake the release its own new mapping required --- so
it is written down as unwitnessed rather than treated as covered by its
neighbours.
Two fixture facts worth keeping: the Q rows cancel through Absence
rather than a mapping advance, because a terminal panel's key tracks its
screen and anchor, not its buffer, so a foreign edit does not move it;
and the helper re-shows AND re-declares the panel, because a panel left
Absent fails the inbound ladder and the row would then be observing the
ladder rather than the slot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of 557ea6d. Capturing the outcome and asserting it LAST
makes the focus assertion reachable: removing the buffer check accepts
the press, an accepted press activates the panel before it replays, and
the row now fails on focus --- WindowId(3) against WindowId(2).
I had recorded this as a limit of the type boundary, claiming no
mutation could reach the effect assertions because the daemon applies
only on Accepted and the disposition gives Refused no target. That was
wrong. The obstacle was that the row asserted the refusal BEFORE
dispatch and aborted there. Ordering, not architecture.
The classification is still checked, at the end, so the row cannot go
vacuous if it ever stops testing a refusal.
Controller and byte assertions stay documented as defence in depth, and
now for an accurate reason: the mutation that reaches them routes
through a document buffer, which touches neither.
Also replaces failure text that still described an out-of-range anchor,
which this fixture stopped using when its refusal lever became a foreign
buffer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of 9028e1b.
P2 recorded `active_frontend`, which is which FRONTEND is current, not
which window has focus. Focus is `views[&fid].active` and `focus_window`
moves it, so the row would have watched a panel steal focus without
noticing. It now records the focused window, and asserts up front that
the panel does not already hold it --- otherwise "focus did not move to
the panel" asserts nothing.
P2's refusal lever also changed, because the old one could not exercise
what the row claims. An out-of-grid cell with the row bound removed
becomes `on_chrome`, so the press classifies Consumed and still reaches
no target: the row bit on its own precondition while focus was never
touched. A foreign buffer at an in-content cell is the refusal whose
mis-gating actually yields Accepted.
And the row now says what is falsifiable about it. Removing the buffer
check makes the press Accepted and P2 fails --- but on the precondition,
which fires first, so the focus, controller and byte assertions cannot
fail under that mutation and no other mutation reaches them: the daemon
applies only on Accepted, and the disposition enum gives Refused no
target to apply. They are defence in depth against a future refactor,
labelled as such rather than presented as coverage.
The ledger claimed every row reads a target effect and never the latch
alone. That was false. P9 and P10 read the LATCH, and correctly so ---
the defect they fence is a record existing for a gesture that never
began, so the record is the artifact, and an effect assertion would not
distinguish their mutations. The line now separates effect rows from
arming-gate rows and names P2's third case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of 2ea39aa. Two of my claims were wrong and the code
disproved them.
WIDE PANELS HAD DEAD POINTER INPUT. A panel deliberately does not
inherit the terminal's per-axis PTY caps (Bet B5'), and the renderer
clamps through terminal_projection_size so a band wider than
MAX_TERMINAL_COLS paints correctly. Pointer routing passed the RAW panel
width, and view_status_for_size refuses anything over the cap --- so on
exactly those panels every click inside the visible terminal resolved to
None while the band looked perfectly normal. Routing and the recorded
viewport now go through the same clamp, with P12 as a POSITIVE control
at MAX_TERMINAL_COLS + 128.
P10 IS REACHABLE, and my note saying otherwise was wrong. I claimed
anchor_at resolves every in-grid cell of a live view, "measured, not
assumed". I had measured ROWS and generalised to cells. anchor_at
refuses coord.col >= row.cells.len(), and the fixture's band is 80
columns over a 20-column child, so columns 20..79 are painted padding
inside accepted content. The row exists now and the begin_selection gate
is witnessed rather than excused.
THE DISPOSITION IS AN ENUM. As {outcome, Option<target>} the invalid
pair --- refused, yet carrying a target --- stayed representable inside
editor.rs, so my "the type makes it impossible" was also wrong. Refused
now holds no target at all. ResolvedPanelTarget is public as a type and
opaque as a value: every field stays private, so the daemon hands the
disposition back rather than reading a derivation out of it.
P2 also now asserts what it observes: the classification is Refused, and
focus and terminal-controller ownership are preserved. A misclassified
press focuses the panel and claims the controller BEFORE its
out-of-range anchor fails, so byte and latch assertions alone stayed
green through exactly that bug.
MY MUTATION HARNESS WAS READING SUCCESS FROM SILENCE. `cargo test --lib
"r4_p\|g5k_"` is a literal substring filter, not a regex: it matched
nothing and ran ZERO tests, and I read the absence of failures as
"the mutation did not bite". One whole round proved nothing. The harness
now prints the run count and says so loudly when it is zero. Re-run
properly, all three fixes bite their named rows.
Also updates the ledger's witness list, which omitted P2, P9, P11, the
recorded viewport and the exact-byte strengthening.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of 48057d7. The G5k routing fix held; the record still
leaned on ambient state in three places.
TerminalLocal now records the accepted content VIEWPORT. Replay fetched
the current panel_grid_size and returned when it was None --- which is
exactly what a hidden or absent panel produces, so a cancellation could
not finish the drag it was cancelling, and a size-changing cancellation
would have finished against the successor's geometry.
A press that anchors NOTHING no longer arms. The document path returned
Some(Document) unconditionally even when panel_cell_byte found no byte,
and the terminal path returned Local while discarding begin_selection's
answer. Both now report what actually began.
The child rows assert EXACT BYTES rather than a count: a wrong event or
encoding passed the old length checks. The literals are written out
rather than built with the encoder's own formula, which would only
assert that the encoder agrees with itself. G5k(b) pins the ruling that
the SGR framing comes from the record while the modifier bits still
report live state, so its release carries code 4 rather than 0.
New rows: P2's effect half (a refused press reaches no target), P9 (a
document press that anchors nothing does not arm), P11 (a recorded local
completion still runs with the panel HIDDEN, which is what the recorded
viewport is for).
THREE ROWS IN THIS ROUND WERE VACUOUS BEFORE THE MUTATIONS CAUGHT THEM,
and the fixtures now assert their way past each cause. The panel grid in
this fixture is FOUR rows, so content is rows 0..=2: my first P9 and P10
cells were out of grid and refused before reaching the path they claimed
to test, and P9's earlier cell clamped to byte 0 instead of failing to
anchor. Both rows now assert the disposition is Accepted before
asserting anything about the effect.
P10 IS DELIBERATELY ABSENT AND RECORDED AS UNWITNESSED. The
begin_selection gate has no reachable false branch through the daemon:
classify has already established the buffer is the side window's live
terminal, and anchor_at resolves every in-grid cell of a live view ---
measured on the fixture, not assumed. The gate is kept as insurance and
the gap is written where the row would have been, rather than covered by
something that would pass whether or not the gate existed.
P2's effect half is likewise not falsifiable by any mutation I could
construct, because a Refused disposition carries no resolution, so no
path can apply it. That is a stronger guarantee than a test, and it is
stated rather than dressed up as coverage.
Also corrects the last false ledger tense.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Answers review of 39b6fa7. The three-state disposition was sound; the
record and effect half downstream of it were not.
apply_panel_pointer returned a bare bool, so the record carried neither
the resolved target nor the reporting contract, and the daemon drove
accepted Drag/Up back through the mode-sensitive adapter. That adapter
re-reads Shift, the scrollback position and the child's mouse modes on
every event --- which is G5k's named mutation verbatim. A press reported
to the child followed by a release re-evaluated after the child turned
reporting off leaves that child holding a button down; the reverse
transition sends a child an Up for a Down it never saw. The recorded
completion had the same defect and additionally re-derived the current
side window, returning when it had changed --- precisely the transitions
task 19 must terminate, so the completion they need was the one thing
that refused to run.
The press now resolves a PanelGestureDomain --- Document{window},
TerminalChild{window, buffer, modes} or TerminalLocal{window, buffer}
--- and the record carries it. Tails and completions route through
replay_panel_gesture_in_domain, which gates on nothing: not Shift, not
the scroll position, not the child's current modes, not the panel's
current identity. apply_terminal_gesture reports which way it routed so
the domain is measured where the branch is taken. Arming now requires an
effect: a press the target refused records nothing.
G5k(a)-(d) plus P3's reporting leg, P4 and P5. Every row reads a TARGET
EFFECT --- the child's byte stream in order, the terminal drag state, or
the document selection --- never the latch. Each bites its own mutation,
and G5k's four legs all fail under the framing's own named mutation
applied verbatim.
Two seams exist because nothing else exposes what the child received: an
opt-in child-input tap, off by default, and a drag-state read.
Also corrects the recovery ledger, which still said implementation was
paused and the bool collision unfixed.
Records for the ci-red registry rather than hiding it: during this work
composition_overhead_under_ten_percent and pty_mode_child_sees_a_tty
redded together in one --lib run at load 21 and each passed in isolation
immediately after --- U9's signature, and neither path is touched here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Replaces the panel dispatcher's `bool` with a three-state
PanelPointerOutcome decided BEFORE any target effect, and moves the
gesture lifecycle into one table in the daemon.
The old shape could not express the rule it needed. It validated,
classified and mutated in one pass, so an Up or Drag with no accepted
Down had already reached the child or the selection by the time the
daemon consulted the latch. classify_panel_pointer now returns the
disposition and carries the resolution it was decided from;
apply_panel_pointer acts on that same resolution, so the editor stays
the only authority and the daemon never re-derives chrome, target kind
or content bounds.
The table: a chrome press begins nothing; a left tail with no live
record is inert; an Accepted release performs the ordinary in-content
completion and takes the record; a Consumed release did not reach
content, so it terminates from the record at the gesture's last valid
content cell. Never both --- that is P5.
apply_terminal_gesture now returns whether the gesture REACHED THE
CHILD, so the latch is armed from the effect result rather than from a
prediction about the modes. complete_panel_gesture routes both terminal
domains back through that same shared path, which is what keeps "what a
release does" from having a second implementation.
Four witnesses, each reading a TARGET EFFECT and not the latch, and
each biting its own mutation: P1 chrome press (classify chrome as
Accepted), P3 chrome release on a terminal (drop the recorded
completion), P7 orphan release (remove the Up live-gate), P8 orphan
drag (remove the Drag live-gate).
Two of those rows were vacuous when first written and are recorded here
because the mutations are what caught them. P8 dragged over an EMPTY
panel buffer, so panel_cell_byte returned None and point could not move
whether the gate was there or not. P3 was written against a document
panel --- but R-c lets document chrome Up fall through to content, so it
classifies Accepted and never reaches the Consumed path it claimed to
test; it now uses a terminal panel, on the legacy arm, because reading
the live mapping generation ADVANCES the key and SS5b wired a key
advance to cancel the live gesture, so the mapped fixture destroyed the
gesture it was trying to complete.
Adds view_is_dragging_for_test, the observable that separates a
delivered completion from a latch that merely emptied.
Also re-homes a doc paragraph that described peer_uses_mapped_panel_family
while sitting above update_accepted_gesture; deleting the latter's doc
with the function made the misplacement visible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Make the three-state panel-pointer result a pre-effect disposition and
replace the competing latch matrices with one complete lifecycle table.
Accepted tails now require a live gesture, accepted release replay and
record-driven termination are mutually exclusive, and the witnesses
cover stale tails plus immediate and later duplicate completion.
Place projection-raised release delivery at the enforceable daemon
boundary after semantic rendering returns and before its messages are
written. Keep the arm invariant as a backstop behind pointer, detach,
and frame-order witnesses.
Split horizontal-wheel ground truth by terminal reporting/local and
document targets. Record the missing document-panel effect explicitly
under GUI Stage 1b B1--B3, update that framing's surface scope, and
synchronize the active-work checkpoint.
Four blocking gaps, all of them cases where revision 14 asserted a
guarantee its own mechanism did not provide.
1. Q#BP-R4's release half was bookkeeping in an effect's clothes. It
ruled that a chrome Up "consumes" the latch and claimed this stops a
child being left button-down. It stops nothing: update_accepted_gesture
does `let _ = state.consume_accepted_gesture()` at daemon.rs:1067, and
terminal chrome returns at editor.rs:2862-2870, BEFORE
apply_terminal_gesture at :2899. P3/P4 asserted only that the latch
empties, so they would have passed while the child heard nothing ---
the failure looking fixed because the bookkeeping looked right. Now
ruled as TERMINATE, with a per-target completion effect (child release,
or local terminal-selection completion, or the document completion SS5b
already assigned here), a no-duplicate leg, and P3 written explicitly
against the latch-only assertion.
2. The pending-release slot had an invariant and no ordering. The seam
replays before it arms (daemon.rs:2608-2616), so a check inside
arm_accepted_gesture could only fire after the new press had already
reached the child. Drain order is now part of the ruling: before any
subsequent panel-pointer effect, before detach teardown, before the
next frame --- with an old-release-before-new-press row that reads the
child's byte order, and the invariant demoted to a backstop.
3. The ground truth was knowingly false, and the workflow requires
scouting before framing. RE-MEASURED at 2c0d3ff instead of deferred.
Two clauses the old table called MISSING are DONE on this branch ---
panel selection and terminal mouse reporting --- the vertical wheel
works, and the horizontal wheel turns out to be an UNRULED gap sitting
in a catch-all arm with no comment. Revision 15 names it rather than
ruling it, because inventing a ruling inside a re-measurement is how an
unexamined default becomes a decision. The 72da24a table is kept as
history.
4. "This lane MOVES the grade" was wrong against the authoritative
scorecard. Step 5/GPU is floored Partial by 5(a) IME, so completing
5(c) cannot lift it, and step 8 is already Works on all three columns.
The draft had read this section's MISSING column as if it were the
scorecard; COHERENCE.md is the only thing that decides grades.
Also drops a false TUI claim --- dispatch_semantic_panel_pointer has
exactly two callers, both FrontendEvent arms, so only semantic legacy and
mapped peers reach it --- and clears two merged-state contradictions: SS5b
no longer says "Nothing implemented", and the lane no longer says the v26
correction is owed by 1b while the block below records SS5b making it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The ledger listed the merge's duplicate-SS5b defect but not the worse
one: the merge commit does not compile and its message asserts that it
does. Recorded with the bisect consequence spelled out, so a reader
crossing that range knows why the build fails there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The merge commit b758c2e DOES NOT COMPILE, and its message says
"Workspace compiles clean, all targets, no warnings". That claim is
WITHDRAWN. It was true of the tree I checked and false of the tree I
committed.
What happened: I staged the conflict resolution, ran cargo check, hit
`cannot find value mods in this scope` at the mapped arm, edited
src/daemon.rs to bind it, re-ran cargo check clean --- and then
committed without re-staging that file. The verification and the commit
were of different trees. This is the same defect class as gating one
step of an edit-then-commit chain and leaving the next ungated: a
commit that does not depend on the edit it claims.
The fix itself is unchanged from what was verified. SS5b left `mods` in
`..` on PanelPointerMapped; the mapped family carries the same
modifiers, so leaving it there gives a v25 session the inverted Shift
behaviour that parent 48 R-a fixed for v24.
Verified at THIS commit, after staging: cargo check --workspace
--all-targets, no errors and no warnings.
Not amended away. b758c2e stays in history with its false claim
standing and this withdrawal attached, because erasing a bad record is
worse than carrying a corrected one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 13 blocked this lane on a protocol-bearing mapping generation.
That slice merged as #242 (47b5463), so the block is DISCHARGED and
revision 13 stands as history rather than as a gate.
Also fixes a defect I introduced in the merge b758c2e: it kept BOTH
copies of SS5b, this branch's stale pre-split one and main's
authoritative one. I discarded the uncommitted stub edit as obsolete
and missed that its deletion half was still owed. Exactly one SS5a and
one SS5b now remain.
Q#BP-R4 is new, and the merge created it. SS5b and this lane agreed on
`bool` while disagreeing on its meaning --- accepted-as-a-gesture versus
consumed-here --- so a press on the band's mode line currently ARMS the
accepted-gesture latch for a gesture that never began in content. That
is the defect class SS5b's review round four fixed, re-entering by merge
rather than by edit.
RULED: a three-state PanelPointerOutcome (Refused/Consumed/Accepted),
with an asymmetric latch rule --- arm only on Accepted + Down(Left),
consume on any Up(Left) that was not Refused, because a button-up ends
the gesture wherever it lands while a refused release cannot be known
to concern it at all. The asymmetry is what earns the third state: it
makes all three outcomes behaviourally distinct at the latch, so P1-P4
each have something to bite. A two-state bool with the corrected
meaning would be right today and would let the next author restore the
collision without touching a test.
Revision 14 also takes the rows SS5b's split table assigned here, rules
a bounded per-frontend pending-release SLOT for the cancellation record
SS5b leaves nowhere to wait --- with the no-arming-over-pending
invariant asserted rather than assumed --- and records that the four
transitions SS5b left inert become defects the moment this lane
attaches effects.
Records a re-measurement obligation with evidence rather than
suspicion: every production anchor in SS5a has moved, four of them
measured here, and the DONE/MISSING verdicts are not assumed to have
survived a dispatcher SS5b rewrote.
Coherence: journey steps 5(c) and 8(b), and unlike SS5b this lane MOVES
the grade --- it supplies what SS5a's own table lists as missing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Merged rather than rebased, by decision: the lane's 12 commits include
10 framing revisions that all touch the same 800-1000 line doc regions,
so a rebase meant twelve rounds of large-block conflict resolution ---
the operation that produced a committed diff3 marker on the last lane.
One pass instead, with all 12 commits preserved.
Resolutions:
- src/daemon.rs --- took main's structure whole, both inbound arms with
the latch gated on the dispatcher's answer, and threaded replay's
`mods` through both call sites. `mods` is newly BOUND in the mapped
arm, which SS5b left in `..`; the mapped family carries the same
modifiers, so leaving it would have given a v25 session the inverted
Shift behaviour that parent 48 R-a fixed for v24.
- pmacs-gpu/src/main.rs --- additive throughout: both new struct fields
(`gesture_last_content_cell`, `last_pointer_generation`), both resets
at each site, and both test blocks.
- src/editor.rs --- auto-merged; the merged dispatcher keeps SS5b's
`#[must_use]`, its four rejection paths and its `-> bool`, plus
replay's `&mut self`, `mods`, chrome/mode-line handling and terminal
gesture application.
- docs/active-work.md --- the active replay lane above main's corrected
#239/#240/#242 headers.
- docs/bottom-panel-framing.md --- 5a then 5b. The paragraph arguing
the v25->v26 bump should be "recorded as required rather than made"
is marked superseded: SS5b made it and merged as #242.
Workspace compiles clean, all targets, no warnings.
THE MERGE SURFACES A SEMANTIC COLLISION THE FRAMING MUST RULE ON, and
it is not resolved here. The two branches give the dispatcher's bool
different meanings: for SS5b `true` means the gesture was ACCEPTED, and
it drives the accepted-gesture latch; for replay `true` means the event
was CONSUMED HERE, including chrome swallows. So a press on the band's
mode line now returns true and ARMS the latch --- a gesture that never
began in content, which is the defect class SS5b's review round four
found and fixed. Recorded, not patched, because which rows own the
answer is a framing question and the next revision owes it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
SS5b merged as #242 (47b5463) at approved head 61f0faf, via
--match-head-commit so the merge is provably of the reviewed head.
PROTOCOL_VERSION is now 25. panel-pointer-replay is unblocked and is the
next step in the arc.
#239 (ca92796) and #240 (72da24a) both merged on 2026-08-13 and the
ledger has been calling them OPEN for a week. Their blocks are kept for
their reasoning, relabelled for their status.
#240's block gains the postscript it earned: its TMPDIR isolation was
the thing I defeated during SS5b review round 4 by running the CRDT
sweep by hand, outside scripts/gate. Two m4_24 base-resolution rows
failed, I reported them as pre-existing and proposed CI as the arbiter,
and the actual cause was /tmp/.git being inherited as an ancestor
project root. Through the gate, both pass.
Adds U11 to the red registry, the row deferred during #242's review so
that no docs commit would invalidate that PR's head-exact gate evidence.
It carries the exact selector and panic fragment, both attempt IDs, the
1960/1 counts, and the fact that the margin is unrecoverable because
duration_ms is omitted from the assertion message --- which is why a
recurrence owes a merge-base control rather than a comparison. The four
exact-head local passes, the two macOS/luajit greens and the identical
async_runtime.rs blob are recorded as narrowing evidence and explicitly
not as causality.
Per the standing rule, this absorption does not advance any canonical
base to its own commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two consecutive scripts/gate runs at 70b334d, worktree verified clean
before and after each run, were 15/16 green apiece. Run A redded
13-sweep on dired_open_renders_10k_entries_under_200ms at 263.961465ms
against a 200ms budget; run B redded 15-sweep-crdt on
criterion_1_end_of_line_typing_completes_sub_frame_per_keystroke at
1.044609ms against a 1ms budget. Each red is green in the other run,
and both are green isolated at load 9.34.
That excludes the tree more strongly than U7 could: not "the diff
touches no render path" but the SAME COMMIT passing and failing each
row. Neither failing path is touched by the branch under test.
It does NOT establish load as the cause --- load was not sampled during
either failing step, and the row says so rather than borrowing a
reading taken elsewhere in the run.
Honours both escalation rules it trips. U7 says a repeat of one of its
selectors is a separate incident, and run A repeated one; U6 says one of
its pair redding alone is a separate incident, and run B did that. Both
are filed here rather than appended. The row also declines to pick
between the repeat and the rotation, because both are true of these
observations.
Closes one rival U7 left open: per-worktree gate target directories mean
no sibling shared this one.
Names the standing discriminating control U9 already specified and which
remains unrun --- pin test-binary concurrency to 1, and separately load a
lone --lib binary --- and asks that this family stop consuming review
rounds until it runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The branch was two behind, not one --- 24e4039 and 5f2015c both landed
on main after the first rebase. Rebased onto 5f2015c cleanly, no
conflicts; both commits picked up are docs-only.
The previously recorded 16-stage green ran about a minute BEFORE
3d08255 was committed, which makes it worktree evidence rather than
head-exact evidence. This commit is the last content change before the
gate is re-run, so the run that follows measures the head that is
proposed for merge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Both inbound panel-pointer arms discarded the bool from
dispatch_semantic_panel_pointer and called update_accepted_gesture
unconditionally. The ladder authenticates the SENDER; only the
dispatcher re-derives the TARGET, so an event can clear every rung and
still be refused --- for an out-of-grid coordinate, an absent side
window, or a buffer that is no longer the one in the side window.
A rejected Down therefore ARMED the latch, so a later authority loss
counted a cancellation for a gesture that never began, and once replay
attaches effects it would deliver a release to a child that was never
pressed. A rejected Up CONSUMED a real armed gesture, so the authority
loss that should have ended it found nothing armed and that child holds
the button down for good. A rejected Down on top of a live gesture was
worse again: arm_accepted_gesture ends what it overwrites, so it also
counted a spurious cancellation.
Both arms now gate on the return. The dispatcher is #[must_use], so the
class cannot recur silently --- clippy runs with -D warnings, making a
future discarded answer a build failure rather than a review finding.
Four rows, g5_substrate_a_refused_{press_never_arms,
release_never_consumes}_on_the_{legacy,mapped}_arm. Each drives the
refusal from a coordinate one past the last grid row and ends in a
positive control differing only in that coordinate; without the control
a row would pass just as well if an unrelated rung had dropped the
event. Mapped rows read the generation through the validator's own
accessor so a mapping-rung refusal cannot masquerade as a dispatcher
refusal. Three mutations, each biting its named rows: ungating the
legacy arm fails exactly the two legacy rows, ungating the mapped arm
exactly the two mapped rows, and relaxing the dispatcher's >= to > fails
all four.
Also removes a committed diff3 ancestor marker this file carried since
8c9afde --- the only one in the branch's 32 commits, and invisible to a
clean-worktree `git diff --check`.
Also withdraws the claim that the local CRDT sweep could not go green.
The full 16-stage gate is green, sweep-crdt included. The two m4_24
failures came from running that sweep outside scripts/gate, where it
inherited TMPDIR=/tmp; /tmp/.git exists on this machine and project
detection walks upward, so both base-resolution rows resolved against
the wrong root. That is the exact hazard #240's isolation exists for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Rebased onto f13506c at 3e68b76 --- 31 ahead, 0 behind, clean, signed.
One conflict, in this file, where main's merged-#241 block and this
lane's own first-commit block landed at the same position; both kept.
No code file overlapped: #241 touched scripts/, tests/common/ and two
suites, while this lane touches the protocol, daemon and GPU sources.
The blocker is gone --- #241 fixed the sweep-crdt defect this lane was
held behind.
Gate run in two labelled pieces, because 16 stages exceed the session's
10-minute command cap and the guard rightly forbids the backgrounding
that would evade it: 14 stages green (plain gate plus all six
--acceptance suites, at load 5.28), build-crdt green, and sweep-crdt red
on exactly two rows --- m4_24_bare_string_glob_stays_relative and
m4_24_d3_fallback_base_is_the_smallest_attachment_dir.
Those two are PRE-EXISTING and local-only: they fail identically at
current main f13506c, and they passed in CI on #241's Test (crdt) job.
Also records that an earlier attempt is not evidence. I wrapped it in
`timeout 580` to fit the command cap, which killed the sweep mid-run and
reported it as a stage failure --- the same self-inflicted shape as the
SIGTERM truncation in the probe lane, and it should not be read as a
result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The held lane still identified the teardown framing as revision 8, so
the approved state was not portable from this branch. Revision 9 adds
the total run classifier, the void budget, the endpoint verdict table,
the bisect-step policy and counterbalanced ordering --- none of which a
reader arriving here would have found.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two state pointers were wrong. It named ledger tip fb40d88 while the
branch was four commits past it, and identified the teardown framing as
revision 5 on one line and revision 8 on the next.
The tip pointer is removed rather than corrected. A literal SHA naming
this branch's own head is stale the moment the next commit lands ---
which is precisely how it came to say fb40d88 --- so the ledger now
points at the branch head itself and says why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two corrections.
- This ledger still said 72da24a being an ancestor of the passing
7599661 while failing today is something "no source-monotonic cause
explains". Withdrawn: the two observations differ in commit AND
environment AND time, so they are non-comparable and support no
causal conclusion of any kind. Different commits can
deterministically produce different outcomes, so the pair says
nothing about determinism either.
- It identified the teardown lane's framing as revision 5; the
portable lane is revision 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two statements this ledger still carried after 3313d8a said withdrew
them.
- The reduction entry still said the artifacts "are byte-different".
Historical byte identity is UNKNOWN and is not claimed: target dirs
have been overwritten, so a hash computed today is the current
occupant's. What is established is differing Cargo suffixes, hence
differing metadata hashes, hence different compilations --- enough
to void the comparison, and all that is asserted.
- The onset entry still called the window the sharpest lead and
called for bisecting it. Cleanliness was captured at neither
endpoint, so the first move is reproducing 7599661 and 724b785
clean in isolated target dirs, and deciding from that.
Also corrects the onset counts here to match the manifest: per test copy
the 17 sweep-crdt logs are 13 both-copies-ok, 1 where neither copy
executed (the stage died compiling pmacs, error[E0308]), and 3
both-failed --- not "three earlier reds on unrelated rows".
And narrows the ancestry note. 72da24a being an ancestor of the passing
7599661 while failing today shows only that outcome is not determined by
commit alone. It does not discriminate an environmental change, a
source/environment interaction, or a fix before 7599661 with a
regression before 724b785; an ancestor outside the interval says nothing
about whether the interval regressed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three corrections to what this ledger still asserted.
- It called the reductions a "bisect". They were not: no run isolated
a variable, because the subsets and the sweeps executed different
Cargo compilations.
- It said those compilations were "byte-different". Withdrawn --- the
bytes a historical run executed are not knowable now, since target
dirs have been overwritten and a hash computed today is the current
occupant's. What IS known is the differing Cargo suffixes, i.e.
differing metadata hashes, which is enough to void the comparison
and is all that is claimed.
- The onset was described as a bisectable window. It is not. Reflog
and commit times put HEAD at 7599661 during the last green ---
3c06176 landed 40s after that run finished --- and at 724b785
during the first red, since 5174f73 landed 08:45:41 after that run
ended 08:42:01. Cleanliness was captured at neither endpoint.
Decisively, 72da24a is an ANCESTOR of the passing 7599661 yet fails
today, and no source-monotonic cause produces that. Reproduce clean
endpoints before considering a bisect.
Also corrects the onset counts: per test copy the 17 sweep-crdt logs are
13 both-copies-ok, 1 where neither copy executed (the stage died
compiling pmacs with error[E0308]), and 3 both-failed. "14 passes" and
"the earlier reds failed on unrelated tests" were both wrong --- one of
those runs never reached the test at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
779a6bd corrected four claims but left the ones that mattered most. This
ledger still said --workspace unification was refuted, that R9 ran the
"same binaries, same order, same tests", that later packages cannot be
implicated, and that the cause is cumulative across the preceding 37
binaries. All four are withdrawn:
- R9 executed gpu_initial_target_acceptance-91f51d0b and
gpu_invocation_acceptance-6b4b8223; the sweeps executed -5d9105cb
and -d4dae4f0, byte-different. Command shape changes Cargo's
fingerprint, so the comparison was never made. --workspace
selection and the preceding tests are both OPEN, not refuted.
- "Later packages cannot be implicated because their targets run
after the failure" ignores that they affect the build graph and
fingerprints before their tests execute.
- No cumulative cause follows from reductions that ran different
binaries. What is established is narrower: reproducible in the full
sweep, not reproduced in any subset attempted.
And a finding that reframes the defect. sweep-crdt appears SEVENTEEN
times in this target dir's gate logs. The ctrl_c failure appears in
exactly the last three, and the test passed --- both copies, "... ok" ---
inside the stage before them. Last green 20260815T185708Z, first red
20260816T063330Z, no reboot between; the three earlier red sweeps failed
on unrelated rows. "Pre-existing on main" still holds, since 72da24a
reproduces it, but "always broken" is contradicted, and bisecting that
window is now the sharpest lead. The red full-sweep count is SEVEN, not
five, each enumerated with its own log digest in the teardown lane's
docs/probe-sigint-evidence.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Pushing 16cf3a2 made the retraction portable but not the correction.
This ledger still carried four falsified statements, and a held branch
that transports them is worse than one that never recorded them.
- "119 binaries green, one red" -> 119 green result summaries and TWO
red binaries. `gpu_initial_target_acceptance` includes the suite as
a module, so a reproducing sweep reds twice (:3097 and :3131).
- The ">=8s lifetime" arithmetic behind the retraction is FALSE. Both
reproducing binaries finish in ~5.19s INCLUDING the 5s timeout, so
the failing launcher lives about 5.1s --- inside what the sampler
saw. The ">6s selector" proposed as the remedy would have captured
nothing. The "mechanism located" claim is therefore not refuted by
that argument; it stays unproven because the suite spawns root
launchers from six call sites under --features crdt, so command
line alone cannot attribute one to this test.
- "the probe should die on SIGINT's default action" -> withdrawn.
Absence of handler code does not establish default disposition;
SIG_IGN is inherited across fork and survives exec, which is why
inherited ignore is the leading hypothesis.
- "never blocked indefinitely" -> only the event loop is bounded, at
50ms. The process is not: the stdin reader blocks in read_to_end
and the loop leaves only when stdin closes.
Also records that the defect now has its own lane,
`gpu-probe-sigint-teardown`, whose framing supersedes every diagnostic
claim here, and that §5b is held behind it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
a92ef7f said the mechanism was located: the launcher blocked in
`do_wait` on a GPU probe child stuck in `futex_do_wait`. Checking the
instrument against the claim shows it does not support it.
The sampler caught 394 distinct launchers across a reproducing sweep and
the longest-lived was 5s TOTAL. For this test to fail, a launcher must
outlive its SIGINT by 5s, so its lifetime would be 8s or more. The
failing instance was never captured. What I described is a healthy
launcher from one of the suite's other tests --- the normal teardown
shape, reported as the defect.
This is the same error as the `available`-memory reading earlier in this
lane: a measurement that looked conclusive, reported before checking
that it discriminated. Retracted here rather than left to be found.
Two facts do survive and constrain the next attempt: `pmacs-gpu`
installs no signal handling at all, so the probe should die on SIGINT's
default action; and its main loop is a 50ms `recv_timeout`, so it never
blocks indefinitely. The next instrument must key on the failing
instance --- launchers outliving ~6s, or a PID recorded by the test
itself --- rather than sampling every launcher and hoping.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Sampled the process table twice a second through a reproducing sweep.
After the test SIGINTs the launcher's process group:
- `pmacs --gpu --socket ...` sits in `do_wait` for the full 5s. It is
waiting on a child, not ignoring the signal.
- `pmacs-gpu --headless-managed-probe ...`, its child and in the same
process group so it received the SIGINT, sits in `futex_do_wait`
and never exits.
The deadline is missed because the GPU probe does not tear down under
SIGINT. The fix belongs in the probe's shutdown path; raising the 5s
would only hide it. Why the child hangs ONLY in a complete sweep is
still open --- every prior GPU suite has exercised the adapter by then,
which is where to look first.
Five explanations are recorded as refuted so nobody re-runs them: load,
leaked daemons, inotify, `--workspace` feature unification, and any
specific preceding test --- all 37 preceding targets plus the suite run
green, which is the genuinely strange part.
The tmpfs hypothesis got a real experiment rather than an argument:
/tmp went 21G -> 1.2G, available memory 27G -> 45G, and the sweep stayed
red. Recorded with the note that my earlier `available`-based dismissal
was itself unsound, since tmpfs pages are not reclaimable yet still
appear in buff/cache --- right conclusion, wrong reasoning, and it took
the experiment to know which.
Also notes the test exists in two binaries: gpu_initial_target_
acceptance includes it as a module, so a reproducing sweep fails it
twice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Attribution is settled. The identical `build-crdt && sweep-crdt` pair,
run at the merge base 72da24a in the primary worktree with its own
target dir, fails the SAME test: 119 binaries green, one red,
`ctrl_c_on_launcher_group_does_not_reach_spawned_daemon`. This branch is
not implicated, and no branch can pass this gate stage on this machine
until the underlying defect is fixed --- main included.
The mechanism is unknown, and this records two explanations I offered
and then refuted, so nobody re-runs them:
- Load: refuted. Red on a quiet machine, load 2.77 at launch.
- Memory pressure: refuted by correcting my own instrument. The
sampler showed free memory at 543MB, which looked damning, but it
recorded `free` --- not the meaningful figure on Linux. `available`
was 27G. The 543MB was reclaimable cache. I had already reported
the memory story before checking that, which was wrong.
- Leaked daemons: refuted. Peak 58, up only 8 during the sweep, and
the green standalone runs already ran at 46-50.
What the bisect did establish: green in every smaller context tried ---
the test alone three times at 0.15s against its own 5s deadline, its
whole suite, a workspace run filtered to just it, the lib binary then
the suite, and the three GPU suites in sweep order --- and red 4/4 in
the full workspace sweep across two trees. Cumulative across the 37
binaries preceding it, and not flaky.
Also notes that /tmp is a 30G tmpfs holding 21G of an unrelated
project's stale target directories. Recorded as an observation about
this machine, not as the cause, and not touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Review corrections:
- The header said "ACTIVE, framing only" and named 3c06176 as the
checkpoint. Now code-complete at 5174f73, ledger tip fb40d88, gate
red.
- The gate-status entry claimed isolation established an
ENVIRONMENTAL cause. It does not. Isolation establishes
INTERMITTENCE. Narrowed to: intermittent; foreign load is a
measured confound; cause unresolved. No experiment here separated
sweep contention, foreign load, and a genuine defect in the row.
And one found while checking the user's "eleven-stage" phrasing against
what actually ran. Yesterday's runs printed ELEVEN stages, today's
printed TEN, and the difference is `05-acceptance-bottom_panel_stage2b_
daemon_acceptance`. `--acceptance` is an explicit repeated flag; the
gate derives nothing from the diff. Both of today's runs used bare
`--protocol`, so **no acceptance stage ran at all** --- the suites were
verified by hand instead, which is not the gate.
The `Gates:` line named suites but never the flag form, which is how
that happened. It now carries the exact invocation and the expected
stage count, with the note that a ten-stage run is missing every
acceptance stage. It also said "the four `bottom_panel_*` suites" when
there are FIVE on disk; `bottom_panel_stage2b_protocol_acceptance` is
the fifth and belongs in a protocol-bearing lane above all others. All
five are listed rather than guessing which four an earlier writer meant.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Nine of ten stages green at 5174f73. `09-sweep-crdt` red twice on
`ctrl_c_on_launcher_group_does_not_reach_spawned_daemon` --- "child did
not exit within 5s" --- which is a NEW signature, absent from
`ci-red-signatures.md` under any id.
The row holds a fixed 5s wall-clock deadline for a spawned child to
exit after a signal, and it runs inside the heaviest stage the gate has.
It is green alone and green with all fourteen of its suite siblings, the
latter in 0.15s against that same 5s deadline. Nothing about it touches
panels or the wire.
Sweep contention could not be separated from foreign load, and that is
recorded as inconclusive rather than dressed up: running the sweep-crdt
command alone red the same row, but `uptime` hit 59.51 during that run.
An unrelated turso workload is running in a LOOP on this machine,
holding a 16-core box at load 20-60, so clean gate evidence is not
obtainable here. The remaining work on this lane is one clean gate run
and nothing else.
Recorded in the lane ledger rather than the registry, per this branch's
standing reason: the registry here ends at U9 while the unmerged replay
branch already holds a U10, and duplicate ids have survived a clean
merge in this file once before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Found by reading the arming path back after the G5 checkpoint, not by a
failing row. `arm_accepted_gesture` overwrote an already-armed latch, so
a dropped `Up` --- one lost to an outbox that closed under a stall ---
was followed by the next press silently discarding the first gesture's
record, without counting it as a cancellation.
Inert on this base, where records are only counted. Once
`panel-pointer-replay` attaches a child release to each record, the
discarded one leaves a button held down with nothing left to release
it, and the arming code that decides this is this slice's.
Mutation: restore the plain overwrite -> the new row alone.
Also records three CI-red observations from the slice-completion gate,
which is the first entry on this lane with a MEASURED confound instead
of the standing uncontrolled one. Three wall-clock-deadline rows red in
one run --- criterion_1 by 0.12%, a PTY lifecycle race, and a 5s child
-exit deadline --- all green in isolation, the last in 0.15s against
its 5s deadline. `uptime` during the run went 14.02 -> 28.35, from an
unrelated turso test suite on the same machine with one binary at 693%
CPU. Not a controlled experiment, but the same evidence U9's synthetic
-load control was meant to produce, and it points at load.
Two process traps are recorded with them, because both were made here.
The Bash tool caps a command at ten minutes and SIGTERMs it, which the
gate reports as `FAILED (exit 143)` on whatever stage was running and
which reads exactly like a real failure. And `pkill -f <pattern>` kills
the invoking shell when the pattern appears in its own command line, so
the intended target survives while the operator believes it died --- and
here `pkill -f "cargo test"` would have destroyed an unrelated
project's build. Identify by PID.
Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; the four §5b G5 rows. The full protocol gate follows on
a quieter machine; the run described above is not evidence for this tree
and is recorded as an observation only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Closes the rest of this slice's G9-G15 obligations. One production
change, one design correction found by mutation, and the rest witnesses.
G9b is the production change. `panel_motion_is_new` compared the cell
alone, so the FIRST motion after the mapping moved was eaten --- the
pointer has not travelled, but the cell now denotes different text, and
that is exactly the motion the daemon needs to re-anchor the gesture.
Now keyed by `(generation, cell)`. Deliberately read at the motion site
rather than reset from the frame path: resetting on every accepted
repaint would re-arm within one generation and bring pixel-rate traffic
straight back.
G9a, G9c, G10, G10a, G10c are witnesses over behaviour that was already
correct: a generation change ships even when the visible cells are
byte-identical; an identical frame at a higher generation still moves
authority; invalid frames, zero generations and lower generations are
each refused with frame AND generation retained; `Absent` does not erase
the high-water mark.
G15 is the TUI structural control, and its fixture IS the control: the
session has no `SemanticRenderState` at all, so every local click, drag
and wheel effect below is reached without a producer in existence. That
is a stronger claim than asserting a value was not consulted. Both wheel
ticks must land, for the same reason the mapped family needs its
exemption.
The design correction: G10a's first version asserted the zero refusal
with a generation already HELD. Mutating the zero check away left it
green --- zero is also *lower* than the held value, so the nondecreasing
clause refused the frame and the row proved nothing about zero. Zero is
only isolable before any authority exists, which is also the case the
framing names: a sender that never initialised the field. Split into its
own row with that setup, and the row says why.
Mutations, each biting only its named row:
- dedupe compares the cell alone -> G9b
- the frame path re-arms the dedupe on every accepted frame -> G9b
- daemon dedupes across the generation change -> G9a
- return early on frame equality before applying the generation -> G9c
- apply the generation before validating -> G10
- accept generation zero -> G10a (after the split; before it, this
mutation SURVIVED)
- `Absent` erases the high-water mark -> G10c
- a lower generation is accepted -> G10c
- panel input requires a token the TUI cannot have -> G15
Deferred, per SS5b's split table and unchanged here: G11b (exhaustion
cancellation), G12a/G12b (both two-tick wheel EFFECTS), G6c/G7c.
Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; `cargo test --lib` (1959); `cargo test -p pmacs-gpu
--bins` (280); `bottom_panel_stage1_acceptance` (47),
`bottom_panel_stage2b_daemon_acceptance` (39),
`bottom_panel_stage2b_gpu_acceptance` (2); `git diff --check`. Clippy
caught two findings in the new test code after the suites were already
green, which is why it runs as its own step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three rows that share a shape: each is a gate this slice owns whose
downstream EFFECT belongs to the rebased replay lane.
G10b --- ordering, and a carve-out. `panel_mapping_is_current` now takes
the event kind. Zero is refused FIRST, then coordinate-free wheels skip
the freshness comparison. The order is the row: run the carve-out first
and a sender emitting zeroed wheels faces no check at all, which is an
inbound opt-out through the exempt path. The exemption exists because a
tick changes `view_top` and so advances the key --- the next tick already
queued behind it echoes the previous generation, and without the
carve-out the panel scrolls once per frame and appears dead. It returns
before the read, so a wheel does not advance the key either; advancing
would make a wheel invalidate the press after it.
The framing's carve-out-to-the-carve-out, re-imposing the check for
CHILD-REPORTED terminal wheels where SGR carries row and column, is
replay's. Whether a wheel is forwarded is decided by the reporting mode,
and no panel pointer coordinate is consumed on this base at all.
G11a --- exhaustion fails CLOSED. `saturating_add` froze the key at the
ceiling while the mapping kept moving underneath it: the stale-gesture
hole the key exists to close, with the check still appearing to pass.
Now `checked_add`, and overflow publishes `Absent`, clears input
authority, and latches for the session.
G13a/G13b --- `PanelPointerMapped` fell through `coalesce_kind` to
`None`, so pixel-rate mapped motion was lossless and filled the bounded
outbox. Two tags of its own; tail-replacement takes the whole event, so
coordinate and generation advance together and a collapsed run can never
pair a new coordinate with a stale one. Press, release and every wheel
kind stay lossless.
Mutation results, including two that changed the design:
- exemption before the nonzero check -> G10b(zero) alone
- no wheel exemption -> G10b(exemption) alone
- saturating instead of checked add -> G11a alone
- no exhaustion latch -> G11a, but only AFTER the row was extended.
The first version of G11a did not bite: the latch had no proven
job, because the overflow path already returns before storing the
ceiling snapshot, so the next read re-takes the changed arm anyway.
Measured, the two are ALTERNATIVES --- either alone keeps the band
down; only removing both resurrects it. The latch is kept as the
primary because it has a job the ordering does not: `peek` now
honours it, so the peek and the authoritative read agree that an
exhausted session has no key rather than reporting the ceiling.
The source comment says this, rather than the "second half" claim
it made before the measurement.
- mapped variants untagged / one tag for all kinds / sharing the
legacy tags -> the mapped coalescing row alone, three times
Witness-shape note: the two G10b rows call the predicate directly, and
say why. A wheel has no dispatcher-visible effect on this base --- a
document panel focuses on `Down` only --- so asserting focus for a wheel
would prove nothing. Each row carries a press leg, which does have an
effect, to show the predicate is wired into the production arm.
Verified: `cargo fmt --check`; `cargo clippy --workspace --all-targets
-- -D warnings`; `cargo test --lib` (1959); `cargo test -p pmacs-gpu
--bins` (275); both `bottom_panel_stage2b_*` suites (39); `git diff
--check`. `composition_overhead_under_ten_percent` red once during this
work and green in isolation --- a second occurrence of a signature the
lane ledger already carries, now recorded there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
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
The G6/G7/G8 matrix, plus the probe correction and the corrections
review found in my first attempt at these rows. This is the coherent
checkpoint: full eleven-stage `--protocol` gate, green.
**THREE OF THESE ROWS PASSED WITHOUT PROVING THEIR CLAIM**, and each
failed differently:
G8b/G8d had no atomicity. G8b installed a LEGACY frame and then
switched the session to Mapped, so `mapping_generation` was `None`
throughout --- asserting it stayed `None` after the refusal asserted
nothing. Each direction now uses an independent state, accepts a
CORRECT-FAMILY baseline so there is real authority to preserve, and
primes both pointer latches. Two new mutations pin it: clearing
authority before refusing fails G8b, discarding the retained frame
fails G8d. The family-gate mutation touched neither.
G8e covered one direction. An authority check that only holds one way
is one a peer walks around by choosing which identity to forge, so a
mapped session now also fails to borrow a legacy identity --- and
BOTH claimed identities have real registered sessions, or a
payload-keyed lookup fails for want of a session rather than for want
of authority. That was why G8e's own named mutation did not bite on
the first attempt.
G6b measured ambient state. It pre-focused the panel and then asserted
against `active_window_id()`, which tracks `active_frontend` too ---
satisfiable by a frontend switch that never routed anything. Every
routing and refusal row asserts `views[fid].active` now, with the
document precondition stated rather than assumed.
**And the probe measured the payload rather than the band, twice over.**
Its identity tuple was `(panel_epoch, geometry_epoch, size)`, which
ordinary content, focus, cursor and generation updates all leave
unchanged --- so accepted frames went uncounted, including the
identical-frame/higher-generation case this slice requires, and a
fixture waiting for two frames would wait forever. It snapshots the
complete accepted authority now, `(presented frame, mapping_generation)`,
and keeps the raw payload kind ONLY to tell a real `Absent` from a
refusal: inferring absence from `presented() == None` turned a rejection
into "the daemon says there is no band", a different fact entirely.
Nine rows, ten mutations, each biting its own:
G6a legacy outbound G7a mapped outbound, live generation
G6b legacy inbound routing G7b mapped inbound routing
G8a bare from v25 refused G8c mapped from v24 refused
G8b legacy at v25 refused, atomically
G8d mapped at v24 refused, atomically
G8e both forgery directions
plus: an Unsupported session accepts NEITHER family
G6c/G7c remain replay-lane effects.
**The gate earned its keep**: it caught a real regression I would have
shipped. `one_daemon_serves_a_v21_panel_session_and_a_shipped_v20_client`
counter-offers `PROTOCOL_VERSION`, now 25, so it is a MAPPED session
whose helper drained for legacy `Present` and timed out. Third suite
whose helpers assumed one family --- daemon acceptance, the GPU probe,
now GPU acceptance --- each written when only one family existed and
each quietly deciding what "a panel arrived" means.
Four `--protocol` runs were needed. Three failed on unrelated
signatures: the composition budget twice, in different steps, and
`setsid_escapee_is_not_reaped_and_teardown_reclaims_readers` once, a
new signature. All are recorded in the lane ledger rather than
`ci-red-signatures.md`, which ends at U9 here while the unmerged replay
branch already holds a U10.
Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260815T185708Z, verified by exit status.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
**`Unsupported` WAS ACCEPTING LEGACY FRAMES.** I gated the legacy arm
on `!= Mapped`, and `Unsupported` is neither --- so a session below
`PANEL_MIN_VERSION` accepted a band it never negotiated. The
`carries_panel()` check I had in mind guards
`next_geometry_declaration`, a different seam entirely. Both present
arms gate on their POSITIVE family now, which is the shape that cannot
grow this hole again when a fourth family appears.
**AND THE PROBE MEASURED THE PAYLOAD, NOT THE BAND.** It recorded panel
facts before `apply_attach_message` ruled on the message, so once one
valid frame had landed, a REJECTED frame --- wrong family, invalid,
stale generation --- still supplied the expected text while the
retained old frame supplied the rendering. The probe would report the
band showing something it does not show, which is a false positive in
the one place that exists to tell us the band is real. (The false
NEGATIVE, observing only the legacy family, was the previous commit.)
Facts now come from `state.panel.presented()` after the apply, and
`panel_frames` counts only when the retained frame's identity actually
moved: a duplicate or a refusal leaves the band exactly as it was, and
counting either would say the daemon is painting when it is not.
**Third rustdoc split in this slice**, same mechanism each time ---
`screen_size`, `peer_may_send_panel_events`, now
`send_panel_pointer`. I insert a function at what reads as a gap
between declarations, when the lines above it are the NEXT function's
documentation. The check is to look UP from the insertion point, not
just down, and I will apply it rather than keep reporting the same
correction.
Verified by exit status: clippy 0, `-p pmacs-gpu` 0 (271 passed).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The frontend half of bilateral gating. The nine discriminating rows
(G6a/b, G7a/b, G8a-e) and the full protocol gate are the checkpoint
after this.
**ONE ENUM, DERIVED ONCE.** `PanelFamily::{Unsupported, Legacy,
Mapped}` replaces the `panel_wire` bool, classified from
`session_protocol_version` at all four existing sites --- initial
attach and each reconnect --- and read by BOTH payload acceptance and
pointer production. Deriving those two independently is how a frontend
ends up accepting one family while producing the other: it would speak
v25 inbound and v24 outbound, and neither side could tell.
Acceptance refuses both wrong-family cases, atomically: a mapped
session rejects legacy `Present` rather than painting a band whose
cells it cannot safely invert (G8b), and a legacy session rejects
`PresentMapped` (G8d). The mapped arm also refuses generation zero and
any generation BELOW the one held --- nondecreasing, so a frame delayed
across a hide cannot roll authority backward --- while a duplicate
frame at a HIGHER generation is still accepted, because the daemon has
re-keyed the mapping and echoing the stale value would have every
gesture refused.
Production goes through one family-aware sender used by both send
sites, so they cannot drift. A mapped session with no retained
generation sends NOTHING rather than the legacy variant: falling back
is the frontend half of the bypass, and the daemon refuses it anyway.
**And the live probe observed only the legacy family.** Left alone,
mapped production could have worked end to end while the probe reported
no panel --- a false negative in the one place that exists to tell us
the band is real. It matches both now.
One thing NOT done here, deliberately: the gesture-latch reset on an
identity change is R-d, owned by `panel-pointer-replay`. I had copied
it into the mapped arm before noticing `gesture_last_content_cell` does
not exist on this branch --- it is replay-lane state. Two branches
resetting the same latch would conflict at the rebase and neither would
own the contract, so this arm installs the frame and its generation and
nothing more.
Verified by exit status: clippy 0, `-p pmacs-gpu` 0 (271 passed).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three review corrections ahead of the witness rows.
**THE LEGACY CONTROLS NEGOTIATED TWO DIFFERENT VERSIONS AT ONCE.** I
moved their `SessionRegistry` to v24 and left their retained
`SemanticRenderState` on `PROTOCOL_VERSION`, so the producer shipped
`PresentMapped` to a session the daemon believed was v24. The rows
passed, which is the problem: a control that negotiates two versions
proves nothing about either. Five producers across the four rows now
take `LEGACY_PANEL_VERSION`, the same constant the registry does.
**The boundary is a LITERAL 24, not `PANEL_MAPPING_MIN_VERSION - 1`.**
G6/G14 make the family boundary absolute; arithmetic against a moving
constant would drag these rows forward on the next bump and they would
silently begin testing v25 as "legacy".
**And I split a rustdoc from its function again.** Inserting
`peer_uses_mapped_panel_family` above `peer_may_send_panel_events` left
the latter's doc block stranded, so my function inherited two
incompatible descriptions and the documented one had none. Same mistake
as `screen_size` two commits ago --- inserting an item at what looks
like a blank line between declarations, when the line above is the next
function's documentation. Each block sits directly above its own
function now.
Verified by EXIT STATUS, not by reading filtered output: `cargo test
--lib` 0 (1945 passed), clippy 0, focused suite 0 (37 passed). The
`composition_overhead_under_ten_percent` flake appeared once mid-run
and passed isolated; it is the known wall-clock budget signature, not
this diff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The receiving half of bilateral gating, plus the constructor fix review
found. The eight G6/G7/G8 witness rows and the GPU side are next; the
full eleven-stage `--protocol` gate runs once they discriminate.
**THE FAMILY IS DECIDED FIRST, FROM THE AUTHENTICATED SESSION.**
`peer_uses_mapped_panel_family` reads `session_state(source)`, never the
payload's `frontend_id` --- that field is untrusted on every inbound
variant, and looking negotiation up by it would let a peer claim
another session's family. The order is stated at both arms: family,
then the existing epoch ladder, then the mapping generation, then
dispatch. It cannot depend on the variant's contents, because it
decides which variant is admissible at all.
Both wrong-family cases are REFUSALS, not fallbacks. A `>= v25` session
sending the bare `PanelPointer` is dropped rather than handled under
legacy semantics --- handling it would leave the mapping hole reachable
by choosing a discriminant, which is the entire bypass. A `<= v24`
session sending `PanelPointerMapped` is dropped too, even though a peer
compiled from this crate can encode the discriminant: negotiation is a
gate, not a sender convention.
`panel_mapping_is_current` is the ladder's finest rung. `buffer_id`
catches an A->B replacement, `panel_epoch` a close/reopen,
`geometry_epoch` a declaration race, and this catches the text under
the cell changing --- a foreign edit, a fold, a reload, none of which
moves an epoch. Zero is refused outright, and the check reads through
the same accessor projection stamps with, so the two cannot drift.
**And `new()` contradicted its own contract.** It documents a
current-build peer and enables every other current capability, but I
initialised `peer_knows_mapped_panel` to `false` --- so an implicitly
current peer was sent the LEGACY family. Set true, with the doc
extended to say the assumption covers later capabilities too.
**Four daemon rows broke, and that is G8a firing.** They drive
`FrontendEvent::PanelPointer` through sessions negotiated at
`PROTOCOL_VERSION`, which is now 25 --- so the bare variant is refused,
correctly. They negotiate `LEGACY_PANEL_VERSION` now, which both fixes
them and makes them explicit legacy positive controls rather than rows
that happened to pass.
Note for the eventual rebase: this branch is based on main, where
`dispatch_semantic_panel_pointer` still takes four arguments. Threading
`mods` is R-a, owned by `panel-pointer-replay`; the mapped arm
destructures `mods` into `..` here and will pass it through when the
lanes meet.
Verified: focused suite 37/37, `cargo test --lib` 1945 green, clippy
clean.
**Amended.** The first version of this message claimed the lib suite
green when it was not: I piped `cargo test` through `tail`, so the
pipeline exited 0 and my `&&` chain committed on a failure I had not
read. The four rows above are what it was reporting.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The producer half of bilateral gating. Inbound routing, the four
refusal quadrants and the positive routing controls are the next
commit; the full eleven-stage `--protocol` gate runs at that
checkpoint, once all of them discriminate.
`SemanticRenderState` gains `peer_knows_mapped_panel`, set in
`for_peer` beside the six existing capability bools --- the pattern
this codebase already uses to bake a negotiated version into the
producer. A `>= v25` peer receives `PresentMapped`; every older peer
receives legacy `Present`, unchanged.
**THE ORDERING IS LOAD-BEARING AND IS WRITTEN DOWN AT THE SITE.**
Projection first, THEN capture-and-advance the mapping key, THEN
construct the payload. A terminal projection registers the view whose
scroll anchor the key reads, so capturing earlier stamps a frame with a
key derived from an anchor that does not yet exist.
One case is deliberately not a fallback: if a `>= v25` peer has no
presentable mapping to stamp, the producer publishes `Absent` rather
than dropping to legacy `Present`. Falling back would hand a peer the
family it did not negotiate --- the bypass the gate exists to close,
arriving from the daemon's own side.
**Thirty existing rows broke, and that is the gate working.** The
fixture negotiates `PROTOCOL_VERSION`, so every panel test suddenly
received the mapped family where it matched `Present` literally. Those
rows are about the PROJECTION, not the wrapper, so the fixture helpers
are family-agnostic now and which family carries a frame is pinned by
the G6/G7/G8 rows rather than incidentally by thirty others.
Verified: focused suite 37/37, `cargo test --lib` 1945 green, clippy
clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Closes the fourth terminal gap. The row I could not drive was
reproducible; **my delta had the wrong sign** --- `scroll_view(key,
viewport, 3)` moves, `-1` does not --- and I concluded the fixture was
at fault after three attempts rather than trying the other direction.
The sequence: `frame()` registers the panel terminal view, the key is
built from the side window, the viewport is `panel_grid_size` less its
mode-line row, forty published line feeds build the history, and the
baseline is taken AFTER that history exists. Taking it after is what
makes the leg discriminating --- a constant anchor leaves the
post-history baseline unchanged, so the row fails even while
`mapping_revision` is perfectly live.
Two mutations, each failing this row alone:
constant ANCHOR, live revision -> the anchor is not in the key
constant REVISION, live anchor -> the screen is not in the key
Neither passes on the other's evidence, which is the separation the
terminal half needed: `screen.rs` proves the counter classifies events,
the domain row proves the branch is taken, and these two prove the
daemon's key actually reads both halves of what
`view_mapping_identity` returns.
The owed-witness note is removed from the ledger.
Verified: focused suite 37/37, clippy clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three of the four terminal gaps. The fourth is recorded as owed rather
than faked; see below.
**THE STABLE CLASSIFICATION WAS INCOMPLETE.** Cursor motion still
advanced the mapping revision: `restore_cursor`, `horizontal_tab`,
`move_vertical`, `move_horizontal`, `set_col` and `set_row` all called
`changed()`. Moving the caret denotes nothing new, and a child that
merely repositions its cursor would have cancelled a drag. All six take
the display-only path now.
Worse, **rewriting the same glyph under another style advanced it**,
which is precisely the control SS5b requires to hold. `write_character`
now compares the glyph before writing --- sampled BEFORE
`clear_wide_at`, which blanks a cell that is part of a wide pair and
would otherwise make every rewrite look like a change. That ordering
was found by instrumenting the failing row, not by reading the code.
**THE SNAPSHOT CARRIED DOCUMENT-ONLY STATE FOR TERMINALS.**
`view_top`, `view_left`, wrap, content columns, fold policy and folds
describe a document projection and take no part in a terminal's, where
the child's screen decides the mapping. They live inside the `Document`
arm now; only common geometry --- buffer identity, rows, columns ---
stays outside.
**AND THE REVISION WAS NOT PUBLICATION-CONSISTENT.**
`view_mapping_identity` read the LIVE screen revision while
`projection_ref` returns the last PUBLISHED cells, so buffered output
under synchronized-output would stamp displayed cells with authority
they were never painted under --- a frontend echoing a generation
matching nothing it can see. `ScreenProjection` carries
`mapping_revision` now and the published value is what is read.
**The witnesses were separated across the seam**, which review named
exactly: `screen.rs` proved the counter, the daemon proved enum
selection, and a `view_mapping_identity` returning a constant would
have left both green. A daemon-level row now drives real events through
a panel terminal and asserts the daemon's generation moves on a new
glyph and holds across a style-only rewrite and across cursor motion.
**OWED, NOT DONE: the scroll-anchor row.** The anchor is in the key,
but three attempts failed to drive a scroll from this fixture ---
`scroll_lines` wants a viewport the projection registers on its own
schedule, and `scroll_view` with an explicit size reports no movement
after forty line feeds. Recorded in the ledger rather than faked or
quietly dropped: without it, a constant ANCHOR alongside a live
revision still passes every terminal row that exists.
The two test hooks are `#[doc(hidden)] pub`, not `#[cfg(test)]`,
because the rows needing them are integration tests and those link the
library without `cfg(test)`.
Verified: focused suite 37/37, `cargo test --lib` 1945 green, clippy
clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
All four closure gaps in one correction. Three of them are defects in
what I committed as G1-G4; the fourth overturns a claim I made about
what could not be witnessed.
**THE KEY IS NOW EXACT, NOT PROBABILISTIC.** It was a `DefaultHasher`
digest, so authoritative equality rested on the absence of collisions
--- and a collision silently ACCEPTS a stale gesture, which is precisely
the failure the key exists to prevent. It is a `PanelMappingSnapshot`
struct compared structurally now. The emitted `mapping_generation`
stays a `u64` on the wire; only the daemon's own comparison changed.
**THE TERMINAL DOMAIN WAS ABSENT, AND THE BUFFER REVISION WAS WRONG.**
The key hashed the panel buffer's content revision for every target
kind. For a terminal that is doubly wrong: SS5b says the buffer revision
does not decide the mapping, and what does --- the screen --- was not
consulted at all. `PanelMappingContent` now splits by kind, and
terminals carry the screen's mapping revision plus the view's scroll
anchor.
That revision had to be built. `Screen::generation` cannot serve:
it advances from 39 sites including style, title, bell, tab stops and
cursor motion, none of which changes what a coordinate denotes.
`Screen` now carries `mapping_revision`, and the classification FAILS
SAFE --- `changed()` bumps both by default, and only the eleven
explicitly display-only arms call `display_only_changed()`. Anything
unclassified is treated as content, because over-cancelling a gesture
is a nuisance while under-cancelling one lets a stale coordinate reach
a child.
**"NO PRODUCTION PATH REACHES A TRANSPOSITION" WAS WRONG.** I recorded
the rows/cols product mutation as unwitnessable and kept the separate
hashing on principle. Resize plus redeclare reaches it: 4x80 -> 8x40
holds the area at 320 while swapping the dimensions, and
`last_content_cols` is not refreshed until the next render, so the two
grid fields are isolated. The row exists and the product mutation now
fails.
**G3 WAS INCOMPLETE.** It covered idle and cursor only. Focus is added
at the daemon level --- the tempting error is folding the whole frame,
which carries a `focused` flag, into the key. Styling is pinned
structurally instead: the snapshot has no style field, so there is
nothing a recolour could touch. The terminal controls live in
`screen.rs`, at the level the classification lives, with a positive
half so a revision that never advanced at all cannot pass them.
**AND MUTATION TESTING FOUND ANOTHER UNWITNESSED BRANCH.** Routing
terminal panels through the DOCUMENT arm left all thirty-five rows
green --- the `is_terminal` branch had no daemon-level witness at all.
A row now pins that the snapshot picks its domain by target kind, and
that mutation fails.
Mutations: display-only events bumping the mapping revision (the
screen-level control fails); terminal panels keyed on the buffer
revision (the domain row fails); rows*cols as an area (the
transposition row fails); plus G1-G4's original five, still biting.
Verified: focused suite 36/36, `cargo test --lib` green, clippy clean.
Full `--protocol` gate reserved for the checkpoint after bilateral
gating, per the standing procedure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The key itself, its domain, and the witnesses for both. No gating and no
replay yet.
**DERIVED FROM A FINGERPRINT, NOT BUMPED AT MUTATION SITES.**
`panel_mapping_fingerprint` hashes what actually decides which byte a
cell means --- buffer identity, grid rows and columns, `view_top`,
`view_left`, wrap mode, content columns, fold POLICY and fold CONTENT,
and the buffer's content revision --- and the generation advances
whenever that changes. This makes the changing/stable split
STRUCTURAL: an input that is hashed moves the key by construction, and
one that is not cannot. Bumping by hand at each mutation site would
have made "advances after any mapping mutation" a promise about
someone remembering.
Folds are hashed at their SOURCE, the registry's ranges, rather than
through the derived `VisibleLineMap`, whose only public summary is
`is_identity()` --- too coarse, since a fold edit that leaves the map
non-identity still changes which source line a row shows.
**ONE SEAM, READ BY BOTH SIDES.** `panel_mapping_generation` advances
if the fingerprint changed and returns the current value; projection
will stamp with it and inbound validation will compare against it, so
"what the frontend was shown" and "what the daemon checks" cannot
drift. Computed ON DEMAND, deliberately: a mutation not yet painted has
still changed the inverse, and a gesture arriving in that gap must be
refused. Deriving from the last emitted frame recreates the hole.
**Nondecreasing, and never cleared.** `Absent` yields no key to stamp
--- which is not a key of zero --- but the high-water mark survives, so
a frame delayed across a hide cannot roll authority backward. First
establishment takes 1; zero is the wire's invalid value.
**MUTATION TESTING FOUND MY WITNESSES UNDER-SPECIFIED, TWICE.**
With only the content-edit row present, dropping `view_left` from the
key stayed GREEN, and so did collapsing the grid to `rows * cols`. That
is exactly what the closure predicted --- "a key that ignores
`view_left` passes every row that only scrolls vertically" --- and it
is why G2 is enumerated per input rather than asserted in aggregate.
Six legs now, one per input, each touching only its own.
Mutations that bite: omit the content revision (3 rows), omit
`view_left`, omit wrap, omit fold policy, and include the CURSOR --- a
stable input, caught by G3.
**One mutation does NOT bite, and the row says so rather than
pretending.** Collapsing rows and columns to a product stays green,
because `last_content_cols` co-varies with a column change and the row
count co-varies with a resize: the key moves by another input either
way. Only a transposition (2x6 -> 6x2, identical product) would isolate
it, and no production path reaches one --- rows come from the band's
height, columns from the frame declaration, and nothing swaps them. The
key hashes them separately anyway; hashing a product because no test
can currently tell the difference would be choosing the weaker
construction for the suite's convenience.
G4b --- that an in-flight drag survives a selection repaint through
real replay --- stays owed by the rebased replay lane, which is the only
branch where replay exists.
Verified: focused suite 34/34, `cargo test --lib` green, clippy clean.
Per the standing procedure the eleven-stage `--protocol` gate is
reserved for the next coherent checkpoint.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
SS5b's first implementation commit: the two appended variants, the
version constants, and the pins that hold them in place. No gating, no
key, no replay --- those are the next commits, and the variants are
REFUSED everywhere until their gate lands.
**APPENDED AT THE TRUE END, confirmed by the discriminants.**
`PanelPointer` is 15, `TextInput` 16, `PanelPointerMapped` **17**;
`Present` 0, `Absent` 1, `PresentMapped` **2**. "Beside `Present`" would
have been adjacent insertion, which shifts every discriminant below and
silently re-interprets an older peer's bytes. `mapping_generation` is a
`u64`, last within each variant, documented invalid at zero --- the
value a default-constructed sender produces, so accepting it would let
a peer opt out of the check by sending nothing.
**THE COMPILER NAMED EVERY SEAM.** Four non-exhaustive matches:
`semantic_render`'s declaration accessor now sees through both
families, and the three routing sites REFUSE the mapped variant rather
than unwrapping it to legacy meaning. Refusal is the correct default at
an intermediate commit, not a placeholder --- until the frontend can
prove it negotiated v25 it IS a `<= v24` peer for gating purposes, and
painting first would ship a window in which the band is hit-tested with
no mapping identity at all.
**Five mutations, each biting its own rows:**
insert `PanelPointerMapped` before `TextInput`
-> the TextInput pin and the mapped pin. `PanelPointer`'s v23 pin
correctly SURVIVES: its discriminant did not move, which is the
"only the pin whose discriminant moved fails" behaviour G0a
specifies
insert `PresentMapped` before `Absent`
-> the Absent pin and the mapped-frame pin
swap `geometry_epoch` / `panel_epoch`
-> the exact-bytes assertion, while the round-trip stays green.
That is the blind spot G0b exists for, and it is why every
adjacent same-typed field carries a distinct value
bump the wire version without extending the supported set
-> both new tripwires and 1a's v6 ladder
move `ADVERTISED_PROTOCOL_VERSION` to 25
-> the baseline pin
**Version fallout, enumerated rather than discovered one gate at a
time.** Four acceptance-suite tripwires (`bottom_panel_stage2b_gpu`,
`discovery_stage2` x2, `vterm_stage3`, `statusline_segments`) each say
"a wire bump must be a conscious edit here" and each worked. Rather
than fix them one run at a time I grepped the tree for version
assertions and updated all four in one pass.
Review folded five further corrections, two of which fix reasoning of
mine that was wrong:
- I claimed reversing `frame` and `mapping_generation` "fails to
compile" because they are different types. **False for NAMED
variant fields** --- the initializer uses names, so reordering the
declarations compiles and shifts postcard's positional bytes
silently. The pin is the only thing catching that.
- Ladder loops now track `PROTOCOL_VERSION` while TRIPWIRES stay
literal. I had flattened both to `25`. A tripwire is literal so a
bump is a conscious edit; a ladder must move, or the next bump
silently stops testing the top rung. G14b is unaffected ---
`PANEL_MAPPING_MIN_VERSION` stays literal, because there the
arithmetic is exactly the hazard.
- `assert!(24 < MIN)` was a compile-time tautology holding for every
value above 24. Replaced with the literal equality plus
`assert_ne!` against `TEXT_INPUT_MIN_VERSION`: the mapped family
must not share v24's gate, or it is admitted on sessions that
negotiated only `TextInput`.
- Statusline support loop reaches `PROTOCOL_VERSION`; public protocol
history records v25.
**CI-red observations are in the LANE LEDGER, not the registry**, and
that is deliberate: `ci-red-signatures.md` here ends at U9 while the
unmerged replay branch already added a U10, so a row from this branch
would duplicate an id or invent one blind --- which this file's own
history records going wrong, two branches' entries merging "without a
conflict, producing duplicate ids across four sites". R7 twice and the
composition budget once, fragments verified, owed to the registry by
whichever branch merges second.
Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260815T103555Z. Four runs were needed; three were lost to those
two signatures, not to this diff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Close the producer, receiver, protocol-family, and gesture-lifecycle
cross-products in one pass. Separate route witnesses from replay effects,
freeze both old boundaries and new variant fields, and record the bounded
review method for the remaining chain.
Answers review of 15. Framing only. Three items reverse a rule 15
introduced, and one retracts a mutation that was not a defect.
**THE PRODUCER RULE CONTRADICTED PROACTIVE CANCELLATION.** The daemon
cancels BEFORE emitting the replacement frame, so the frontend needs
only to clear its local latch when that frame arrives, and send
nothing. Revision 15 asked it to emit a cancellation tail or retain the
latch: the tail is redundant --- the daemon would receive a release for
a gesture it has already settled, which is the duplicate release the
latch exists to prevent --- and RETAINING IS ACTIVELY HARMFUL, because
it manufactures a `Drag` under the NEW generation with no accepted
`Down`. That is the exact orphan the section exists to prevent,
produced by the rule meant to prevent it.
Ordering is what makes the simple rule safe: cancel, then emit. The
frame's arrival IS the cancellation signal; no second channel is
needed. Witnessed as `Down` -> key advances -> replacement frame ->
motion and physical `Up` produce no new drag and no duplicate release.
**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. And an ordinary accepted `Up` must clear the latch,
or a later invalidation finds a gesture it believes live and
synthesises a duplicate release for a button already up --- the replay
lane's D1/D2 orphan race, arriving from the daemon's side.
**G9b's MUTATION 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. Requiring it to fail would have forbidden a correct design.
Replaced with two real defects: compare only the cell and never key or
reset by generation (the first post-change motion is eaten), and reset
on every same-generation repaint (pixel-rate traffic returns).
**"PROJECTED CELL IDENTITY" CONTRADICTED THE STYLING CONTROL** in the
same section. 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 --- while the stable
controls rule style out. Terminal identity is now glyph and row
TOPOLOGY plus the view anchor, excluding face, style and cursor, with a
same-glyph/different-style control: the row that catches an
implementation reaching for `Cell` equality because it is right there.
P2s: zero-generation rows added in BOTH directions as independent legs
(a valid `PresentMapped` with generation zero must be rejected
atomically; a zero-generation `PanelPointerMapped` must be refused);
G7 split into outbound mapped-frame and inbound mapped-pointer legs,
since its old mutation only withheld the frame; G2's grid rows/columns
and fold-map-content/`fold_projection`-policy composites split; and
SS20 now names journey steps 5 and 8 while stating neither grade
changes --- an auditor scanning for grade movement alone would
otherwise conclude this slice touches no journey.
**AND R7 RECURRED, ON A DIFF THAT IS ENTIRELY DOCUMENTATION.** The
first `--protocol` run of this tree failed the `gpu` step on
`managed_retry_survives_transients_and_uses_the_successful_stream`,
with all three required fragments verified from the durable log
(`20260815T072601Z`). Recorded as R7's FIFTH occurrence.
It carries the strongest tree exclusion the row has had: occurrences 1
and 4 argued "unrelated lane", while this branch cannot be related at
all --- no Rust, no wire surface, no `pmacs-gpu` file. The line moved
to `attach.rs:1728` from `:1680`, which the row already treats as
occurrence-specific rather than a fragment. Isolated rerun green, and
the full gate green on the re-run (271/271 in the `gpu` step) --- which
per this file's rerun rule establishes INTERMITTENCE ONLY, though here
there is no tree change to exonerate.
What five occurrences across three flavors and five unrelated lanes now
support is that the failure is NOT LANE-CORRELATED. That is evidence
about where the cause is not. The retirement condition is unchanged.
Gates: all eleven green under `env -u TMPDIR` with `--protocol`,
log 20260815T073556Z.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai