Commit Graph

360 Commits

Author SHA1 Message Date
Levi Neuwirth 74d735ff8d
docs: gate framing revision 5 --- pin both emitter pairs, correct print-plan
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
2026-08-09 18:11:14 +02:00
Levi Neuwirth 49bc141500
docs: record the acceptance run, and the red that came with it
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
2026-08-09 17:15:56 +02:00
Levi Neuwirth 898a98120c
docs: one normative home for the crdt sweep's build precondition
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
2026-08-09 16:39:35 +02:00
Levi Neuwirth 12affd78e1
docs: gate framing revision 3 --- a witness that can fail, and the ledger
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
2026-08-09 16:10:09 +02:00
Levi Neuwirth 4e02cb09b4
docs: gate framing revision 2 --- one normative home, a real witness
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
2026-08-09 15:57:47 +02:00
Levi Neuwirth 01316cbc74
docs: frame the scripts/gate --protocol build step (revision 1)
--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
2026-08-09 15:22:04 +02:00
Levi Neuwirth 4bc55e8dd2
build: scripts/gate — a target dir per worktree, and one gate suite (#225)
* 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>
2026-08-09 09:43:33 +00:00
Levi Neuwirth dcb852e740
fix(tests): bound the LSP fixture's project detection — retires R8 (#226)
* 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>
2026-08-09 08:54:06 +00:00
Levi Neuwirth b833b139e2
docs: retire the QoL arc's three lanes, re-homing their durable residue (#224)
* 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>
2026-08-08 13:10:24 +00:00
Levi Neuwirth 9a26ac8f9f
feat(gpu): horizontal scroll — QoL Stage 5, closing the long-lines arc (#223)
* 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>
2026-08-08 10:55:49 +00:00
Levi Neuwirth b29e94d29b
docs: the long-lines lane records PR #222
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
2026-08-07 22:44:49 +02:00
Levi Neuwirth bec8fc9aae
feat(view): horizontal scroll, text and decorations together
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
2026-08-07 22:43:17 +02:00
Levi Neuwirth 403fb7fb07
docs: Stage 4 revision 4 — the tab rule was already there
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
2026-08-07 21:23:23 +02:00
Levi Neuwirth baa71b607e
docs: Stage 4 revision 3 — a setter-time snap cannot exist
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
2026-08-07 21:14:58 +02:00
Levi Neuwirth 5d041a4f17
docs: Stage 4 revision 2 — three answers, and the contract revision 1 forgot
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
2026-08-07 21:01:46 +02:00
Levi Neuwirth b110684a62
docs: frame QoL Stage 4, horizontal scroll
#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
2026-08-07 20:46:39 +02:00
Levi Neuwirth 4d70ff6931
fix(tests): eight version assertions the v22 bump broke, five of them defects
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
2026-08-07 20:15:33 +02:00
Levi Neuwirth 8c64a4597b
docs: the long-lines lane records PR #221
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
2026-08-07 19:40:41 +02:00
Levi Neuwirth 8cd1da41ac
test: the report itself, at a real PTY — and §1.1 was wrong
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
2026-08-07 19:37:46 +02:00
Levi Neuwirth cad9393ef0
Merge main into long-lines
Stage 2 (GUI zoom, #220) landed. Stage 3 shares no code with it, so this
is a currency merge rather than a dependency --- taken now so the
Stage 3 PR opens against a base it has already been tested on.

# Conflicts:
#	docs/active-work.md
2026-08-07 10:27:04 +02:00
Levi Neuwirth a03786c9c9
docs: the classifier belongs where the frontends cannot disagree
Section 5d.6 answered as (b): ScrollPosition and the pure classify go
in pmacs-protocol, string rendering stays in each frontend.

The reading that settles it is sharper than "shared vocabulary". Each
frontend computes its own local layout facts --- which rows are on
screen is a question only it can answer --- while the shared crate owns
the common semantic decision those facts feed. That is not presentation
leaking into the protocol crate; it is the DECISION placed where both
frontends are structurally unable to disagree, with rendering left
where it belongs.

Two properties bound the change: no wire message and no protocol
version bump, since classify is a pure function over values each side
already holds; and the one-copy-fixed defect from 5d.3 becomes
unrepresentable rather than reviewer-guarded, because there is only one
classifier to fix.

Q#LL8 approved. All eight questions answered. Implementation begins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 23:02:40 +02:00
Levi Neuwirth 7b2f90c77e
docs: one signature cannot serve two contracts
Revision 16 said the scroll formatter could keep its signature and
change only what callers pass. It cannot, and the byte fallback in
revision 17 made that unavoidable rather than merely untidy.

Every branch of format_scroll_indicator derives from total_lines:

  total_lines <= 1                      -> All
  visible >= total_lines                -> All
  view_top == 0                         -> Top
  view_top + visible >= total_lines     -> Bot
  pct = (cursor_row + 1) * 100 / total_lines

Revision 17 removed the total. Passing byte counts makes the Bot branch
compare rows against bytes --- it would return plausible strings that
mean nothing. Passing any stand-in small enough to satisfy the guards
restores the false All this section exists to remove. And "local
predicates" is not something that signature can evaluate, because it
has no parameter for them.

Resolved by not asking it to. truncate calls the existing formatter
UNTOUCHED, with the same arguments in the same units, so its output is
byte-identical by construction rather than by assertion, and every
existing formatter test stays valid --- including the GPU's
format_scroll_indicator(0, 10, 1, 0) == "All", which correctly pins
line-space behavior. wrap calls a new classifier:

  classify(first_visible: bool, last_visible: bool,
           byte_pos: u64, byte_len: u64) -> ScrollPosition

All = first && last; Top = first && !last; Bot = last && !first;
otherwise Percent from bytes. No count of rows enters it, so the unit
mixing is not avoided but unrepresentable.

The actual mistake was trying to serve two genuinely different
contracts from one four-count signature. It could only do that by
making units implicit, which is how the contradiction arose. This is
5b.5's identity-case strategy applied to the indicator itself: the old
mode keeps the old code, and the new mode gets code shaped to it.

Leaves one question open. pmacs-gpu depends on pmacs-protocol only,
never on the pmacs lib, so format_scroll_indicator is duplicated
STRUCTURALLY and the classifier faces the same fork. Duplicating it
preserves exactly the condition that produced the earlier defect, where
one copy was fixed and the other was not. Sharing it through
pmacs-protocol makes the agreement structural rather than maintained,
which is the principle that chose byte-anchoring, additive sub_row, and
a content-derived cache key --- but it widens that crate from wire
vocabulary toward presentation, which is a COHERENCE section 16
layering question and not this lane's to settle alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:56:35 +02:00
Levi Neuwirth d6b5285234
docs: the GPU cannot count rows it never shaped
Revision 16 gave the GPU its scroll-indicator total on the premise that
"cosmic-text already knows each line's visual height". It does not.
rebuild_code_slice feeds cosmic-text current_text[vstart..vend] and
nothing else, because Session S1 found that feeding the whole rope made
large-file editing O(file) per keystroke. The GPU's layout holds the
viewport slice plus overscan; it cannot yield total visual rows, nor
the cursor's or top's row ordinal.

Re-shaping the whole document to recover them would reintroduce exactly
the cost that design exists to prevent --- for a status-line readout.

So the aggregate is abandoned rather than relocated. NN% is computed
from BYTE POSITION in both frontends; truncate keeps today's
visible-line percentage. No total, no cache, no invalidation.

Three reasons it is abandoned rather than approximated. Rows-per-line
could be computed as ceil(width / cols) without shaping, but
cosmic-text decides the real break points, so the number could disagree
with what is on screen --- the same approximate-parity trap Q#LL5
rejected for whitespace wrapping. Letting the TUI use row ordinals and
the GPU use bytes would show two percentages for one buffer, which is
this lane's own defect a third time. And All/Top/Bot are unaffected
either way: they are local predicates, they stay exact, and they are
the states a user actually reads.

This retires the cache from revision 15 and the fold-key correction
from revision 16. That correction was right for the design as it stood;
the design moved under it. Section 5d.2 is marked SUPERSEDED rather
than deleted, because "the key was fixed" and "there is no key" are
different states and a later reader should be able to tell which one
happened here.

Adds the large-file guard as a witness rather than an intention: an
indicator paint must leave the GPU's view_range and shaped_top
untouched, must not lay out beyond the viewport in the TUI, and
open_100mb_under_200ms must still pass with wrap as the default mode.
Without those, the decision is an unenforced comment and a later
"improvement" to a real row count would silently restore O(file) work.

Known imprecision, stated rather than left to be discovered: under
folds a byte percentage counts hidden bytes. Folding is TUI-only today,
and it matches Emacs. It belongs to the GPU folding lane, not this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:51:03 +02:00
Levi Neuwirth b95506f6ca
docs: LL8 fixed one frontend and keyed a cache on the wrong state
Two corrections to Q#LL8, both found by review of 1c9ff6a, and both the
same shape: a fix that read as complete because it was correct in one
of two places.

The GPU has its own scroll indicator. format_scroll_indicator is
DUPLICATED, not shared --- src/editor.rs:5509 and
pmacs-gpu/src/main.rs:10114, each with its own tests --- and the GPU
passes current_line_starts.len(), a source-line count. So revision 15
would have fixed the indicator in the TUI and left the GPU reporting
"All" for a one-line wrapped buffer.

That is worth naming plainly: it is this lane's own defect, reproduced
inside the section written to close it. The lane exists because two
frontends disagree for reasons nobody chose, and the fix disagreed for
exactly that reason one more time.

Both copies keep their signature. The formatter is a pure function over
counts and is correct as written; what changes is what the callers
pass --- visual rows rather than source lines, visible rows rather than
visible lines. Every existing formatter test stays valid, including the
GPU's own format_scroll_indicator(0, 10, 1, 0) == "All", which pins
line-space behavior and must not silently change meaning.

The lazy total's cache key omitted fold state. Folds are built per
rendered window and can be collapsed or expanded with no edit, no
resize and no mode change --- so all three of revision 15's key
components sit still while the projection underneath them moves. Cache
NN%, toggle a fold, and the stale total is served for the new
projection.

Now keyed on (buffer generation, content width, mode, fold projection),
and specifically on the projection's own components rather than a
revision counter on the fold registry. Components is one entry per
collapsed region, so comparing it is O(folds), and the key IS the thing
it guards --- it cannot be forgotten. A maintained counter can, on
every present and future mutation path, which is the same
did-you-remember hazard as LL7's buffer-switch trigger. Same principle
as byte-anchoring over a row index and additive sub_row over redefining
row: self-validating beats maintained.

Content width, not window width. Wrapping happens in the text area, and
the gutter's width changes at the line-count digit boundary (9 -> 10,
99 -> 100) --- something the GPU's sync_buffer_dimensions comment
already records for its own shaping. Keying on window width would serve
a stale total across that boundary.

Three witnesses added: the reported case in BOTH frontends, a "cache,
then toggle a fold" case that fails against revision 15's key, and a
digit-boundary case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:47:14 +02:00
Levi Neuwirth 1c9ff6a033
docs: the GPU had no wire, and "everything is local" was false
Two holes in revision 14, both found by review of this lane's own first
commit, and both would have been discovered during implementation at
much greater cost.

Q#LL7 --- the mode never reached the GPU. Section 4 resolves
ui.line-wrap into Viewport, which reaches the GRID renderer. The GPU is
not a grid consumer: it lays out locally, ignores CellDelta,
BufferSnapshot carries only CRDT bytes, and no InstanceMessage variant
expresses a wrap mode. So truncate would have changed the TUI and left
the GPU wrapping --- the two frontends still disagreeing, which is the
one thing this lane exists to fix. Q#LL5's "character wrap in both" was
equally unreachable: setting Wrap::Glyph at startup is not honoring a
mode that can change.

Specified as an additive variant at v22, appended after the current
final variant with the advertised baseline left at 20 --- the path
FontFacts took at v17 and the panel shapes at v21, and the baseline
constant's own doc reserves moving it for changes that cannot be
expressed additively. This one can.

It carries buffer_id, and is resent on attach, on config change, AND on
buffer switch. The third trigger is the one a FontFacts-shaped design
misses: font size is global, wrap mode is per buffer, so switching from
a truncate buffer to a wrap buffer changes the effective mode with no
config event at all. A design listening only to on_change is silently
wrong and passes every single-buffer test.

Q#LL8 --- the scroll indicator falsifies the locality claim. Revision
14 asserted every vertical consumer is local. format_scroll_indicator
is not: a one-line buffer wrapping to fifty screen rows has
total_lines == 1, so the first branch returns "All" while forty-nine
rows sit below the viewport. The indicator claims the whole buffer is
visible when almost none of it is.

The claim is narrowed rather than abandoned, because the distinction
that bounds the cost survives: a TOTAL is one number, lazily computed
and cached; a PREFIX-SUM INDEX is O(N) resident storage. Stage 3 needs
the first and still does not need the second. All, Top and Bot need no
aggregate at all --- each is a local predicate falling out of the
render walk --- so only NN% pays, which matters because the M1 gate
measures open time on a 100MB file.

Also fixes a notation hazard. Section 7 said pos_to_display returns
"the visual row", which reads as redefining row --- the exact thing
5b.5 forbids, stated two sections apart. Every wrap-point example is
now the explicit triple {row, sub_row, col}. Writing them out makes the
point visible: both coordinates at a soft break share the same row,
because a wrap does not cross a source line. That is the information a
redefinition would have destroyed, and the pair notation hid it.

Status returns to not-approved. Both questions change what gets built,
not how, so implementation waits on them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:38:51 +02:00
Levi Neuwirth bd752f242b
docs: frame QoL Stage 3, long lines
A line wider than the window cannot be read past the edge in the TUI.
The GPU is not in that state --- it already wraps --- so the
cross-frontend defect is not unreadability. It is that NEITHER behavior
was chosen: the TUI truncates because a cell walk breaks at max_cols,
the GPU wraps because cosmic-text's Wrap::WordOrGlyph default was never
overridden. Two accidents that disagree, and no way to state a
preference in either.

Eleven revisions of review, and the corrections were the substance.
Revision 1 claimed both frontends render from the same CellGrid, which
inverted the entire cost model --- pmacs-gpu ignores the grid variants
and lays out locally, and its own comment says so. Later rounds caught
an impossible round-trip invariant, a "collision" between two
coordinates that were one position, an off-grid argument against a
function that has no grid, a false binary for view_top, and a
renumbered visible-line space that does not exist. Each is recorded
with its reasoning rather than quietly fixed, because the pattern ---
an assertion that reads as precise while resting on something
unverified --- is more useful to the next reader than any single fix.

All six questions are answered and the framing is approved:

  LL1  wrap + truncate, default wrap; horizontal scroll is Stage 4
  LL2  buffer-local mode, resolved into Viewport like folds
  LL4  do not adopt editing.fill-column; ours is ui.line-wrap
  LL5  character wrap in BOTH frontends
  LL6  no global map; byte-anchored view_top; additive DisplayCoord

Two of those deserve to be found later rather than discovered:

GUI users lose word wrap. Character-wrap parity is cheap, true, and
matches Emacs, whose default wrap is also a character wrap. But the GPU
has word-wrapped since it existed and nobody opted into losing it. The
alternative was a UAX #14 dependency, because a whitespace-based grid
wrap would give only APPROXIMATE parity against cosmic-text's Unicode
line breaking --- which is worse than honest divergence, since it looks
unified until it is not.

The audit strategy is asymmetric deliberately. The coordinate functions
gain a required context parameter so the compiler enumerates every call
site; DisplayCoord gains an additive sub_row so untouched consumers
stay CORRECT rather than merely findable. Compiler-enforced where
enforcement is possible, correct-by-default where it is not.

Also corrects two ledger headers that still said OPEN for PRs merged
earlier today (#218 at 09:59Z, #217's absorption; #219 at 13:41Z).
Neither lane is removed --- rule 4 removes a lane when its ARC is done,
not when a PR merges, and the QoL arc has this stage left. Read each
block before cutting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 22:29:54 +02:00
Levi Neuwirth 95d17a11ea
docs: a tip SHA goes stale in the commit that writes it
The zoom lane recorded `1e054f7` as the authoritative tip. Committing
that line advanced the tip to b645fe7, so the claim was false before it
was pushed --- self-invalidating, not merely out of date.

The ledger already says this, twice: the docs-absorption and
signal-integrity lanes both note that any edit to their block advances
past whatever SHA it records, so the REF is the thing to trust. I wrote
a new lane without following the convention the file states about
itself.

Removes the SHA and says why, so the next lane inherits the reason
rather than the rule alone.

The base SHA stays. 218d2e7 is a merge commit that already exists and
nothing here can move it --- it is a fact about where the branch
started, not a claim about where it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 17:55:48 +02:00
Levi Neuwirth b645fe7f3b
docs: the zoom lane recorded revision 4 and three findings
The lane went stale across two review rounds. It said framing revision
4, 13 acceptance tests, and "the three findings review caught" --- all
true when written, none true now.

Brings it current: revision 5, the pushed tip, 15 tests, and the two
later findings. The quantization one gets a real entry rather than a
mention, because its content is a REUSABLE fact about this codebase ---
ConfigKind::Number validates finiteness and bounds and nothing else,
and on_change cannot veto --- so any future setting needing a stronger
predicate than a range meets the same wall.

Also records the verification asymmetry that mattered: the raw-step
bite fails the new witness while the pre-existing 0.37 test still
passes. That is why it had to be a separate test, and it is the kind of
detail a lane summary usually drops.

New section for pre-checkout CI reds, which #220 hit three times. The
job dies in "Set up job" before actions/checkout; grepping the full log
for checkout/cargo/test-result returns 0. No code is fetched, so the
red says nothing about the commit in either direction --- re-running it
is a first execution, not a retry-to-green.

It gets a section here and not a registry row on purpose: matching
requires an exact test selector plus fragments, and no test ran. Making
it a row would mean deforming the row shape. Whether the registry wants
a non-row section for the class is left to its owner.

Written down rather than acted on silently because the registry's
opening instruction is to read it BEFORE attributing a red run to the
environment, and that is exactly the attribution being made here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
2026-08-06 17:52:11 +02:00
Levi Neuwirth 8fd8f585aa
docs: the zoom lane records PR #220
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 16:22:05 +02:00
Levi Neuwirth aa99ab39d2
feat(zoom): GUI zoom over the font preference that already existed
Ctrl +/- had no effect whatsoever in the GPU frontend. Stage 1 (#219)
fixed what zoom did to the TUI; this is the other half.

NO RENDERING WORK. FontMetrics::scale already derived every GUI
dimension — code size, line height, status band, divider, menu rows,
minibuffer dropdown, gutter advance — and apply_font_facts already
re-metriced all seven buffers in one transaction. This drives the
preference that existed: two settings, three commands, and a restore.

Q#Z1 = (c). Relative zoom needs an origin and the daemon is built never
to know one — font_pref.rs is explicit that it "never learns metrics,
advances, or what resolves". Hardcoding 16.0 would put a pixel constant
on the daemon side; always sending a size would destroy the `None`
state for everyone who never zooms. A configured base is the only
option where the daemon still infers nothing, and the untouched path
stays byte-identical.

THREE THINGS REVIEW CAUGHT THAT REVISION 1 HAD WRONG.

Q#Z3 was not implementable as framed. `keymap_stack::Scope` is
Buffer | Mode | Global and carries no frontend identity, so "bind on
GPU frontends only" does not exist; and FrontendEvent has no
command-invocation variant, so the GPU cannot ask for a command by name
either. A global binding would capture the chord in the TUI and take
away the terminal's own zoom — the very thing the user is pressing it
for. Commands ship; the binding waits on capability-aware keymap
resolution, which is now a named follow-on rather than something
smuggled in here.

The restore seam did not exist. Builtins and init.lua both run BEFORE
install_state_dirs, so a pmacs.state.read at module load returns
nothing, always. saveplace and recentf never meet this because both
read lazily inside functions; zoom must apply with no user action,
which makes it this project's first eager state consumer. Restore lives
at the end of install_state_dirs — by definition the moment state
becomes readable, so it cannot be ordered wrongly and a future third
startup path gets it without knowing it had to ask.

Every size write clobbered the family. set_font replaces both fields
unconditionally, so { size = n } alone silently cleared a configured
family until restart.

BITTEN, THREE WAYS. Dropping family preservation fails 3 tests.
Reverting to the framing's own first parser `^(%d+)$` fails 4 including
the seam restore — it anchors to end-of-subject and rejects the
newline-terminated file the writer emits, which is the contradiction
review caught in the framing before it reached code. Hardcoding the
16.0 origin fails the base test.

Also recorded: a loaded crdt run failed two m6_1 PTY tests with
`stty -a output was: ""`. That is R4/R6's empty-content readiness
family, and it means the readiness-helper audit's scope is wider than
three wait_for_file copies under tests/ — src/process.rs's own tests
carry the shape. Undiagnosed, load-sensitive, green isolated and on a
quiet full run; a scope note for that lane, not a registry row, since
the registry judges red CI runs and these were local.

Verified: fmt, clippy, diff-check, --lib 1900/0, crdt 2085/0,
gui_zoom 13/13, journey 47/0, m4 150/0, gpu 221/0, full_grid 1/1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 16:20:46 +02:00
Levi Neuwirth 3361da08df
docs: the full_grid lane records PR #219
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:08:45 +02:00
Levi Neuwirth 899aaf2249
fix(frontend): honor full_grid — the flag existed and nothing read it
Zooming a terminal with Ctrl +/- left the TUI showing the previous
frame through the new one. Q#FG1 = A, as approved.

THE RULE WAS ALREADY WRITTEN DOWN, ON A PRIVATE FIELD.
src/instance_render.rs:36 says remote frontends "must blank their local
buffer before applying the deltas" — the binding contract, in the one
place a consumer author will never look. The protocol type said only
that full_grid marks "the initial sync ... versus an incremental
frame": a label, from which no obligation follows. So FG-INV now lives
on InstanceMessage::CellDelta, where whoever writes the next frontend
reads it. A resync is a picture of the screen's INK, not of the screen.

The producer diffs against a blank grid, so a cell that should be blank
produces no span. src/frontend.rs then took `CellDelta { spans, .. }`
and discarded the flag. That was correct for exactly one frame — the
fresh-attach frame, which follows Frontend::new's Clear — and wrong for
every resize after, which follows nothing. A font-size change is the
worst case because the terminal reflows in place rather than dropping
content, so the maximum number of stale glyphs survive.

emit_cell_delta joins emit_span and emit_status_overlay as a pure
helper over a writer; apply_message routes through it. No struct
change, no generic parameter, no new pattern.

WHY SEVEN TESTS MISSED IT. Every one asserts the producer SETS the
flag; none asserted a consumer ACTS on it, and no runtime reader
existed workspace-wide. "Add a test for the flag" had already been
done and did not help. Handoff §5's enforcement-vs-documentation drift,
in a second register.

Three unit witnesses, each bitten independently. The empty-spans case
earns its own test rather than folding into the others: under the
plausible `spans.is_empty()` early return the ordering test still
PASSES and only that one fails — and an empty resync is exactly the
frame whose entire content is the blanking.

The PTY acceptance drives a real SIGWINCH, and its mark is anchored to
CONTENT rather than time. A time-based settle was written first and is
unusable: a settled pmacs screen emits per-frame bytes forever, so
"output stopped growing" never becomes true. Anchoring just past the
first painted byte excludes both startup clears by construction —
Frontend::new clears before any frame exists, and the first frame is
itself a resync whose clear precedes its own spans. Bitten against the
original defect: 34,831 bytes after the first painted frame, no CSI 2 J
anywhere in them.

What it does not prove, stated here rather than found in review: the
suites assert on raw bytes, with no screen model and no vt100/termwiz/
vte dependency. This shows pmacs emitted a blank at the right moment,
not that the screen ended correct.

Verified: fmt, clippy, diff-check, --lib 1900/0, crdt 2085/0, m4 150/0,
gpu 221/0, and the grid-driving suites — full_grid_resync 1/1, vterm
1/2/3 9+9+5, m5_5 15, m5_8 5, bottom_panel_stage1 47.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 15:05:59 +02:00
Levi Neuwirth b172347945
docs: four review defects, and a documented error that never happens
1. THE CENTRAL RULE LACKED ITS TIME QUALIFIER. It said any red matching
a retired row is a recurrence — under which this PR's own R2 finding
reopens R2. Now: a match POSTDATING the retirement challenges the
disposition; a match predating it corroborates. That is not a
technicality. An occurrence scan reaches backwards by construction, so
most matches it finds are the earlier kind, and the old wording would
reopen every retired row the first time anyone scanned.

2. R5 AND R6 SAT UNDER "RETIRED ROWS" while declaring themselves live
and undiagnosed. I inserted them before R2's heading, which put them in
the wrong section — presentation contradicting classification in a file
whose whole job is classification. Moved under Live rows.

3. RECOVERY ANCHORS STILL DECLARED 12f2970, so the check accepted a
checkout lacking #216 and #217 while the same file described both as
complete. Advanced to db1bbe9, with the ancestry verified rather than
assumed.

4. THE HANDOFF'S DURABLE TREE FACTS STILL SAID IDS ARE OPAQUE AND
COMPARED BY EQUALITY — the contract ef99b64 deliberately narrowed. A §5
lesson explaining a correction does not fix a summary that still states
the uncorrected fact; the summary is what a new agent reads first.
Corrected there and in the header, which also still called Stage 2 in
flight and anchored main at f186253.

AND ONE FINDING FROM RE-EXERCISING THE RECOVERY PATH RATHER THAN
SWAPPING ITS SHA. This file claimed `git worktree add <path>
githubsucks/<branch>` fails with "fatal: invalid reference". It does
not fail. On git 2.55.0 it SUCCEEDS and leaves a detached HEAD — no
branch, no upstream.

Still use -b, but the reason is the opposite of the one recorded: the
hazard is not an error that stops you, it is that nothing stops you.
Work committed there sits on no branch and is not pushed by a bare
`git push` — the "uncommitted work does not travel" hazard wearing the
shape of committed work. A documented error message that never appears
is worse than no documentation, because the reader waits for a signal
that is not coming.

Verified: fmt, diff-check, --lib, listview 26/26. Recovery path re-run
from an empty directory at the new base; all four steps clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:40:54 +02:00
Levi Neuwirth d4d7ea605c
docs: the absorption lane records PR #218
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:18:43 +02:00
Levi Neuwirth d4931acba6
docs: this lane's own block, written before its PR
The ledger requires a lane for every open PR. #171 drifted 153 commits
while invisible here and #215 had no lane until review caught it, so
the block goes in with the lane's own commits rather than after someone
asks for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:18:05 +02:00
Levi Neuwirth 2c59fac510
docs: retire two arcs, and file two reds the scan turned up
RULE 4 REMOVES A LANE AFTER MERGE, and two were overdue: the tree
primitive (#217) and macOS CI signal integrity (#215, #216).

The macOS arc could not simply be deleted. It owned R1 and R3, and a
lane removed while it still owns undone work does not close that work,
it hides it. R1 goes to a new async-runtime block; R3 goes to the
reap-ledger lane, which already parks every disposition change pending
exactly its question. Re-homing first is why this did not happen at
merge.

AN OCCURRENCE SCAN (last 25 main runs: 23 green, 2 red) found both reds
unrecorded, and I misattributed both on the first pass — by theme
rather than by required fragment, which is the exact error the registry
exists to prevent.

- Run 30710662474 is NOT R3. Same test, same EPERM, same
  measured_group=unobservable(ESRCH...) — but R3 requires `leader=live`
  and this reads `leader=exited(signal SIGUSR1)`, R2's exact fragment.
  It is a second R2 occurrence, four days BEFORE R2's retirement, so it
  corroborates the row rather than falsifying its disposition. It also
  adds something: macOS luajit where R2's evidence was lua54, so the
  mechanism was never flavor-specific. Filing it as R3 would have
  attached a live possible product defect to an occurrence of a fixed
  test race.
- Run 30555667095 is NOT R1. Different test, different module,
  different assertion; they share only "supersede under a deadline on
  macOS". Sharing a subject is not sharing a signature. Filed as R5,
  undiagnosed.

R6 is the acc28 readiness timeout from #217's CI. Its scope is the
AUDIT, not the call site: three independently written readiness helpers
now exist and they disagree, with bottom_panel_stage1's carrying only
the zero-byte half of #216's hardening. R4's disposition predicted this
recurrence under a new selector. Patching acc28 alone repeats the
mistake this arc already made once, when the empty-file predicate was
fixed in one helper and left in its neighbour.

The registry carries a rate for the first time — a floor, not a
measurement: main only, 25 runs, readable reds only.

Four durable lessons to the handoff, two from the tree review: an
optional field the shape depends on is not optional, and a contract two
mechanisms must honour is only as strong as the weaker one.

Verified: fmt, diff-check, --lib, listview 26/26. Docs-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 10:17:46 +02:00
Levi Neuwirth 3f6a2837de
Merge githubsucks/main into tree-primitive-framing
#216 landed while this branch was open. Both conflicts are in the
ledger and the handoff, and both sides had independently written up the
same shared-CARGO_TARGET_DIR hazard.

THE TWO WRITE-UPS ARE NOT ABOUT THE SAME OCCURRENCE, and merging them
carelessly would have been a real error. Stage 2's is established: seven
failures against a clean baseline, failure text naming its own cause,
pgrep confirmation, and a dedicated-target-dir re-run at 41/41. This
lane's is the one whose signatures were destroyed before being read —
it has no captured text to match against Stage 2's, and it keeps two
non-causal hypotheses. A mechanism established in one occurrence is not
evidence about a different occurrence that was never characterized, so
the merged bullet says so explicitly rather than letting proximity
imply it.

The ledger records #216 merged and stops saying Stage 2 is in flight.
It does NOT retire the arc, though rule 4 now would: R1 belongs to the
async-runtime lane and R3 is an unresolved possible product defect for
the process-signal lane, and neither has a block yet. Re-homing them is
an absorption pass — not something to fold into a feature PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:42:03 +02:00
Levi Neuwirth f36b1fcf20
docs: the tree lane records PR #217
The ledger requires a lane for every open PR, and this one described a
hold that has ended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:29:27 +02:00
Levi Neuwirth d92f0ad4aa
docs: the tree lane was calling a pushed branch unpushed
It said "Unpushed while held" while githubsucks/tree-primitive-framing
sat at the same tip as HEAD. Held means no PR is open; it does not mean
the work is stranded locally, and the handoff's portability rule cares
about the latter.

The commit list also claimed to be exhaustive and then excluded the
commit that updated it — a list of that shape is wrong the moment it is
written. It is now the substantive arc, with `git log` named as the
place to get the complete one, and the remote ref rather than a pinned
SHA named as the authoritative tip: any edit to this lane advances past
whatever SHA the lane records, including this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:23:22 +02:00
Levi Neuwirth 62993e068f
docs: pin the tree lane to the review-round commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:17:41 +02:00
Levi Neuwirth c59de959e7
fix(listview): flat panels keep their TAB, and a selection test that bites
FOUR REVIEW FINDINGS, and the first two were right about the tests.

1. THE SELECTION ACCEPTANCE WAS VACUOUS. `tr_1` toggles the selected
root, which sits on line 1 before and after collapsing — so it passes
unchanged under the line-based re-seating that id-keyed re-seating was
built to replace. It proves collapse hides descendants; it proves
nothing about selection. `tr_4` adds the case that discriminates: an
`on_refresh` inserts a row ABOVE the selected node, so the node moves,
and the assertion is that selection follows the NODE. Bitten by
restoring `seat_cursor(p, saved)`: `tr_4` fails with left "  kid2",
right "sibling", while old `tr_1` passes — which is the finding,
reproduced.

The substantive assertion is deliberately ordered first. It was second
at one point, behind the fixture check that the node moved, and a
regression then reported as "the insert must move the selected node" —
reading like a broken fixture rather than a broken re-seat.

2. FLAT PANELS WERE NOT BEHAVIOUR-IDENTICAL. `bind_local_keymap` binds
TAB on every listview, so a depthless panel that previously fell
through to the global binding — and to Q#P3's read-only intercept —
began answering "listview: no node here". `listview.toggle` now
delegates to `buffer.tab` when no row carries an id, restoring the
prior path exactly; leaf feedback is kept for panels that really are
trees. `tr_3` asserts the absence of both tree messages rather than
merely that the panel still renders.

3 and 4 are documentation. The lane now lists 38e94dc, and no longer
says the PR is held "pending review of the documentation" that the same
commit supplied — it is held pending the decision to open it. §20 said
to BUILD the tree primitive while §14 already carried ◐; it now says
what actually remains, which is adoption: dired's `i` is the next
constraint source, DAP's variables view is why this was worth building
before them.

ONE RED, CLASSIFIED RATHER THAN RERUN AWAY. The crdt lib gate failed
`composition_overhead_under_ten_percent` at 30.7%. It is an incumbent
handoff hazard, and the branch cannot reach it — the diff versus main
touches no src/, no crate, no manifest. Alone it ran 5/5 green at
-0.6% to +0.2%; the next full run was green. Recorded in the handoff as
a MEASUREMENT, not a cause: five isolated greens establish that the
ratio is nowhere near the threshold when alone, not that contention is
what pushed it over. Not a registry row either — that file judges red
CI runs, and this was local.

Verified: fmt, clippy, diff-check, --lib 1896/0, --lib --features crdt
2081/0, listview 22/22, m4 150/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:17:33 +02:00
Levi Neuwirth 38e94dc33f
docs: give the tree lane its entry, and move Tree off ✗
Three updates, one of which was a broken cross-reference of my own
making.

THE DAEMON-LEAK ENTRY POINTED AT A LANE THAT DID NOT EXIST. It said the
unclassified failure was "recorded in the tree lane below"; this branch
had no tree lane. A pointer to nothing is worse than no pointer — it
reads as though the record exists and sends the next reader looking. The
tree lane is added, and since it now sits ABOVE that entry the direction
is corrected too, with a second pointer to the framing §6a where the
occurrence is recorded in full.

The lane carries branch, base, framing revision, every commit, the gate
table with both sweeps and their exact reconciliations, the bite
verification for both behavioural claims, the held-PR state and the
recovery command. It also names what is NOT in scope, because the four
unadopted §14 consumers and dired's `i` will otherwise read as omissions
from this stage rather than as later ones.

COHERENCE.md §14: Tree moves ✗ -> ◐, implemented with ONE consumer. Not
✓, and the row says why: the LSP outline is the only adopter, dired's
`i` remains the deferral in its §13, and the other four named consumers
have not adopted. The organising fact goes in the section rather than
only in the framing — folding is LOCAL PROJECTION STATE, NOT A REFRESH
PROTOCOL, which is why a consumer with no `on_refresh` can fold at all.
The §0 scorecard row moves with the body; a grade table disagreeing with
its own section is the defect this document keeps correcting in others.

The framing's status moves from "approved" to implemented-and-gated,
held for PR review, and four durable facts go to the handoff §1: folding
as projection state; identity being consumer-supplied and compared by
equality, with `line:col` chosen because the `::` parent chain collides
on overloads; `has_children` having to read the full row array rather
than the rendered subset, since a collapsed node's children are absent
from it by construction and the bug would look like fold working and
unfold silently not; and that a bite which passes validates the pair
rather than the test.

The daemon leak stays a separate reap-ledger candidate. It is not tree
scope, it predates this work, and folding it in would make a lane
responsible for a leak it did not cause.

Verified: fmt, diff-check, listview 21/21, --lib 1896/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 16:50:29 +02:00
Levi Neuwirth 8f64c3b2a1
test(listview): byte-identity for the flat consumers, and two findings
ACCEPTANCE 5, and it needed a real test rather than a weakened claim.
`listview_acceptance` says in its own header that the references panel
"needs a live LSP and is validated manually / via the m4 harness", so it
does not exercise `*references*` at all; the m4 hover test asserts
content PRESENCE, not exact output. Neither would notice a flat consumer
silently gaining an indent column — the regression a tree extension can
introduce. So the coverage is written against the real entry points
through the fake language server.

`*references*` is pinned EXACTLY: the row is the location string and
nothing else. `*lsp*` formats its own two-space indentation, so
"starts with a space" is not a violation there; what must hold is that
the primitive reproduces the consumer's text verbatim, matched as a
WHOLE LINE — a substring would still be found inside a further-indented
copy of itself. Volatile parts (pid, elapsed) are deliberately excluded,
the same normalization reasoning the CI registry uses.

THE FIRST BITE PASSED, AND THAT WAS THE FINDING. Injecting
`string.rep("  ", row.depth or 0)` did not fail the test — flat rows
carry no depth, so it added nothing. I had simulated a regression the
flat path is immune to and would have recorded the test as verified.
The regression this criterion actually guards is an UNCONDITIONAL
column, a fold gutter on every row; with that injected the test fails on
"the flat references row renders verbatim". A bite that passes validates
the pair, not the test — and injecting the wrong defect teaches nothing
while feeling like assurance.

A VERIFICATION RECORD, including one unclassified occurrence. The first
local crdt sweep of this branch reported 7 failures and its SIGNATURES
WERE DESTROYED before being read, piped through an aggregation that
emitted only totals. That is the failure the CI registry exists to
prevent, committed one lane after writing it, and it is why the cause
cannot now be established rather than merely being unknown.

It is recorded in this lane's own framing and deliberately NOT as a
registry row: that registry keys on a normalized signature, and an
occurrence with none would be granted a recognisability it cannot
support — the same reasoning that made the unevidenced incumbents audit
notes rather than rows.

Four re-runs are tabulated with what each supports. Two were not
isolated, including one where my own guard printed "aborting" and did
not abort. TWO GENUINELY ISOLATED RUNS ARE BOTH CLEAN, which supports
repeatability under isolation and establishes nothing about the cause.

Two mechanisms are recorded as NON-CAUSAL hypotheses, because both were
present and neither can now be tested: a shared CARGO_TARGET_DIR (whose
reciprocal case another lane observed independently, with `pgrep`
evidence and failing text that named its own cause), and ~40 resident
leaked daemons. Having two plausible mechanisms and no way to
discriminate IS the result; naming either would repeat the reasoning
this project has rejected — concluding something about an occurrence
from something that was not about that occurrence.

Both mechanisms are recorded as standing hazards in the handoff, and the
daemon leak gets its own candidate lane: 42 orphans, oldest four days,
reparented to systemd with deleted sockets, from
`gpu_invocation_acceptance`'s one-command tests, leaking 3-4 per sweep
as measured rather than estimated. It predates this work and belongs to
the reap-ledger family — a process outliving its supervisor with nothing
watching it — but the existing ledger arms only for `spec.group` and so
does not cover it.

Verified: fmt, diff-check, luajit sweep 3453/0 and crdt 3722/0, each
exactly +4 on its baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 16:31:31 +02:00
Levi Neuwirth 8ab20b5d68
docs(ledger): the second CI checkpoint, and the row a table cannot carry
Both heads that carry code are 14/14 green — `2d9c678` (the fixes and
the framing) and `668fc72` (this block). The tip row is explicit rather
than missing: a checkpoint table can never record the head that adds
the checkpoint, and #215's lane used the same convention. The branch
tip stays authoritative over any row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:51:28 +02:00
Levi Neuwirth 668fc72f23
docs(ledger): the lane's PR and CI checkpoint, and the sweep that could not attribute itself
Fills in what could only be filled in after the fact: PR #216, the
opening head's CI run, and the four-way sweep table. Both branch totals
are exactly +4 on their baseline — the four witnesses, and nothing else
moved.

**A shared `CARGO_TARGET_DIR` makes a local sweep unattributable, and it
bit this lane.** The branch `crdt` sweep first reported seven failures
in three suites while the baseline `crdt` sweep was clean. All three
spawn the REAL `pmacs` binary out of the target directory, and the
failure text named its own cause — "daemon does not advertise required
capabilities … start the daemon built with the `crdt` feature". A
concurrent `cargo test --workspace` in a DIFFERENT WORKTREE, at default
(non-`crdt`) features and the same `CARGO_TARGET_DIR`, had overwritten
`target/debug/pmacs` mid-sweep. Confirmed with `pgrep` while it was
happening, and discriminated by re-running the same three suites from
the same tree with a dedicated target directory: 41/41 green, then the
whole configuration swept again there. Recorded in the handoff's
standing hazards, beside the feature-blindness rule it rhymes with: a
feature-flavored binary is a shared mutable file, not an artifact
private to your invocation.

The clean re-sweep left ONE failure, and it is recorded rather than
rerun away: `lsp_dispatch_seams_acceptance acc33_...`. It is a new
incident by the registry's rules, and it is not attributable to this
branch on a STRUCTURAL argument rather than on its green rerun — the
only Rust change lives in `#[cfg(test)] mod tests`, which compiles into
the lib test target alone, so an integration-test binary linking the
non-`cfg(test)` lib is exactly what `main` builds. CI's `Test (crdt)`
job passed at the same head and runs that suite. The 15/15 repetition
set is the weakest of the five points, not the argument. No registry
row is opened: the registry judges red CI runs and keys on linked CI
occurrences.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 14:33:36 +02:00
Levi Neuwirth bd532dde6b
fix(test): retire R2 and R4 — two readiness predicates weaker than their assertions
Stage 2 of `docs/macos-ci-signal-integrity-framing.md` revision 3
(acceptance 6–9). Two test races, both the same shape: the thing waited
for was weaker than the thing asserted, so the wait could return inside
the window the assertion needs closed.

R4 — `wait_for_file` returned as soon as `fs::read` succeeded, which
succeeds on a ZERO-BYTE FILE. The probe publishes with
`open(path,'wb').write(b'1')` and `open()` creates the file before
`write()` fills it, so the helper handed `[]` to a caller asserting
`== b"1"`. It now takes the expected bytes and waits while the file
holds a STRICT PREFIX of them — the states a write in flight can be
observed in — returning anything else immediately so the caller's
`assert_eq!` stays the discriminating assertion rather than becoming a
timeout inside a helper that does not know what was expected.

All four callers pass their expectation. `wait_for_published_file`, one
function away in the same suite, gated the real-TUI smoke's
`assert_eq!(…, b"1")` on the identical predicate and is fixed with it:
leaving it would have let R4 recur under a different selector, which the
registry would then have had to judge a new incident.

R2 — the USR1 fixture waited on `ProcessEventKind::Started`, emitted at
SPAWN, not when `/bin/sh` has parsed `trap '' USR1`. SIGUSR1's default
disposition is terminate, so a signal inside that window kills the
child. The child now publishes a marker AFTER the trap and the test
waits for that marker's CONTENT (the same zero-byte trap applies to a
shell's `>` redirection). `exec` replaces the forked `sleep`, so the
group holds exactly one process and the ignored disposition survives by
POSIX rather than by the shell's fork-suppression optimization — an
unstated dependency the old fixture had, since these signals are
group-directed and a forked `sleep` is an untrapped group member.

Four witnesses, each verified by REVERTING the fix and observing the
failure rather than by reasoning about it:

- `wait_for_file_does_not_return_a_zero_byte_readiness_file` fails
  `left: []`, `right: [49]` — R4's two required fragments, verbatim;
- `wait_for_file_does_not_return_a_partial_write` fails on the torn read
  a length check alone would admit;
- `wait_for_file_returns_divergent_content_rather_than_timing_out` fails
  against an over-strict helper that waits for an exact match;
- `usr1_readiness_waits_for_the_trap_not_for_the_spawn` fails
  `left: Some("SIGUSR1")`, `right: Some("SIGTERM")` with the readiness
  wait removed. Its fixture sleeps before `trap` so the pre-trap window
  is deliberate rather than load-dependent, and it proves survival by
  the child's EXIT DISPOSITION rather than by an absence observed within
  a window.

R1 is NOT touched — referred to the async-runtime lane (Q#MCI3), because
widening its budget would make it pass and measure nothing more. R3 is
NOT touched and remains UNRESOLVED, owned by the process-signal /
reap-ledger lanes.

`docs/ci-red-signatures.md` moves R2 and R4 to a "Retired rows" section
with their dispositions and adds the rule the file needed and lacked: a
red matching a retired row is a RECURRENCE that puts the retirement in
question, never a known flake. `docs/active-work.md` carries this lane
from its first commit rather than after review asks for it.

Repetition sets, not single runs: the two `--lib` process tests 15/15,
the whole `vterm_stage2_acceptance` suite 15/15 at default parallelism.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 13:41:05 +02:00
Levi Neuwirth 2e0617fddd
review round 2: precision fixes, and a checkpoint table that cannot go stale
Three corrections.

"NO OCCURRENCE WAS EVER OBSERVED" OVERSTATED WHAT THE AUDIT CAN SAY.
Someone may well have seen one of these fail and simply not recorded it;
what is established is the absence of a RECORD. The framing now says
"linked or captured" and states the distinction explicitly, because an
audit that claims to know what nobody saw is making the same kind of
unfounded assertion this lane exists to remove — one level up.

A missing closing quotation mark in acceptance 3.

THE LANE CHECKPOINTED THE REVIEWED HEAD, WHICH GOES STALE ON THE NEXT
PUSH — the exact mechanism by which #171 became invisible while it
drifted 153 commits. Recording one head is a snapshot; what a resume
ledger needs is the sequence. It is now a table of head -> CI run ->
result, newest last, with the rule stated: the branch tip is
authoritative over any row, verified by `git rev-parse`, because the
table is written by hand and the tip is not.

Verified: fmt, diff-check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 11:11:57 +02:00
Levi Neuwirth f76897c284
review round 1: audit notes are not rows, and R2's signature was weakened
Five corrections, one blocking.

BLOCKING — the contract, not the implementation, was what needed
changing. Revision 2's acceptance 3 offered a binary: carry an incumbent
in with a signature and evidence, or remove it as never substantiated.
Stage 1 shipped a THIRD state because both incumbents are neither. The
framing is now revision 3 and names all three states, because a
governing criterion that says "two" while the branch does five is the
framing describing something that does not exist.

"MECHANISM NAMED" OVERSTATED THE EVIDENCE, and the phrase is retired.
The a33 audit proves an assertion string exists; the m6_8 audit proves a
test is timing-based. NEITHER ESTABLISHES A FAILURE MECHANISM — no
occurrence was ever observed, so nothing is known about how, or whether,
either fails. They are now AUDIT NOTES A1/A2 rather than registry rows,
with `R`-numbers reserved for signatures carrying linked evidence. The
distinction is not row-versus-weaker-row: a row says "this was seen,
here is the evidence", a note says "someone recorded a belief and no
occurrence backs it". Both remain unmatchable, so a red in either test
is a new incident.

R2's SIGNATURE WAS WEAKENED AND IS RESTORED. Splitting
`leader=exited(signal SIGUSR1)` into `leader=exited(` plus `SIGUSR1`
would match a child that exited by some OTHER disposition while SIGUSR1
appeared elsewhere in the output — precisely the name-style
over-matching this registry exists to refuse, reintroduced one level
down as fragment-style over-matching. It is one exact fragment again,
and the row says why.

THE HANDOFF STILL ISSUED LIVE IMPERATIVES. Its historical block opened
with "rerun isolated before treating a sweep failure as a regression"
and closed with "rerun the test alone before investigating", so the
supersession note I added sat between two instructions it contradicted.
Both are rewritten as record: the block now reports what that lane
OBSERVED, and the retired instruction is marked retired with its reason
— an isolated green reproduces nothing about a load-sensitive failure.

#215 HAD NO LANE, in the file that requires one for every open PR and
records why: #171 drifted 153 commits while invisible there. That is the
same defect, caught in review rather than 153 commits later. The lane
now carries branch, base, PR, reviewed head, what Stage 1 ships, the
verification, what Stage 2 owes, and the recovery worktree command. Both
snapshot headers are bumped.

Also narrowed a claim the PR body overstated: the registry is NOT
macOS-only. All four EVIDENCED rows are macOS, which is a property of
these occurrences; A1's job is `GPU Render (headless)`, which runs on
Ubuntu, and A2's job was never recorded. A future row from any job
belongs in the same table.

Verified: fmt, diff-check, clippy, --lib 1896, --lib --features crdt
2081.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 10:52:07 +02:00
Levi Neuwirth d33bf4df78
docs(ci): the red-signature registry, and an audit that found no immunity
Stage 1 of the macOS CI signal-integrity lane. `docs/ci-red-signatures.md`
is now the single authority for judging a red CI run.

NOT NAMED "FLAKES", DELIBERATELY. One of its rows is a possible product
defect, and a filename calling it a flake would confer immunity the
evidence does not support.

A ROW MATCHES ON SIGNATURE, NEVER ON TEST NAME. All three of selector,
job/flavor and every required fragment must hold. Fragments are
normalized rather than pasted: PIDs, elapsed times and rendered
OS-error suffixes vary between runs, so a verbatim key would match
nothing, and where a fragment lists alternatives (ESRCH / "No such
process") those are one condition rendered differently by platform. A
failure in a listed test WITHOUT that row's fragments is a new incident.

The process test is why that rule exists: it produced TWO signatures
with different mechanisms and different causal status, and only one is a
test bug. Four incidents, three tests, four signatures — the registry
counts signatures.

THE RERUN RULE IS REPLACED, NOT SOFTENED. A green rerun establishes
INTERMITTENCE ONLY — never environmental cause, harmlessness, or
retirement. The same signature again is a second occurrence and stays
blocking pending investigation or a merge-base control. A different
signature is a new incident.

RETIREMENT IS CAUSAL. A test race retires by hardening that removes the
mechanism plus a discriminating witness; a measurement-design row by its
owning lane replacing or justifying the measurement; an unresolved row
by diagnosis and disposition. Main-branch greens accumulate as
occurrence evidence and retire nothing.

THE AUDIT FOUND A THIRD CATEGORY the framing's acceptance 3 did not
anticipate. It said each incumbent is either carried in with a signature
and evidence, or removed as never substantiated. Both incumbents are in
between: the tests are real and the mechanisms plausible — a33's "blue
pixels" is a genuine assertion string at pmacs-gpu/src/main.rs:17973,
and m6_8 exists and is timing-based — but NEITHER HAS A LINKED
OCCURRENCE. Deleting them would discard a real observation; carrying
them as peers of the evidenced rows would grant exactly the reputation
this lane exists to deny.

They are recorded as "mechanism named, no occurrence recorded", with it
stated that nothing there confers known-flaky status and that a red
matching one is a FIRST recorded occurrence to be investigated. R6 goes
further: with no signature ever captured it CANNOT BE MATCHED AT ALL, so
a red in that test is a new incident by default. That is the correct
outcome for an entry that never carried evidence.

LIVE POLICY IS CENTRALIZED; HISTORICAL EVIDENCE IS NOT MOVED. The
handoff's hazards rule becomes a pointer. Its landed-lesson block at
§5 keeps its own evidence and gains a note that the registry's rerun
rule supersedes "rerun isolated". active-work.md's two mentions are
verification records for the reap-ledger and bottom-panel lanes and are
left alone; only the CI-weakness block's triage half points at the
registry, because the job-cost question there is genuinely separate.

Verified: fmt, diff-check, clippy with and without crdt, --lib 1896,
--lib --features crdt 2081, pmacs-protocol 19, m4 149, required GPU 221.
All three tests the registry names pass locally on Linux, which is
consistent with every row being macOS-only and is not evidence about
any of them.

Docs only. Stage 2 (hardening) is a separate PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 22:55:25 +02:00
Levi Neuwirth b2aa16d6ed
docs: retire the bottom-panel lane — Arc 7 is done
Rule 4 removes a lane when its ARC is done AND its durable facts are in
docs/agent-handoff.md. Both hold: #213 merged, and §1 carries the five
facts Stage 3 produced. The lane goes; the distribution and CI-CRDT
lanes stay, rewritten, because each keeps named follow-ons.

The canonical base and the RECOVERY FLOOR both advance to f186253. The
floor moving with the base is this file's own rule, and the reason is
worth restating: a check that accepts an older commit than the declared
base passes on a tree the rest of the file does not describe. The three
places that named cfc1710 — the base line, the prose, and the `git log`
verification step — now name f186253 together, because a floor that
advances in one place and not the others is worse than one that does not
advance at all.

TWO CI WEAKNESSES STAGE 3 EXPOSED, recorded in the crdt lane that owns
that job rather than lost with the lane being retired:

  * CI does not pass --no-fail-fast, so a multi-suite break reports as a
    single-suite one. #213's first crdt failure showed ONE suite where a
    local --no-fail-fast sweep of the same tree showed THIRTEEN. The
    Stage 3 census hit the identical trap and recorded it; CI has it
    too.

  * The crdt job pairs the heaviest build — lavapipe, full workspace
    with crdt, the largest test count — with real-PTY smokes on
    5-second deadlines. #213 saw two different such suites fail on two
    runs of the same commit; one was a real regression, the other was
    load. That ambiguity is the actual problem: a job where noise and
    signal look alike trains people to rerun rather than read.

Neither is a defect and neither is fixed here. They are decisions about
what the crdt job should cost, and they belong to whoever picks that
lane up.

Two ops lessons move to the handoff §5, both learned the expensive way
on #213:

  * A LOCAL SWEEP IS BLIND TO WHICHEVER FEATURE CONFIGURATION IT DOES
    NOT BUILD. Stage 3's census and every verification sweep ran
    `--features luajit` without `crdt`, so compile_mode_crdt_acceptance
    reached CI broken. Sweep both configurations before calling a corpus
    green.

  * "MY CHANGE MADE THIS FRAGILE" IS A DIFFERENT FINDING FROM "THIS WAS
    ALWAYS FLAKY", and only one of them is yours to fix. Two CI runs on
    one commit failed different PTY/GPU suites — the load-sensitivity
    signature, on suites the flake list already names. The tell that it
    was neither: the failures kept landing on GPU TERMINAL tests, and
    terminal placement was what the PR changed. Ask which tests and why
    those, before reaching for a rerun.

Docs only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 19:08:39 +02:00