Resolve review round 8 by making the withdrawn resource ordering
contract singular, assigning every revision-8 acceptance item to 2a,
and specifying one exact-pair LSP tombstone lifecycle.
Replace name/path equivalence with explicit buffer-name provenance,
pin both uncorrelated state creators and tombstone reclamation, correct
the independent-order test's bite, and refresh the active-work lane.
All seven citations re-verified against the tree; all seven hold, one is
understated. The split with #186 is untouched.
Revision 7's process read every mechanism it CITED but never read the
mechanisms it RELIED ON: it verified that tick exists and returns
settled ids without reading that the drain is a bare try_recv with no
execution token, and it verified that kill_buffer exists without reading
the order in which it mutates before it can refuse. Reading a symbol to
confirm it is there is not the same as reading it to confirm it does
what the surrounding argument needs.
F1 — reply order is not execution order. AsyncRuntime::tick
(src/async_runtime.rs:1003) drains the reply bus with no execution
token, so a worker can finish, be descheduled before sending, and have a
later reply arrive first. Asked the cheaper question first as directed:
reconciliation does NOT need to be order-dependent. Independent
mutations commute; interdependent ones cannot arise from any production
path (dired serializes, apply_resource_op is synchronous, the fs
primitives have zero production callers); and fs.lua:155-165 already
instructs callers needing ordering to serialize. So the guarantee is
withdrawn rather than engineered. A token under a mutation lock was
rejected: it serializes every fs mutation to close a hazard with no
production reachability and a documented caller-side remedy. No static
ordering rule is offered because none works — worked through in §6.
F2 — preflight, and the review understates it. kill_buffer clears
round_trip_buffers, closes side windows and redirects ordinary windows
before BufferRegistry::remove can refuse. Rev 7 said to treat the
refusal as "keep the buffer", which reads as though skipping phase 2
restored something; it does not. reconcile_delete now preflights
editing_in_progress — sound because phase 1 is pure EditorCore with no
Lua handle, so nothing can re-enter between check and removal.
Acceptance 53b is three separately-asserted properties, each with its
own named bite.
F3 — path-equivalence, not string equality. Names come from
path.display() as given while only file_path is normalized, so a
relative open leaves a short name a string rule mistakes for
user-chosen. Tested both directions; the custom-name arm is what stops
the fix becoming a name-clobberer.
F4 — bounded tombstone, and the census corrected this revision's own
first answer. A first pass checked only handle_notification and
concluded publishDiagnostics was the only uncorrelated writer. Wrong
lens boundary: mark_document_stale takes no LspServerId and creates URI
keys across three stores for every server. The full census also found
that diag_store has zero correlated writers and that
DiagnosticStore.by_uri is keyed by URI alone with no server component —
so the store most needing protection is the one a route purge cannot
help, and a (sid, uri) tombstone would not match it. Recorded as the
round's own defect class occurring inside the round.
F5 — three outcomes, because pmacs.minibuffer.read already has three.
Cancel does not fall through to on_no: for C, declining means copy the
non-colliding entries while cancelling means abandon, and conflating
them would make C-g perform a partial copy.
Sweep for the same defect class: three candidates, two cleared by
reading (run_all_must_succeed really does collect and not abort;
commit_to_refuses_an_await_and_restores really exists), one real and not
raised by review — the framing never said which HookKind the new hooks
register with, and short-circuit would let one subscriber silently stop
every later one reconciling. Both are now all-must-succeed.
P2: pmacs.lsp.forget_uri named with an error contract and pinned; the
ledger note rewritten — it claimed single-file scope while that same
revision edited the ledger — with both line counts measured at this tree
rather than quoted.
Status unchanged: PROPOSED, needs explicit user approval. No runtime
code. Do not merge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
PR #186 frames a guard for apply_resource_op's delete arm that refuses
before touching disk when any affected buffer is modified. Rev 6 said the
opposite for the same call site: the file is deleted, the modified buffer
survives orphaned, and §11 named that orphaning as accepted residue. Two
lanes, two answers.
Rev 7 adopts the settled split, recorded verbatim in the framing and in
the ledger lane so the two cannot diverge again: #186 owns the urgent
pre-filesystem refusal for synchronous apply_resource_op; #171 later owns
full post-delete lifecycle reconciliation, including the async race where
a buffer becomes modified after dired dispatch. Not re-litigated.
Census of orphaning claims: 20 matched lines resolving to 13 distinct
passages, classified by reading each enclosing passage rather than the
matched line. 4 ownership claims (this lane closes the LSP data-loss
defect) reassigned to #186; 4 policy claims (LSP intentionally deletes
and orphans) deleted; 3 ground-truth statements kept, because main really
does destroy unsaved work on that arm, but attributed to #186 as fixer;
2 async-race claims kept and narrowed.
Q#DR18 is rewritten, not withdrawn. The reconcile_delete seam survives
and is explicitly not cancelled. Three changes: the synchronous path
refuses before disk (#186 Q#RD1, adopted not re-decided); the policy
becomes symmetric rather than asymmetric; and the walk rule is adopted
from #186's Q#RD6 rather than reinvented — scan every path-bound buffer,
normalize once, component-aware Path::starts_with — which is
character-for-character what rev 6 wrote independently. Whichever lane
lands first owns the query, per #186's own boundary wording.
DeleteReconcile.kept_modified stays and is not dead. The synchronous
caller can no longer produce it; the asynchronous one still can.
Residue kept, and narrowed to one path: dired never goes through
apply_resource_op — it calls pmacs.fs.remove, which dispatches a worker
that an in-applier guard cannot see. So the LSP path has no residue and
dired's is its own. Rev 6 called it "one deferral, two paths"; that was
backwards. Also newly named: pmacs.fs.remove itself is guarded by
neither lane, since both guards sit one layer above it. Latent — zero
production callers.
The full post-delete lifecycle stays here, and #186 hands it over
explicitly rather than by omission: its Q#RD8 parks the window and
last-buffer defects and independently reaches rev 6's R4 finding that
kill_buffer and remove_buffer_and_fire clean disjoint sets; its Q#RD5
keeps reconciliation exact-path precisely so as not to promote those
defects tree-wide. That parked work is this lane's Q#DR27.
LSP failure handling: this document never made the claim — grep returns
10 lines, all about hook fan-out or dired's batch semantics, none about
WorkspaceEdit recovery. Verified the spec anyway and recorded it, since
the two framings will sit side by side: only textOnlyTransactional
degrades to abort for resource ops, transactional covers them, and LSP
3.18 states no default for a client advertising no failureHandling.
Rev 6's one adjacent claim — "refusing mid-edit leaves a half-applied
refactor" — was the load-bearing support for the deleted policy and goes
with it.
Noted without re-litigating: #186's Q#RD5 cites the ledger's "OPEN,
STALE, 153 commits behind, under re-scout" assessment of #171 in support
of taking the delete side now. That citation is stale — the re-scout is
done and this lane is integrated to ad41cf1 — but the conclusion stands
on urgency alone, so nothing about the split changes.
Status unchanged: PROPOSED, needs explicit user approval. Do not
implement, do not merge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Revision 5 was reviewed and not approved. Round 5 has one theme, not
six: rev 5 changed the slice split and the ownership of a decision, and
the prose did not follow. Four of the six findings are that same defect
in different places. All six verified against the tree before acting;
two are understated by the review.
R1 (P1) — rev 5 was superseded by its own ledger entry and by a PR body
older still. active-work.md:507 records Q#DR25 as moved out of this lane
(dired's paint is one instance of a CLASS bug — the same
erroring-intercept-over-writable-rope idiom is in listview, compile and
search/grep, and no Lua caller anywhere sets read_only because there is
no Lua set_read_only), while the framing still called it mandatory and
assigned it to 2b, and the PR body still described revision 1's
two-slice plan. All three reconciled: Q#DR25 is withdrawn, §3.1 becomes
a handoff to the generated-buffer-immutability lane, and the PR body is
rewritten. Also corrected in the other direction: rev 5's claim that
Q#DR25 "closes dired's quarter" of the §14 gap was itself wrong — dired
was never a quarter of anything.
R2 (P1) — acceptance allocation contradicted the code split, and the
review undercounts by one. Items 23-24 test apply_resource_op and the
drain harvest (2a substrate) but sat under a 2b header; item 33 needs a
dired.lua subscriber while 2a is defined as containing no dired code.
23-24 moved to 2a; 33 moved to 2b, because 2a's whole review rationale
is that its diff contains no dired code. The cost is stated: between 2a
and 2b a directory rename leaves dired handles stale, which is the
status quo and not a regression. The undercount: that leaves 2a shipping
resource.renamed with no acceptance, so item 50 pins the hook contract.
R3 (P1) — TickOutcome could not carry deletions. Replaced the two ad-hoc
vectors with one ordered Vec<ResourceOp>, and PendingJob carries a
single Option<ResourceOp>. Ordered because a directory rename and a
delete beneath it can settle in one tick; one enum rather than two
Options because two admit a both-Some state that cannot occur, the
argument ResolvedTarget's own doc makes at editor_core.rs:100-102.
R4 (P1) — reconcile_delete stopped short, and the substrate is worse
than the review says. Right that removal is two phases; what it does not
say is that NO existing Rust path composes them. pmacs.buffer.kill
(mod.rs:5476-5491) does, and its doc says it is late-bound precisely to
redirect windows — but apply_resource_op uses remove_buffer_and_fire,
which is phase 2 without phase 1, and BufferRegistry::remove touches
only buffers and order. So an LSP-authored delete leaves a displaying
window pointing at a removed id: a third defect on that arm. §6 now
specifies both phases, the last-buffer and ConcurrentEdit refusals, and
items 51-53.
R5 (P2) — the "every consumer" claim was false for Lean. The deferral
was right and the summary was wrong; fixed the summary. Stage 2 supplies
the hook, Lean's state stays stale until its owner adopts it.
R6 (P2) — pre-three-slice file inventory. minibuffer.lua joins
src/editor.rs's include_str! sequence, so 2b touches a Rust file; "2b is
dired.lua plus one killring binding" was false. + and C retagged 2c.
The sweep the review asked for caught four more stale labels it had not
cited: §4's "C (copy, 2b)", §7's "(in 2b) a recursive delete", §8's op
sections, and §8's d/x line — which was wrong in BOTH halves, saying 2a
where it meant 2b and 2b where it meant 2c, a pure two-slice-era
leftover. Historical round sections keep their original labels, flagged
rather than retconned.
The ledger lane rides this PR and is updated to match.
Status unchanged: PROPOSED, needs explicit user approval. Do not merge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
The lane described the pre-re-scout state: head ab42a79, 1,570 lines,
"re-scout under way", and a five-owner rename census. All four are now
wrong. It rides this PR rather than a standalone refresh, because a
separate ledger PR with several others open re-conflicts on every merge.
What the lane now records:
- Head e7f811b, 2,304 lines, integrated to main @ ad41cf1, status
PROPOSED and never formally approved — four revisions and three review
rounds are not approval and must not be read as one.
- Seven of rev 4's own claims about pmacs were wrong. The load-bearing
one is kept in full because it is a trap anyone can repeat:
drain_external_cancelled (:1561) and drain_cancelled_externals (:1596)
are two different functions 35 lines apart, and rev 4 named one while
citing the other's line. Following the citation gives the per-tick
token sweep instead of the unconditional server-scoped drain, so the
drain half becomes a silent no-op and the awaiting coroutine hangs.
- The path-owner census is six, not five. The sixth is lean.lua's
M.file_progress, a URI-keyed Lua table in no Rust store, which is the
first evidence outside dired that the hook rather than the Rust method
is what scales.
- The journey ratchet is split across journey_acceptance.rs and
gpu_invocation_acceptance.rs; both are gates, and a scout checking only
the first concludes #183 added nothing.
- open_directory now commits under pmacs.window.commit_to, whose scope
refuses an await, which constrains the proposed batch contract.
Scope moved OUT of this lane: Q#DR25's adoption of
set_generated_contents turned out to address a class bug shared with
listview, compile and search/grep, so it is owned by the
generated-buffer immutability lane now. This PR needs a rev 6 deferring
Q#DR25 there once that framing is approved.
Also records two live bugs this lane confirmed but does not fix, both
owned elsewhere: apply_resource_op's delete arm destroying unsaved work,
and View still lacking rename_resource.
PR #185 merged during the re-scout, so `docs/active-work.md`,
`docs/agent-handoff.md` and `COHERENCE.md` are no longer held by an open
PR. #185 changed docs only — no source — so nothing in rev 5's census
moves, and the two facts rev 5 cites from the durable records survive
intact: the handoff §4 inventory still names `builtin/runtime/dired.lua:371`
as a non-adopter of `set_generated_contents`, and COHERENCE §14 still
names dired and still classifies it as the cheap half.
This branch continues to touch only `docs/dired-stage2-framing.md`. A
standalone docs-refresh PR is exactly the ledger contention treadmill
the ops lessons warn about, and the ledger's own protocol puts a lane
refresh with the work rather than ahead of it.
What §16 now records instead: the landed `docs/active-work.md` lane
still has this branch at head `ab42a79`, calls the document 1,570 lines,
says the re-scout is under way, and states the rename census as FIVE
path owners. Rev 5 makes all four stale — and the census is SIX (§5,
W5). Same lesson in the other direction: a census is a reading, not a
constant. The refresh should lift §5's table rather than re-derive it,
and should note that Q#DR25 added scope the lane does not mention.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
`main` moved mid-re-scout: #174 (fix-m4-sink-races) merged. It touches
`tests/m4_acceptance.rs` only and changes no source, so nothing in the
rev 5 census moves — but it intersects this document at one point, and
in the direction that strengthens it.
#174 fixed the CONFIG sink by waiting for a complete JSONL record
(`ends_with('\n')` instead of `contains("probe")`) and deliberately left
the ROOTURI predicate alone, adding a comment at
`m4_acceptance.rs:5486-5496` giving the same three reasons §11's
deferral gives: it is the same weak-predicate class, it has never been
observed failing, and waiting for the expected value would trade a
precise regression diff for a vague five-second timeout. Closing it
needs a record terminator in the fake server first.
So that deferral is no longer a claim of this framing; it is a claim of
the tree. Item updated with the current line (`:5499`, shifted by
#174's own comment) and the in-tree backing.
The header now records both anchors: the census was read at `6bee09d`,
and it holds unchanged at `0442d78`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
Rev 4 was scouted at c8ec8f3, which is dired Stage 1's own merge commit.
Canonical main has moved 153 commits since. This is that re-scout. Every
claim below was read on the tree at 6bee09d, not inferred.
Four dependencies arrived that rev 4 could not have known about:
- #178 landed `Buffer::set_generated_contents`, and dired has NOT
adopted it — the handoff §4 and COHERENCE §14 both name
`dired.lua:371` in the non-adopter inventory. Dired pairs an erroring
intercept with a bypass_intercept write over a still-writable rope, so
`M-x buffer.undo` empties a listing today, with no keybinding needed.
Stage 2 writes that buffer on every mark. New decision Q#DR25 adopts
the primitive at the head of 2b, with acceptance items 48-49.
- #182 (Journey Stage 1a) demoted dired to a replaceable slot
(`pmacs.path.directory_handler`, deliberately not a hook subscriber)
and rewrote `open_directory` around `pmacs.window.commit_to`, whose
scope REFUSES an await. That constrains §9's serialize-and-await batch.
- `tests/journey_acceptance.rs` is a declared ratchet (24 tests, "stages
add rows, none removes them"), seven rows assert on dired, and #183
put the GPU row in `gpu_invocation_acceptance.rs` instead — so the
ratchet is split across two files and both are gates now.
- #179/#181's typed-edit chain: dired participates in neither it nor
`buffer.after-edit`, and `set_generated_contents` fires no hook, so
Q#DR25 does not drag dired onto the chain. The chain's lessons still
bind §5's two NEW hooks.
Seven of rev 4's own claims about pmacs were wrong:
- W1 (load-bearing): §5 named `drain_external_cancelled` and cited
`lsp.rs:1596`. Those are two different functions.
`drain_external_cancelled` (:1561) is the unconditional server-scoped
drain and is the right precedent; `drain_cancelled_externals` (:1596)
is a per-tick token/timeout sweep. A rename flips no token, so
following the line number yields a `forget_uri` whose drain half is a
silent no-op and the awaiting coroutine hangs forever — the exact
failure step 2 exists to prevent.
- W2: there is no `fn restart`. The teardown is `start_generation`
(:1307-1345), and there is a second precedent rev 4 never named,
`LspManager::forget` (:3015-3042). Neither clears the 14 result
stores, so rev 4's "surprising" note now holds twice.
- W3: `ResponseRoute` has 15 variants, 14 URI-bearing; there are 16
insert sites, 15 URI-bearing. Rev 4's fifteen was a correct count of
URI-bearing inserts and an incorrect count of sites. The purge
predicate must retain `WorkspaceSymbol { query }` explicitly.
- W4: `rec.uri` is read at 57 lines in lsp.lua, not "~20".
- W5: the path-owner census has grown to SIX. `lean.lua`'s
`M.file_progress` is a URI-keyed Lua module table in no Rust store, so
`forget_uri` cannot reach it — independent evidence that the hook, not
the Rust method, is the mechanism that scales.
- W6: the ledger note named PR #169, which merged. #185 holds the
durable records now.
- W7: the C1 seam is at mod.rs:7104-7115, and mod.rs defines three
`_tick` bindings in different classes.
The 2a/2b/2c/Stage-3 cut was re-examined and holds unchanged; §16 adds
the ownership warning that 2a overlaps editor_core.rs, lsp.lua and the
URI-keyed LSP state with Journey Stage 1b and must not run concurrently
with it. §0.5 now cites COHERENCE §20 Priority 1 and §14 by number, and
records that #182 put dired on journey step 3 — a claim rev 4 could not
make.
Status is stated honestly: PROPOSED, never formally approved, and
needing explicit framing approval before any implementation branch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
#174 merged while this PR was open, so the lane it had just been given
is already a merged lane. Rule 4 removes those rather than relabelling
them — but only once the durable facts are in the handoff, so do both
here in that order.
docs/agent-handoff.md
- §5 gains the lesson: a wait predicate WEAKER than the assertion it
guards is a race on whichever platform loses it, and "wait until the
file mentions X, then assert Y" races whenever Y is stricter than X.
- Two things that generalize past the fix ride with it: a race you
cannot reproduce can still be bitten at one remove (an unsatisfiable
predicate proves the wait is load-bearing; the old predicate still
passing proves a local run cannot tell them apart), and the obvious
fix is sometimes worse — the sibling m4_26 is deliberately left alone
because closing it the easy way trades a precise assertion diff for a
vague five-second hang.
- Header and §1 anchor advance to 0442d78, noting #174 is test-only and
#184 remains the substantive change at that head.
docs/active-work.md
- Remove the #174 lane; the header paragraph now says where it went.
- Canonical base and recovery floor advance to 0442d78. The floor
explicitly rejects 6bee09d as well as 7fd646d, per its own rule that a
check accepting an older commit than the declared base passes on a
tree the file does not describe.
- Bottom-panel lane: 2B-2 is IN FLIGHT, not merely "next" — branch,
worktree, and the fact it is branched fresh rather than stacked. Its
boundaries are restated because they are easy to overrun: production
Hello stays v20, panel_capable stays false, and 2B-3 may not simply
bump the unsolicited Hello.
- dired lane: the re-scout is under way on the existing branch, so #171
keeps its three-round history and the product is a rev 5.
- Both lanes note that main advancing to 0442d78 changes nothing for
them, since #174 is test-only.
The bottom-panel framing doc keeps its 6bee09d ground-truth line: that
is a scouting statement about what was actually read, and re-anchoring
it to a commit nobody re-scouted against would be a false claim.
Neither #174 nor #171 had any entry in this ledger. An open PR is
exactly the volatile work the file exists to record, so its absence is a
defect rather than a tidy omission — #171 drifted 153 commits while
invisible here, and its still-green old CI run describes a tree nobody
has looked at since.
#174 — M4 config-sink race, revived and regated. Records the predicate
fix, the gate numbers on the integrated head 302c21c, and two things
verified rather than inherited: the "one writeln! is the only writer"
argument re-checked against a fake_lsp that DID change upstream, and the
wait bitten two ways (unsatisfiable predicate fails on the pump deadline;
the old predicate still passes locally, confirming a green local run
cannot tell them apart).
#171 — dired Stage 2 framing, stale, do not merge as-is. Measured 4
ahead / 153 behind with merge base c8ec8f3, which is itself #165's merge
— so dired Stage 1 and find-file are its base, not new arrivals. Names
the three landed changes that move ground under it: #178's
generated-buffer write invariant (dired is a non-adopting writer),
#182's demotion of dired to a replaceable directory-handler slot, and
#179/#181's typed-edit consumer chain. Restates the five-path-owner
rename transaction the framing still has to answer.
Also records that three review rounds in a commit history is not the
same as approval, and that dired 2a's file overlap with Journey 1b needs
assigning before either runs.
PR #184 (bottom-panel Stage 2B-1) merged, but the four documents that
new work is supposed to start from still described it as open. Advance
every canonical anchor and close the one obligation that was blocking a
lane removal.
docs/agent-handoff.md
- §1's head-of-main anchor moves 7fd646d -> 6bee09d.
- State schema support and production advertisement as two separate
facts: SUPPORTED is now v6..=v21, the server-first Hello still says
v20, and #184 landed only the first.
- New §1 bullet for Stage 2B-1 carrying its durable substrate: the
server-first incompatibility argument, the shared wire_grid boundary
and why per-axis caps are a parameter, authoritative Absent, and the
panel_epoch / geometry_epoch split.
- New §1 bullet for the PTY terminate diagnostic (#176), plus two §5
ops lessons: a drain that ticks is not an observation, and how to
prove a child exited without waitid or unsafe.
docs/active-work.md
- Canonical base and the recovery floor both advance to 6bee09d.
- Remove the #176 lane. It was retained only because rule 4 requires
its facts to reach the handoff first; that move is in this commit, so
the lane goes.
- Rewrite the bottom-panel lane as 2B-1 MERGED with 2B-2 next, branching
fresh from landed main rather than stacking.
- Annotate the documentation lane with measured staleness (1 ahead, 320
behind) without deleting it, since nothing has decided its fate.
docs/bottom-panel-stage2-framing.md
- Status header records 2A and 2B-1 as merged and 2B-2 as next. No
revision bump: the header is landed state, and no design decision in
the document changed.
COHERENCE.md
- §16 names #184 as the PR that landed the reserved v21 family.
No runtime code, no protocol change, no gate suite implicated.
Capture the follow-up review finding, the evidence-driven completion
contract, the exact corrected CAT duration, and the proportional green
gate matrix at 9c79ce1.
Capture the exact review-fix and GPU probe checkpoints, the full green
gate evidence, and the classified sandbox-only socket failure in the
cross-machine active-work ledger.
Reserve the additive v21 panel schema without advertising it in the
server-first production handshake. Pin a real shipped-v20 client attach,
make the two aggregate-budget ratchets exactly one byte over, and update
the framing, coherence audit, handoff, and volatile lane record.
Record the complete post-#183 gate matrix for bottom-panel Stage 2B-1
at c8895a8 and mark the lane ready to open for review.
Retain the required-GPU first-pass classification: one unrelated math
render assertion failed, passed immediately in isolated single-threaded
execution, and the mandatory complete 202-test rerun passed.
Integrate main through the Journey/GPU directory-target ratchet (#183).
The public managed-GPU probe additions compose with the v21 wire layer.
Resolve the volatile ledgers to the landed state: advance the canonical
anchor to 7fd646d, remove the completed ratchet lane, preserve the
approved Stage 2 revision-5 recovery facts, and mark the integrated
2B-1 tree as awaiting its from-start full gate rerun.
Drive pmacs --gpu . through the root broker and real managed GPU
connector, keep the session alive through the asynchronous dired
replacement, and assert its canonical listing before daemon reuse.
Expose snapshot count and materialized text through the private
display-less acceptance probe so the public path is observable.
Consume the directory session's later replacement snapshot and assert
the canonical dired header plus a known listing entry before checking
daemon reuse.
Correct the bottom-panel revision-5 recovery branch and advance the
durable handoff to the Journey Stage 1a main anchor.
Capture the complete green gate matrix and retain the diagnosed setup
and transient full-sweep failures so the lane remains recoverable and
the evidence is not flattened into an unexplained rerun.
Replace the stale directory-negative in the GPU initial-target suite
with an explicit snapshot-first readiness path, while retaining all
genuinely malformed and unloadable failure cases. Record the portable
side-quest and bottom-panel dependency state.
Make the statusline and Vterm Stage 3 acceptance suites track the
bottom-panel v21 bump, including the real daemon and headless GPU probe.
Record the full gate result and the unrelated stale directory-target
assertion reproduced on canonical main.
Integrate the Journey Stage 1a merge without rewriting the already
reviewed protocol branch. Record the approved three-way Stage 2B split,
advance the canonical recovery anchor, remove the landed Journey lane,
and put 2B-1 into its full-gating state.
The ledger preamble already says Lean 4's merged lane was removed, and
the durable Stage 4b facts already live in the handoff. Remove the stale
section that still called Stage 4b in review so PR #182's post-merge
state is internally consistent.
Rev 4 §9 scoped Stage 2B as a single PR: v21 protocol, daemon panel
projection, GPU band, and the negotiated `panel_capable` flip.
Implementation showed that to be roughly four thousand lines across
three crates with three different failure modes. Rev 5 splits it into
2B-1 (the wire layer), 2B-2 (the daemon projection and epoch machine),
and 2B-3 (the GPU band and the flip), on the rule that a slice ends
where the next thing to build has a different authority.
No decision changes. What changes is the allocation:
- §7.2 becomes three subsections, and criteria that span a boundary are
named in every slice they touch with their half stated, rather than
assigned wholesale to one. Parent 39 is the clearest case: its
shared-validation and transport-budget halves are wire properties
provable in 2B-1, while "the previous valid frame is retained" and "a
duplicate does no work" need the epoch machine and are 2B-2. A2B-1
splits the same way — grid exhaustion in 2B-2, the frontend latch in
2B-3.
- §9 lists four serial PRs instead of two, each cut from `main`, and
states that every slice runs the full gate set rather than the subset
its own crate suggests.
- §6 records which slice pays the coherence debt. The journey claim
belongs to 2B-3 alone: with `panel_capable = false`, a GPU user still
gets the Stage 1 non-side fallback after 2A, 2B-1 and 2B-2 have all
landed. Three quarters of this stage is preparation.
Two things recorded because they are easy to inherit silently:
- This revision is retroactive for slice 1. `bottom-panel-stage2b`
already carried the v21 protocol layer, written before the revision
existed, which inverts framing -> approval -> branch -> implement. The
slicing was sound; taking it in code rather than in the document is
how a stage's scope drifts without anyone deciding that it should.
- 2B-1 and 2B-2 ship dark. The bump advertises a capability whose only
distinguishing feature is unreachable until 2B-3, so the arc must not
stall between them. Safe for compatibility — appended variants,
extended ladder, a v20 peer still negotiates 20 — but a stall should
be visible as a decision, not inherited as a default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Two open documentation PRs editing the same three ledger files
re-conflict on every merge to `main` — the treadmill #169 and #176 each
paid three resolution rounds for. #180 was docs-only and already
CONFLICTING against `main`; folding it here retires one of the two
writers instead of resolving the same file twice.
Also integrates `githubsucks/main` @ `42025e4` (Lean 4 Stage 4b #181).
Conflicts resolved, and three claims corrected rather than picked:
* **The head-of-`main` anchor.** Both sides named `74301d1`, which is no
longer true — `#181` has landed. The anchor now reads `42025e4` and
leads with it, in `docs/agent-handoff.md` §1 and the active-work
canonical-base line, and **the recovery floor moved with it**: the
`git log` check requires `42025e4` or newer, per that section's own
rule that a floor accepting an older commit than the declared base
passes on a tree the file does not describe.
* **The Lean 4 bullet contradicted the anchor I had just written.** Its
header still said "stages 1, 2, 3a, 3b LANDED" and its Stage 4b
sub-bullet still said "implemented and in review", while the anchor
above it now records #181 as merged. Both corrected: all six stages
landed, #181 named.
* **The COHERENCE §2 grade.** #180's side still carried "broken at step
3" / "Broken at entry" in both the ground truth and the §0 scorecard;
this branch's corrections win, since they are what the code change in
this PR makes true.
The removal note at the top of `docs/active-work.md` is reattributed:
the terminal config + copy mode lanes were #180's removal, and it now
says so and says why the fold happened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A count is a reading, not a constant. Recorded as of rev 8 with an
explicit instruction to re-read rather than quote the line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 1 of PR #182. One implementation gap and two stale claims.
**The scope pins the frontend; it does not pin the window.** Framing
§4.4 specified `display{ window = dest:window() }`, but dired's commit
still ended in `pmacs.window.switch_buffer`, which targets whatever
window the scoped frontend has selected. A split or panel that took
focus while `read_dir` was pending therefore received the listing, and
`prev` was captured from it too — with every preflight check passing,
because the captured window was still live and still held its captured
buffer. Both sites now read the captured window: `display` routes to it
with `select = true` (the later `seat_cursor` acts on the active
window), and the `prev` read asks it directly.
N4c pins both halves. The suite's existing routing pins all varied
*frontend* identity; none varied the selected window within one
frontend, which is exactly why 23 green pins missed this. Bite: dired's
`display` back to `switch_buffer` fails N4c alone; `prev` read from the
ambient window fails N4c alone.
Two stale documentation claims, both of which this PR was supposed to
have already fixed:
* **The §0 scorecard still graded §2 "Broken at entry"** while §2's own
ground truth had been rewritten. The scorecard is a second copy of the
same claim and §25's protocol covers both. §19's row and ground truth
were stale the same way — this PR creates the first cross-subsystem
suite, which §19 says should exist and grades as missing — and are
corrected too.
* **P4 still read "leaves exactly one buffer"**, the exact claim rev 6
corrected as false everywhere else in the framing. Restated to what it
actually pins: the file is in the *active window*. The test was
already written correctly; only the framing lied.
Framing rev 8.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes Journey Stage 1a: the `commit_to` acceptance suite (framing
§6 N4, N6, N6b, N6c, P1, P2, P3) plus the documentation updates
COHERENCE §25 requires the PR to carry.
Bite-testing the new pins found a real gap. Deleting the
`ScopedFrontend` arm from `acting_frontend` left N4 green, because
`ScopedFrontend::enter` also swaps `core.active_frontend` and the
ambient fallback then answers correctly on its own. The arm is
load-bearing in exactly one case — a commit reached from inside an
interactive command, where the origin sits between the override and the
ambient value — and nothing pinned it. N4b is added, driven through
`dispatch_key` because that is the only thing that establishes an
interactive origin, and the mutation now bites it.
Two smaller corrections found the same way:
* `commit_to`'s forged-destination message was unreachable. Typed as
`AnyUserData`, mlua rejected a table during argument conversion, so a
caller got "error converting Lua table to userdata" — true, but naming
neither the rule nor the remedy. The parameter is now `mlua::Value`
and the pointed message fires.
* P1 and P2 also fail on full revert, since `commit_to` does not exist
on the pre-image, so §6.0's "legitimately green on the pre-image" does
not describe them. They stay in the P list because their
discriminating falsifier is the named mutation — a revert-only check
cannot distinguish "validates" from "validates in time" — and each pin
now says so at its own site rather than being silently mislabelled.
Bite results, each run against the whole suite:
scope stops swapping `core.active_frontend` -> N6a, P3 fail; nothing else
preflight moved after the callback -> P1, P2 fail; nothing else
drop the `ScopedFrontend` arm -> N4b fails; nothing else
Docs: COHERENCE §2 grade + step-3 verdict row, §20 Priority 1 and the
arc list; the GPU initial-target framing's Q#GT6 and acceptance 10,
whose directory case this stage deliberately supersedes; handoff §1;
the active-work ledger; framing rev 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 11 put the nesting count in the expander, which is optional. A
consumer at a lower priority can CLAIM and stop the chain before the
expander runs, while that fan-out's deferred-expansion subscriber still
runs — so the nested pass went uncounted, looked like the outermost
one, expanded early, and outer pairing resumed with a record the
replace had invalidated. `\alp(` gave `α(` again.
The count now comes from a no-op consumer registered at the minimum
priority, which runs first in every chain invocation that reaches any
consumer at all. Its guarantee is exactly the ordering contract the
chain already rests on, and it degrades safely: the only thing that can
skip it is a claim ahead of it, which skips the expander too, so
nothing is queued in that fan-out either.
The other plausible home does not work and the comment now says why: a
subscriber registered beside `run_deferred` is too late, because the
whole nested fan-out completes inside the OUTER chain's subscriber,
before either of them runs.
Acceptance 45o pins the short-circuit path — a consumer at 25 that
claims when the record is nil, so the nested pass never reaches the
expander. 45n passes against this bug, which is why both exist.
Counting in the expander fails 45o and nothing else.
Framing rev 12 also names the shape rounds 10–12 share: each fix was
correct about the failure it was shown and wrong about the boundary of
the mechanism it leaned on — the chain's copy semantics, then its
re-entrancy, then its short-circuit. A queue that outlives the thing
that filled it has to name that thing, not approximate it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
`buffer.after-edit` fan-outs NEST — the typed-edit contract supports a
consumer calling `pmacs.hook.run`, and typed_edit.lua's header says so
in its second paragraph. A nested run re-enters every subscriber,
including the deferred expansion's, while the OUTER chain is still
walking its consumer list and pairing has not yet seen the terminator.
So a consumer registered at priority 75 — between the expander at 50
and pairing at 100 — that runs one nested fan-out made `\alp(` yield
`α(` again: the nested pass consumed the queued expansion and edited,
and outer pairing then resumed holding a record the replace had
invalidated. That is round 10's failure reached through the chain's
documented re-entrancy seam rather than through claiming, which is why
deferring alone did not close it.
Deferring work past a fan-out means owning WHICH fan-out it belongs to.
The chain's subscriber and this module's each run exactly once per
fan-out, in that order, so counting invocations of the first and
matching them off in the second identifies the nesting level. Only the
outermost pass expands; a nested one leaves the expansion queued. No
new seam in typed_edit.lua, which is merged Stage 4a substrate.
Both halves bite: removing the level check and never counting
invocations each fail the new acceptance 45n.
Also fixes a test comment that still described the span design round 10
discarded — it claimed the expansion replaces the span "INCLUDING the
terminator". The behaviour asserted was right; the explanation was
stale. Framing rev 11.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Three defects, all about what happens AROUND the expansion rather than
about resolving an abbreviation.
A pair character that TERMINATES an abbreviation never reached
auto-pairing: `\alp(` gave `α(`. Q#LN22 already said the terminator is
not claimed and the implementation claimed it whenever an expansion
succeeded. Merely declining is not enough either — the chain hands each
consumer a copy of the record made before any consumer ran, so
expanding inside the chain invalidates the copy pairing is holding and
the closer is silently lost. Verified by mutation rather than assumed:
expand-then-decline reproduces `α(` exactly.
The expansion therefore runs on its OWN `buffer.after-edit` subscriber,
registered after typed_edit.lua's and before lsp.lua's. A claim stops
the chain but not a separate subscriber, which is the point: pairing
claims the terminator it reacts to. The replaced span now covers only
the leader and the typed text, so pairing's closer lands outside it and
survives. One undo restores the same text either way, because the
terminator was always its own insert.
That second subscriber is a new instance of Q#AP7 — lsp.lua flushes
didChange synchronously on the signature-trigger path, and `(` is a
trigger — so acceptance 45m pins it with the sighelp fake server: no
didChange may ever carry the unexpanded text.
The relevance check is now three-part, as pairing's has been since
#110: buffer, window, AND `ed.cursor() == rec.post_cursor`. A redefined
self-insert can insert the completing character and then move the
point, and expanding over a span the user has left teleports them back
into it.
Cursor placement after the replace is context-guarded, as
`repair_cursor` is. A buffer intercept may switch buffers while
`buf:replace` runs; the unguarded `goto_byte` then translated the Lean
buffer's pre-edit point through the Lean buffer's edit and applied it
to whatever was ambient.
Q#LN22, criterion 38's span wording, and the ledger are corrected to
describe the deferred design rather than the one that shipped — the
rationale's source, not only the sites quoting it. Acceptance 45j/45k/
45l/45m added; framing rev 10.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Round 6. Documentation only; no code, no protocol change.
1. **The four-writer table mapped the wrong buffers to `ensure_slot`.**
Verified at the call sites: `ensure_slot` has exactly two callers,
`*compilation*` (`compile.lua:1090`) and `*shell-command*` (`:1125`).
`*search-results*` is an **independent panel** in
`builtin/commands/default.lua` with its own intercept (`:869`),
round-trip mark and writes; `compile.lua` declares its name only to
answer a predicate (`:216`), which is what made it look like a third
slot. Round 5 fixed an undercount and introduced a misattribution in
the same paragraph — the count was right, the mechanism was not.
The table is now keyed by **writer**, not by buffer, so the mapping
cannot silently drift again: four mechanisms, five buffers. It carries
an explicit "do not read `ensure_slot` as covering the search panel"
note, because that is the specific wrong inference.
Corrected identically in `COHERENCE.md` §14 and the framing's
deferred-lane text, which both carried the error.
The scope claim is narrowed with it. "Every generated buffer outside
copy mode" was too wide: `*workers*`, `*help*` and `*buffer-list*` are
generated but do not use this idiom, and the REPL package's intercept
(`packages/repl/init.lua:187`) is an op-filtering editing policy
rather than a read-only panel. The claim is now "every remaining
intercept-protected writer", and the two excluded groups are named so
the next reader does not have to re-derive the boundary.
2. **The recovery floor contradicted itself.** The canonical-base line
said the check accepts `a27f646` or anything newer while the check
below required `74301d1`. The floor genuinely advanced; the prose now
says so outright — a tree at `a27f646` no longer passes — and states
why the floor must move with the base rather than trailing it.
3. **Two anchors survived the integration.** Lean 4 Stage 4a said it was
part of "the `fe8b8ba` anchor above" when the anchor had become
`74301d1`; it now refers to the anchor rather than restating a commit,
which is what let it go stale. And #168's closed entry called its own
`fe8b8ba` figure "the live figure" — it is a reading taken at
`1b6a084`, kept as history, and now says so and points at the
coverage lane as the single authority with an explicit "do not quote
this one forward".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Four review findings, all confirmed against the tree.
Q#JR3 was false. `replace_active_buffer` does not drop the startup
scratch buffer -- its body is one `switch_active_buffer` call, which
reassigns the window's buffer_id and removes nothing. The claim came
from that function's own doc comment, wrong for as long as it has
existed, and rev 5 propagated it into the framing and into new
documentation this branch added. Both comments are corrected here,
because this PR was adding further false references to a claim P4
depends on. Actually removing the stale scratch is buffer-lifetime work
and stays out.
The daemon bootstrap could report the wrong buffer. The directory arm
captured the destination id, ran the resolver chain synchronously, then
returned the captured id -- so a handler that opened something
synchronously through commit_to had already replaced the window's
buffer, and the reply paired one buffer's snapshot with another's
identity. It also returned early, skipping the post-hook revalidation
the framing said stayed active. The arm now re-reads the destination
after dispatch and rehomes through `non_side_target` as the file arm
does. Pinned by a test whose handler claims synchronously.
N11 tested neither RET nor self-insert: it called display_file and
buf:insert directly, so it stayed green with dired's RET binding, its
entry dispatch, and the editor's self-insert path all broken. Both
gestures now go through dispatch_key.
P7 is removed rather than weakened. Q#JR12 has nothing to pin --
`had_file = file.is_some()` and a directory is Some like any other, so
no directory-specific branch exists to break. The old test never armed
restore and hard-coded had_file, so it could not fail against any
implementation.
Also adds the daemon bootstrap pins (N2, N5) and fixes an insertion that
had orphaned a `#[cfg(feature = "crdt")]` from the test it guarded --
which would have made one new test dark and one existing test escape its
gate.
Framing: docs/journey-stage1a-framing.md rev 6.
Rev 5 is approved and the branch is cut, so the ledger's "no branch,
commit, or PR exists yet" line no longer describes reality. Records the
recovery command, notes that PR #177 has merged and therefore unblocks
implementation, and carries the standing obligation that dired Stage 2
re-scouts around commit_to before its branch is cut.
Serves COHERENCE.md §2 (the golden product journey), §19 (coherence
acceptance tests), and §20 Priority 1, which grades the journey broken
at step 3 because `pmacs .` exits 1.
Stage 1a ships four things: the directory argument routed into dired's
buffer on both the local and daemon/GPU startup paths; EditorState::open
adopting resolve_target_buffer so the two path-open implementations
become one; a scoped-destination commit primitive so an async open lands
where it was requested or nowhere; and the first cross-subsystem journey
acceptance suite.
Framing only -- no implementation. Rev 5 after four review rounds.
Two doc conflicts, both in favour of `main`, and both are this PR's own
findings arriving from the other side:
- **The dired lane.** #169 did exactly what round 5 finding 5 said it
would: absorbed dired Stage 1 into handoff §1 and removed the ledger
lane per rule 4. This branch carried a placeholder saying #169 owned
that text; the placeholder is dropped and #169's version taken whole.
This is the rule working — one PR, one authority per paragraph.
- **The canonical base.** Both sides edited it. Resolved to `74301d1`,
now naming #169 and #176, keeping `main`'s added guidance that a
recovery threshold must move with the base it declares canonical (a
check that accepts an older commit passes on a tree the file does not
describe). The threshold moved with it.
Consequences of the merge, folded in:
- **The census is re-measured on the merged tree**: 3,176 vs 3,449 —
still **273 dark**, 185 of them in the library. #176's six new tests
are not `crdt`-gated, so both totals moved and the gap did not. The
per-target table is unchanged.
- **The `crdt` Clippy failure is re-measured too, and the previous count
was wrong** — it was read off a different checkout. At `74301d1` it is
seven errors before the build aborts: four in `src/daemon.rs`, three in
`tests/vterm_stage3_acceptance.rs`. Recorded as a **lower bound**,
because Clippy abandons the remaining targets once one fails, and an
older tree showed a further error in
`tests/auto_indent_crdt_acceptance.rs` this run never reached.
- **The PTY-terminate lane (#176) is marked MERGED and retained**, with a
note at its head saying why it survives rule 4: no landed-doc PR owns
moving its facts to the handoff, so removing it now would delete the
record rather than move it. The ledger preamble's stale-on-purpose
paragraph is rewritten around that one lane; the Lean 4 and
GPU-terminal-input headers it used to disclaim no longer exist.
- The flake note's "`src/process.rs` last changed by the Darwin PTY
signal-name fix" is dropped — #176 changed it. The load-bearing half
(#178 did not touch that file at all) is what remains.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Reconciles the handoff and ledger against a main that advanced past
this branch's base: the header, `main` anchor, and canonical-base
description take main's richer versions restamped to 74301d1, main's
new PTY-terminate lane is kept alongside the Lean lane, and main's
Stage 4a/rev-8 lane history is dropped in favour of the Stage 4b lane
that supersedes it — per this ledger's own rule to remove entries when
their PR merges.
Also fixes the coherence census's second count, which still said eight
settings three paragraphs below the nine it now lists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Documentation only; no code, no protocol change. All five findings
reproduced before fixing.
1. The CRDT-dark census was stale. Re-measured at `fe8b8ba` under CI's
exact flags versus the same flags plus `crdt`: 3,170 vs 3,443 —
**273 dark, 185 in the library**, not the 264/177 #168 measured at
`1b6a084`. The per-target table is regenerated (it gains a
`terminal_copy_mode_acceptance` row, acc16e's, from this very arc),
the rows are stated to sum to the total, and the lane now says the
number moves with every merge and must be re-measured rather than
quoted. #168's figure is kept as a dated historical reading with a
pointer to the live one.
2. The generated-buffer non-adopter inventory was short by half. It is
**four writers, not two**: listview panels (`listview.lua:60-61`),
`*compilation*` **and** `*search-results*` — both through
`compile.lua`'s shared `ensure_slot`, so naming only the first
undercounts a mechanism rather than a buffer — and dired
(`dired.lua:371`). All four pair an erroring intercept with
`bypass_intercept` writes over a still-writable rope, and all four are
emptiable by `M-x buffer.undo`. Corrected in the handoff §4 (as a
table, with each writer's shape), `COHERENCE.md` §14, and the
framing's deferred-lane text. The adoption estimate gains a
consequence: the two `compile.lua` slots append and need a streaming
variant; listview and dired are whole-buffer replaces and are the
cheap half.
3. The ledger's canonical base still named `a27f646` while the same file
recorded #168 and #178. Now `fe8b8ba`, with the recovery check's
accept-or-newer floor moved with it — a stale floor is what lets a
wrong base pass verification.
4. The completed terminal lane is **removed**, not marked complete. Rule
4 of this file's own update protocol says a lane goes when it merges,
and its opening contract says the file records only what has not
landed. Its durable facts moved first: a new arc bullet in the handoff
§1 (the snapshot materializes, so the dispatch-shadow count stays at
six; `prune` reacts to removal rather than causing it; ownership means
the handle table, never found-by-name; profiles are a raw Lua table
and why the escape cache lives on `TerminalSession`; what criterion
17 must assert when it can finally be written; the `cat -v` echo probe
and count-don't-match rule), with the `set_generated_contents`
invariant already in §4. A compact entry remains under "Closed since
the last snapshot". The gate-run flake the lane carried moved to the
CI `crdt`-coverage lane, which owns its discrimination — verbatim,
including its explicit refusal to claim a root cause.
5. The refreshed handoff was internally stale: it anchors on a `main`
that contains #179 and #165 while still calling both "in review".
Both now read MERGED. Dired's durable facts are deliberately **not**
absorbed here — that is open PR #169's job, and writing it from two
PRs would put two authorities on one text — so the dired lane stays,
with a note saying why it survives rule 4 and who removes it.
Also recorded while verifying finding 4's new home: the `crdt` Clippy
failure on `main` is re-verified with exact sites (four in `src/daemon.rs`,
one in `tests/auto_indent_crdt_acceptance.rs`), because any CI job that
compiles the `crdt` targets is red on arrival until they are fixed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gGQC6eqHJVbZJ5Hg7aLer
Typing `\alpha` in a Lean 4 buffer gives `α`; `\<>` gives `⟨⟩` with the
point between them. The abbreviation table is vendored from
vscode-lean4 and the expander is a typed-edit consumer registered on
the Stage 4a chain at priority 50, ahead of auto-pairing.
The ordering is load-bearing. 64 abbreviation keys contain a character
in the `lean4` pair set, so with pairing first, typing `\[` would
insert `[]` and corrupt the pending key to `\[]` before the second `[`
arrives — `\[[]]` becomes unreachable. The consumer therefore claims
every keystroke that EXTENDS a pending abbreviation, not only one that
completes an expansion; claiming only completions would hand each
intermediate `[` to pairing by a different route.
The vendored table is an ORDERED SEQUENCE, not a map. Upstream breaks
equal-length ties by source declaration order — 101 prefixes depend on
it, and `\f` resolves through `f<` rather than `f>` — which a
`pairs`-iterated Lua table cannot express. `scripts/regen-lean-abbrev`
takes a vscode-lean4 commit, emits the file with its provenance header,
and aborts on a duplicate key, invalid UTF-8, or a round-trip mismatch.
Undo is cross-peer-degraded on CRDT frontends and that is accepted and
named, not papered over (Q#LN21): `\alpha` arrives as six source-peer
optimistic inserts while the expansion is one daemon-peer replace.
`set_round_trip_input` would fix it and also makes `dispatch_idle`
report false, so RET would stop inserting a newline.
Round 9 corrects three approved acceptance criteria that the real table
contradicts, found by simulating the state machine over all 1,855
entries and re-reading upstream at the pinned commit rather than
re-reading the prose. `\to` is not eager — `top`, `to0` and `toa`
extend it. `\zzzz` expands to `ζzzz ` because `ze`, `zeta` and
`zsqrtd` exist; only `$ % , ; @ W` open no key at all. And `\alpha`'s
undo does not restore `\alpha ` because `alpha` IS eager, so the
terminator is a separate edit. Criteria 38, 41 and 42 now state both
paths, and the false halves are asserted too: they read as correct
until the table is consulted.
Three implementation traps worth the record. The generator's own
round-trip check was broken twice and failed closed both times:
`str.splitlines()` splits on U+2028, which 53 symbols contain, and
escaping through `chr(byte)` produced a latin-1-shaped string that the
UTF-8 write re-encoded. The first check compared in-memory strings and
agreed with itself; it now stages the file, re-reads the bytes from
disk, and renames into place only on a match. And the expansion SHRINKS
the buffer, so the point must be placed explicitly — pairing's
no-cursor-motion rule holds only for an insert AT the cursor, and
without this every self-insert after the first expansion is silently
rejected and the editor looks dead.
25 acceptance tests plus one `--lib` test for the optimistic CRDT
producer (45f), which is where the gate list's `--features crdt` run
reaches it; a crdt-gated integration test would be dark in CI and in
the gates both. Fifteen mutations bite, each failing its target. Three
of these tests were vacuous when first written and biting is what
found them: the abandonment test asserted text a surviving record
would also produce, the re-arm test used an example that never reaches
the re-arm branch, and both switch tests ran through
`find_or_open`'s fresh-load path rather than `buffer.after-switch`.
No protocol change (Q#LN14). Also reconciles the handoff and ledger
for Stage 4a (#179) and adds `lean.abbrev` to COHERENCE.md's
config-registry adoption census, now nine settings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LFvC4FQtux4y32KuevZ7B
Re-integrated after #178 (terminal copy mode) landed. One conflict, in
COHERENCE.md's journey table: row 7 keeps this branch's text (dired #165
is merged, so "PR #165" is wrong), row 8 takes main's, which adds copy
mode and the missing close/kill command to the terminal step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuhVYUPHXMHG8r2z4tsDPR