Revision 7 of `docs/destination-capture-framing.md`, closing the
correctness blocker review found in `0efc8c0` and the smaller
reachability hole beside it.
THE BLOCKER. The `"panel"` commit profile skipped preflight checks 2-4
on the claim that a panel result never touches a document window. That
claim is false: panel placement FALLS BACK to an ordinary document
window when the frontend is not `panel_capable` or its one side slot is
dedicated elsewhere -- `apply_placement` says so in its own comment --
and then installs the result there. So a `"panel"` commit could replace
a NEWER document with every stale-intent guard skipped: capture A, the
user opens B, the continuation lands, B is gone. That is the exact
failure `commit_to` exists to prevent, reached through the profile
meant to be the safe one.
WHY NOT A PREFLIGHT PREDICTION. Revision 6 proposed predicting the
fallback at preflight, arguing nothing could change in between because
the body cannot `await`. Refusing `await` prevents another COROUTINE
interleaving; it places no restriction on the body itself, which is
arbitrary Lua running synchronously and can invalidate the snapshot in
two statements -- take the panel, set it `dedicated`, then request a
side display. No preflight predicate closes that, however phrased: the
measurement is taken before the thing it measures is decided.
WHAT THIS DOES INSTEAD. `EditorCore::display_buffer` refuses between
`resolve_placement` and `apply_placement` when a side request resolved
to `PlacementKind::Ordinary` under an active `"panel"` contract whose
destination fails the document preconditions. That is the first moment
the fallback is a fact rather than a guess, and refusing before
`apply_placement` means a refused fallback mutates nothing. The
contract rides on the core, installed and restored by the same
`ScopedFrontendGuard` that scopes the frontend, so a profile can never
outlive the body that declared it; the field is crate-private, so Lua
cannot claim a profile for a placement it did not commit to.
The preflight predicate SURVIVES as an early refusal and not as the
guarantee. `panel_placement_can_fall_back` still gates the relaxation
in `commit_destination_refusal`, so the statically knowable case -- a
frontend that cannot render a panel at all, and will not acquire the
capability mid-body -- refuses before the body allocates a buffer,
registers a handle and paints. That is the same reason `commit_to`
preflights at all. Both layers are pinned, and neither pin subsumes the
other.
The four document checks now live once, in
`EditorCore::document_destination_refusal`: they are evaluated from two
sites, and two hand-written copies is how a backstop ends up weaker
than the thing it backs.
THREE DELIBERATE LIMITS, each a different decision rather than a
stricter version of this one. The document profile is untouched --
re-running its checks at placement would newly refuse dired's own
documented panel path, which is a preservation-suite stop signal. Only
a fallback is guarded, not every `Ordinary` placement -- a `"panel"`
body calling `display_file` is pinned as succeeding. And the refusal is
of the PLACEMENT, not of falling back: a `"panel"` commit with an
intact destination still degrades gracefully into the document window,
because turning graceful degradation into an error would regress every
consumer that works today on a frontend without panel capability.
THE SECOND HOLE. `commit_profile` did `name.to_str()?`, but Lua strings
are BYTE strings, so a `string.char(255)` profile hit mlua's generic
UTF-8 conversion error before `BAD_COMMIT_PROFILE` was ever
constructed -- the same reachability class as the `Option<String>`
defect revision 5 fixed, one layer down. The comparison is on bytes
now, and the invalid-UTF-8 row joins the number/table/boolean rows
asserting on message content.
FOUR DOC SITES repeated the false claim (`ViewDestination`'s own doc
twice, `capture_view_destination`, `ViewDestinationLua`) and are
corrected. Nothing else relied on it: dired, the only Lua `commit_to`
consumer, takes the two-argument document profile and already had all
four checks; `compile.lua`'s `already_in_panel` queries live state; and
the terminal adopter's rollback keys off `created_side`, already false
on a fallback.
Tests: 12 pins, up from 8. Three carry the enforcement split and none
subsumes another -- the pre-established fallback (both causes, the body
must not run), the inside-the-body transition (the body runs, the
result must not land), and the graceful fallback (a valid destination
still lands). Mutation-checked four ways; the pattern of which rows
survive each mutation is in `docs/active-work.md`.
`journey_acceptance` (47) and `dired_acceptance` (31) pass UNCHANGED.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Review found revision 6 fix unsound for the same reason its target was.
Revision 6 moved the panel/document decision to a preflight prediction,
arguing nothing could change before placement because commit_to refuses
await. That refusal stops CONCURRENT INTERLEAVING --- another coroutine
mutating state while this one is parked. It does nothing about the body
itself, which is arbitrary synchronous Lua and can obtain the panel,
set dedicated = true, and then request panel display. Preflight sees a
reusable panel and relaxes checks 2-4; the body causes the fallback;
the result replaces a stale document.
No preflight predicate closes this, however phrased --- the measurement
is taken before the thing it measures is decided. So enforcement moves
to the placement boundary, where resolving to Ordinary for a request
that asked for a side IS the fallback rather than a forecast of one.
The commit scope is already Rust-side app data, so the profile and the
destination can ride there.
The tempting non-fix is named so nobody reaches for it: widening the
predicate from "will it fall back" to "could it ever" is always true,
since the body can always dedicate the slot --- which collapses the two
profiles and buys nothing.
Section 7 gains the test that distinguishes the designs: the callback
dedicates the side slot MID-COMMIT. Both fallback tests revision 6
asked for establish their state before commit_to is entered, so a
preflight-snapshot design passes them. A design passing only those two
has not been shown to work.
The ledger claimed the lane implemented with eight pins covering
section 7. Those pins were written against revision 5 matrix, which
review disproved --- none exercises a fallback placement. A recovering
machine reading that entry would have prepared a PR from a lane with an
open correctness blocker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Q#DC-2 gave the panel profile only check 1, on the stated claim that a
panel result never touches a document window. That claim is false, and
the tree says so in its own comment: reaching Ordinary while a side was
REQUESTED means the request fell back --- not panel-capable, or the one
side slot is dedicated elsewhere --- and the result is then installed
into an ordinary document window.
So a "panel" commit on a non-panel-capable frontend could replace a
NEWER document while skipping every stale-intent guard, reintroducing
exactly the failure this API exists to prevent. Reproduced in review,
not theorised. That makes it a correctness defect rather than a
strictness preference, and it is my framing error: I wrote the matrix.
The relaxation is now conditional on the placement really being a
panel. Both fallback causes are readable from core state at preflight,
and nothing can change between preflight and placement because
commit_to runs its body synchronously in a scope that refuses await ---
so the prediction cannot go stale under the commit it guards.
What is deliberately NOT the fix: refusing a panel commit that would
fall back. Falling back is existing, intentional behaviour for a
frame without panel capability, and refusing would turn a graceful
degradation into an error. The panel profile relaxes checks; it does
not get to change where things land.
Also closes an invalid-UTF-8 hole in the profile diagnostic. Lua
strings are byte strings, so string.char(255) reaches to_str() and
produces mlua generic conversion error before the documented message
naming the accepted values is ever constructed. Same reachability class
as revision 5 Option<String> defect, one layer further down --- which
is worth noticing, because I fixed that one and did not look for the
next one.
And the header said "Pre-implementation. Awaiting approval" through
revisions 2 to 5 while the ledger recorded the lane approved and
implemented.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Section-local edit to the lane's own block; several lanes edit this file
concurrently.
Records the two commits, the green gate line (both preservation suites
unchanged), the mutation checks that show a matrix of deliberate
omissions is not passing vacuously, and the two places the framing did
not match the tree:
- the rename was 11 references across 5 files, not 8 across 4 —
`src/daemon.rs` also calls the capture;
- Q#DC-4's "frontend with no document window" is a DEFENSIVE branch.
Q#BP6 asserts a layout always keeps a non-side window, with a
`debug_assert!` in `non_side_target` that fires under `cargo test`, so
a registered frontend in a healthy editor always has a live document
window. The decision stands, but #227 should not expect to meet that
refusal.
Neither changed a decision, and both are recorded rather than quietly
absorbed: the framing says "counted, not estimated", and the next reader
will check.
`ViewDestination`'s own doc comment is corrected in the same commit,
because it repeated the framing's over-claim ("a frontend showing only a
side window") in the one place a reader would trust it, and
`capture_view_destination` now says how reachable its empty pair
actually is. Code, not only ledger, since the ledger is not what someone
reads when they wonder whether that branch can fire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
`docs/destination-capture-framing.md` §7, eight tests.
The one that decides the lane is `the_preflight_matrix_holds_in_both
_profiles`. Every cell Q#DC-2 marks "not applicable" for the panel
profile is asserted as NOT refusing, not merely left untested: a check
deliberately omitted and a check someone forgot look identical from the
outside, and the next reader restores the second one. The document
column re-asserts all four refusals in the same table, so a mutation
that collapses the two profiles fails one column or the other.
`a_bad_profile_is_refused_by_one_message_that_names_the_accepted_values`
is the guard on the argument's TYPE, not only on its behaviour. It
asserts the number, table and boolean cases produce the same message as
an unrecognized string --- which stops being true the moment the
argument is retyped to `Option<String>`, because mlua then rejects the
value during argument conversion and the pointed message is never
reached.
`a_captured_destination_survives_a_frontend_switch` runs under both
profiles. The panel profile drops three of the four preflight checks,
and a plausible way to implement that is to drop the frontend scope with
them --- which would leave a panel continuation resolving its target
from ambient state, the exact defect the lane removes.
`a_two_argument_commit_takes_the_document_profile` witnesses the default
through a check the panel profile omits (a stale buffer), because
asserting merely that a legacy call does not error would pass on one
silently downgraded to the panel profile.
ONE FINDING, RECORDED IN THE TEST RATHER THAN WORKED AROUND. Q#DC-4's
"a frontend with no document window" reads as a frontend showing only a
bottom panel, and that state is asserted impossible: Q#BP6 says a layout
always retains at least one non-side window, and `non_side_target`
carries a `debug_assert!` that fires under `cargo test` if one ever
does. So with Q#BP6 held, a registered frontend in a healthy editor
always has a live document window, and the absent document pair is a
DEFENSIVE branch rather than a routine one. The decision still stands
--- capture stays total, and an adopter with nowhere to land gets a
refusal naming that rather than permission to guess --- and the two
Q#DC-4 pins drive the reachable spelling of the same condition: a layout
whose document window has gone while the view remains. The helper says
so at its definition.
`tests/journey_acceptance.rs` (47) and `tests/dired_acceptance.rs` (31)
pass UNCHANGED, which is §7's stop signal and the reason the profile
default is the document one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Journey Stage 1a built `pmacs.window.commit_to` for the continuation
boundary --- "the listing settles a tick or more later, and by then the
ambient frontend, selected window, and active buffer may all name
something else" --- but nothing outside the `path.open-directory`
dispatch could mint a destination to hand it. Every other async Lua
continuation therefore resolved its target from ambient state a tick
after the request, which is PR #227's P1a finding: run `git.status` in
frontend A, let B become active, and A's panel opens in B.
This is the prerequisite lane #227 blocks on
(`docs/destination-capture-framing.md`, revision 5). No adopter here:
git's adoption is #227's work, since a prerequisite that converts its
own first consumer cannot be reviewed separately from it.
Three parts.
**`pmacs.window.capture_destination()`** returns the same
nonconstructible userdata for the current frontend. No arguments, and
that is load-bearing rather than minimal (Q#DC-1): a Lua-supplied
frontend id would reintroduce exactly the fabrication hole the userdata
design closes. Profile-blind for the same kind of reason (Q#DC-4) ---
capture freezes what is true now, and what a commit depends on is
declared later, at the commit.
**`DirectoryDestination` -> `ViewDestination`**, with the Lua userdata
and the capture renamed to match. The captured triple was already
generic; only its name and its capture site were not. The document pair
is now `Option`, set and cleared together, so a frontend with no live
document window still captures rather than returning nothing and
sending the caller back to the ambient state this exists to replace.
**`commit_to(dest, body [, profile])`** (Q#DC-2/Q#DC-5), a closed set of
two. The document profile keeps all four preflight checks. The panel
profile keeps only the first --- the requesting frontend still has a
layout --- because a panel result does not occupy the captured document
window, does not replace its buffer, and does not need it to exist, so
each of the other three would refuse for a reason unrelated to what the
continuation does. Omitting the profile means `"document"`, which is
what makes the preservation promise contractual rather than careful:
every existing two-argument caller keeps all four checks by definition
of the signature.
The profile argument is typed `mlua::Value`, NOT `Option<String>`, so
its error is REACHABLE: with the narrower type mlua rejects a number or
a table during argument conversion, before the closure body runs, and
the message naming the accepted values never appears. That is the same
trap the `dest` argument documents one position to its left. `nil` and
absence are the same answer; anything else is refused by one message
that names both accepted values.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 4 API spec contradicted itself at the binding boundary. It
required profile: Option<String> AND a pointed error naming "document"
and "panel" when a non-string arrives. mlua rejects a number or table
during argument conversion, before the closure body runs, so that
message was unreachable: a caller passing 42 would have got mlua
generic conversion error instead.
This is the identical trap the existing binding already documents for
dest --- typed Value rather than AnyUserData specifically so the
message stays REACHABLE and names the rule --- and revision 4 quoted
that comment as its reasoning while repeating the mistake one argument
to the right.
The profile is now mlua::Value, validated in the body. Nil and absence
BOTH mean document, spelled out because a Lua caller threading an
optional variable produces nil rather than absence and a third
behaviour there would stay invisible until someone hit it. A
non-string is refused by the same message that names the accepted
values.
The verification bullet is now the guard on the type choice rather than
on the behaviour: the non-string refusal is asserted ON ITS CONTENT, so
retyping the argument to Option<String> later stops the assertion
matching rather than silently degrading the error a user sees.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revisions 2 and 3 said "the profile is declared at commit_to" and never
said how. That is not a detail. The binding accepts exactly (dest,
body) today, so without a specified form #227 has no stable API to
adopt against, and the promise that existing callers keep their
semantics was a hope rather than a contract.
Q#DC-5: commit_to(dest, body [, profile]). An optional TRAILING string,
typed Option<String>, so there is no arity sniffing and no
table-or-function dispatch on argument 2 --- the existing binding chose
Value over AnyUserData specifically to keep its error message reachable
and naming the rule, and a polymorphic second argument would undo that.
Trailing reads badly after a long inline closure, but that is not the
call shape in use: dired defines a named local commit at dired.lua:670
and calls commit_to(opts.dest, commit) at :717. Verified, not assumed.
Against a named body the trailing profile reads fine.
The value set is CLOSED --- document and panel, exactly Q#DC-2 two
profiles. A third is a decision, not a spelling.
Omitted means document, and that is the load-bearing part: every
existing two-argument call keeps all four preflight checks by
definition of the signature, so journey_acceptance passing untouched
follows from the API shape rather than from care.
An unrecognized profile is an ERROR naming the accepted values, not a
silent fallback to document. A fallback would hand a caller stricter or
looser checks than it asked for, which is the failure the whole
parameterization exists to prevent. Its witness asserts the legacy
two-argument form through a check the panel profile OMITS --- a
stale-buffer refusal --- because asserting merely that it does not
error would pass on a call silently downgraded to panel, which is the
regression that would quietly void Journey Stage 1a guarantees.
Git mapping settled here rather than rediscovered during adoption:
*git-status* takes panel, *git-diff* takes document.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Q#DC-4 contradicted Q#DC-2, and on the primary panel API. Q#DC-2
concluded a panel profile depends only on a live frontend, so it can
commit with no document window at all; Q#DC-4 still voted to return nil
in exactly that case and told git to fall back to ambient behaviour.
Those cannot both hold, and the fallback advice was independently
wrong: falling back to ambient IS the P1a bug this lane exists to
remove.
Decided rather than voted on, since it is the primary API. The
destination document pair is optional; capture_destination() is
profile-blind and argument-free, because making capture profile-aware
would force a caller to know at capture time what it will do at commit
time, which is the opposite of why capture exists. The profile is
declared at commit_to, where Q#DC-2 parameterization already lives, and
a document-profile commit with no document pair is refused alongside
the other four preflight refusals. Capture never returns nil while a
frame exists.
Section 4 outline and Q#DC-1 were updated to match rather than left to
disagree --- Q#DC-1 no-arguments answer is now load-bearing rather than
incidental, because no arguments is what keeps capture profile-blind.
The ledger gate line said "new suite plus dired". --acceptance is
repeatable, so it now carries the executable command including
journey_acceptance and dired_acceptance, both named as preservation
suites and a stop signal. A volatile ledger that understates required
coverage is how a recovering machine runs a weaker gate than the lane
agreed to.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three review findings.
Q#DC-2 parameterization was incomplete. Revision 1 said only "skip the
stale-buffer check for a non-replacing continuation", but a panel
result does not depend on the captured document window at all: it does
not replace that window buffer (check 3), does not occupy it (check 4),
and does not need that specific window to exist (check 2). Retaining
any of the three can reject git.status for an unrelated document-window
change; dropping them without an explicit profile risks weakening
document replacement. The question now carries a four-row matrix with
two profiles, and check 1 --- the requesting frontend still has a
layout --- is the entire panel profile.
That has a consequence the framing now states rather than leaving to be
discovered: if the panel profile needs only the frontend, a frontend
with no document window can still host a panel, so Q#DC-4 return-nil
rule is right for the document profile and possibly wrong for the panel
one. Settled as part of answering Q#DC-2, not after it.
tests/journey_acceptance.rs joins dired as a named preservation suite
and stop signal. It carries 27 commit_to references across nine named
pins --- forged destination, scope-and-restore on normal return and on
raise, await refusal, delivery to the requesting frontend, the
declining-listener redirect guard, and two already named preservation_*
--- and Journey Stage 1a own framing treats it as a required gate. A
lane that generalizes its substrate does not get to relax that. The
stop signal now covers both suites: a suite edited to accommodate the
change under test has stopped being evidence.
The coherence-impact section was missing entirely. CLAUDE.md and
COHERENCE.md section 25 both require one for coherence-affecting work,
and this lane qualifies twice over --- new Lua API surface, and a
generalization of a Journey substrate. Section 16 is the section it
serves. Journey steps: none added, one protected. Islands, config
registry: none. Section 9: neutral, and stated precisely, because
knowing which frontend a result belongs to is NOT knowing who asked for
it --- that is the worker-identity arc and the two should not be
conflated just because both concern async continuations.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
PR #227 review found that git async completions surface in whichever
frame is active when git exits, and named the right mechanism:
commit_to exists for exactly this continuation boundary, built by
Journey Stage 1a Q#JR14 because the work settles a tick or more later,
by which time the ambient frontend, window and buffer may all name
something else.
The fix is not available to git, which is why this is a lane rather
than a line in #227. commit_to takes a DirectoryDestinationLua that is
nonconstructible from Lua by deliberate design, and the only site that
mints one is inside the path.open-directory listener dispatch, from a
pub(crate) capture. Any async Lua continuation that is not a directory
open has no way to say where its result belongs.
The captured data is already generic --- frontend, window, buffer, with
nothing directory-specific in it. Only the name and the capture site
are, and the rename is 8 references across 4 files, counted rather than
estimated.
The substantive question is Q#DC-2, and scouting is what surfaced it.
Git two continuations are different in kind. *git-status* goes to the
bottom panel, because listview.open resolves display with a "panel"
default. *git-diff* replaces a document window, deliberately, so the
status panel it was invoked from stays visible beside it. The
stale-intent check that commit_to preflight runs --- the window still
shows the captured buffer --- is right for the second and wrong for the
first: the panel never touches that window buffer, so refusing because
the user switched files there is a refusal with no relationship to what
the continuation does. One shape either over-refuses the panel case or
under-checks the document case, and the framing votes for a
parameterized preflight while holding that vote loosely.
No adopter in this lane. Git adoption is #227 work after this lands; a
prerequisite that also converts its first consumer makes the two
impossible to review separately.
Verification carries a stop signal rather than a target: if any
existing dired test needs editing, the generalization changed Journey
Stage 1a semantics and that is cause to stop, not to adjust a test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
`latex_within_boundary` answered a question about path COMPONENTS with
string arithmetic:
dir:sub(1, #boundary + 1) == boundary .. "/"
With a `/` boundary the needle is `"//"`, which no canonical path
begins with. Every ancestor was therefore judged out of bounds, the
marker walk never examined a single directory, and each chapter of a
thesis got its own root — two texlab processes for one document tree.
The lane's headline behaviour, silently off, with all fourteen shipped
tests still green because every one of them clamps the boundary to its
own tempdir.
Fixed by comparing segments rather than characters, so the root is a
boundary with zero segments — containing everything by construction
instead of by a special case, and tolerating a trailing separator for
free. The same root-is-special trap sat at two other points on the same
path and is closed with it:
* `latex_parent_of` returned nil for a top-level directory, making `/`
the one directory the walk could never examine — the identical bug
from the far end. It now yields `/`, matching `walk_for_marker`'s
`Path::ancestors` on the Rust side, and still terminates because `/`
has no component to strip.
* `latex_root_for` sliced `/paper.tex` to an EMPTY directory, which
canonicalizes to nothing and made the resolver DECLINE — and a
decline is the one path that falls through to `pmacs.project.detect`,
whose walk includes `.git`. A document at the filesystem root now
roots at `/`.
* `latex_marker_in`'s join is guarded for `dir == "/"`, which this
change makes reachable for the first time; the naive form produces
`//name`, the one spelling POSIX leaves implementation-defined.
Two new pins plus a strengthened one, 16 tests:
* `two_chapters_share_one_server_under_a_root_search_boundary` — the
defect end to end through ATTACH, not on the predicate, because the
symptom is two servers rather than a wrong string. Restoring the old
comparison fails exactly this test, with the two-server output.
* `latex_root_walk_stops_at_the_search_boundary` now asserts BOTH
directions. "Stops at the boundary" is also satisfied by a walk that
never runs — which is precisely what a `/` boundary produced — so the
hermeticity property (R8's shape: a stray `latexmkrc` above the
tempdir must stay invisible) is now paired with the walk still
climbing to, and examining, the boundary directory itself.
* `latex_root_for_a_document_at_the_filesystem_root_is_the_root`.
Also corrects `docs/active-work.md`: §3 no longer awaits a revision 3 —
`b5eaf27` IS revision 3 — and the lane entry now records that boundary
handling has been this resolver's interesting part twice, so a reader
weighing whether to trust it knows where to look first.
Gates: ALL GREEN via `./scripts/gate --acceptance lsp_latex_acceptance`.
`/tmp` on this machine is a near-full tmpfs and three grep tests fail
there with `QuotaExceeded`; the green run used
`TMPDIR=/home/jeans/build/pmacs-gate-tmp/lsp-latex`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The .texlabroot caveat is discharged, and discharging it falsified the
reasoning behind it.
.texlabroot is real, and texlab marker set is wider than this document
listed --- the bare texlabroot and latexmkrc spellings count too, and
the walk takes the innermost. The implementation ships texlab own set.
But the caveat framed marker 1 as conditional on texlab honouring the
FILE, which was the wrong question. Every arm of texlab ancestor walk
searches documents already loaded, and that workspace is built from the
folders the CLIENT supplies. Observed: with rootUri at chapters/, no
ancestor marker widened texlab view and its dependency graph never
reached the parent document; with rootUri at the marker directory the
parent resolved either way.
So texlab honours the root it is handed and never corrects a too-narrow
one. config.latex.root IS the project scope, which makes the resolver
the whole value of the lane for a multi-file thesis rather than a
nicety --- the opposite of how section 2 "Slice 1 is one config entry"
reads.
Second correction, and this one was a real trap. Revision 2 said .git
is deliberately excluded from the walk and stopped there. Omitting it
is not sufficient: project_root_for falls through to
pmacs.project.detect when a resolver returns nil, and that walk lists
.git among its markers at src/project.rs:184. A resolver that politely
declined on a markerless file would hand texlab the monorepo by the
back door, with the exclusion looking correct at every line you would
think to read. The resolver never declines for a file with a directory,
and the fixture asserts the shared detector really would have answered
the repo root so the pin cannot pass vacuously.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Section-local edit to the lane's own block in `docs/active-work.md`.
The heading moves from "BRANCHED, framing in review" to "IMPLEMENTED,
gates green, no PR yet", and the §3 `.texlabroot` caveat is written up
as discharged rather than merely resolved: what texlab actually does,
how it was observed, and the one place the framing now reads stale.
The substantive finding recorded here is not "the marker works". It is
that texlab's own root walk only sees markers belonging to documents
already in its workspace, and the workspace comes from the folders the
client supplies — so texlab honours the root pmacs hands it and never
corrects a too-narrow one. That makes `config.latex.root` the project
scope rather than a hint, which is worth carrying forward whether or
not anyone rereads the framing.
Also recorded: the shared-`CARGO_TARGET_DIR` trap, because a bare
`cargo test` in this worktree fails with compile errors from a sibling
lane's code and reads exactly like a broken branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Framing: `docs/lsp-language-coverage-framing.md` (revision 2, §3 and
§6). One `pmacs.lsp.config.latex` entry — command `texlab`, args none,
a function-valued `root`, and deliberately nothing else.
STEP ZERO: the §3 caveat, discharged by observation.
Revision 2 marked marker 1 (`.texlabroot`) UNVERIFIED and blocking:
only texlab's version and CLI had been checked, and the CLI exposes
just `run` / `inverse-search`. Driving a minimal LSP client against
`texlab run` by hand, plus reading texlab's own source at the exact
installed tag, settles it — and corrects the premise underneath it.
`.texlabroot` IS a real marker. `crates/distro/src/language.rs` at
v5.25.1 maps `.texlabroot`/`texlabroot` -> Root, `Tectonic.toml` ->
Tectonic, `.latexmkrc`/`latexmkrc` -> Latexmkrc, and
`ProjectRoot::walk_and_find` (`crates/base-db/src/deps/root.rs`) walks
ancestors testing all three, innermost wins. So the shipped marker set
is texlab's own rather than a plausible-looking guess, and marker 1
stays.
But texlab CANNOT apply that walk to rescue a root pmacs gets wrong.
Each arm of `walk_and_find` searches `workspace.iter()` — documents
already loaded — and the workspace is built from the folders the CLIENT
supplies. Live sessions confirm it: with `rootUri` at a `chapters/`
subdirectory, no marker above it (`.texlabroot` included) widened
texlab's view, and its dependency graph never reached the parent
document; with `rootUri` at the marker directory the parent resolved,
marker present or not. texlab honours the root it is handed and never
corrects a too-narrow one.
That inverts the significance of the resolver rather than weakening it:
whatever `config.latex.root` returns *is* the project scope. It is the
whole value of the lane, not a nicety.
Also observed, because the entry depends on it: bare `texlab` serves
LSP over stdio — `initialize` returns `TexLab 5.25.1` with no
subcommand — so `args = {}` is right and `run` is not needed.
WHY `.git` IS EXCLUDED, AND WHY THAT IS NOT AN OMISSION.
texlab wants the document root; a thesis inside a monorepo must not get
the monorepo. The subtlety is that leaving `.git` out of the marker
list does not achieve this on its own. `project_root_for` falls through
to `pmacs.project.detect` when a resolver returns nil, and that walk
does include `.git` — so a resolver that declined on a markerless file
would hand texlab the repository root by the back door. The resolver
therefore never declines for a file that has a directory: no marker
means the file's own directory, which is also framing marker 4. The
acceptance pins this end to end through attach, not just on the
resolver's return, and asserts in the same fixture that the shared
detector really would have answered the repository root.
NO FILETYPE MAPPINGS, per revision 2 §2 — verified, not inherited.
`src/syntax.rs` already declares `name: "latex"` with `extensions:
["tex", "latex", "sty", "cls"]`, and grammar-extension detection sits
ahead of the LSP filetype map in `detect_buffer_language`
(`syntax.lua`). A `.tex` buffer already resolves to `latex`. The suite
asserts both halves — the extensions resolve, and `pmacs.lsp.filetypes`
is empty for them — so a later "helpful" addition cannot be mistaken
for the thing that made attach work.
Q#LX1: no `settings`, no `init_options`. Build-on-save and
forward-search are both opinionated and forward-search needs a
configured viewer.
Fixtures bound detection with `pmacs.project.set_search_boundary` and
assert the boundary took — R8's hazard is exactly this fixture's shape,
and one test pins the walk stopping at the boundary directly. Attach
fixtures use `pmacs_fake_lsp`, and the missing-server fixture an
asserted-absent path: texlab is installed on this machine, so relying
on either its presence or its absence would behave differently here and
in CI.
Verification: fourteen tests, one per §6 bullet plus the boundary and
decline cases. Seven mutations each fail the suite — resolver declining
on no marker (6 tests), no marker walk (4), a redundant `filetypes.tex`
(1), boundary ignored (1), `io.open` truthiness so a directory counts
as a marker (1), marker set narrowed (4), command renamed with
opinionated settings added (1).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Recorded as "implementation authorized" rather than "approved", because
that is what happened: the user authorized dispatch after a summary of
revision 2 four corrections, not after returning findings on the
document the way they did for the other four lanes.
The distinction matters for one reason. Section 3 .texlabroot caveat is
unverified and blocking --- texlab 5.25.1 is installed and its version
and CLI were checked, but the CLI exposes only run and inverse-search,
so its LSP-level behaviour was never established. A framing marked
plainly "approved" invites a reader to treat that caveat as settled
prose. It is step zero of the work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 1 was untracked, on main, in a single checkout. By the handoff
own rule --- work is portable only after it is committed and pushed ---
it did not travel. Committing it here is the first fix.
Three factual corrections, all from checking rather than reading.
haskell-language-server IS installed on this machine, along with its
wrapper. Revision 1 said it was not, and that claim was the entire
basis of its Slice 1 / Slice 2 split. Only OCaml still lacks a server.
The argument for leaving Haskell out survives, but on use evidence ---
the .hs files are a rarely-edited Hakyll generator --- not on
dependency cost.
Slice 1 is smaller than framed. Revision 1 proposed filetype mappings
for .tex/.latex/.sty/.cls "so highlighting and LSP agree on what a
LaTeX file is". They already agree, by construction: the grammar
carries exactly those extensions at src/syntax.rs:1111,
grammar-extension detection sits AHEAD of the LSP filetype map in the
precedence chain per the merged grammar framing at :166-171, and
lsp.lua:267-270 calls the filetype map "mainly the LSP-only fallback".
The real missing piece is one config entry.
Q#LX3 deferral argument read a stale line. COHERENCE.md:124 and :867
both record multi-root LSP affinity as merged in #161; only :1669 still
says "first slice in flight", contradicting the same document twice.
The one item revision 1 said could justify deferring therefore
dissolves. The COHERENCE.md inconsistency is real and is left for
whoever next touches section 20 rather than smuggled in here.
Q#LX2, which revision 1 called the question most likely to make the
entry wrong in practice, is now answered rather than shrugged at. An
upward marker walk through config.latex.root, which already accepts a
resolver function. .git is deliberately excluded: a repository root is
the wrong answer for LaTeX, since texlab wants the document root, and
this is the one place where copying the other fourteen entries
instinct would be actively wrong.
The .texlabroot marker is marked UNVERIFIED and blocking. texlab 5.25.1
is installed and its version and CLI were checked directly, but the CLI
exposes only run and inverse-search, so its LSP-level behaviour was not
established. Confirm against a live session before implementing. Same
discipline the gate-protocol-build lane applies to its own
precondition: the thing the design rests on gets observed, not
reasoned about.
Also renumbered Q#HS1 to Q#LX4, because revision 1 Q#LX2 collided with
a live question ID in the merged grammar framing for the same language.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three corrections. The first two are the same error in two rows.
I wrote each control as if it DECIDES, when each is informative in only
one direction.
U4: extending the deadline establishes "emitted late" IF the clear
arrives. If it does not arrive, that establishes only "not observed by
the longer deadline" --- not "never emitted" --- because transport loss
produces the same absence. No deadline, however long, separates
non-emission from transport loss. That needs producer-side emission
evidence, did pmacs write the clear, cross-checked against the
collected stream. The row now states both branches and names what the
negative branch cannot conclude.
U5: one isolated run cannot decide whether the gate suite is
implicated. A matching isolated RED proves the gate suite is not
necessary for the failure. An isolated GREEN proves nothing beyond that
run, because the failure is intermittent and absence under one run is
not evidence of dependence. I had written it as though either outcome
settled the question.
This is worth naming as a class rather than two typos: a control whose
positive branch is conclusive and whose negative branch is not, written
up as though both were, is how an inconclusive result gets recorded as
an exclusion. Two rows in this file had it.
Third, minor: "three docs" was accurate at the occurrence tip and is
not now --- #229 has since added this registry file. Replaced with
"documentation" so the claim does not rot again with the next commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Four review findings, all mine.
Normalization. The byte count and the :LINE suffix are
occurrence-specific --- the count is the collected suffix length, which
varies per run, and the line moves with the file --- so neither can be
a required fragment. Both rows now carry stable fragments and an
explicit NOT-fragments field naming what must not be matched on.
The LuaJIT-pass argument was the rejected overreach again. I used a
passing sibling leg as a STRUCTURAL exclusion; a deterministic defect
can be Lua-flavour-specific, so it is corroboration only. The row now
says so in those words. The real grounds are stronger anyway and were
sitting there: the workflow never invokes scripts/gate, and
full_grid_resync_acceptance runs BEFORE the changed gate suite, which
closes even the leaked-state path.
Three contradictions inside U4, each removed.
"It never emitted the blank" asserts a mechanism the next field
simultaneously calls open. Now: no blank was OBSERVED after the mark
within the deadline.
The 25,362 bytes were not a capped window. suffix.len() is the ENTIRE
post-mark output; only the displayed head is truncated, to 400 bytes.
Verified in the test source. So my control --- capture the full stream
rather than the window --- was solving a gap that does not exist. The
gap is arrival TIME, and the control now instruments that instead.
The ~20s failure duration IS the fixed Duration::from_secs(20) timeout,
so the ratio against a fast pass is mechanically determined and is not
independent timing evidence. Also verified in the source.
U5 control relabelled. Running m5_8_acceptance alone decides whether
the gate suite is implicated --- cross-suite attribution --- and
nothing more. It cannot separate injected-before-raw-mode from
raw-mode-lost from a third cause, and another isolated pass cannot
either however often it is repeated. Mechanism discrimination needs
readiness and raw-mode state observed AT INJECTION, which is now a
second, separately labelled control.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
PR #229 first CI run went red on Test (macos-latest / lua54) with
a_pty_resize_blanks_the_host_before_repainting. The rerun turned that
selector green in 0.50s against 20.26s failing, and went red on a
DIFFERENT selector, ctrl_c_during_reconnect_sleep_yields_clean_exit.
Per this file matching rule that is a new incident, not the resize
signature occurring twice, so they are two rows.
U4 records the resize failure with its exact fragments. The diff is
excluded on two structural grounds that do not depend on a rerun: #229
touches no src/ at all and no test but gate_script_acceptance, and the
sibling luajit leg passed on the same commit. A deterministic platform
defect fails both flavours --- that is how #227 non-UTF-8 fixture
presented.
U5 records the Ctrl-C failure at deliberately WEAKER exclusion
strength, and says so in its own field. The changed gate suite ran
earlier in the same job and creates worktrees and directories. No
leaked child or persistent signal-state mutation was observed, but "the
diff touches no src/" is not the same argument here as for U4, because
cross-suite leaked state is a path reachability reasoning does not
close. Its control is to run m5_8_acceptance alone, without the gate
suite ahead of it, before attributing anything either way.
Neither row claims a mechanism. The exit status shows only that Ctrl-C
arrived as SIGINT rather than as the raw-mode key event the test
drives; whether injection preceded raw mode, raw mode was lost, or
something else happened is open, and the fragment does not separate
them.
Also flagged: worker-identity-stage1 independently defines its own U4
and U5. This lane merges first, so that branch must renumber on rebase
--- a conflict resolved textually without renumbering would leave two
different incidents sharing an id, which is the failure the matching
rule exists to prevent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 5's ledger edits were written while the witness was still the
open blocker, and landed alongside the commit that closed it --- so the
entry asserted both at once: an OPEN BLOCKER bullet saying this lane
"currently ships without the regression guard it was created to
provide", and, further down, that same gap closed at 677fd25. A
recovering machine reads the top of an entry first, so the stale half
is the half that gets acted on. Reconciled in place: the heading, the
framing bullet and the blocker bullet now say re-opened by review and
CLOSED at 677fd25, and point at the bullet that closed it.
The script header cited framing revision 4; it is revision 5.
Also recorded, from auditing whether any OTHER assertion in that suite
is detached from the thing it names: renaming every other plan step ---
fmt, clippy, lib, m4, gpu, sweep, diff-check, acceptance-<suite> ---
leaves all 20 tests green. For most that is only a log filename and a
FAILED: entry. `sweep` is not: the runner's end-of-run listing globs
*-sweep.log and *-sweep-crdt.log, so renaming that step silently
empties the "read these, do not re-run and grep" listing that is the
U2/U3 remedy, with the suite still green. Left open deliberately and
said so --- that listing exists only on the RUN path, and every test in
this file is no-gates by design, so there is no cheap witness for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Revision 4 said it would close the rename hole for the sweep as well as
the build, and then did not: its verification bullet required only a
singular real-plan pair in the context of build-crdt. The hole is
symmetric --- renaming sweep-crdt slips through exactly the same gap
--- so section 7 now pins BOTH emitter pairs explicitly, name and exact
command, asserted from the emitter where the name still exists.
Two older bullets also still claimed named steps appear in
--print-plan. They do not; that mode prints commands only, which is the
wording that let the attribution witness drift away from the step it
names in the first place. --print-plan is now described as the command
and order witness, and nothing more.
The ledger recorded revision 4 as approved and implemented, and
presented the synthetic self-test as the attribution witness. Both were
read as done. A recovering machine or a PR preparation from that entry
would have shipped this lane without the regression guard it exists to
provide.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The lane exists to guarantee two things --- that the crdt sweep is
preceded by the build producing its binary, and that a build failure is
attributed to `build-crdt` rather than to `sweep-crdt`. It shipped with
neither guaranteed, because NEITHER WITNESS COULD SEE A NAME.
--print-plan renders `emit_plan | cut -f2-`, so the ordering test
compared commands and never saw the names beside them.
--self-test hardcodes the string `build-crdt` inside its OWN synthetic
plan, so it proves things about the runner and nothing about the real
emitter.
Review demonstrated the consequence: renaming the real build step to
`sweep-crdt` left both tests passing --- a plan that would report a
build failure under the sweep's name, sitting green, which is exactly
the misattribution the separate step exists to prevent.
--print-plan-named prints emit_plan VERBATIM: the same `name<TAB>command`
text the runner reads back from PLAN_FILE. The new assertion compares
WHOLE LINES against it, so name and command are pinned together and a
rename of either step fails. The sweep's own pair is asserted too ---
asserting only the build's name leaves the identical hole open in the
other direction.
WHY A RENDERING AND NOT A SEAM. PLAN_FILE stays uninjectable: a test
that supplied the runner's plan would turn its `eval` into a general
command executor, the same class of defect this script's own review
caught in --acceptance and fixed with a parse-time refusal. Re-deriving
the plan test-side would be a second implementation of the thing under
test, which is the failure being repaired one level up. A distinct mode
rather than a --with-names modifier leaves --print-plan's contract ---
runnable lines --- exactly as it was, and defines no flag combination
that has no meaning.
--self-test STAYS. It witnesses the runner: failure naming, the FAILED:
list, log paths, non-zero exit, and continuation past a failure via the
sentinel. That is a different thing from attributing the real step, and
what it may no longer do is stand in for it.
A second test pins that the two renderings are one plan --- the stripped
one is the named one minus its names --- so a later edit giving either
mode its own text is caught rather than leaving an assertion on a name
the runner never uses. It also pins the `name<TAB>command` shape the
runner's `IFS=<tab> read` depends on.
Both new tests stay on the no-gates paths, so the suite stays cheap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Review mutated the REAL build step name to sweep-crdt and both existing
witnesses still passed. That is the gap: --print-plan strips names
before printing, so the order assertion sees only commands, and
--self-test hardcodes build-crdt inside its own synthetic plan. Neither
witness is connected to the step it claims to describe, so this lane
shipped without the regression guard it exists to provide.
Section 7 now requires asserting the real emitter (name, command) pair
together, so a rename cannot pass. The synthetic failure and
continuation test stays --- it tests the runner, which is a different
thing --- but it can no longer stand in for attribution of the actual
step.
Also fixed the header, which read "Pre-implementation. Awaiting
approval" through three revisions while the ledger recorded this lane
as approved and implemented. That is the same contradiction class this
project keeps correcting elsewhere, left standing in the document that
keeps correcting it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The acceptance criterion is witnessed: `scripts/gate --acceptance
gate_script_acceptance --protocol` on a target root that did not exist
beforehand goes green in all eleven steps, with `09 build-crdt ok`
producing `debug/pmacs-gpu` and `gpu_invocation_acceptance` at 15
passed / 0 failed where the same suite is 3 / 12 without the build
step. No manual build anywhere, which is the thing that was false.
An EARLIER attempt at that same cold run went red, and it is recorded
rather than dropped once a later run was green. Fifty failures across
m5_5/m5_6/m5_7/m5_8 --- all real-daemon suites --- with the signature
"daemon exited with exit status: 101 before socket appeared;
socket=/tmp/.tmpXXXX/pmacs.sock" and an EMPTY daemon stderr. Not the
pmacs-gpu signature, and no row in docs/ci-red-signatures.md matches
it.
Re-running the same test binary from the same target directory gave
36/36. Per that registry's own rule a green rerun establishes
INTERMITTENCE ONLY, never environmental cause, so this is left open
rather than blamed on the load it happened under.
What DOES rule out this lane's change is a construction argument, not
the rerun: the root crate declares `default = ["luajit"]`, so
`--no-default-features --features luajit,crdt` enables exactly the same
feature set as the sweep's `--features crdt`. `build-crdt` cannot hand
the sweep a differently-featured binary, so it has no mechanism by
which to break a daemon suite.
Also records that the new assertions were mutation tested --- wrong
features, wrong position, unconditional emission, an aborting runner,
and the build folded into `sweep-crdt` each fail the suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Found by mutation-testing the assertion rather than by reading it.
Emitting `build-crdt` AFTER `sweep-crdt` does fail the test --- so the
position criterion was never vacuous --- but it failed by panicking
inside the slice with
begin > end (427 > 282) when slicing `cargo fmt --check ...`
which names neither step and reads as a bug in the test. A gate test
whose failure has to be decoded is a gate test nobody trusts, and this
suite exists precisely to be trustworthy about the gate.
An explicit ordering assertion ahead of the slice says what is wrong:
the build must run before the sweep, because a sweep that builds its
own precondition afterwards has already failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The durable half of this lane is a boundary question, not the missing
line. `scripts/gate`'s header names handoff section 3 as the owner of
its reasoning, and this precondition lived in section 5's hazard
register --- a coherent cause for the omission rather than mere
oversight. A requirement the script was never told to encode is one it
will keep not encoding.
So section 3 gains it NORMATIVELY: the build joins the protocol-bump
block as a third line, with its own load-bearing bullet covering the
mechanism (pmacs-gpu has no tests/ directory, so cargo never uplifts
its bin), the measurement that makes it conditional, and why it was
latent until per-worktree target directories stopped hiding it.
Section 5 keeps the INCIDENT and its signature, which is history rather
than contract, and now says so: twelve
`gpu_invocation_acceptance::crdt::*` failures on a target directory
with no `debug/pmacs-gpu`, first seen on PR #228's first gate run.
Recast so that seeing the signature again reads as "the script was
bypassed", not "the requirement moved".
The script's header keeps citing section 3 and ONLY section 3. Citing
both would split one executable contract across two homes and weaken
the script's only clean boundary at the same time as Q#GR-4 declines to
build any automated check for prose drift. A boundary that is neither
enforced nor singular is not a boundary.
The ledger records Q#GR-1's observed answer rather than the question:
both sweeps run alone from the same cold disposable target with
`debug/pmacs-gpu` asserted absent beforehand --- default exit 0 with
the binary still absent afterwards, crdt exit 101 with exactly twelve
failures --- plus the silent-skip finding, which is the part nobody was
looking for: a54 reported `ok` in that cold crdt sweep because its only
non-spawning path is its skip branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
`scripts/gate --protocol` emitted `sweep-crdt` with no build step. The
crdt sweep spawns `pmacs-gpu` as a process, and nothing in a
`cargo test` run produces that binary --- `pmacs-gpu` has no `tests/`
directory, so cargo never uplifts its bin to `debug/pmacs-gpu`. On a
cold target directory the sweep therefore fails twelve
`gpu_invocation_acceptance::crdt::*` tests on "build pmacs-gpu before
this acceptance suite".
The hazard was never the red gate. Before per-worktree target
directories (#225) every worktree shared one, which nearly always
already held the binary, so the precondition was satisfied BY ACCIDENT
for the whole life of that arrangement --- a GREEN `--protocol` run
whose crdt sweep was decided by the state of the build directory rather
than by the diff.
Q#GR-1 SETTLED BY OBSERVATION, not by reading. On a disposable target
directory with `debug/pmacs-gpu` asserted ABSENT before each run
(recorded, not assumed), each sweep run alone from the same cold state:
default cargo test --workspace --no-fail-fast -- --skip basedpyright
exit 0, 114 test targets green, and `debug/pmacs-gpu` was
STILL ABSENT afterwards --- the default sweep never builds it
and never needs it.
crdt cargo test --workspace --features crdt --no-fail-fast
-- --skip basedpyright
exit 101, exactly twelve failures, all
`gpu_invocation_acceptance::crdt::*`, matching the signature
handoff section 5 recorded.
So the step is conditional on `--protocol`, which the framing voted for
on an inference this run confirms rather than assumes.
Also observed, and worse than the twelve: `a54_real_daemon_real_pty_and_
headless_gpu_render_one_panel_hosted_terminal` reported `ok` in that
same cold crdt sweep. Its only path that does not spawn `pmacs-gpu` is
its skip branch, so a test whose whole purpose is real wgpu rendering
passed having rendered nothing. The missing build does not only fail
twelve tests --- it silently voids coverage in tests that report green.
A NAMED STEP, NOT A FOLDED COMMAND. `cargo build ... && cargo test ...`
would report a BUILD failure under the name `sweep-crdt`, a wrong
attribution in the one place this script exists to be trustworthy
about.
`--self-test` is how that attribution is witnessed at all. The existing
suite drives only no-gates paths, so plan assertions can prove a step's
name and order and NOTHING about what the runner does when a step
fails. The mode runs a HARDCODED three-line synthetic plan through the
real runner: a passing step, a failing one named `build-crdt`, and a
passing SENTINEL after it. The sentinel is load-bearing --- with the
failure last, an aborting runner and a continuing one produce identical
output, so the witness would pass on a runner doing the opposite of the
stated policy.
The plan is a literal inside the script. Making `PLAN_FILE` injectable
would work and would turn the runner's `eval` into a general command
executor --- the same defect this script's own review caught in
`--acceptance` and fixed with a refusal at parse time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two findings, both about a claim that could not be falsified.
The --self-test plan put the failing step last. With the failure last,
a runner that ABORTS on failure and one that CONTINUES produce
identical output, so the witness for Q#GR-2 policy --- the suite keeps
going --- would have passed on a runner doing the exact opposite. The
plan is now three lines with a passing SENTINEL after build-crdt,
asserted to have written its own log. That is the only thing that
distinguishes the two behaviours, and it turns Q#GR-2 from a declared
policy into an observed one.
The plan test also now pins the EXACT command, not only the step name
and its position. A build-crdt running plain cargo build would leave
the gate exactly as unsound while looking repaired --- the crdt sweep
needs those specific features, which is the whole defect.
The ledger still recorded the superseded boundary decision: "section 3
gains it, section 5 keeps the incident, and the script cites both".
Revision 2 replaced that with section 3 as the sole normative home and
the script citing section 3 alone. active-work.md is the volatile
cross-machine record, so a recovering machine reading the stale entry
would have rebuilt revision 1 wrong boundary. Now updated, and it says
which decision it supersedes rather than silently replacing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Three review findings.
The normative build requirement goes entirely into handoff section 3.
Revision 1 proposed section 3 gaining it while the script header cited
both sections, which splits one executable contract across two homes
and weakens the single clean boundary the script has --- at the same
time as Q#GR-4 declines to build any automated check for prose drift. A
boundary that is neither enforced nor singular is not a boundary.
Section 5 keeps the incident and its signature, which is history rather
than contract.
Q#GR-1 observation procedure was unsafe and insufficient. "Delete
pmacs-gpu from a target directory" mutates a live worktree build
directory, and removing one binary does not establish that the other
artifacts and feature permutations are cold --- a stale dependency
graph can satisfy the run for reasons the experiment never sees. Now: a
disposable target, the binary asserted ABSENT before each run as a
recorded precondition, and the two sweeps run separately so neither can
be explained by the other having built the binary first. That last
point is the same accident that hid this defect for the whole life of
the shared target dir.
The attribution criterion had no feasible witness. gate_script_acceptance
deliberately runs no gates, so plan assertions prove name and order and
nothing about runtime behaviour. The obvious seam is a trap: making
PLAN_FILE injectable would turn the script into a general command
executor through its runner eval --- the same class of defect this
script own review already caught in --acceptance and fixed with a
parse-time refusal. Reintroducing it one lane later, in the tool whose
purpose is to be trustworthy, is not a trade worth making.
Q#GR-5 proposes --self-test over a HARDCODED two-line synthetic plan,
true and false, with the failing one named build-crdt. No injection,
no real gate, and it tests the thing actually under test: whether the
runner names the right gate when a command fails. Whether cargo build
really fails is cargo business.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
--protocol promises the CRDT workspace sweep. That sweep documented
precondition is cargo build --workspace --no-default-features
--features luajit,crdt (handoff section 5:532-535), and the plan
emitter at scripts/gate:187-204 has no build step at all --- read from
the source, not inferred from the failure.
The interesting part is why it stayed invisible. Before #225 every
worktree on this machine resolved to one shared CARGO_TARGET_DIR, which
almost always already contained a pmacs-gpu binary, so the precondition
was satisfied by accident on essentially every run. Per-worktree target
dirs start empty. So this is not a bug #225 introduced; it is a
pre-existing gap in the documented procedure that #225 stopped hiding.
That also decides the urgency. A red gate is fine --- it stops you. The
hazard is the reverse: a GREEN --protocol run whose crdt sweep was
decided by what happened to be in the build directory rather than by
the diff. A gate reporting coverage it does not have is exactly what
#225 exists to prevent, so the tool shipping with this gap teaches the
opposite of what it is for.
Observed on PR #228 first gate run: twelve
gpu_invocation_acceptance::crdt::* failures, all "build pmacs-gpu
before this acceptance suite", with debug/pmacs-gpu absent from the
fresh target dir.
The durable half is a boundary question rather than a missing line. The
script header names handoff section 3 as the owner of its reasoning,
and this precondition lives in section 5 --- a coherent cause for the
omission, not oversight. Q#GR-3 proposes section 3 gains it, section 5
keeps the incident and its signature, and the script stops naming
section 3 as its only source.
Q#GR-1 is marked as the one thing this lane will not accept on
reasoning: whether the default sweep also needs the binary must be
established by deleting it and running both sweeps. The whole defect is
a precondition nobody checked, and establishing its replacement by
reading would repeat the error at one remove. The mechanism section
states its own inference (the failing tests are namespaced ::crdt:: and
so are probably feature-gated) and marks it unverified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* build: scripts/gate — a target dir per worktree, and one gate suite
Parallel worktrees do not work on this machine, and the reason is one
exported variable: every checkout builds into one CARGO_TARGET_DIR, and
cargo takes an EXCLUSIVE LOCK on it. Two lanes building at once do not
run in parallel — the second blocks — and they invalidate each other's
artifacts, so alternating between them recompiles from scratch. Parallel
development under that arrangement is slower than serial.
MEASURED, BECAUSE THE FIRST PLAN WAS WRONG. The shared directory is
285G, which drove a proposal to add sccache so per-worktree directories
would not lose artifact sharing. That number is years of accumulation
across TWO projects (pmacs and levcs share it). Measured directly: a
cold `cargo test --workspace --no-run` is 80s and 19G. And sccache
across two target directories hits 50% on C/C++ and **0.00% on Rust** —
rlibs embed their target-dir path, so dependency artifacts are not
bit-identical between directories and `--extern` hashes cascade into
misses. There is no sharing worth buying back. sccache stays configured
and earns its keep on C/C++; it is not what makes parallel lanes work.
The script also owns the FIXED gates, because a procedure living only in
prose gets executed differently each time — twice in the session that
motivated this:
- a sweep run with `--tests` instead of `--workspace`, silently
dropping pmacs_protocol and pmacs_gpu, including protocol tests that
same lane had just written;
- a sweep piped through `grep` before anyone read it, so an
intermittent red could not be matched against ci-red-signatures —
a row needs its fragments. That is registry note U2, and then U3
when it happened AGAIN.
Hence durable per-gate logs with the sweep paths printed. The remedy is
real: this lane's own run diagnosed its failures from the log without
re-running anything.
WHAT THE SCRIPT IS NOT AUTHORITATIVE FOR. Handoff §3 keeps policy and
keeps CHOOSING the touched acceptance suites, which arrive only via
`--acceptance`. No script can infer those from a working tree, and one
that guessed would report coverage it does not have.
THREE HAZARDS SPECIFIED RATHER THAN LEFT TO CHANCE:
- `cmd | tee log` reports TEE's status, so a failing gate would exit 0
and the suite would read green. `pipefail` is not POSIX.
- `cmd > log; rc=$?` never reaches the assignment under `set -eu`
(which scripts/bite already uses) — the shell exits at the failing
command, so nothing prints which gate failed or where its log is,
destroying the point of capturing it. The runner is therefore an
`if` condition, the only `set -e` exemption.
- CARGO_TARGET_DIR (env) OVERRIDES build.target-dir in config.toml, so
a per-worktree config file silently does nothing. Only a
per-invocation value beats it.
Pruning is dry-run by default, `--force` to delete, and refuses any
directory without a `.pmacs-gate-target` marker. "Live" means a git
worktree record carrying NO `prunable` line — git keeps listing a
worktree whose directory was deleted without `git worktree remove`, and
treating listed as live would make exactly the reclaimable directories
permanently ineligible.
ONE HONEST FINDING FROM MUTATION TESTING. Three mutations came back
vacuous, and all three are redundant defences rather than test holes:
git already returns resolved physical paths from both
`rev-parse --show-toplevel` and `worktree list --porcelain`, so canon()
is belt-and-braces; and the prune path guards the marker twice. Recorded
in the script and the tests so a later reader does not mistake a
"vacuous" result for a gap — or delete a defence because a test did not
notice.
VERIFICATION. 11 acceptance tests over the no-gates paths (running the
script for real inside the suite would recurse), each pointed at a
tempdir via PMACS_GATE_TARGET_ROOT so the real managed root is
unreachable — a prune bug is unrecoverable. Mutation-tested: `--tests`
in the sweep, an unconditional CRDT sweep, and pruning on a dry run all
fail their intended test.
Observed in a real run, which is how the framing said to confirm the
parts a test cannot: the failed-gate names and log paths print, the
ambient directory is created and reaped by the exit trap, and every log
appears. The run exits non-zero because of R8 — the pre-existing,
merge-base-confirmed listview failure — which means `scripts/gate`
cannot go green on this machine until R8 is diagnosed. That is a
property of the tree, not of this change.
Framing: docs/gate-script-framing.md (revision 4, approved).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* fix(gate): two ways the script could do harm, and four smaller defects
Review round 1 on #225. Neither blocking finding was a design gap ---
both were the implementation failing to honour its own framing, which
is the case a framing document cannot prevent by itself.
PRUNE COULD DELETE EVERY MANAGED DIRECTORY. §2.6 requires the live
worktree set to be ESTABLISHED. The code piped `git worktree list`
straight into awk and the caller masked the result with `|| true`, so
running from outside any repository produced an EMPTY live set --- and
an empty live set means "every managed directory is an orphan", so
`--prune --force` would have deleted all of them, live lanes' artifacts
included. The failure mode was silent and total.
Two refusals now, and they are deliberately redundant: not inside a
worktree, and the enumeration itself failing. `live_worktrees` captures
git's output and returns non-zero rather than emitting nothing, so
"I cannot tell what is live" is unrepresentable as "nothing is live".
An empty porcelain listing counts as failure too --- a repository always
has at least its own worktree.
--ACCEPTANCE WAS SHELL-INJECTABLE. The name is interpolated into a
command the runner evaluates, and nothing validated it, so
`--acceptance 'x; rm -rf ~'` would have run. Now an allowlist of what a
cargo test target can actually be named --- letters, digits, underscore,
hyphen --- refused at parse time, before any gate. Rejection rather than
escaping: there is no legitimate suite name that needs quoting.
FOUR SMALLER ONES:
- Log directories carried a whole-second timestamp, so two runs in the
same worktree within one second shared one and could overwrite each
other's evidence --- reintroducing U2/U3 through a naming choice.
The PID is now part of the name.
- The ownership marker is DOCUMENTED as one line, so it is enforced as
one line instead of read head-first. Acting on the first line of a
file we did not understand is how a corrupted marker authorises a
deletion.
- The `prunable` test returned green when `git worktree add` failed,
so the only coverage of that rule could silently never run. It now
fails loudly.
- Its cleanup ran after the assertions, so a panicking assertion would
have left the real repository carrying a stale worktree record. Now
a `Drop` guard.
MUTATION TESTING, HONESTLY REPORTED. The injection and marker fixes bite
individually. The two prune guards do NOT --- each alone satisfies the
outside-repo test, so mutating one at a time reads as vacuous. Removing
BOTH fails the test, which is what establishes that the test detects the
unsafe state rather than being blind to it. Recorded in the test so a
later reader does not delete one guard on the grounds that nothing
noticed.
ALSO: handoff §3's ambient-root caveat still said "until the
ambient-root isolation lane lands". #206 merged; the five variables are
now belt-and-braces for external and integration paths, and `scripts/gate`
sets them regardless.
R8 PROMOTED. `docs/ci-red-signatures.md` gains the reason it stops being
a catalogued curiosity: with the gate suite reduced to one command, R8
makes that command exit non-zero on a clean tree EVERY TIME, and a gate
that is always red is a gate nobody reads. `docs/active-work.md` gains a
lane. It is still not a regression from #223 or #225 --- the merge-base
control says so --- and the lane's first job is diagnosis, because a
change that made the assertion pass without explaining the prefix strip
would convert a visible failure into an invisible one.
15 acceptance tests. Observed run re-confirmed: failed gates named with
log paths, ambient directory created and reaped, distinct log directory,
exit 1 from R8 alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: the #225 lane, and R8 diagnosed to a stray /tmp/.git
TWO LEDGER GAPS, both found by review.
and — the part that matters — an explicit GATE STATUS: NOT GREEN
section. `scripts/gate` exits 1 on this branch and on a clean `main`
because R8 fails m4_acceptance and therefore the sweep. That is a merge
blocker under the standing rule, and #225 is the worst possible lane to
grant a silent exception to: it is the lane that makes the gate suite
authoritative, and a tool shipping with its own gate red teaches the
opposite of what it exists to teach.
The lane also records that it was written after the PR existed, again,
because review asked again. Two lanes in a row now. The correction from
only evidence of that.
R8 DIAGNOSED, and the `TMPDIR` hypothesis was right:
1. `display_path` (builtin/runtime/lsp.lua:2397) shortens a location
against the DETECTED PROJECT ROOT before rendering it.
2. `project.detect` walks UPWARD for a marker; from
/tmp/.tmpXXXX/r.rs it reaches /tmp.
3. This machine has a stray `/tmp/.git` — an EMPTY DIRECTORY, not a
repository. The `.git` marker is directory-only, so an empty
directory still matches.
4. Root resolves to /tmp, the prefix is stripped, and the rendered row
is exactly the observed `.tmpXXXXXX/r.rs:12:3`.
Controlled, not inferred: the same test with TMPDIR outside /tmp PASSES.
THE CODEBASE ANTICIPATED THIS BY NAME. src/project.rs:208 documents
`detect_project_within(start, markers, stop_root)` as existing "so a
stray marker in a temp-dir's ancestor (e.g. a developer's /tmp/.git)
can't leak into a fixture that lives below it." The mechanism exists;
this fixture does not use it.
So the row splits, and the halves need different fixes. The failure is
ENVIRONMENTAL — nothing about pmacs is wrong when a real project root
sits above a file, that is the feature, and removing /tmp/.git makes the
gate green immediately. The fixture being ENVIRONMENT-DEPENDENT is a
real defect, and bounding its detection is what retires the row.
PROVENANCE UNRESOLVED, and I am not going to assume in my own favour:
/tmp/.git is dated 2026-08-07 23:17, inside this session's window, and
may have been created by this session's own work — a stray git
invocation from /tmp would do it. The earlier merge-base control stays
valid as "this tree has it" but says nothing about WHEN the environment
acquired the marker, so "pre-existing" must not be read as
"long-standing".
Nothing deleted: /tmp/.git is outside the repository and I cannot
confirm I created it, so removing it is the user's call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: rebase onto the R8 fix; scripts/gate now exits 0
#226 (`dcb852e`) retired R8 by bounding the LSP fixture's project
detection. This branch rebases onto it, and the thing that was blocked
is now demonstrable: **`scripts/gate` exits 0** --- all nine gates green
in one command, the first time the tool has passed the suite it exists
to run. That is #225's own acceptance criterion, and it could not even
be stated while the script did not exist on `main`.
REBASE RESOLUTION, per the standing rule that #226's R8 documentation is
authoritative. Every conflict was in R8 text this branch wrote while the
row was still an open investigation:
- two in `docs/ci-red-signatures.md`, both resolved to #226's retired
row with this branch's pre-fix copy dropped;
- the framing-doc pair --- e71e1bd added `docs/r8-fixture-boundary-
framing.md`, 7cfba73 removed it --- both SKIPPED. They are net-zero
here and `main` owns that file authoritatively; replaying the second
would have deleted `main`'s copy, which is the one failure mode a
mechanical "resolve each conflict in turn" would have walked into.
TWO STALE LANES REMOVED. This branch's "R8 --- NEEDS A LANE"
investigation block describes a diagnosis that has since happened and a
fix that has since landed. And #226's own lane arrived through the
rebase still saying "OPEN, HELD FOR REVIEW"; Rule 4 retires it now that
it has merged, its durable facts already being in the retired registry
row and the handoff section 6 census. Leaving either would have left the
ledger asserting that a merged fix was still an open investigation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: frame the R8 fixture-boundary fix (revision 2)
R8 fails m4_acceptance deterministically on one machine, and the
diagnosis is that the fixture never bounds its own project detection.
`display_path` (builtin/runtime/lsp.lua:2397) shortens a location
against the DETECTED PROJECT ROOT. `pmacs.project.detect` walks upward
for a marker; from /tmp/.tmpXXXX/r.rs it reaches /tmp, where this
machine has a stray EMPTY `.git` directory. The `.git` marker is
directory-only, so an empty directory matches, the root resolves to
/tmp, and the prefix is stripped.
THE PRODUCT BEHAVIOUR IS CORRECT AND IS NOT CHANGING. Shortening a
location against its project root is the feature. The defect is that
the fixture's assertion depends on whether the developer's /tmp happens
to contain a `.git`.
THE MECHANISM ALREADY EXISTS AND THIS SUITE ALREADY USES IT.
`src/project.rs:208` documents `detect_project_within(.., stop_root)` as
existing "so a stray marker in a temp-dir's ancestor (e.g. a
developer's /tmp/.git) can't leak into a fixture that lives below it."
It is exposed to Lua as `pmacs.project.set_search_boundary`; eight test
files make fourteen real calls to it, five of them in m4_acceptance
itself --- one carrying that same hazard as a comment. `open_against_fake`
(tests/m4_acceptance.rs:7985) is one helper that missed the pattern.
THE WITNESS PLANTS ITS OWN HAZARD, so the proof is not a property of
this machine: an empty `.git` in a temporary ancestor, the file one
level below, boundary at the file's parent. With the boundary the row
renders absolute; reverting it strips the prefix deterministically on
every machine, including CI where /tmp/.git does not exist. The
/tmp/.git observation stays as corroboration, not as the bite.
`scripts/gate` is deliberately NOT a criterion: this lane branches from
main, where that script does not exist (it is unmerged on #225). Naming
it would make this lane depend on an artifact absent from its own base.
R8 lands first on its own merits; #225 then rebases and takes "gate runs
green" as ITS criterion.
Q#R8-1 records a limitation rather than discovering it later:
parent-as-boundary is correct only while fixtures put the file as a
direct child of the fixture root. A future nested fixture cannot fix
itself by passing a deeper path --- the boundary is DERIVED from the
parent, so a deeper path clamps sooner, never later.
Provenance of /tmp/.git is left permanently unresolved, and the document
says why no timestamp is treated as authoritative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* fix(tests): bound the LSP fixture's project detection — retires R8
`open_against_fake` never set a search boundary, so the panel tests'
rendered paths were shortened against whatever project root detection
found ABOVE their tempdir. On a machine with a stray `/tmp/.git` that
meant `/tmp` --- and the assertion that spells a path out failed
deterministically. Registry row R8.
THE PRODUCT BEHAVIOUR WAS NEVER WRONG AND IS NOT CHANGED. Shortening a
location against its project root is the feature; a file that really is
inside a project really should render relative to it. What was wrong is
that a fixture's assertion depended on the contents of the developer's
/tmp.
THE MECHANISM WAS ALREADY THERE. `src/project.rs:208` documents
`detect_project_within(.., stop_root)` as existing "so a stray marker in
a temp-dir's ancestor (e.g. a developer's /tmp/.git) can't leak into a
fixture that lives below it" --- naming this exact hazard. It is exposed
to Lua as `pmacs.project.set_search_boundary`, eight test files make
fourteen real calls to it, and five of those are in this same file, one
carrying that hazard as a comment. This was one helper that missed a
pattern its own file already used.
THE WITNESS PLANTS ITS OWN HAZARD, so the proof is not a property of one
machine. `a_planted_ancestor_marker_does_not_reach_the_rendered_row`
creates an empty `.git` in a temporary ancestor with the file one level
below, and asserts the row stays absolute. Reverting the boundary fails
it with `proj/r.rs:12:3` --- relative to the PLANTED marker, not to
/tmp, because the nearer ancestor wins. That is what makes it bite in
CI, where no /tmp/.git exists; confirmed by also running it with TMPDIR
outside /tmp.
Resting the bite on /tmp/.git would have been the same mistake as a test
that passes only where the developer happens to be standing.
/tmp/.git IS DELIBERATELY LEFT IN PLACE. Deleting it would hide the
hermeticity defect rather than fix it, its provenance is unresolved, and
it is the only thing on this machine that reproduces the row --- which
makes it useful, not merely untouchable. The R8 fix is verified WITH it
present.
VERIFICATION. The R8 test passes on the machine that reproduces it. Full
m4_acceptance 151/0. `--lib` 1920, `--lib --features crdt` 2105,
`-p pmacs-gpu` 241, fmt, clippy, `git diff --check`. The full workspace
sweep exits 0 across 113 targets --- the first fully green local sweep of
this session, R8 having been the only obstacle.
`scripts/gate` is deliberately not a criterion: this branches from main,
where it does not exist. #225 rebases onto this and takes a green gate
run as ITS criterion.
R8 is RETIRED CAUSALLY --- mechanism removed plus a discriminating,
portable witness --- and moved to the retired section with its
disposition. What the retirement does NOT claim is stated there: 113
`new_with_roots` constructions in this suite alone, an unknown number
equally unbounded, harmless only while their assertions do not render a
path. That census is now a named §6 follow-on, because the next one will
otherwise look like a fresh mystery rather than a known class.
Framing: docs/r8-fixture-boundary-framing.md (revision 2, approved).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs(tests): reunite the listview doc comment with its test; state the PR
Three review findings, one of which is mine to own plainly.
I REPORTED A SHA I NEVER VERIFIED. The previous message named the PR
head as `21f0ed1`. That object does not exist in this repository. The
true head is `78d8e1c` --- local tip, `githubsucks/r8-fixture-boundary`,
and the PR all agree, and it is what was reviewed. No command in that
turn ever printed `21f0ed1`; I asserted an identifier instead of
reading one, which is precisely the failure a head-SHA check exists to
catch. Verified this time before writing it down.
THE DOC COMMENT DOCUMENTED THE WRONG TEST. Inserting the new witness
anchored on `#[test]\nfn flat_listview_...`, which sits BELOW that
test's 17-line doc comment --- so the comment about outline and flat
listview consumers ended up introducing the planted-marker test, which
touches neither, while the test it was written for was left bare. Moved
back. No behaviour change; both tests still pass.
That is a general hazard of anchored insertion worth naming: anchoring
on the `fn` line silently steals whatever documentation precedes it.
STALE STATE IN TWO DOCS. The framing still said "Pre-implementation.
Awaiting approval" after being approved and implemented, and the ledger
lane said "PR PENDING" after #226 opened. Both now record approval,
implementation, the PR link, and that it is held for review with no
merge authorization.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: retire the long-lines lane, the QoL arc having closed at #223
Rule 4, applied in its stated order: durable facts to
`docs/agent-handoff.md` FIRST, then the lane goes. The ledger is for
volatile branch/checkpoint/recovery state, and none of that survives a
merge worth keeping.
What moved to the handoff, because it is architecture rather than
bookkeeping:
- The GPU is not a grid consumer, so it could not inherit
`view_left`; `code_scroll_left` is its own, in pixels, local
viewport state with no wire and no version bump.
- The work was ONE transform and ONE clip, written before any
consumer moved. glyphon honors `TextBounds` so the text layers
clip themselves; the manual quad and squiggle renderers do not,
and nothing needed them to before this stage because no
code-relative x could be negative.
- Washes crop rather than drop — the same boundary Stage 4's review
caught the TUI painter getting wrong.
- Both lifecycle resets are observed PRE-MOTION, because a later
cursor motion repairs the offset anyway.
- `pmacs_protocol:📜:follow_left`: the follow rule beside
`classify`, called by both frontends. The approved exception to
Stage 5's scope, recorded in the framing doc §1.2a.
- The two witnesses that exist because mutation testing found the
TESTS wrong rather than the code — the unwitnessed glyph layer, and
the completion predicate a far-off-left test could not catch.
"Test a boundary AT the boundary" is the transferable part.
The `## Long lines (QoL arc)` block is removed whole; nothing else in
the ledger referenced it.
NOT REMOVED, and not an oversight: the QoL Stage 1 (#219) and Stage 2
(#220) blocks. Rule 4 would now reach them too — the arc they belong to
is closed — but Stage 2 carries a live named follow-on, capability-aware
keymap resolution (Q#Z3 option A, deliberately deferred), plus the
"first eager state consumer" restore-seam finding. Those need re-homing
before their blocks can go, which is a judgment beyond retiring the lane
that was asked for. Flagged rather than done.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: complete the retirement — Stages 1 and 2, and the stale anchors
The previous commit removed the long-lines lane and stopped short. Two
things were still wrong.
STALE ANCHORS. The handoff was dated 2026-08-06 with `main` at
`db1bbe9` — five merges behind. Now 2026-08-08 at `9a26ac8`, with the
arc's five merges named in the ancestry chain, and the same in
active-work's canonical-base record and recovery floor (a tree at
`db1bbe9` no longer passes; it would lack the whole arc).
**The recovery path was re-exercised, not SHA-swapped.** That file
warns that advancing the base is exactly when the commands are most
likely to have rotted and that a swapped SHA reads identically to a
verified one — so: fresh clone into an empty directory, `githubsucks`
alias added, `git fetch --prune`, `9a26ac8` confirmed an ancestor of
`githubsucks/main`, and a worktree recovered with the three-argument
form. All four steps clean.
STAGE 1 AND STAGE 2 BLOCKS. Keeping them left false live planning
standing: "Stage 3 is long-line wrap/scroll, which is a design round:
no horizontal viewport exists at all" — written before #221–#223 built
one. A merged lane that still describes the future is worse than no
lane.
Re-homed first, per Rule 4's order:
- **FG-INV is a CONSUMER contract**, and it lives on the protocol
type because that is where consumer authors read it. It had been a
doc comment on a PRIVATE PRODUCER FIELD, which is why the one
consumer never honored it.
- **Seven tests covered that flag and all seven tested the
producer.** None asserted a consumer acts on it. "Add a test for
the flag" had already been done — §5's enforcement/documentation
drift in a second register.
- **`install_state_dirs` is the eager-state-consumer seam.**
Builtins and `init.lua` run before it, so `pmacs.state.read` at
module load returns nothing, always. `saveplace` and `recentf`
escape it only because both read lazily. Any future eager consumer
belongs at the same seam.
- **A GPU-only binding cannot be expressed**: `Scope` has no frontend
identity and `FrontendEvent` no command-invocation variant. #220
shipped commands without bindings for that reason, not preference.
- **Capability-aware keymap resolution** is now a named §6 backlog
item: CROSS-CUTTING, NOT STARTED, needs its own framing. It says so
explicitly, and says not to start it as a half-lane attached to
another stage's branch — which is how it would arrive by accident.
No implementation, no lane, no design.
What is deliberately NOT preserved: the recovery commands for
`full-grid-resync` and `gui-zoom`. Those branches are merged; a
recovery command for a branch nobody should check out is the kind of
stale instruction this ledger exists to avoid. The framing docs remain
on disk as the historical record.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: the live schema range is v6..=v22, and #224 gets its own lane
Two review findings against the retirement.
THE SCHEMA RANGE. The canonical-base paragraph still said `v6..=v21`.
The upper bound moved to **v22 at #221**, which added
`InstanceMessage::LineWrapFacts` — so that line had been wrong for two
merges, including the one this branch is retiring the lane for.
Verified against `pmacs-protocol/src/message.rs` rather than carried
forward: `SUPPORTED_PROTOCOL_VERSIONS` is `6..=22`, `PROTOCOL_VERSION`
is 22, and `ADVERTISED_PROTOCOL_VERSION` is **20** and did not move.
The paragraph now says so, and says the advertised constant must not be
edited to chase the range — it is a permanent baseline, and the session
version is settled one message later by the frontend's counter-offer.
It also now states which claims it governs: historical `v21` statements
elsewhere describe a stage as it landed and are correct there. Only this
current-state paragraph tracks the live range, so only this one goes
stale when the range moves.
A LANE FOR #224. This file requires a lane for **every open PR**, and
the PR that retires other lanes is not exempt. Added with the branch,
the ref-not-a-SHA recovery command, the docs-only scope, and the
verification — including that the recovery path was re-exercised rather
than SHA-swapped, and that the full gate suite is deliberately not
re-run for a change that cannot reach it.
The entry is honest about its own lateness: it was written AFTER the PR
existed, which is the standing correction from #171 and #215 being
missed again, and it took review asking. Back-dating the block to look
compliant would have destroyed the only evidence that the practice
still slips. It carries its own retirement instruction — next
absorption after #224 merges.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: scope the two provenance paragraphs that claimed to be current
Both said "this line is the head-of-`main` anchor". Neither was, and a
provenance note that claims currency is worse than a plainly historical
one — it disagrees with the real anchor at the top of the file while
looking authoritative.
The bottom-panel paragraph keeps its `v6..=v21` facts, which are correct
for the stage they describe. What changes is scope: "a current session"
becomes "a session at that anchor", and the closing clause now says
these statements describe the historical `6c9e765` anchor, with the
live range pointed at "Repository authority" in `docs/active-work.md`
(`v6..=v22` since #221; advertised baseline still v20). The
counter-offer mechanism is called out as still current independent of
which numbers it carries, since that part did not go stale.
The second was MY inconsistency, introduced earlier on this branch. I
rewrote that bullet's opening to "Beneath the QoL arc, at `db1bbe9`"
and left its closing claiming to be the head-of-`main` anchor, so the
bullet contradicted itself. It now names what it actually is — the
`db1bbe9` ancestry chain — and points at the top of the file.
Both cross-references name the file they point into. "Repository
authority" is a section of `docs/active-work.md`, not of this one, and
an unqualified "above" would resolve to nothing here.
Noted, not acted on: the bottom-panel arc has its own currency drift in
this file (§1 says Arc 7 COMPLETE at #213, two later paragraphs still
call Stage 3 the remaining step). Pre-existing, unrelated to the QoL
retirement, and not this PR's to fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: frame QoL Stage 5, GPU horizontal scroll
Stage 4 merged as #222, so the lane advances to its last stage. Rule 4
still does not apply — the arc closes when Stage 5 merges, not before.
THE FRAMING'S FIRST FINDING CORRECTS STAGE 4'S. §1.3 there said the GPU
"needs a mechanism that does not exist", named it the fact most likely
to invert the cost estimate, and I endorsed the Stage 4/5 split partly
on that basis.
Half of it holds: `Scroll::horizontal` really is discarded throughout,
because glyphon 0.11 never applies it when placing glyphs — three
doc sites and three asserting tests. But that is not the only
mechanism. The document `TextArea` already carries an explicit `left`
origin and a `TextBounds` clip whose `left` is `gutter_clip_left`, and
horizontal scroll is `left: text_left - offset_px` with the clip
unchanged. glyphon then drops what falls left of the gutter — the same
"paint from column 0, clip at the edge" shape the grid renderer uses,
expressed in pixels. It is machinery the file already depends on, not
new machinery.
The split stays right for the reason that survives: the three consumers
Stage 4 named — caret (`code_byte_px`), decoration geometry
(`push_glyph_extent_rects`), hit testing (`gutter_aware_rel_x`) — each
produce x relative to `text_left()` and each need the same offset,
applied ONCE or they disagree. Shipping that inside Stage 4 would have
made one reviewable change into two unreviewable ones. But it was
justified partly by an overstatement, and saying so is cheaper than
letting a future reader inherit it.
No wire, no version bump: the GPU owns its viewport locally, exactly as
it owns `scroll_top` and `code_scroll_residual`. The parallel with
`ui.line-wrap` is misleading and the doc says why — the MODE is buffer
state and needed v22, the OFFSET is viewport state and needs nothing.
Five questions, each with my vote. Q#G3 is the one I am least sure of:
the GPU can resolve a proportional family, where "column" has no fixed
pixel width, so column-for-column parity with the TUI is unachievable.
I lean to defining the behavior in pixels and accepting imprecise
correspondence rather than gating a navigation feature on a font
choice — but that is a product call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 revision 2 — a clip, not just an offset
Two functional findings and two record repairs.
Q#G3 WAS BUILT ON A FALSE PREMISE, and the correction makes the lane
stricter rather than looser. Revision 1 said the GPU can resolve a
proportional family and proposed accepting a new TUI/GPU divergence to
accommodate it. It cannot: `family_is_monospace_everywhere` gates the
family across all four weight/style combinations,
`apply_font_facts` falls back when that fails, and
`unresolvable_and_proportional_families_fall_back` REQUIRES the
fallback. Answered as monospace-only by the font contract that already
exists — and the consequence is that the TUI-parity witness becomes
UNCONDITIONAL for every font the GPU supports. Revision 1 would have
introduced a font-dependent behavior difference to solve a problem the
codebase had already solved, in the lane whose purpose is removing
unchosen divergence.
"THREE CONSUMERS" WAS INCOMPLETE IN A WAY THAT WOULD HAVE SHIPPED A
DEFECT. Shifting the `TextArea` clips glyphon's text because glyphon
honors `TextBounds`. The manual quad and squiggle renderers have no
code-area scissor at all — nothing stops them painting into the gutter,
and today nothing needs to, because no code-relative x can be negative.
Scrolling makes that false.
So the framing now requires TWO shared things: one screen↔code
transform, and one code clip rectangle every code-relative painter
intersects with. The paths are tabulated with sites — caret rect
(`:9698`), caret-painted predicate (`:9734`), glyph extent rects
(`:9766`), inline math origins (`:9434`), completion anchor (`:7606`).
The two caret sites are the sharpest, and one of them falsifies a claim
revision 1 made: `:9734` has no left-edge test, so "the scroll
indicator inherits the fix" was false — `code_byte_painted` reuses it
and would call an off-left byte painted. And `:9698` does not merely
lack a check, it DOCUMENTS the absence as safe ("the caret x can't
precede `text_left`"). A comment asserting an invariant this lane
deletes is worse than silence.
Q#G2: "inert under wrap" was too weak. The offset must be RESET to zero
on the wrap transition, as the TUI already does — `horizontal_follow`
assigns `view_left = 0` on the wrap branch. Inertness hides a stale
value that reappears the moment the buffer toggles back to `truncate`,
before any cursor motion. G5 gains a witness that an inertness-only
implementation fails.
RECORDS. Rule 4's Stage-5 removal precondition was not actually met:
the handoff still described Stage 4 as upcoming work. Stage 4's durable
facts are now transferred — the unsnapped per-window column with a
per-line effective edge, the line-absolute walk, the three-way cell
designation, `Viewport::visible_cols` and its five adopters, the
wrap-branch reset, the `#[serde(default)]` persistence, and the absence
of any wire. The ledger's "Stage 4 ahead" / "Stage 4 plan" text is
corrected to Stage 5, and its Rule 4 note now says the removal is
legitimate BECAUSE those bullets exist.
And the journey-step claim is withdrawn. Revision 1 said this lane
completes journey step 4; step 4 is scored on welcome/help/tutorial
discoverability and COHERENCE.md:395 holds it Partial for reasons this
lane does not touch (`C-h` deletes a word, no tutorial). Restated as
preserving interface comprehension with no scorecard movement. §16 is
the direct target. Writing an unearned mark into a scorecard is how a
coherence document stops being ground truth.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 revision 3 — four corrections, one of them impossible
Q#G1 CONTRADICTED THE Q#G3 ANSWER IN THE SAME DOCUMENT. It still said
the GPU's font "need not be monospace" and that Q#G3 makes "column"
ill-defined — both falsified by the answer two sections below, in the
same revision that wrote it. The pixel-storage vote is unchanged, but
its reasons narrow to the ones that survive, and the conversion is now
stated as EXACT: columns × the supported monospace advance. That is
what makes the unconditional parity witness checkable at all.
Also removed `follow_cursor`, which I invented. The GPU's pass is
`ensure_caret_painted`, and it is now named rather than cited by line —
robust against the transposition that put these two sites at each
other's line numbers in review.
Q#G2 WAS MISSING THE BUFFER-SNAPSHOT RESET. The GPU zeroes `scroll_top`
and `code_scroll_residual` when a snapshot installs a new buffer; the
horizontal offset must reset there for the same reason. Without it a
buffer switch INHERITS the previous document's leftward viewport,
showing the new buffer scrolled sideways until a cursor motion repairs
it — a worse symptom than the wrap case, because nothing about the new
buffer explains it.
THE GUTTER ASSERTION WAS IMPOSSIBLE, not merely imprecise. Revision 2
proposed asserting that nothing paints left of `gutter_clip_left`. With
line numbers on, the gutter DELIBERATELY holds digit glyphs and
diagnostic-sign quads, so that assertion fails on a correct
implementation — a test that can only be satisfied by removing the
gutter. Replaced with the checkable form of the same intent: the gutter
rectangle is byte-identical before and after a horizontal scroll, and
the left-edge rule is checked against code-relative geometry only. It
still catches a code painter bleeding into the gutter, because that
changes those pixels.
THE COMPLETION ANCHOR HIDES, IT DOES NOT CLOSE. `completion_anchor_px`
already returns `None` when the anchor scrolls out, so nothing draws
while the daemon-owned completion state and its key handling are
retained; actual closure is `CompletionPopup { anchor: None }`, which
is the daemon's to send. Revision 2 said "closes", which would have had
a viewport-geometry lane quietly redefining when a completion ends.
Specified as: no completion paint while the anchor is off-left, popup
reappears when it scrolls back, session semantics unchanged.
Ledger drift fixed: it still called the framing revision 1 with five
questions open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 revision 4 — witnesses for the two rules that had none
Both additions cover requirements the framing had already stated and
then left untested, which is how a rule becomes a comment.
THE SNAPSHOT RESET (Q#G2). Revision 3 added the buffer-snapshot reset
and tested only the wrap one. The witness now scrolls buffer A to a
non-zero offset, installs a buffer B snapshot, and asserts the offset
is zero and B renders at its code origin BEFORE any `CursorByte`
arrives.
The pre-cursor scoping is the entire test. A later cursor motion
repairs the offset regardless, so a witness that waits for one cannot
distinguish "reset on snapshot" from "repaired on first motion" — and
the second is the defect. Same shape as the wrap witness, which is also
scoped to before any motion, and for the same reason.
THE MINIMAP (Q#G4). The vote is "no movement", and the implementation
already supports it: the minimap derives from the summary, the surface
dimensions and `scroll_top`, with no horizontal input. So the witness
pins an existing property rather than requesting work — which is
exactly why it is worth writing. An offset threaded one seam too far
would break it silently, and nothing else in G5 would notice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 approved, five questions resolved
G1 pixels with exact conversion via the supported monospace advance; G2
automatic cursor-follow only, zeroing on both the wrap transition and
BufferSnapshot; G3 monospace-only by the existing font contract; G4
minimap unchanged; G5 accepted whole, including the snapshot-reset and
minimap-stability witnesses.
The scope boundary is restated in both documents because it is what
keeps this lane small: local GPU viewport state, no wire message, no
protocol bump, no command surface, no minimap movement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* feat(gpu): horizontal scroll — the transform, the clip, and both resets
Stage 5, partial. The mechanism and lifecycle are in; two consumers and
the whole verification set are NOT yet done — see the tail of this
message, which is a status, not a summary.
WHAT IS IN.
The offset, `code_scroll_left`, in pixels (Q#G1). Column parity stays
exact because the code font is monospace by contract, so
`columns × advance` is a definition rather than an approximation.
Local viewport state: no wire, no version bump.
One screen↔code transform (`code_x_to_screen` / `screen_x_to_code`) and
one code clip (`code_clip_left` / `survives_code_clip_left`), which is
the pair framing §1.1 requires. Written before any consumer moved,
because five sites deriving the same offset independently is how the
caret and the glyphs it sits among come to disagree.
The glyph-side mechanism is one line: the document `TextArea`'s `left`
shifts while its `bounds.left` stays at the gutter, so glyphon clips
and the gutter keeps its own pixels.
BOTH LIFECYCLE RESETS (Q#G2), which were the two rules most likely to
be left as comments. The wrap transition zeroes the offset in
`apply_line_wrap` — inertness would park a stale value that reappears
the instant the buffer toggles back to `truncate`. The buffer snapshot
zeroes it beside `scroll_top` and `code_scroll_residual`, or a buffer
switch inherits the previous document's leftward viewport and shows the
new buffer scrolled sideways until a cursor motion repairs it.
`code_caret_rect_in_clip` gains its left-edge test, and its comment is
REWRITTEN rather than extended: it used to assert "the caret x can't
precede `text_left`", an invariant this stage deletes. A comment
asserting something a later stage falsifies is worse than silence. That
also repairs `code_byte_painted`, which reuses it — revision 1's claim
that the scroll indicator "inherits the fix" was false precisely here.
`gutter_aware_rel_x` is now the exact inverse of the transform, with
the gutter clamp applied in screen space first: a click in the gutter
band means "the first visible column", which after scrolling is the
offset, not column 0.
The completion anchor HIDES when scrolled off-left and does not close —
the daemon owns completion state and its key handling, and closure is
`CompletionPopup { anchor: None }`, which is the daemon's to send.
`horizontal_follow` mirrors the TUI's: automatic only, scroll just far
enough, so a caret already visible never moves the view. It runs after
`normalize_code_scroll` because it reads the caret's laid-out x, which
vertical normalization can change.
WHAT IS NOT IN, and must land before this is reviewable:
- `push_glyph_extent_rects` — washes, squiggles and selection extents
still paint at unshifted x and are not cropped at the gutter.
- Inline math origins (`:9434`) — same.
- Every Q#G5 witness. The 228 existing GPU tests pass, which says
only that nothing regressed at offset 0; not one of them exercises
a non-zero offset.
Gates so far: fmt; clippy --workspace --all-targets -D warnings;
PMACS_REQUIRE_GPU=1 -p pmacs-gpu 228/0; git diff --check. The full
two-configuration sweep is deliberately not claimed — the lane is not
finished.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* feat(gpu): the last two painters move, and twelve witnesses say so
Completes Stage 5. `62fb93e` landed the transform, the clip and both
resets but left two code-relative painters at unshifted x and the whole
Q#G5 witness set unwritten; its 228 green tests said only that nothing
regressed at offset 0.
The two painters:
- `push_glyph_extent_rects` — selection/search washes, peer presence
and diagnostic squiggles. Shifted through `code_x_to_screen`, then
CROPPED at the gutter rather than dropped: a selection running in
from off the left edge must paint the part that is visible. That is
the same boundary Stage 4's review caught the TUI painter getting
wrong, and it would have been easy to reproduce here.
- Inline math. The glyph mini-buffers only needed their origin moved —
their layer already carries the code area's `TextBounds`. The
fraction rules are quads in the background batch with no scissor of
their own, so those are cropped by hand.
`crop_to_code_clip_left` is the crop, and `survives_code_clip_left` now
delegates to it, so a caret the crop would discard is never painted.
One boundary rule, not two that agree today.
TWELVE WITNESSES, EACH MUTATION-TESTED. Eleven production mutations —
unshifted wash x, uncropped wash, unshifted math origin, uncropped math
rule, untested caret left edge, missing snapshot reset, missing wrap
reset, unhidden completion anchor, unscrolled glyphs, inverted hit-test
sign, pixel-instead-of-column snap — each fail the intended witness as
an ASSERTION failure, not a compile error. The minimap-stability
witness was mutation-tested separately by threading the offset into
`minimap_vertex_bytes`.
That battery earned its keep immediately. The gutter byte-identity
test's "the code area must actually have moved" assertion is satisfied
by a decoration wash and the caret alone, so it PASSED with
`TextArea.left` pinned to `text_left` — the entire glyph-side mechanism
was unwitnessed and nothing in review would have shown it. Its
replacement isolates the glyph layer: no decorations, and a source line
carrying no caret, whose band is blank at offset 0 and inked after.
ONE DELIBERATE STEP OUTSIDE THE APPROVED SCOPE, and it needs a ruling.
Q#G5 asks for frontend agreement that is "checkable rather than
asserted". Two tests in two crates asserting the same literal is not
that; it is the structural duplication `pmacs-protocol::scroll`'s own
module docs condemn, and that module exists because THIS ARC already
shipped that defect — the scroll indicator, fixed in one copy and left
wrong in the other. So the follow rule moved to
`pmacs_protocol:📜:follow_left`, beside `classify`, and both
frontends call it: `src/editor.rs::horizontal_follow` delegates, and the
GPU converts px <-> columns around it, exact by Q#G3.
The cost is that Stage 5 now touches `src/editor.rs`, which "local GPU
viewport state" does not cover. No wire message and no version bump —
the same argument `classify` already makes. If rejected, reverting is
small: restore the four-line conditional, drop `follow_left` and its
four protocol tests, rewrite the parity witness as a two-sided pin.
GATES, both configurations, five ambient roots isolated: fmt; clippy
`--workspace --all-targets -D warnings`; `--lib` 1920 and `--lib
--features crdt` 2105; horizontal_scroll 11, long_line_readable 3,
line_wrap 6, full_grid_resync 1; `PMACS_REQUIRE_GPU=1 -p pmacs-gpu`
239; `-p pmacs-protocol --lib` 29; both full workspace sweeps;
`git diff --check`.
TWO SWEEP FAILURES, NEITHER THIS LANE'S, both logged:
- R8, new row: `flat_listview_consumers_render_byte_identically...`
fails DETERMINISTICALLY, and the merge-base control is done — it
fails identically on `main`. The row renders with a leading
directory stripped; it is a prefix strip, not width truncation, and
the mechanism is NOT diagnosed. Deliberately not fixed here.
- U3: the R7 selector failed once and passed on rerun. Recorded as a
new incident, NOT an R7 match — different flavor, and its fragments
are unverified because I filtered the sweep output before reading
it. U2 records me making that exact mistake already. Sweeps go to a
file from now on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* fix(gpu): the completion anchor is a point, and the witness now says where
Review round 1. One defect, and a lesson about the witnesses that
missed it.
THE DEFECT. `completion_anchor_px` reused `survives_code_clip_left` and
passed `line_height` as the horizontal extent — a VERTICAL dimension
standing in for a horizontal one. The predicate is
`screen_x + w > code_clip_left()`, so an anchor up to a whole line
height left of the gutter "survived". `completion_dropdown_rect` bounds
`ax` against the right margin only, so that x reached the popup's left
edge and painted over the line numbers.
An anchor is a position between glyphs. It has no width, and the popup
it places is drawn to its right. So the predicate is a point:
`screen_x < code_clip_left()`.
The absent left clamp downstream stays absent, deliberately. This
predicate is what guarantees `ax >= code_clip_left()`; a second clamp
would be a duplicate of the same rule, which is the failure mode this
stage's shared-transform design exists to avoid. It is witnessed
instead.
THE LESSON, which is the more useful half. The existing test placed the
anchor 200px off-left — and 200px off-left fails a width-based
predicate too, so it stayed green straight through the defect. The
mutation battery agreed with it, because every mutation asked only
whether REMOVING a check was caught, never whether the check had the
right shape.
A boundary must be tested AT the boundary. The new witness straddles it
by ±0.05px — the same anchor either side of the edge, which no
width-based predicate can separate — and additionally asserts the
popup's own left edge stays out of the gutter, making "no left clamp
needed downstream" a checked claim rather than a comment. Verified both
ways: the new witness fails against the original predicate, the old one
passes against it.
THE AUDIT that finding prompted. Stage 5 has one other left-edge
predicate, the caret's. Its use of `survives_code_clip_left(rect.x,
rect.w)` is correct — a caret quad genuinely is `CARET_WIDTH` wide —
and it was also only tested far from the edge. It is now walked ACROSS
the boundary a column at a time, asserting painted carets are wholly
inside the code area and hidden ones wholly outside.
That pins an argument that was load-bearing and invisible: because
`horizontal_follow` snaps to whole columns, a caret is never partly
behind the gutter, since `CARET_WIDTH` (2px) is far below any code
advance. Substituting `rect.h` for `rect.w` — the exact error above —
fails it. An over-width smaller than one advance does not, and that is
the invariant rather than a gap.
SCOPE. `follow_left` recorded as the one approved exception to "local
GPU viewport state" in the framing doc, new §1.2a: what it is, why the
Q#G5 parity witness cannot be real without it, and what it does not do
— no viewport state moved, no wire message, no version bump.
GATES, both configurations, five ambient roots isolated, sweeps
redirected to files per U3's lesson: fmt; clippy `--workspace
--all-targets -D warnings`; `--lib` 1920 and crdt 2105;
`-p pmacs-protocol --lib` 29; `PMACS_REQUIRE_GPU=1 -p pmacs-gpu` 241;
horizontal_scroll 11, long_line_readable 3, line_wrap 6,
full_grid_resync 1; both full workspace sweeps; `git diff --check`.
The only sweep failure is R8, confirmed by its recorded fragments —
pre-existing, deterministic, merge-base controlled against `main`, and
not this lane's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: Stage 5 is PR #223, head 55faa45
The ledger said "no PR opened yet", which stopped being true the moment
it was. Records the PR, its head SHA, and the standing do-not-merge.
Rule 4 still applies at merge, not now: the long-lines lane stays until
#223 lands.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
* docs: the tip is the ref, not a SHA the commit itself invalidates
The previous commit wrote "head 55faa45" into the ledger and, by
existing, made it false — recording the PR moved the head to 4902048.
A SHA pinned in a document that the act of writing it stales is a trap,
not a record.
The ledger already states the correct convention two paragraphs down
("the authoritative tip — the ref, not a SHA"); this follows it, and
says to verify CI against the PR's live headRefOid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The suite header still said "the four call sites", written before the
selection painter joined `Viewport::visible_cols`. Four is now the count
of decorator FAMILIES — syntax/LSP styling, diagnostic underlines,
search washes, `BufferStyleOverlay` — and five is the count of adopters,
selection being the fifth.
Also points at where selection's own witnesses live, since a reader of
this file would otherwise look for them here and find nothing:
`paint_local_selection` is private, so they are in `src/editor.rs`.
Comment only; no behavior and no assertion changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Stage 4 is open as #222; Stage 5 (GPU) still closes the arc, so the
Rule 4 exemption above stands unchanged.
Records the two things review added after the framing was approved: the
`Viewport::visible_cols` single clip rule with its five adopters, and
the corrected `ui.line-wrap` description. Both are lane facts rather
than framing ones — the framing decided the coordinate contract, and
these are what implementing it against a real frame turned up.
Also notes R7, so a reader of this lane finds the unrelated red without
having to reconstruct why a sweep in this window went 112/113 once.
No SHA — `githubsucks/horizontal-scroll` stays the authoritative tip,
per this ledger's own rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Stage 4 of the QoL arc, framing revision 4 (approved). Under
`truncate`, text past the right edge was UNREACHABLE; moving the cursor
now brings it into view. Automatic only — no commands, no bindings, no
new interaction island (Q#HS2).
THE CONTRACT. `view_left` is an unsnapped per-window display column
(Q#HS7(a)), and each line derives its own effective edge during the
walk it already performs from column 0. Starting at 0 is not laziness:
tab expansion depends on the absolute column from the line start, so a
walk beginning at the edge would put tab stops in the wrong place. The
walk stays line-absolute and only the emit translates.
Where the edge bisects a wide glyph on a given line (Q#HS7(c′)), its
trailing cell paints a styled BLANK rather than a `Continuation` — that
glyph means "the cell before me is a wide glyph's head", and here that
cell is off-screen, so emitting it would name a cell nobody painted.
The mapping designates that cell to the glyph's START byte, which keeps
`byte_at_place` total over visible cells and makes the character the
user scrolled toward clickable. Tabs keep FORWARD rounding (Q#HS7(c″))
— preserved, not chosen.
DECORATIONS TRAVEL WITH THE TEXT. The first version of this commit
translated the base glyph walk and nothing else, which split the frame
in half: at `view_left = 10` a glyph from source column 10 painted at
screen column 0 while its syntax style, diagnostic underline, search
wash and `BufferStyleOverlay` span painted at screen column 10 — or
vanished. Decorations drifting off the characters they describe,
silently, and only once a window had been scrolled.
Every such site carried the same two lines (`start_col.min(max_cols)`,
`end_col.min(max_cols)`), correct only while the left edge was pinned
at zero. `Viewport::visible_cols` is now the one rule all FIVE adopters
share — syntax/LSP styling, diagnostic underlines, search washes,
`BufferStyleOverlay`, and the selection painter — so a future decorator
inherits the translation instead of re-deriving it. It also subsumes
the old `end_col <= start_col` guard rather than sitting beside it.
`StyleSpanOverlay` and `VirtualCellOverlay` are deliberately untouched:
they are documented as viewport-relative, so translating them would be
the mirror defect.
The selection painter was nearly a sixth site with its own copy of the
rule, which I justified by a width it supposedly needed and the
viewport lacked. That was FALSE — the render viewport's
`cell_size.cols` is already `rect.size.cols - gutter_w` and its origin
already sits past the gutter. It now takes that same viewport and drops
its `rect`/`gutter_w` parameters entirely. A canonical rule with one
honest exception is not canonical.
The selection painter had the same defect with a worse failure mode: it
asked `pos_to_display` through the LIVE context, which returns `None`
for a position left of the edge, so a selection beginning off-screen
and reaching into view took `continue` and painted NOTHING. That is the
common shape, not an edge case — select rightward from column 0 past
the window width and the view scrolls with the cursor.
TWO THINGS THE TESTS FOUND, both in `pos_to_display`. My framing note
said a caret sits between characters so never lands inside a glyph;
true for the caret, false for the DESIGNATION direction — the glyph's
start byte must map to its visible trailing cell, so `screen_col` needs
the straddle rule and not a bare subtraction. And the `take == 0` early
return short-circuited the translation entirely, so byte 0 looked
visible at every offset.
`view_left` is inert under `wrap` BY CONSTRUCTION —
`LayoutCtx::effective_left` and `Viewport::left_edge` return 0 while
wrapping — rather than by every caller remembering.
Persisted per leaf at DESKTOP_VERSION 1 (Q#HS5) with both approval
conditions: `#[serde(default)]` and a literal v1 JSON fixture omitting
the field, hand-written because a generated one would gain the field
and prove nothing.
Also: `view_left: window.view_left` in the render viewport, not a
literal 0. My mechanical fill put 0 there and it is EXACTLY the
`aa3cd4d` defect — coordinates and the indicator following the scroll
while the painter stays pinned at column 0.
BITE, per clause. Forcing `bisected = false` fails the multi-line
straddle witness; dropping the backward designation fails the
round-trip witness; removing `#[serde(default)]` fails the v1 fixture;
pinning `visible_cols` to an absolute clamp fails all three decorator
witnesses; restoring the selection painter's live-context lookup fails
the off-screen-start selection witness. Each alone. And with selection
now reading the shared helper, pinning `visible_cols` to an absolute
clamp fails the selection witnesses TOO — which is the check that the
duplication is really gone rather than merely reworded.
One unrelated red, logged as R7 in ci-red-signatures.md — the first
this session with a COMPLETE signature, so a matchable row rather than
a U note. `pmacs-gpu`'s managed-retry attach hit a BrokenPipe once
under full-sweep load and did not reproduce (6 isolated runs plus a
clean 113-target sweep). Per the rerun rule that is intermittence only,
and the row explicitly does not claim harmlessness. Not attributed to
this lane: Stage 4 touches no `pmacs-gpu` file and adds no wire
surface.
Gates: fmt; clippy --workspace --all-targets -D warnings, both
configurations; `cargo test --workspace --no-fail-fast -- --skip
basedpyright` 113 targets exit 0, and the same with --features crdt,
113 targets exit 0; git diff --check. No protocol change, so no version
bump and no protocol-bump matrix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Two small revisions, and every Stage 4 question is now answered.
Approval is NOT recorded; no implementation may begin.
Q#HS7(c″) — the tab-straddle mapping. Recorded as PRESERVED rather
than chosen, because it already exists: `byte_at_place`'s doc comment
says it rounds forward to the next character boundary, and the walk
accumulates past the tab byte and returns on the NEXT character's
start column, so every column inside a tab's expansion already yields
the post-tab offset (src/text_view.rs:224, :243-254). The requirement
on Stage 4 is therefore that horizontal scroll not PERTURB it — which
makes its witness a regression test, and one that should fail if the
walk is ever "optimized" to start at the effective edge instead of
column 0.
The obvious objection is that (c′) rounds backward and (c″) forward,
so the framing answers it. A wide glyph's two cells belong to ONE
character: forward-rounding its trailing cell would designate it to
the next character and leave the straddling glyph with no visible cell
mapping to it at all — unreachable by click exactly when it is what
the user scrolled toward. A tab's expansion cells are whitespace
BETWEEN the tab byte and the next character, and forward-rounding them
is already how clicking in indentation lands at the start of the text.
Different directions, one principle: every visible cell is designated
to the byte a user would mean by clicking it.
With (c′) and (c″) the (d) contract is total over visible cells:
ordinary character → its own start; bisected wide glyph → the glyph's
start; tab expansion → the byte after the tab.
Q#HS5 approved as stated, with both conditions written into the
approval rather than attached as advice: `#[serde(default)]` and a
literal v1 JSON fixture omitting the field, asserting restore at zero.
The handoff said "Stage 4 is the remainder". It now says Stages 4 AND
5 remain and the arc closes at Stage 5, carries the Q#HS1 time box,
and states explicitly that Rule 4 must not retire the long-lines lane
at Stage 4's merge. It also records that the unreachable caveat is
missing from the setting's description — the #221 gap — so that fact
lives in the durable doc and not only in a lane block that will
eventually be removed.
Ledger: the question list is consolidated (the accepted answers had
begun duplicating the blocking entries they resolved), keeping the
withdrawn (c)'s reasoning because the trap generalizes to any future
window-wide value derived from per-line content.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
P1 — Q#HS7(c) WITHDRAWN, and my vote was wrong rather than vague.
Revision 2 voted to snap `view_left` to a valid boundary at the moment
it is set. That cannot exist. `view_left` is ONE per-window display
column, but "does column N bisect a wide glyph?" is a PER-LINE
question: column 11 can be a wide glyph's trailing cell on line 3 and
ordinary ASCII on line 4. No setter-time value is canonical for every
visible line. Snapping per line instead is worse — the same source
column would appear at different screen columns on different rows,
destroying the alignment a column-oriented view exists to provide.
Replaced by (c′), a per-line effective edge. `view_left` is stored
unsnapped; each line derives its own edge during the walk it already
performs from column 0. Where the requested edge bisects a wide glyph
on THAT line, the finding's actual question — what occupies the
leftmost cell — is answered: it paints as a space carrying the glyph's
style, and the mapping DESIGNATES that cell to the wide glyph's start
byte. That keeps `byte_at_place` total over visible cells, preserves
the round trip (`place_of_byte(start)` reports the straddle and
designates cell 0), and gives a click there the character a user would
expect. Bytes lying entirely left of the edge are reported not-visible
rather than clamped to column 0, because clamping would make
arbitrarily many bytes share cell 0 and destroy (d).
Recorded as deliberately NOT the mirror of Stage 3's right-edge rule:
under wrap a too-wide glyph is pushed to the next row entirely, and at
the left edge under truncate there is no next row, so the same intent
requires a different rule. Stated so nobody "fixes" one to match the
other.
(d) is amended accordingly: the invariant is a property of
`(view_left, line)`, not of `view_left` alone — which is what makes a
multi-line fixture with differing glyph widths at the same column the
DISCRIMINATING test rather than an extra one. A single-line sweep
passes against the withdrawn design.
P1 — Stage 4 no longer closes the arc, and the stale claim was
load-bearing in the wrong direction. Rule 4 removes a lane when its ARC
is done, so a framing asserting Stage 4 closes it would license
retiring the lane at the TUI merge — orphaning the very Stage 5 that
Q#HS1's time box exists to guarantee, while `truncate` is still a dead
end in the GUI. Both the framing opening and the lane header now say
the arc closes at Stage 5, and the lane carries an explicit "Rule 4
does not apply at Stage 4's merge".
P2 — the stale Stage 3 residue in the ledger claimed the unreachable
caveat is in the setting description, contradicting revision 2 forty
lines above it. Corrected in place: the caveat lives only in the
toggle's status message and a source comment, and a user who sets the
mode in init.lua is told nothing.
Q#HS5 now states the concrete condition rather than an instruction to
check one. Verified: SavedLeaf carries no #[serde(default)] anywhere in
src/desktop.rs, so serde would REJECT a version-1 desktop JSON omitting
a new `view_left`. "Yes, no version bump" is sound only with the
annotation AND a regression fixture holding literal v1 JSON without the
field. The reverse direction already works — an old binary meets an
unknown field, which serde ignores absent deny_unknown_fields, and
there is none in that file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
P1 — THE PROTOCOL-BUMP GATE WEAKENED THE SWEEP IT MEANT TO STRENGTHEN.
Revision 1 said `--tests --no-fail-fast` REPLACES the `--workspace`
line. Measured: `--tests` selects 108 targets, `--workspace` selects
110, and the two it drops are `pmacs_protocol` and `pmacs_gpu`. On a
PROTOCOL bump, dropping the protocol crate's tests is the wrong loss —
and it also silently dropped `--skip basedpyright`.
Worse, this lane's own remediation sweep used `--tests`, so it never
ran pmacs-protocol's 25 tests, including the `scroll::classify` tests
this lane had just written. They pass (verified 25/0), but by luck. A
correction that reproduces the shape of the mistake it corrects is
worth naming, so §3 and §5 both say `--workspace`, additive, with
`-- --skip basedpyright` retained in both feature configurations.
P1 — NO COORDINATE CONTRACT FOR view_left. Revision 1 decided what
moves the viewport and never said what its offset IS — the same
omission as shipping WrapMode with no DisplayCoord. Its verification
sketch named tabs and wide characters with no oracle for either,
because nothing defined what a left edge is.
Q#HS7 is new and BLOCKING, in four coupled parts: the unit; which
columns may be a left edge; the snap rule for an invalid one; and the
invariant rendering and coordinate mapping share. Votes recorded —
display column (tab stops come free, since the walk must start at
column 0 either way and a byte offset buys nothing); a left edge may
not fall inside a wide glyph; snap toward the line start (snapping left
can only reveal a character, snapping right can hide the one the user
scrolled to reach); and snap when the value is SET, not in the painter,
so one canonical value serves both readers.
Part (d) is why it blocks: if the painter clips where the mapper does
not, clicks land on the wrong character — silently, and only on lines
wide enough to scroll.
P2 — THE CAVEAT IS NOT IN THE SETTING DESCRIPTION. Revision 1 said it
was. builtin/runtime/linewrap.lua:23 says only "truncate at the edge";
"unreachable" lives in the toggle's status message and a source
comment, neither of which a user sees who sets the mode in init.lua.
That is a real, small user-facing gap shipped in #221. Claim corrected,
and amending the description is now a Stage 4 deliverable (§6) with the
text depending on which stage has landed.
THE THREE ANSWERS, recorded with the reasoning that decided them:
HS1 — GPU is Stage 5. The distinction that matters is that Stage 3's
defect was never "the frontends differ" but "the frontends differ and
nobody chose that". Time box made concrete per the request: Stage 5
is the immediately-next QoL lane, `wrap` stays default until it
lands, release notes state the asymmetry, and the truncate
affordances name the GUI gap meanwhile.
HS2 — automatic only. No command surface; the cursor-visibility pass
gains a horizontal component.
HS6 — `wrap` stays default, and the reason given is stronger than the
one revision 1 reasoned from. I had framed it as "if scroll makes
truncate good, reconsider the default". With the GPU deferred, a
truncate default would ship a mode navigable in the TUI and a dead
end in the GUI for every user who never opened the setting. HS1 and
HS6 are coupled: the split is only safe because the default does not
move.
Q#HS4 is deferred rather than closed — not live under automatic-only,
but the snap-back hazard is real and rediscovering it costs more than
carrying the paragraph. Q#HS5 stands, with the caveat that §1.4 cites
the struct shape and not serde's behavior on a missing field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
#221 merged, so the long-lines lane is REWRITTEN rather than removed —
rule 4 removes a lane when its arc is done, and Stage 4 is ahead. Stage
3's durable facts move to the handoff §1, which is rule 4's actual
precondition.
THE FRAMING LEADS WITH THE FACT MOST LIKELY TO INVERT ITS OWN COST
ESTIMATE, because that is what Stage 3 revision 1 got wrong. The GPU
cannot honor horizontal scroll through cosmic-text: `Scroll::horizontal`
is discarded throughout, and not by oversight — glyphon 0.11 never
applies it when placing glyphs. Documented in three places and asserted
by three tests. So the GPU's half needs a mechanism that does not exist,
touching caret placement, decoration geometry, and hit testing, each of
which assumes x starts at `text_left()`. That is Q#HS1: whether the GPU
is in Stage 4 at all.
Also verified rather than recalled: there is NO horizontal scroll
anywhere in the tree (greenfield, not an extension); `paint_line` starts
every walk at column 0, so `view_left` enters the functions Stage 3 just
rewrote and the wrap rule must stay written once; `view_top` is
persisted per leaf at DESKTOP_VERSION 1; `scroll_window`'s comment
already records the cursor-follow hazard; and `goal_col` is unexamined
horizontal state on the same window.
Six questions, each with my vote and the argument against it. The one I
am least comfortable with is Q#HS6: Stage 4 adds capability that exists
only under a NON-DEFAULT mode, which is a conditional surface rather
than a uniform improvement — and Stage 3 chose `wrap` as the default
partly BECAUSE scroll did not exist. If scroll makes `truncate` good,
that default deserves re-examination rather than inheritance.
ALSO A GATE CORRECTION, and it changes what I said in 4d70ff6. I wrote
that "the touched acceptance suites" is the standing gate. That is
CLAUDE.md's list. `docs/agent-handoff.md` §3 — which CLAUDE.md tells me
to read FIRST — already required `cargo test --workspace -- --skip
basedpyright`, a full sweep. I ran the short list. So the eight broken
version assertions were not a gap in the documented gates; they were me
following a summary instead of the gate suite.
§3 now says so, and adds the protocol-bump form (`--tests
--no-fail-fast` in both feature configurations), because even the full
sweep stops at the first failing target and builds one configuration —
it would have shown one or two of the eight, not all of them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
CI red on #221: all five Test jobs, one identical test, every platform
— deterministic, not a flake. The production code was never wrong.
WHY MY GATES MISSED IT. The standing gate is "the touched acceptance
suites", selected from the diff. A PROTOCOL_VERSION bump breaks
version-assertion tests that appear nowhere in it. Worse, CI showed
only ONE of the eight, because cargo stops at the first failing
target; the rest surfaced only under `--tests --no-fail-fast`, and one
at a time would have cost four more red rounds.
Three of the eight were invisible even to that, because they are
crdt-gated real-daemon tests asserting on a live socket. Found by
`--tests --features crdt --no-fail-fast`. That is the handoff's
existing "a local sweep is blind to whichever configuration it does
not build" lesson, hit again by a different lane.
THREE TRIPWIRES, WORKING AS DESIGNED. `assert_eq!(PROTOCOL_VERSION,
21)` in statusline_segments, bottom_panel_stage2b_gpu, and
vterm_stage3 are meant to fire and take a deliberate edit; each says
so in its own comment. Updated to 22 with the reason recorded. Worth
noting the pin that must NEVER be edited —
ADVERTISED_PROTOCOL_VERSION == 20 — did not fire, which is the
mechanism behaving exactly as designed.
FIVE DEFECTS, ONE SHAPE: an absolute contract expressed as arithmetic
on, or equality with, a MOVING constant. Each was true when written
and silently false afterwards.
- `PROTOCOL_VERSION - 1` meaning "below the panel version". Held
only while PROTOCOL_VERSION == PANEL_MIN_VERSION; at v22 it
equalled PANEL_MIN_VERSION exactly, so the fixture's "old" peer
became panel-capable and the daemon correctly sent it a frame.
Now `PANEL_MIN_VERSION - 1`.
- `assert_eq!(PANEL_MIN_VERSION, PROTOCOL_VERSION)` — a coincidence
true only while panels were the newest feature. Replaced by the
two durable bounds: above the advertised floor, at or below this
binary's wire.
- `assert_eq!(PROTOCOL_VERSION, 21)` in a test named
`the_panel_stage_takes_protocol_v21` — the current wire as a proxy
for the panel stage's own version, in a test whose name says which
one it means. Now PANEL_MIN_VERSION.
- `session_protocol_version == "21"` in two real-daemon probes. What
the counter-offer activates is THIS BINARY's wire, so the literal
was only ever right by accident. Now PROTOCOL_VERSION, plus an
explicit `>= PANEL_MIN_VERSION` for the panel capability the
literal had been carrying implicitly.
The codebase already had the right idiom: src/daemon.rs and
pmacs-gpu/src/main.rs spell it `PANEL_MIN_VERSION - 1` in five places.
Every outlier was in tests/.
ALSO LOGGED, NOT FIXED: U2 in ci-red-signatures.md.
`process::tests::m6_1_pty_raw_mode_disables_kernel_echo` failed once
during a full corpus run and did not reproduce (108 targets exit 0,
plus 3 isolated --lib runs at 1917/0). It is in no registry row, so it
is a new incident, and leaked `pmacs --daemon` processes remain an
unexcluded rival explanation. Recorded with a selector this time —
unlike U1, whose name I destroyed by piping through `tail`.
Gates: fmt; clippy --workspace --all-targets -D warnings, both
configurations; --lib 1917/0; --lib --features crdt 2102/0; --tests
--no-fail-fast 108 targets exit 0; --tests --features crdt
--no-fail-fast 108 targets exit 0; PMACS_REQUIRE_GPU=1 -p pmacs-gpu
228/0; git diff --check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The standing correction from #171, #215 and #220: the lane block is
written with the first commit, so it says "no PR yet" until something
updates it. Recording the number here rather than leaving it to be
reconstructed from the branch.
No SHA — `githubsucks/long-lines` stays the authoritative tip, since
any edit to this block advances past whatever SHA it records. That
rule is the lane's own, from #220's review.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
Closes Stage 3's remaining obligations.
THE PTY TEST. Every other test in this lane checks a mechanism.
tests/long_line_readable_acceptance.rs checks the complaint: the
shipped binary, a real 80x24 PTY, one source line 200 columns wide,
and an assertion that its tail marker reaches the host. It bites —
`scripts/bite HEAD~2 src/editor.rs --test long_line_readable_acceptance`
against the pre-aa3cd4d editor never paints TAILZQX in 20s.
Its truncate control (an isolated init.lua pinning the mode) is what
makes that marker discriminating, and it is also the honest statement
of what truncate costs today: those bytes are not off-screen, they are
unreachable until Stage 4.
What it does not prove: the workspace has no screen model and no
vt100/termwiz/vte, so this shows the tail was WRITTEN to the terminal,
not which row a human would point at. That is nonetheless the whole of
the report — under truncation the bytes are never emitted at all.
§1.1 WAS WRONG, FOR NINETEEN REVISIONS. `editing.fill-column` is not
an orphaned registry setting "of the exact shape Stage 1 just fixed".
Both cited occurrences are inside `#[cfg(test)] mod tests` — fixture
names in round-trip tests covering one setting per ConfigKind. Two of
those five names are real; three, including this one, are defined
nowhere else in the tree. There is no shipped setting, so the Q#LL4
deliverable "sharpen its description" had no object.
The mechanism is worth more than the correction. A grep hit at a src/
path, a genuine `r.define(...)` call that is real API usage rather
than a mock, and `#[cfg(test)]` about fifty lines above the citation.
Every later revision inherited the conclusion instead of the evidence,
and three review rounds reasoned about the consequences of an orphaned
setting rather than re-checking that it existed. A file:line citation
is not a substitute for reading the scope it sits in.
Had it gone unchecked into implementation, Stage 3 would have shipped
an edit to a unit-test fixture believing it was rewording a
user-visible setting — a no-op with a misleading commit message.
§1.1 is withdrawn in place, keeping the original text and the
reasoning that produced it; §6's answer is unchanged (a setting that
does not exist is a stronger reason not to adopt it) and its premise
corrected. Both fixture sites now say they are fixtures. The approval
is not reopened: nothing else in the document rested on §1.1, which
argued for a display setting separate from fill-column — which is what
shipped.
AND ONE UNCLASSIFIABLE RED, logged as U1 in ci-red-signatures.md. A
`-p pmacs-gpu` run went 227/1 once; every run since is 228/0. The
failing test name was NOT captured, because I piped that command
through `tail -3` and discarded the failure block above the summary.
36 later runs are clean, 6 under deliberate concurrent load — which
per the rerun rule establishes intermittence only, and without a
selector not even that. Deliberately NOT matched against A1 despite
A1 also being GPU-headless-under-load: matching requires an exact
selector and every required fragment, and calling a shapeless red
"probably the known one" is the reputation-by-adjacency that file
exists to deny.
Gates: fmt; clippy --workspace --all-targets -D warnings; --lib
1917/0; --lib --features crdt 2102/0; line_wrap 6/0;
long_line_readable 2/0; folding 21/0; folding_stage2 48/0;
full_grid_resync 1/0; config_registry 16/0; m4 150/0;
PMACS_REQUIRE_GPU=1 -p pmacs-gpu 228/0 (see U1); git diff --check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
The GPU's readout reckoned in source lines while its window holds
visual rows: `format_scroll_indicator` compares `visible` (rows that
fit) against `current_line_starts.len()` (source lines). The GPU has
always wrapped, so this is not new in this lane — but the lane is
where it became nameable, because `ui.line-wrap` is now what decides
which formula applies. A one-line file took the first branch,
`total_lines <= 1`, and reported "All" with most of itself below the
window.
Under wrap it now goes through `pmacs_protocol:📜:classify`,
the same rule the TUI took in eaf3df8, with only the string spelling
local (framing §5d.6).
The load-bearing part is how `first_visible` / `last_visible` are
decided. Two cheaper predicates are available and both are wrong:
- `view_range.0 == 0` / `view_range.1 == len` describe the SHAPED
span, which carries SCROLL_OVERSCAN source lines past the window.
A slice reaching EOF says nothing about EOF being on screen. This
is the guess that broke extreme_sizes_render_with_contained_popups
when it was tried earlier and got reverted rather than shipped.
- `scroll_top == 0` ignores `code_scroll_residual`, so scrolling
into the middle of a wrapped first line still claims "Top".
So `code_byte_painted` asks cosmic-text where the byte actually
landed and intersects it with the drawable clip — `caret_rect`'s
existing test, generalized off the own cursor. Wrapped continuation
runs below the band and overscan lines shaped past the bottom both
fail it, because layout is what decides, not arithmetic over it.
`compose_status_runs` takes `&mut self` for this. That is the point
rather than a wart: the alternative is a cached per-frame
(first_visible, last_visible) pair, which is a value maintained
beside the layout and free to disagree with it — the same shape as
the `code_wrap` shadow field this lane already removed once.
One bug the tests found rather than confirmed. The first version
rejected an empty `view_range`, a guard borrowed from the caret and
completion-anchor paths where it means "nothing shaped". A file
ending in a newline has a final empty line, and a viewport parked on
it is `(len, len)` with one real row — so reaching the bottom of any
such file reported a percentage instead of "Bot". `code_byte_px`
already returns `None` when nothing is shaped, which is what that
guard was reaching for.
Bite, per clause. Replacing the pixel clip with the range test alone
fails a_wrapped_single_line_is_not_all,
a_slice_that_reaches_eof_is_not_yet_bot,
a_sub_line_residual_moves_off_top — and independently
extreme_sizes_render_with_contained_popups, the pre-existing test
that rejected this same shortcut before. Restoring the empty-range
guard fails an_empty_final_line_still_counts_as_bot and
a_slice_that_reaches_eof_is_not_yet_bot.
Gates: fmt, workspace clippy -D warnings, git diff --check,
PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu 228/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai