Three residuals from review, all of the same family --- a claim that
looked covered and was not.
B1's disposal was implemented for panels only. Terminal(BufferId) is a
residual owner too, and exit_terminal_mode dropped four terminal-only
caches without dropping the bank, so leaving a terminal and re-entering
the SAME one inherited it: the key is the buffer id and it has not
changed. clear_terminals() now runs at that teardown, with a row that
enters, banks 0.6, leaves, re-enters the same buffer, and finds nothing
waiting. The type's own doc still said disposal was unimplemented and
owed; it now says where both halves live.
R5 and the panel-disposal row stopped after the successor's first 0.6
produced nothing. Unlike R4, neither completed the successor's own
0.6 + 0.6 --- so both passed just as well against an accumulator that
banks nothing at all, which is exactly the state they exist to rule
out. Both now carry the completion leg and assert the exact expected
effect. Demonstrated rather than asserted: with accumulation broken
outright (bank = delta instead of bank += delta) those two rows now
fail, and before this change they did not.
The Absent arm resets EIGHT fields before the new clear, not nine. Both
places that said nine are corrected.
Gates: fmt; clippy --workspace --all-targets -D warnings; pmacs-gpu 322;
--lib 2009; git diff --check.
Both owed lists are now empty. Nothing is landed-but-unwitnessed, and
nothing is owed outright; the next step is the full pre-PR gate.
The three that closed are recorded with what each actually required,
because two of them were not simply "write the row":
Step 3's witness needed the ORDER to be the argument --- 0.6 vertical
then 0.6 horizontal, both reaching nothing --- since a shared
accumulator would have the second complete the first.
B1's disposal half needed mechanism. BufferId keying distinguishes
panel A from panel B for free but cannot see a panel closed and
reopened on the same persistent buffer, and the Absent arm reset nine
pieces of panel state one line at a time with the wheel residual
missing from the list. That is the third time this lane has found the
same omission shape: a hand-maintained reset list with one field left
off it.
Identity keying answers panel A versus panel B on its own: the bank is
keyed by BufferId, so a different buffer starts from zero for free. It
cannot answer close-and-reopen of the SAME persistent buffer. The
successor carries the same id, so nothing about the key distinguishes
it from the panel the user was actually scrolling, and a notch begun in
a panel that no longer exists completes in its replacement.
The Absent arm already resets nine pieces of panel state one line at a
time --- frame, plan, drag, hover, pointer_held, last cell, gesture
cell, generation. The wheel residual was missing from that list, the
same shape of omission as the horizontal origin missing from the TUI's
four replacement resets.
The row closes the panel and reopens it at panel_epoch 2 on the same
buffer, which is precisely the case buffer_id cannot see, and asserts
the reopened panel's first sub-tick reaches nothing. Dropping
clear_panels() from the Absent arm fires it and nothing else.
This closes the last of B1's owed evidence.
Gates: fmt; clippy --workspace --all-targets -D warnings (which caught
a needless mut); pmacs-gpu 321; --lib 2009; git diff --check.
The two clears beside code_scroll_left have had a unit row asserting
they are distinct operations, with that row saying plainly it was not
R4/R5's witness. These are: a sub-tick banked over the predecessor, the
buffer replaced through the production BufferSnapshot receiver, and a
sub-tick over the successor reaching nothing.
Left standing, a bank completes in the successor a notch the user began
in its predecessor --- a jump with nothing on screen to explain it.
R4 covers the document's bank and, with it, chrome's: chrome's owner IS
the document's, so one reset serves both. It also completes the
successor's own 0.6 + 0.6 afterwards, so it cannot pass by having broken
accumulation outright rather than by having reset it.
R5 covers the minimap's, which B6 made independent. One clear omitted at
a time, as the framing requires: dropping clear_document fires R4 alone
and dropping clear_minimap fires R5 alone, so neither field hides behind
the other's reset.
Gates: fmt; clippy --workspace --all-targets -D warnings; pmacs-gpu 320;
git diff --check.
Revision 20 sharpened this witness because its earlier form was
satisfiable with the mechanism it protects entirely broken: a whole tick
passes straight through #243's vertical receiver even if B1's
accumulator discards every sub-tick it is given. The row now feeds
fractional input end to end --- a first 0.6 produces no gesture, a
second completes exactly one --- and does it per axis, because a single
accumulator fed by both axes passes any one-axis row.
The ordering is the point of the middle leg: 0.6 vertical, then 0.6
horizontal. Under one shared accumulator the horizontal delta would
finish the vertical one and fire a gesture there. Under per-(surface,
axis) banks it reaches nothing, which is what the row asserts before
completing either axis.
Observed on the wire, filtered to PanelPointer / PanelPointerMapped,
because a panel gesture's whole effect is the event it sends.
Two setup facts the harness forced, both asserted rather than assumed.
It negotiates the MAPPED family, which refuses a frame carrying no
mapping generation, so the row drops to the legacy wire like every other
panel row here. And it has already made its one surface declaration, so
the re-declaration uses the Metrics trigger; a second Surface
declaration is suppressed by design and returns None. The row asserts
the frame was accepted before touching it, and asserts the probe is a
panel CELL --- panel chrome banks nothing at all and would satisfy every
"no gesture" assertion for entirely the wrong reason.
Both documented mutations fire: rounding the notch instead of banking it
fires the sub-threshold legs, and collapsing the two axes into one
accumulator fires the cross-axis leg.
Gates: fmt; clippy --workspace --all-targets -D warnings; pmacs-gpu 318;
git diff --check.
Rewrite the live witness and mutation tables around the decision that
GPU horizontal authority is structural, while the TUI owns the literal
latch. Correct L2's driver, give L3 and L7 their frontend-specific
instances, scope L6 and L8 to the TUI state they can observe, and record
the GPU clamp paths and their real mutation dependencies.
Mark the framing amendment revision 21, discharge the ledger's stale
owed wording, and record that L7b now proves the incremental path with
a full-reshape sentinel rather than inferring it from unchanged line
count. Keep historical checkpoints distinct from the one current
verified code head.
Pin L7b to the incremental reshape branch with a sentinel that a full
reshape clears, rather than treating unchanged line count as proof that
the eligible fast path succeeded.
Make GPU L3 assert both the accepted CursorByte and the exact pixel
origin produced by following column five, so an arbitrary partial move
cannot satisfy the row.
Remove the deleted authority field's orphaned documentation from the
cursor-icon field and stop describing the GPU wrap path as clearing a
latch that no longer exists.
The framing claimed L2's height-only resize "invokes the real follow
path (resize -> ensure_caret_painted -> horizontal_follow)". It does
not, in the setup L2 itself mandates: the caret is deliberately outside
the manual viewport, and the painted-before policy skips the follow
precisely there. The paragraph now carries the correction and its
consequence --- that the GPU needs no authority flag, because there is
no follow to outrank --- along with the decision to delete rather than
complete it, and the four rows that replace the single one.
The ledger's stale claims go with it. "Landed but not yet witnessed"
still listed wrap and buffer-replacement clearing, which L8 and L8b-L8e
now cover and whose GPU half described a latch that no longer exists;
"owed outright" still listed L7b and the GPU read side. Both are
corrected rather than quietly dropped, since the entry's purpose is to
tell a recovering session what is actually left.
Also records the shared-target false red's FOURTH occurrence, at the
same four module uses with drifted line numbers --- which is the entry's
own point about which parts of that signature are stable.
Per the decision: the contract is behavioral, and the two frontends
need not share a representation. manual_left_authority, its
initializer and its four writes are gone, and scroll_by_columns no
longer returns a bool nobody read. GPU authority is structural --- the
follow reaches the origin only through ensure_caret_painted, which
Q#F6's painted-before policy skips exactly when the user has scrolled
the caret off screen, and when the caret is on screen it is inside the
viewport where follow_left is a no-op. There is no follow to outrank.
L7b, GPU, found a live gap before it could witness anything. Q#R1's
keystroke case re-shapes only the affected line through
try_reshape_line and skips the full reshape --- and skipped clause 3's
clamp with it. A one-line delete that shortens the widest line could
therefore leave the viewport past the end of the text, with no later
event to repair it. The clamp now runs on that branch too, and the row
drives apply_loro_text_delta_batches and asserts the line count did not
change, so it cannot pass by taking the full-reshape branch instead.
The combined row is split four ways, each with its own mutation: L2
(height-only preserves --- the policy, not a latch), L7a (widening
clamps to the exact bound), L7b (content shrink through the incremental
path), L3 (a moved CursorByte pulls the viewport back). L3 exists
because removing the flag would otherwise leave the "until the cursor
changes" boundary unwitnessed --- the one thing the flag's name claimed
to govern. Its first version re-announced the caret's current byte,
which the arm treats as inert by design; it now moves it.
L2's mutation necessarily bites L7a --- an unconditional follow snaps
the origin to the caret before that row's widen --- and the doc names
that rather than leaving a predicted bite unexplained.
Gates: fmt; clippy --workspace --all-targets -D warnings; pmacs-gpu
317; --lib 2009; git diff --check.
L7b and the GPU work, and one finding the next session should not have
to rediscover: the framing's L2 is offered as the GPU's manual-authority
witness and cannot be one. The origin survives a height-only resize with
manual_left_authority never read anywhere in that frontend, because
Q#F6's painted-before policy skips the follow whenever the caret is off
screen --- and when the caret is on screen it is inside the viewport,
where follow_left is a no-op. Both branches leave the latch unreachable.
The real gap was clause 3's clamp, and it is recorded with the number
that shows it: 960px past the maximum after a widen, most of the
viewport blank.
The entry marks L2's wording as owed to the FRAMING rather than the
code, and says explicitly that deleting the GPU's four inert writes
versus giving them a reader is a framing decision. Recording it as owed
rather than settling it in passing is the point --- an approved contract
does not get amended by whoever notices it is wrong.
The framing offers L2 --- wheel sideways, then a height-only resize ---
as the row that witnesses manual horizontal authority on the GPU. It
cannot. Measured before anything was added: the origin survives that
resize with `manual_left_authority` never read anywhere in the
frontend. What preserves it is Q#F6's painted-before policy. `resize`
runs `ensure_caret_painted` only when the caret was painted, and a
caret the user has scrolled off screen is not painted --- so the follow
that would snap the origin back never runs. Whenever the caret IS
painted it is inside the viewport, where `follow_left` returns the
origin it was handed. Either way the latch is unreachable.
What the GPU actually lacked was the other half of clause 3. Nothing
brought the origin DOWN when the maximum fell, for the same reason: the
follow that would is skipped in exactly that state. Measured: scroll to
the right bound at 640px, widen to 1600px, and the origin stayed 960px
past the new maximum --- most of the viewport blank with the text off
its left edge.
`clamp_code_scroll_left` at reshape's tail, beside B5's icon hook and
for the same reason: one point every geometry settle already passes
through. Gated on a non-zero origin, because it scans for the widest
line and most windows are never in this state.
L2 keeps both legs and says plainly that the first is required
behavior, not a witness of the latch. L5's GPU leg makes the stronger
statement available here: the wire stays silent, because on this
frontend moving point means telling the daemon.
One process note. The L5 mutation appeared not to fire; it had failed
to compile. A mutation that does not build produces exactly the output
of a test that passes, and only reading past the grep separated them.
Both were re-run after the mutant compiled: dropping the clamp fires
L2's widening leg, an off-by-one fires its exact bound, and a wire
event on the horizontal leg fires L5.
Still owed: the framing's L2 wording, which promises a witness this
frontend cannot provide.
Gates: fmt; clippy --workspace --all-targets -D warnings; pmacs-gpu
314; --lib 2009; --lib --features crdt 2202; git diff --check.
L7a moves the viewport; this moves the content. The maximum origin is
`widest − viewport`, so shortening the widest line lowers it with the
viewport untouched --- the half of clause 3's promise widening alone
cannot witness.
The shrink goes through apply_active_edit, the production edit path,
not the registry directly: an edit that left the window's TextView
stale would have the row measuring a document state the running editor
never holds.
The bound is asserted exactly, as L7a's now is --- the new
`widest − viewport`, not merely a smaller number. Both documented
mutations fire: re-clamp releasing authority instead of preserving it
bites L7a and L7b together, as the framing predicts, and an off-by-one
clamp bites both exact bounds.
Gates: fmt; clippy --workspace --all-targets -D warnings (which caught
an items-after-statements const); --lib 2009; --lib --features crdt
2202; git diff --check.
The implementation and census name four live-window buffer replacement
paths, but two test/helper comments still called the set three and the
ledger still said only two paths already reset cursor, selection and
view_top. Correct all three counts: four replacement paths, three of
which already carried the other TUI replacement resets.
The ledger repeated a2d5b26's "three replacement paths", which was a
count taken by recall. It is four, and the entry now says how the
correct one was obtained --- grep every write of a window's buffer_id ---
along with the one site deliberately excluded and why, so the next
census does not re-decide it.
This is the second count in this lane stated from memory and found
short by review; the first was reshape call sites that no geometry path
reached. The pattern is worth the sentence it costs: both times the
list was of things that had been thought of, and both times review
found the member nobody had.
U20's margin comparison named the wrong baseline. 0.879 and 0.592 are
overheads above 1.0, not excesses over the 1.10 budget --- those are
0.779 and 0.492, which make the ratio 1.58x rather than 1.48x. All
three numbers are now given with what each measures.
Also records that L4's caret probe was closed at its right edge, and
why is_some() alone could not see that end.
The replacement census was taken by recall and was short. `kill_buffer`
rebinds every window showing the doomed buffer to a fallback, resetting
cursor, selection and view_top a line at a time --- with the horizontal
origin missing from that list exactly as the other three had it.
Retaken by grepping every write of a window's buffer_id rather than by
listing the paths someone had thought of. Four production sites:
switch_active_buffer_for, install_buffer_in_window, kill_buffer's
fallback rebind, and the daemon's align_primary_document_window.
EditorCore::from_bytes also assigns one and is deliberately excluded ---
a fresh core has no prior origin to inherit --- and the helper's doc now
names that exclusion so the next census need not re-decide it. The
remaining seven buffer_id writes are test code.
L8e covers the new site and fires only on its own call's removal.
And L4's caret probe was open at one end. `pos_to_display(..).is_some()`
proves the caret is not LEFT of the viewport, which is the condition
deciding whether the vertical wheel carries point --- but a caret past
the RIGHT edge also returns Some, and there a normal follow moves the
origin. That is precisely the state in which the row's claim, that the
latch is its only discriminator, would be false. The probe now requires
`col < last_content_cols`. Verified by walking the caret 380 columns
right: the setup assertion fires, where `is_some()` alone let it pass.
Gates: fmt; clippy --workspace --all-targets -D warnings; --lib 2008;
--lib --features crdt 2201; pmacs-gpu 312; git diff --check.
Two corrections to U20 and the lane record for a2d5b26.
U20 said six green control runs establish that the observing diff is
not the cause. They do not. They establish non-reproduction in six
runs, which is all a rerun ever establishes in this registry --- a tree
that fails intermittently can carry a changed failure rate that six
runs are far too few to see. Treating non-reproduction as exoneration
is exactly the reasoning this file refuses when a rerun turns a red
green. What the controls actually do is remove the easy story and leave
the question open, and the row now says so.
The margin comparison was also arithmetic dressed as a phrase. "A third
again worse" than U6's 1.592 is not what 1.879 is: as a ratio it is
1.18x, as budget excess (0.879 over versus 0.592 over) it is 1.48x.
Both numbers are now given, with the note that they answer different
questions --- which is why neither gets compressed into an adjective.
The ledger records clause 5's replacement half and the two witness
repairs that came with it, both being assertions that looked strict and
were not: an origin that merely came down rather than landing on the
exact bound, and a rationale about the caret's position that the
fixture made false.
The latch commit implemented clause 5's wrap half and left its
replacement half undone. Three paths replace a window's buffer ---
switch_active_buffer_for, install_buffer_in_window, and the daemon's
align_primary_document_window --- and none cleared view_left or the
latch. Two of them already reset cursor, selection and view_top one
line at a time; the horizontal origin was simply missing from the list.
A successor inherited both, rendering sideways with nothing about that
buffer to explain it. The GPU carries this reset for exactly that
symptom.
One `Window::forget_manual_horizontal_origin` rather than three copies,
so a fourth path gets the rule by calling it, with each call site
removable on its own --- which is what lets each leg have its own row.
L8b, L8c and L8d, one per site; dropping any one call fires only its
own. L8d lives in daemon.rs because the function is private there, arms
the latch through a real wheel gesture rather than by writing fields,
and is deliberately NOT crdt-gated like its neighbour, so it runs in
the default --lib leg too.
L7a asserted only that the origin came DOWN, which any arbitrary
reduction satisfies. It now asserts `widest − viewport` exactly, with
the fixture's widest named as a constant so the row and the fixture
cannot drift apart. Mutation-checked with an off-by-one clamp, which
the old assertion could not see and the new one fires on.
And L4's rationale was false as written. It said the caret stays inside
the viewport after the vertical wheel; with short filler lines the
caret clamps to their end, LEFT of the origin, so the origin
discriminated too and the row's stated reason for using the latch
instead did not hold. The filler lines are now 120 columns wide and the
row asserts the caret is still inside afterwards, through
`pos_to_display` --- the same rule production uses to decide whether the
wheel can carry point at all.
One repair of my own making: the helper landed between `#[must_use]`
and `layout_ctx`, stealing the attribute and leaving that function
wearing the tail of my doc comment. Third time this file's neighbours
have been damaged by an insertion. The method now sits after
`layout_ctx`, whose attribute and body were read back intact.
Gates: fmt; clippy --workspace --all-targets -D warnings (which caught
the stolen attribute); --lib 2007; --lib --features crdt 2200;
pmacs-gpu 312; git diff --check.
Three records, all from the same session.
**U20.** U6's composition-overhead test redded ALONE, its paired
keystroke test passing in the same run. U6's own closing rule says that
is a different incident, so it is filed as one rather than as a sixth
U6 occurrence: U6's selector requires the pair, and its whole argument
is that two unrelated subsystems failing at once is less likely than
one loaded machine. One test alone does not carry that argument.
The observing diff touches the paint path, so it was a live suspect and
was tested instead of argued about --- three full-lib runs with it and
three with the two files restored to HEAD, all six green. The margin is
recorded per U11: 1.879 against a 1.10 budget, a third worse than U6's
worst. The row says plainly that the size of the margin does not
resolve whether this is load or regression, and that a contemporaneous
load reading is the missing evidence.
**The shared-target false red fired a third time**, giving the complete
set of four error sites that the second occurrence's captured tail had
cut to three. An earlier draft of that entry guessed the missing fourth
was widest_display_columns; the third occurrence shows the guess was
right, and the entry now says it was still right not to record it --- a
signature that is usually right is one nobody can match against.
**And the latch entry was wrong.** It listed the manual horizontal
authority latch as "landed but not yet witnessed". It was written in
four places and read in none on the GPU, and absent entirely on the
TUI. An unread bool preserves nothing. The entry now carries the
measurement that showed it --- origin 30, next paint 0 --- and the three
things the framing's L-table did not anticipate, since those are what a
recovering session would otherwise rediscover from scratch.
The ledger listed the latch as "landed but not yet witnessed". Writing
L1 showed that is not what was landed. `manual_left_authority` exists on
the GPU, is written in four places --- and is **read in none**. On the
TUI it did not exist at all. What had landed was the arming; the
preservation the latch exists for was nowhere.
L1 measured it before anything was changed: a sideways wheel moved the
origin to 30, and the very next paint put it back to 0. In the TUI a
horizontal scroll was undone by the following frame.
This commit implements clauses 2 through 5 for the TUI, per the
framing's five-clause contract (Q#S1-11, revision 20), and witnesses
each clause:
- L1 preservation across a real paint_frame;
- L3 release on a genuine cursor move;
- L4 a vertical wheel PRESERVES authority (clause 3);
- L5 the wheel moves neither point nor selection (clause 1);
- L6 a notch the clamp absorbs arms nothing (clause 2's "effective");
- L7a widening the viewport re-clamps and KEEPS authority (clause 3);
- L8 wrap clears the latch, not only the origin (clause 5).
Three things the framing's L-table did not anticipate, all found by
setup assertions rather than by reading:
L4's stated hazard cannot arise in the setup the same table mandates.
It says the vertical wheel carries point, so a naive release fires ---
true, but only when the caret is INSIDE the viewport. `scroll_window`
carries point through `pos_to_display`, which returns None for a
position left of the edge (Q#HS7(c')), so with the caret outside, as
every other row requires, the vertical wheel carries no point at all.
L4 therefore places the caret inside and uses the LATCH as its
discriminator, since the origin cannot discriminate there.
Clause 3's preservation across a vertical wheel needs its own
mechanism, not just an exemption: `scroll_window` refreshes
`manual_left_cursor` to the point it dragged along, so clause 4's
"genuine cursor change" test stays true. Keying release on the cursor
byte alone releases exactly where clause 3 forbids it.
And `paint_frame` runs the follow BEFORE resolving the frame's wrap mode
and content width, so a geometry or wrap change reaches the follow one
frame late. L7a and L8 paint twice and say why, rather than treating
the second paint as a fudge.
Seven mutations run, each biting its named rows:
follow ignores authority -> L1, L3, L4
never releases -> L3
arms on any wheel -> L6
re-clamp releases -> L4, L7a
wrap leaves the latch -> L8
wheel writes point -> L5
baseline never refreshed -> L4
The first bites L3 because removing the block removes the release with
it; a narrower mutation would bite L1 alone. L3 asserts the origin
DIFFERS rather than which way it moved, so the point-writing mutation
no longer breaks its fixture instead of its claim.
Still owed on this axis: L7b (content shrink), and the GPU's entire
read side with L2 and L5's GPU leg.
Gates: fmt; clippy --workspace --all-targets -D warnings; --lib 2004;
--lib --features crdt 2197; pmacs-gpu 312; git diff --check.
Name f441d3d as the latest verified code head, not the lane's current
head. The documentation commit carrying that old wording made it false
on arrival.
Keep the authoritative remote ref as the recovery source so later
documentation-only commits need no self-referential SHA update.
The ledger still pointed at 175cc7b as the code head and still listed
B6's routing row as owed, two commits after it landed. A recovery
checkpoint that lags is worse than none: it reads as current and sends
the next session to a tree missing the work it is about to redo.
Head is now f441d3d at 312 GPU rows. B6 moves out of "owed outright"
into a block of its own, because the part worth recovering is not that
it landed but what its fixture had to defeat: TWO independent conditions
each make a horizontal-inertness claim vacuous --- four-column lines pin
B7's saturated right bound to zero, and line wrap, on by default, makes
scroll_by_columns return early and pin the left edge whatever the
target. The default harness document has both. A row asserting "the
minimap did not scroll sideways" passes under either while testing
nothing, and the setup assertion caught it twice.
Also records the review that followed: the same row pinned neither
probe's target and discarded both Steps, so an unchanged
code_scroll_left was the whole of "inert". And it notes the asymmetry a
reader would otherwise get wrong --- the contrast leg cannot use the
transcript, because a horizontal document scroll is local and silent, so
both legs' transcripts are empty.
The B5 paragraph keeps 175cc7b as its own historical anchor, now said
plainly to be B5's checkpoint rather than the lane's, with the current
head named beside it.
Two defects in the horizontal row, both of the same kind: it asserted
the absence of one effect and called that inertness.
It never checked what its probes classified as. The other two B6 rows
pin their targets; this one did not, so a probe that drifted onto panel
chrome --- horizontally inert for an entirely different reason, and
banking nowhere at all --- would have satisfied every assertion without
touching B6. Both targets are now asserted exactly. Verified by moving
the minimap probe off the band: all three rows fail at their setup.
And it discarded both Steps, leaving an unchanged code_scroll_left as
the whole of "inert". A notch that scrolled vertically, or put an event
on the wire, would have passed. The minimap leg now asserts an empty
transcript beside the unchanged origin. Verified by rerouting the
minimap's horizontal leg to scroll_by_lines(1): the assertion fires,
reporting [Scroll { top: 1 }].
The contrast leg keeps the left edge as its discriminator, and now says
why: a horizontal document scroll is local and silent, so its transcript
is empty too. Only code_scroll_left separates the two surfaces, and a
reader who assumed otherwise would draw the wrong conclusion from the
row's shape.
Gates: fmt; clippy --workspace --all-targets -D warnings; pmacs-gpu 312;
git diff --check.
B6's mechanism landed with B1: WheelTarget::Minimap maps to its own
ResidualOwner, and the horizontal leg already skips it. What was owed is
the evidence, and apply_wheel had none --- the accumulator had unit rows,
but nothing drove a wheel through dispatch_window_event to the effect.
Three rows on EffectHarness, which holds a real App and a real daemon
socket, so the classifier, the accumulator and the local step are all
production code:
- one notch over the minimap scrolls the document viewport by one notch
and re-declares it, exactly as over text;
- a part-notch over the minimap does not complete one over the document
--- the surface-switch jump B1 forbids, and the minimap is its sharpest
case because both surfaces move the same viewport, so sharing a bank
would look harmless and produce a jump nothing explains. A third step
gives the document bank the rest of its notch, so the row cannot pass
by measuring nothing;
- a horizontal notch over the minimap is inert while the same notch over
text scrolls sideways.
All three mutations fire, disjointly: an inert arm for the minimap fires
the first, ResidualOwner::Document fires the second, dropping the
horizontal guard fires the third.
The third row is why EffectHarness now takes a document. Its first
version asserted inertness over the default fixture and the setup
assertion caught it twice: four-column lines pin the saturated right
bound to zero, and wrapping --- on by default --- pins the left edge to
zero regardless of target. Either would have reported inertness the row
never tested. It now uses wide lines and turns wrap off through
LineWrapFacts, the message production uses, asserting the wrap landed.
`new()` keeps the 200-line fixture and its M22 rationale verbatim.
Gates: fmt; clippy --workspace --all-targets -D warnings (which caught a
float_cmp in the first draft of the inertness assertion); pmacs-gpu 312;
--lib 1997; --lib --features crdt 2190; git diff --check.
The local false compile red remains a machine-local artifact-state
hazard with no CI occurrence. If the same fragments ever appear in CI,
that run is a new incident and must be recorded under its actual job
and flavor, not the local bare-cargo flavor.
Withdraw the rust-cache explanation: workspace crates are not cached by
default and GitHub cache visibility is scoped. Neither replace it with
the equally unmeasured claim that CI builds fresh.
3bd9ad1 justified keeping the shared-target false red out of
docs/ci-red-signatures.md by asserting the registry is for CI reds and
that a row would have to invent a job and flavor. Both halves are false.
The registry carries many `local (Linux)` rows with precise invocation
flavors --- one of them flavored "with sibling worktrees building
concurrently", which is adjacent to this very mechanism --- and this
incident's truthful flavor is plainly statable: local, bare
`cargo test -p pmacs-gpu`, globally shared target.
I asserted a property of a file I had not read. The conclusion happened
to survive; the reason did not, and a reason that cannot be checked is
the part that goes on to be reused.
The entry now keeps it here for the reason that actually holds --- a
machine-local artifact-state hazard with no CI occurrence, every one of
them from bypassing scripts/gate --- and records that a CI appearance
would belong in the registry with that flavor. It also declines the
easy immunity claim: CI restores artifacts through rust-cache from a
cache shared across branches, so the same mixing has a plausible CI
analogue that has simply never been observed. The superseded reasoning
is named in place rather than quietly replaced.
Records, in this lane's infrastructure notes rather than
docs/ci-red-signatures.md, a red that has now fired twice locally. The
registry is for CI reds; a row there would have to name a job and
flavor this has never had.
The signature is E0433, "cannot find `columns` in `pmacs_protocol`",
four errors from a bare `cargo test -p pmacs-gpu`. Both times the module
was entirely present --- columns.rs matching HEAD, `pub mod columns;` at
lib.rs:40 in worktree and HEAD alike. Nothing was missing to find.
CARGO_TARGET_DIR is exported globally here, one directory for every
checkout, and `main` has no columns module at all --- the sharing
refactor is unmerged 1b work. So a pmacs-protocol artifact built without
the module shares a target directory with the branch that needs it. The
entry states what was confirmed (the shared directory, the module's
absence on main, the red clearing) and says plainly that which build
deposited the stale artifact was not determined.
Three of the four error sites are recorded. The fourth scrolled past the
captured tail, so the entry says so instead of naming a likely
candidate.
The control is `scripts/gate` and its per-worktree CARGO_TARGET_DIR, not
the touch that cleared it --- with the warning that the touch papers
over a genuine deletion exactly as readily, and that the two reds look
alike. agent-handoff.md:2609 already carries the general bare-cargo
hazard; it did not carry this signature, count, or control.
The snapshot geometry row enumerated what dropping
`apply_panel_cursor_icon()` from `reshape`'s tail would fire: "this row,
the menu row, and the line-number row below." Two panel rows have since
joined them, so the list read as exhaustive while being short by two.
Executed the mutation at this tree: it fires the two snapshot rows, the
line-number row, and both panel rows. The doc block now says so, and
says the list is expected to grow --- one hook reached by more routes is
the design, not drift, and a fixed enumeration would go stale at every
new one.
Verified: 12 B5 rows green; fmt, workspace clippy --all-targets
-D warnings, and git diff --check clean.
Advance the GUI Stage 1b ledger to the immutable code head and correct
the central-hook claim. Counting reshape callers missed two panel-band
transitions: accepted frame changes and geometry epoch invalidation.
Record the shared inset-change seam, the content-only repaint exception,
the 309-row GPU pass, workspace clippy/fmt/diff verification, and all four
executed mutations.
The B5 cursor icon was re-derived in reshape, but accepted PanelFrame
messages changed the document boundary without reaching reshape. A
stationary pointer therefore kept Text over a new panel, or RowResize
after the panel disappeared.
Route panel inset changes through one helper that synchronizes dimensions
and reshapes. Keep content-only panel frames on the repaint path, so normal
panel output does not rebuild the document. Geometry epoch advancement uses
the same helper because it disowns the retained frame after resize/font
handling has already reshaped against the old presentation.
Add production-path witnesses for Present/Absent, an independent epoch
invalidation row, and a discriminator keeping content-only frames off the
reshape path. All four documented mutations fire.
Moves B5 from "owed outright" to landed at `a4627cd`, and records where
its icon re-derivation ended up: `reshape`'s tail, rather than the two
call sites that had been noticed.
The recovery-relevant part is the failure mode, not the fix. Twice the
patch went to a path someone had pointed at, and twice a second applier
in the same arm restored the icon anyway --- so the row passed with its
documented mutation applied. That is not a weak witness. It is a row
that measures nothing while reading, in the diff, exactly like a row
that measures something.
The previous commit applied the icon in the `BufferSnapshot` arm after
its reshape, and routed menu writes through a `set_menu` helper that
applied it too. Both were wrong in the same way: they patched the two
paths that had been noticed instead of the place the paths converge.
Two consequences, both found in review.
The snapshot's documented mutation did not fire. Replacing
`set_menu(None)` with `self.menu = None` still passed, because the
unconditional post-reshape apply in the same arm restored `Text`. The
two appliers masked each other, so the single-writer property was an
inspection claim wearing a witness's clothes, and the row's stated
mutation named something the row could not see.
And the snapshot is only one geometry transition. The line-number mode
moves `text_left` through its own path; minimap arrival, panel
appearance, window resize and font metrics move the text clip the same
way. A stationary pointer could go from text to gutter, minimap, panel
or outside with the icon still saying `Text`. Ten call sites reshape;
three applied the icon.
So the icon is now re-derived in `reshape`'s tail --- the one point
every geometry settle already passes through --- and the two ad-hoc
geometry appliers are gone, `set_menu` with them. Three production
appliers remain, one per cause: pointer motion, menu ownership with no
geometry, and geometry.
Each is separately witnessed; the matrix is disjoint. Dropping the
`reshape` tail fires the two snapshot rows and the new line-number row;
dropping the `MenuPrompt` applier fires the lifecycle row alone;
dropping the every-motion applier fires the motion row alone.
The new row drives `InstanceMessage::LineNumbers` --- the production
arm, not `apply_panel_cursor_icon` directly --- turning the gutter on
under a stationary pointer and asserting the pixel it swallows stops
being an I-beam. Its setup asserts the gutter actually moved past the
probe, so the row cannot pass by measuring nothing.
Gates at this tree: fmt clean; clippy --workspace --all-targets
-D warnings clean; pmacs-gpu 306 (+1); --lib 1997; --lib --features
crdt 2190; pmacs-protocol 32; git diff --check clean.
`MenuPrompt` was not the only writer. `BufferSnapshot` clears the menu
--- a popup anchored in the prior buffer would hijack input --- and left
the icon alone, so an open-menu arrow survived a buffer replacement over
document text until the pointer moved.
Menu state now has ONE writer, `set_menu`, which re-derives the icon.
A third site added later gets it for free instead of reintroducing the
same defect, and `self.menu = ` appears exactly once in the crate.
The snapshot also needs the icon re-derived AFTER the reshape, for a
different reason: it changes geometry. `text_left` is
`TEXT_LEFT + gutter_width_px` and the gutter is sized from the line
count, so a snapshot moves the text boundary under a stationary pointer
--- a pixel that was gutter becomes text.
BOTH MECHANISMS FIRE ON THE SNAPSHOT PATH, so at first each masked the
other: removing either alone left every row green, and only removing
both fired anything. That is the "one omission at a time" defect R4/R5
is written to avoid, reproduced here. Each now has its own witness:
set_menu's apply removed -> 2 rows (the MenuPrompt legs)
post-reshape apply removed -> 1 row (the new geometry leg)
The geometry row moves the line count between one and four digits so the
gutter widens and narrows under a stationary pointer, and asserts the
icon follows. Its setup steps assert the gutter actually moved, so a
fixture that stopped discriminating fails rather than passing quietly.
The menu case was wrong in a way the motion-path patch only hid.
MENU OWNERSHIP CHANGES WITH NO POINTER MOTION. `MenuPrompt` opens and
closes the menu, and re-deriving the icon only on motion meant opening
while an I-beam showed left it on screen over the menu, and closing left
the arrow over text --- in both cases until the pointer happened to
move. The icon is a function of the state, so it is now re-derived where
the state changes.
That makes the motion-path call REDUNDANT, and it is removed rather than
kept: motion inside an open menu changes no ownership, and a second
writer there is one no row could distinguish from the first. This is the
option review offered, and it is the better half --- the transition is
where the fact lives.
The new row drives `apply_attach_message` --- the production path ---
and asserts `last_cursor_icon`, the value actually written, across both
transitions with the pointer never moving. Dropping the `MenuPrompt`
call fires it. The decision-half row stays separate so a failure says
whether the DECISION or the APPLICATION broke; dropping the
`menu.is_some()` guard fires both.
And the blank-area row documented a mutation that was not the one run.
`hit_test_source_byte` takes `&mut self` and the helper is `&self`, so
the literal substitution does not compile; the executed mutation bounds
`x` by the glyphs' extent, which is the same semantics geometrically.
The row now records what was executed and why the literal one is
unavailable.
Three of B5's claims were asserted nowhere that could fail.
THE EVERY-MOTION FIX WAS UNWITNESSED. Both rows called
`desired_cursor_icon` directly, so reinstating the divider-change gate
left them green --- the gate is on the caller. A new row drives
`apply_cursor_moved` from text into the gutter with `hover_divider`
false throughout and asserts `last_cursor_icon` changes. Reinstating the
gate fires it.
THE GEOMETRIC-VERSUS-BYTE RULING WAS UNWITNESSED. The only positive
point sat over an actual glyph, so a byte hit-test passed. A row now
puts the pointer well past a short line's end, inside the text
rectangle, and requires `Text`. Bounding x by the glyphs' extent ---
byte-hit-test semantics expressed geometrically --- fires it.
THE MENU PATH LEAKED AN I-BEAM. `apply_cursor_moved` returns early while
a menu is open, so an I-beam showing when the menu opened stayed on
screen over the menu indefinitely. The menu now applies the icon on that
path and counts as chrome in `pointer_over_text_content`, with a row;
dropping the guard fires it.
`apply_panel_cursor_icon`'s doc still said it chooses between RowResize
and Default. It chooses among three, and says so, including why calling
it per-motion is cheap.
§2a's CORRECTION 3 said where this had to land: `apply_panel_cursor_icon`
already owns the cursor and writes `Default` in its else branch, so an
I-beam at a separate site would be CLOBBERED by it on the next motion.
B5 extends that owner rather than joining it --- `desired_cursor_icon`
decides RowResize, Text and Default together or not at all, with the
divider outranking the I-beam because a drag handle is never text.
`pointer_over_text_content` is geometric, not a byte hit-test: an I-beam
belongs over the text AREA including the blank past a short line's end,
and a byte test would flicker along a ragged right margin. It excludes
the gutter, the minimap, the panel band and everything outside the
document's text rect, each for its own reason.
The icon now applies on EVERY motion rather than only when divider hover
flips. B5's transitions --- crossing the gutter, crossing the text's
right edge --- do not touch `hover_divider`, so the old gate would have
left the icon stale for exactly the cases B5 is about. The write is
idempotent against `last_cursor_icon`, so per-motion calls cost a
comparison rather than a platform round-trip.
THE FIRST VERSION OF THE ROW COULD NOT SEE ITS OWN MUTATION. With line
numbers off, `gutter_width_px` is 0 and `text_left == TEXT_LEFT`, so
"extend the I-beam over the gutter" changed nothing and the row passed a
broken build --- 0 rows fired. The fixture now turns line numbers on and
ASSERTS a gutter exists before relying on one. Both mutations fire:
I-beam over the gutter -> the coverage row
I-beam outranks divider -> the coverage row
no-pointer guesses a spot -> the no-pointer row
The block said the platform decision "replaces a `cfg!` read". It does
not: production still reads `cfg!(target_os = "linux")` at
pmacs-gpu/src/main.rs:3450. What became injectable is the SELECTION
decision taken after that target fact --- `is_linux` comes from `cfg!`
in a non-test build and from an override otherwise, and
`paste_source_for(is_linux)` is the part a row can drive both ways.
And "L1-L8 still owed" hid implemented state that recovery needs. The
manual authority latch, its wrap/replacement clearing, and R4/R5's two
separate residual resets have all LANDED; what is owed is their
witnesses. The block now splits "landed but not yet witnessed" from
"owed outright" rather than letting one phrase imply neither exists.
It also names the authoritative ref and the exact recovery command,
which is what this file is for and what the block did not have.
The lane has been implementing without a ledger entry, which is the
recovery state this file exists to carry. It records the base, the
framing revision, what has landed, and what is still owed --- including
B1's disposal half, which the code documents as absent rather than
stubbing.
Three findings are recorded because each cost review time and none is
this lane's to fix:
- `scripts/gate`'s clippy step runs default features only, so crdt-gated
code is never linted locally;
- NOTHING runs `cargo doc` anywhere, so broken intra-doc links are
ungated repository-wide --- including one pre-existing, in a file this
lane never touched;
- no non-Linux CI leg runs `pmacs-gpu` tests, which is why B4's platform
decision is an injected parameter rather than a `cfg!` read.
Each names the lane that would own the fix rather than proposing it
here.
Deleting `middle_click_paste_source` left a broken intra-doc link on
`apply_middle_press` and a stale name in the end-to-end row's comment.
Both now name `paste_source_for`, which is what the code calls.
And `an_unused_button_produces_no_effect_of_any_kind`'s opening sentence
was duplicated on one line --- the tail of the same insertion that split
it in the first place. Repaired.
WHY NEITHER WAS CAUGHT, which is the part worth keeping: NOTHING RUNS
`cargo doc`. It is absent from `scripts/gate` and from every ci.yml job,
so broken intra-doc links are ungated across this repository. `git diff
--check` cannot see them because they are syntactically valid, and
clippy does not read doc links. Running it by hand here confirms my link
now resolves --- and surfaces one PRE-EXISTING unresolved link,
`MathNode` at pmacs-gpu/src/math_layout.rs:314, in a file this lane has
never touched.
I have not added a doc step to the gate: that is shared infrastructure
and its own lane, alongside the clippy default-features gap this lane
already recorded. The finding is carried to 1b's ledger block rather
than left in a commit message.
MY "IT RUNS ON THE NON-LINUX CI LEGS" NOTE WAS FALSE. `cargo test -p
pmacs-gpu` appears exactly once in ci.yml, in the Ubuntu-only
`gpu-render` job; the macOS matrix tests the workspace default member
only. So the `unwrap_or(PasteSource::Clipboard)` mutant was green in
every environment that actually executes --- and I wrote that note one
commit after writing about honesty, which is the part worth recording.
The platform is now a PARAMETER rather than a `cfg!` read inside the
decision: `paste_source_for(is_linux)`, with an injectable override on
`App` for tests. The off-Linux branch runs on this host, and a new row
asserts the gesture is completely inert there. The mutant fires it
locally, not hypothetically.
AND "WHOLE TRANSCRIPT" WAS STILL OVERSTATED. Both arms filtered for
`Paste`, so any other outbound event passed, and the release assertion
had the same hole. All three assertions are exact `Step` equality now:
one PRIMARY paste and no local effect on Linux, completely empty off
Linux, completely empty on release. The frontend id is read from the
transcript rather than assumed, so the row pins payload and shape
without pinning an id the handshake owns.
Three mutations, each firing locally:
unwrap_or(Clipboard) -> the off-Linux row
dispatch arm no-op -> the end-to-end row
source = Clipboard -> both
I have not touched ci.yml. Adding a macOS `pmacs-gpu` leg is a change to
shared infrastructure and belongs in its own lane; making the contract
testable where the tests already run was the fix available here.
One slip of mine, fixed in the same change: the first version of this
commit left `middle_click_paste_source` dead --- `apply_middle_press`
calls `paste_source_for` directly now --- and I committed with clippy
failing because I ran the gates after `git commit` rather than before.
The helper is gone, the seam row names both platforms explicitly, and
the gates ran first this time.
The inertness stopped at a seam. `middle_click_paste_source()
.unwrap_or(PasteSource::Clipboard)` at the call site restores the
rejected fallback and passes every row: the helper still returns `None`,
and Linux still receives PRIMARY. A contract asserted only in the
function that decides it is not asserted on the path that acts on it.
The end-to-end row drops its `cfg(target_os = "linux")` and asserts the
complete transcript on both platforms: one PRIMARY paste on Linux, and
off Linux NO paste of any selection and no local effect either.
One honest limit is recorded on the row rather than left implied. On a
Linux host that `unwrap_or` never engages --- the source is already
`Some(Primary)` --- so no row on this machine can fire that mutant, and
a green local run says nothing about it. The `else` branch is what
catches it, and it runs on the non-Linux CI legs. Forcing the source to
`None` everywhere fires two rows locally, which is the closest
demonstration available here.
Also repairs the neighbouring test's documentation, which my insertion
had split: `an_unused_button_produces_no_effect_of_any_kind` was left
with "row that calls it claimed-and-dropped" while its opening two lines
had been absorbed into the B4 row's comment. Both are contiguous blocks
again.
Two process notes, because both recurred:
- This is the THIRD insertion in this lane to damage an adjacent test's
docs or attributes. The cause is anchoring a splice on a `fn` or doc
line without checking what precedes it; from here I anchor above the
doc block and read the neighbour back after inserting.
- The previous commit's message claimed the `cfg` removal it did not
contain: an edit script died partway, wrote nothing, and I committed
on the strength of a later partial edit. Amended rather than left
standing, and the file is now verified per claim rather than per
script exit.
TWO MUTATIONS LEFT BOTH B4 ROWS GREEN. Changing the source to
`Clipboard`, or replacing the dispatch arm with a no-op, was invisible:
one row asserted `middle_click_paste_source` in isolation and the other
asserted `route_pointer` in isolation, and nothing asserted the effect
the gesture produces. Two seams tested separately are not a path tested
once.
A third row drives a middle press through `dispatch_window_event` and
asserts EXACTLY ONE outbound `Paste` carrying the PRIMARY payload, and
that the release sends none. PRIMARY and CLIPBOARD are stubbed with
DISTINGUISHABLE contents, which is the point --- identical stubs would
pass with the wrong selection read.
That needed a seam: `State::set_test_selection`, consulted by
`read_os_selection` before the OS clipboard. A test-only field in
production code is a cost, and it is the smallest one that makes B4's
actual contract --- WHICH selection --- assertable without a real
clipboard. Both mutations now fire: source-to-Clipboard fires two rows,
the no-op dispatch fires the end-to-end row.
AND THE OFF-LINUX FALLBACK WAS UNFRAMED BEHAVIOUR I INVENTED. B4 rules
"PRIMARY on Linux" and rules nothing else. The gesture was inert on
every other platform; my previous commit made it paste the CLIPBOARD
there, and the row adopted that choice permanently. `middle_click_paste_source`
now returns `Option`, `None` off Linux, and the gesture stays inert. A
fallback needs framing and re-approval, not a default chosen while
implementing.
`PointerRoute::UnusedButton`'s own doc named this row: "Stage 1b's B4
gives the middle button a meaning (PRIMARY-selection paste on Linux) and
lands here." B4 splits that variant, as §2a said it would.
A middle PRESS is now `PointerRoute::MiddlePress` and reads the PRIMARY
selection, shipping it as the same `Paste` wire operation Ctrl-V uses.
Its RELEASE stays unused, like the right button's --- the paste happens
once, on the press.
PRIMARY and CLIPBOARD are different selections with different contents:
the clipboard holds what was last explicitly copied, PRIMARY holds what
is currently selected. Reading the wrong one still produces a paste,
just not the one the platform convention promises, so the row asserts
the SOURCE rather than that a paste happened.
`middle_click_paste_source` is the seam that makes that assertable
without an OS clipboard; `read_os_selection` takes the source and uses
arboard's `GetExtLinux` for PRIMARY.
Two rows, three mutations, each firing:
source = Clipboard -> the source row
middle press unrouted -> the routing row
release also pastes -> the routing row
Three existing rows encoded the old behaviour --- that a middle press is
semantics-free. They are updated to keep testing what they SAY rather
than being weakened to accommodate B4: the routing row now covers
Back/Forward/Other plus the middle RELEASE, and the two effect/order
rows switch to `Back`, a button that still has no semantics. Widening
them to accept the new meaning would have left no row asserting that
semantics-free buttons stay inert.
The row claimed to target the inactive pane and did not. It hit-tested
a hardcoded column, took whichever pane that resolved to, and then
branched its assertions to match. If the layout ever put the active pane
under that column, an active-window-routing mutant would pass and the
focus assertion would be a tautology --- the row would look like
evidence while proving nothing.
It now SCANS for a cell the hit-test actually resolves to the inactive
pane, asserts `under_pointer == other` before dispatching, and asserts
directly with no adaptive branch:
the inactive pane moves by three
the active pane's origin is unchanged
the active window id is unchanged
The setup guard is itself falsifiable: pointing the scan at the active
pane makes the row fail on "setup: the pointer must be over the INACTIVE
pane" rather than passing quietly. That check matters more than the
mutation --- a setup assertion nothing can break is the same defect one
level up.
The routing mutant still fires the row, and only it.
Four evidence blockers. The implementation was right; the rows could not
tell right from several wrongs.
THE RIGHT BOUND WAS A RANGE, NOT THE CONTRACT. `origin + viewport >=
widest` plus `origin < widest` admits every origin from `widest -
viewport` through `widest - 1` --- almost the entire viewport blank, and
passing. It now asserts the exact value, `widest.saturating_sub(
viewport)`. The `widest - 1` mutant used to pass and now fires two rows.
THE "SATURATES AT ZERO" ROW ONLY TESTED THE LOWER CLAMP. Its document is
400 columns wide, so it never exercised a buffer NARROWER than the
viewport --- the case where `widest - viewport` underflows. The framed
narrow-buffer row is added: short content, ten right notches, origin
stays zero.
THE FIXTURE FORGED THE WRAP CACHE. `paint_once` painted under the real
default (wrap) and then wrote `last_wrap = Truncate` directly.
Production fills that cache from `ui.line-wrap` during paint, so a row
built on a forged value proves nothing about the path that fills it.
`set_line_wrap` now sets the buffer-local setting, `paint_truncated`
paints and ASSERTS the paint recorded truncate, and the wrap row paints
a genuinely wrapped state at origin zero and shows a right notch is
inert. Deleting the wheel guard still fires it, and only it.
ALL FOUR ROWS USED ONE WINDOW. A mutant routing horizontal scroll to the
ACTIVE window instead of `win_id` passed every one of them, and B2
requires the document surface under the pointer --- horizontal state is
per-window. A split-window row wheels over the pane the pointer names,
asserts only that pane's `view_left` moves, that the other is untouched,
and that focus does not follow. The mutant fires it, and only it.
`dispatch_mouse`'s documentation now covers the horizontal arms, their
bound, and that the pane under the pointer is the one that moves.
`MouseEventKind::ScrollLeft` and `ScrollRight` reached `dispatch_mouse`
and fell into its catch-all, so a horizontal wheel over a TUI document
did nothing at all. B7's row said as much: "events arrive and are
dropped".
They now call `scroll_window_columns` --- the SAME helper B2's panel leg
uses. One contract, one implementation, so the two document surfaces
cannot clamp differently, which is what B3 means by "each document
surface clamps at its own content bounds".
Four rows, each mutation-checked against its own mutation:
three columns per notch step of one -> 1 row
right bound keeps the last clamp at full width -> 1 row
display column visible
origin saturates at zero (covered by the above)
wrap pins the origin to 0 drop the wrap guard -> 1 row, only it
The right-bound row is the one that needed care. "The origin stopped
somewhere" is satisfiable by a bound that lets the origin pass every
glyph and blanks the viewport, so the row asserts the FINAL DISPLAY
COLUMN IS STILL REACHABLE --- origin + viewport >= widest --- and
separately that the origin has not passed the content.
Two fixture facts the rows need, both stated rather than assumed:
- they PAINT first. B7's bound is `widest - viewport`, and a window that
has never painted has `last_content_cols == 0`, so the bound
degenerates to `widest` and the row measures nothing. My first draft
had exactly that hole and passed for the wrong reason.
- they set TRUNCATE after the paint. Under wrap the bound does not exist
and clause 5 pins the origin to zero, so a bound row must be a
truncate row. The paint writes `last_wrap` from the view config, which
is why the order matters --- setting it first, as I first did, is
overwritten and the rows silently exercised the wrap branch instead.
A THIRD copy of the rule lived in the projection that decides where the
GPU actually renders a later tab: manual `stop - column % stop`
arithmetic, and a per-character advance calling `UnicodeWidthChar::width`
directly. So the previous commit's mutation broke the minimap while
leaving the rendering path untouched --- the shared bound could still
drift from the columns the GPU draws at.
Both now delegate to `pmacs_protocol::columns::advance_char`: the tab
width is DERIVED from the shared advance rather than recomputed, and the
per-character step is the shared one.
The projection's stream semantics stay local, because they are real and
distinct: the column runs ACROSS chunks, so adornment text shifts a
later tab, and a newline restarts it. That is why the wrapper still
exists rather than being replaced outright.
Evidence, not assertion: mutating the tab stop in
`pmacs_protocol::columns` now breaks BOTH
`tab_projection_uses_shared_stops_and_unicode_columns` and
`minimap_columns_match_code_tab_and_unicode_widths`. Six adornment rows,
including `caret_projection_accounts_for_inline_adornments`, still pass,
so the stream behaviour survived the delegation.
`TAB_STOP_COLUMNS` and `UnicodeWidthChar` are now unused imports in
pmacs-gpu and are dropped --- which is itself the check that no copy of
the rule remains in this crate.
The previous commit CLAIMED the widest-line rule was shared. It was not.
The daemon called `src/display_width.rs`; the GPU folded through its own
private `advance_display_col`, a second copy of the same tab-stop and
Unicode-width arithmetic. The two agreed for ordinary input, so nothing
failed --- which is precisely why the claim was worth checking and why
asserting structural protection that does not exist is the defect, not
the duplication itself.
`pmacs_protocol::columns` now owns the rule, for the same reason
`scroll::follow_left` lives there: the protocol crate is the one place
both frontends already depend on. `advance_char`, `line_columns` and
`widest_line_columns` live there with their own rows; `display_width`
and the GPU both delegate.
The sharing is now demonstrated rather than described. Mutating the tab
stop inside `pmacs_protocol::columns` breaks the GPU's
`minimap_columns_match_code_tab_and_unicode_widths` --- a row that used
to run entirely through the private copy and could not have noticed.
Also restores `r4_p1_a_chrome_press_neither_arms_nor_moves_point`'s
opening line, "P1 --- a press on the band's MODE LINE begins nothing",
which my insertion had left attached to the B2 test. The attribute came
back last round; the first paragraph did not.
`PKind::ScrollLeft | PKind::ScrollRight` were CLAIMED AND DROPPED in the
panel replay, with a comment assigning the axis to Stage 1b. That is the
"frontend emits, receiver discards" shape the panel-replay lane was
opened to fix, inherited for the horizontal axis. This closes it.
`scroll_window_columns` moves the side window's `view_left` by B7's
bound, stated exactly: `0 ..= widest - viewport`, saturating at zero, so
the final display column stays visible --- clamping at the widest line's
full width would let the origin pass every glyph and blank the viewport.
Wrap pins the origin to zero, matching `horizontal_follow`. It returns
whether the origin actually moved, which is lifetime clause 2's
"effective move".
The widest-line rule is SHARED. `display_width::widest_line_columns`
lives beside the module's other column helpers and both frontends use
it, for the same reason `scroll::follow_left` is shared: two frontends
that compute the right bound differently disagree about where the
document ends.
B2's row asserts the EFFECT --- `view_left` before and after --- not an
emission, and it carries the discriminating setup the bound requires: a
panel whose content fits has a maximum origin of zero, so the move is
absorbed by the clamp and a dropped event reads identical to correct
behaviour. The fixture gets a line wider than the viewport. Mutation:
restore the claimed-and-dropped arm, and the row fires.
Two mistakes of mine in this commit's history, both caught before it:
- I reverted a mutation with `git checkout -- src/editor.rs` on a file
holding UNCOMMITTED work, and destroyed the whole B2 implementation.
Re-applied, and the mutation check redone against a file snapshot ---
the discipline I had used earlier in the CRDT lane and dropped here.
- Inserting the new test above an existing one STOLE ITS `#[test]` and
its doc comment, so `r4_p1_a_chrome_press_neither_arms_nor_moves_point`
silently stopped being a test. Clippy's "never used" caught it. Both
are restored, and the suite count confirms 1994 tests rather than
1993.
Three implementation blockers and one evidence-labelling defect, all
from review.
THE WIRE TARGETS MULTIPLIED ONE NOTCH TWICE. `apply_wheel` banked in
LINES (notch x 3), then emitted one event per banked unit, and the
receiver applied its own SCROLL_LINES = 3 to each. So LineDelta(0, 1)
moved a panel or terminal NINE lines while the document moved three ---
and it broke the "exactly one viewport effect" witness this slice owes
before it was written. The accumulator now banks in NOTCHES, the unit
that survives the wire, and the three-line/column step is applied
exactly once at the point of effect. Pixel deltas divide by a notch's
pixel height rather than a line's.
THE MINIMAP SCROLLED THE DOCUMENT SIDEWAYS. §2a rules the minimap's
horizontal axis inert; the shared local arm was passing its banked x to
`scroll_by_columns`. It keeps its own vertical bank (B6) and no longer
moves the document horizontally.
B3's UPPER BOUND SAW ONLY THE VISIBLE SLICE. `widest_display_columns`
scanned `self.buffer.lines`, which `rebuild_code_slice` populates from
the visible window plus overscan, so every off-screen line was excluded:
horizontal scrolling clamped prematurely and the bound moved as the view
scrolled vertically. It now reads `current_text` --- the whole document
--- through a display-column rule shared with the minimap rather than a
third copy. Cost is O(document) on the wheel path, which is a real risk
against this project's wall-clock budgets and is recorded on the
function rather than pre-optimised: a cache needs an invalidation key,
and the wrong key is a worse defect than a measurable scan.
AND THE R-NAMES WERE WRONG. The rows I called R4 and R5 test
document/chrome sharing and minimap independence; the framing's R4 and
R5 are the two BUFFER-REPLACEMENT resets. The row I called R1 is basic
accumulation; the real R1 is cross-axis. Renamed, and R1's body now
asserts what R1 says --- a sub-tick horizontal followed by a sub-tick
vertical over the same surface reaches no tick on either axis.
The resets themselves are now implemented, on the buffer-replacement
path beside `code_scroll_left`, as two separate clears so that omitting
one is individually visible. Their witnesses --- an actual replacement
through the harness --- are still owed and are labelled as such.
The producer 1b owes. `apply_wheel` used to round to whole lines and
return on zero BEFORE consulting the pointer, so every sub-tick delta
bound for the panel or the terminal was discarded by a decision taken
upstream of routing. §2a CORRECTION 5 measured that ordering; this
inverts it.
The pipeline is now: classify the target, bank the fractional delta
against THAT target's accumulator, route only the whole ticks that fall
out. `WheelTarget` exists because `PointerSurface` cannot name what B1
needs --- it resolves panel geometry only and collapses the document,
the terminal, the minimap and the chrome into one `Elsewhere`, three of
which B1 and B6 must keep apart.
Residual owners follow §2a's enumeration exactly: per panel, per
terminal, the minimap's own, the document's --- and chrome shares the
document's deliberately, so a gesture that strays onto the gutter does
not lose its banked motion. Panel divider and background bank NOWHERE
and clear the panel banks: a residual they could share with a cell would
let motion over an inert strip complete a tick the moment the pointer
entered a live one, which is the surface-switch jump B1 exists to
forbid.
Nine rows, including R1-R5's identity discriminators and §2a's required
crossing witness. `trunc` rather than `round`, so a half-tick that was
never delivered is not spent.
Two things this commit does not do, both recorded rather than stubbed:
- IDENTITY'S SECOND HALF --- disposal --- is owed. A residual keyed to a
surface that goes away must go with it, and this frontend does not
currently track "that buffer is gone". A helper nothing calls would
read as a contract met, so the method is absent and the gap is
documented on the type.
- `scroll_by_columns` and the manual-authority latch land here as B3/B7
and Q#S1-11's foundation, but their witnesses (L1-L8) do not. They
come with the horizontal wire path.
One behaviour regression caught by an existing row and fixed: a wheel
before the first cursor motion has no pointer position, and the first
draft dropped it. It targets the document, as it did before 1b.