Merge pull request #246 from levineuwirth/crdt-identity-undo

Key the crdt_op shape invariant on provenance, not on the Edit's shape
This commit is contained in:
Levi Neuwirth 2026-08-31 20:56:09 +02:00 committed by GitHub
commit 78346de19a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 2097 additions and 153 deletions

View File

@ -301,66 +301,214 @@ waits for a signal that is not coming.
- **THE FIRST DISPATCH IMMEDIATELY FOUND A RED ON `main`**, which is - **THE FIRST DISPATCH IMMEDIATELY FOUND A RED ON `main`**, which is
what this lane was built for. See the proptest entry below. what this lane was built for. See the proptest entry below.
## CRDT identity-replace undo — the proptest invariant may be MIS-SCOPED — NEEDS A LANE ## CRDT identity-replace undo — LANE TAKEN, PR #246 OPEN
**CORRECTION.** An earlier version of this entry called the dispatched **Branch `crdt-identity-undo`, PR #246, based on `aae5b35`.** Framing
run's red "a DETERMINISTIC red" and "not like anything else in this `docs/crdt-identity-undo-framing.md`, **APPROVED at revision 4** after
registry — a property violation with a concrete witness, not a load four review rounds, then **revision 5** as a correction pass answering
artefact", and proposed committing the proptest seed as the first implementation review.
step. **All of that was wrong**, and it was wrong because I recorded a
finding without checking whether `main` already documented it.
**It is a randomly sampled recurrence of known #157 behaviour.** **Every head of this branch is gated head-exact**, with `HEAD` and
`src/buffer.rs:3005` carries an `#[ignore]`d deterministic fixture, `git status --porcelain` captured before and after each run and
`crdt_undo_of_an_identity_replace_reports_a_no_op_edit_carrying_an_op`, identical. The two that matter:
which reduces this exact case and records its mechanism:
1. the inserts produce `aaaaa `; | commit | what it carries | gate | CI |
2. `Replace(5, 1, " ")` replaces the trailing space **with itself** — a |---|---|---|---|
textual no-op but a real CRDT delete-plus-insert; | `2c24303` | the code, and the framing at revision 5 | all 8 green, `20260830T193305Z-4167110`, loadavg 0.90 | 14/14 |
3. `Undo` therefore emits a **version-advancing CRDT operation with no | `6ddce0f` | the registry corrections (U14/U15 split, load claim narrowed) | all 8 green, `20260830T201908Z-84597` | 14/14 |
visible text change**, and `derive_replacement_edit` yields an empty
edit that still carries its `crdt_op`.
**Committing the seed is NOT the first step**, and proposing it was a **This line goes stale the moment another commit lands, which is the
second error: it duplicates a deterministic fixture that already defect review already caught here once** — it named `db24ae3` long after
exists, and its only effect would be to make a disputed assertion fail the branch had moved past it. It is written as a table so the next
permanently instead of occasionally. update is an added row rather than a rewrite.
**What the fixture already verified**, so the lane does not redo it: **The decision, ruled:** a visible TEXT delta and a CRDT-VERSION delta
content stays correct (rope and CRDT projection agree), replicas stay are **independent dimensions** of `Edit`. The invariant is keyed on
converged (**the op IS broadcast** — both `crdt_op` consumers read it **provenance**, and enumerated over three axes rather than defaulted:
unconditionally and neither short-circuits on an empty range), and the an **empty text delta** is a shape both paths reach, and `crdt_op` is
cursor does not jump. what separates them — `None` forward (the three syntactically empty
`EditOp` forms short-circuit at `is_no_op_edit`), `Some` from
`undo`/`redo`, **required** there, because the op is the whole content
of such an edit.
**THE ACTUAL DECISION** is whether a **visible text delta** and a **Revision 5 fixed three things review caught in the implementation:**
**CRDT-version delta** are independent dimensions of `Edit`. The
proptest's invariant assumes they are the same dimension. It was
written for the FORWARD `apply_edit` short-circuit, which returns
before producing an op at all — and **CRDT-mode undo/redo never reach
that path**.
**What the lane owes as evidence:** - the predicate **conflated the empty text delta with a version delta**,
calling every empty-range/zero-insertion edit `version_only` and then
accepting `(History, empty, None)` through a wildcard — contradicting
the lane's own "the op must survive". It is now a full enumeration,
and C5 asserts all four empty-delta quadrants instead of two. **Both
new quadrants were mutation-checked, and neither is caught by the
proptest** — no generated input reaches either;
- the **public `Edit` doc was factually false**: it said forward
`apply_edit` never produces the empty-delta shape, while C2b proves
all three forward empty forms do;
- **R7's write-up overstated what the paired gate runs exclude.** They
exclude the source tree. They do not narrow the cause to three
candidates — scheduler load, kernel and socket timing, and unrelated
machine state all varied too, and a `BrokenPipe` on a socket handshake
is exactly what those can drive.
- **forward textual no-ops still produce NO operation** — whatever the **A GATE COVERAGE GAP, found the expensive way.** The local gate's
resolution, the short-circuit the invariant was actually written for clippy step is `cargo clippy --workspace --all-targets -- -D warnings`
must keep holding; — **default features**, so **no `#[cfg(feature = "crdt")]` code is ever
- **any permitted empty-text undo operation carries valid bytes and linted locally**. CI lints it (`--no-default-features --features
preserves remote replay convergence** — permitting the shape must not luajit,crdt`), so a crdt-only lint passes eight green gate stages and
become permitting a malformed op; then reds `Test (crdt)`. That is what happened here: a
- **an explicit disposition of the arbitrary artifact**: `clippy::match_same_arms` on the new enumeration, invisible to five
`derive_replacement_edit` reports the empty range at the **buffer consecutive local gate runs.
end** rather than at the edit site. The fixture calls this genuinely
arbitrary either way; the lane must say which it is choosing, not
leave it unexamined.
**Un-ignoring that fixture is the first step of whichever resolution The lint itself is `#[allow]`ed with a reason — collapsing the three
wins**, as its own doc comment says. `Ok(())` arms is exactly the conflation this lane removes, and would
hide that `(forward, empty, None)` and `(history, empty, Some)` are
valid for opposite reasons. **The gap is not fixed here**: adding a
second clippy flavor to `scripts/gate` is a change to shared
infrastructure and belongs in its own lane. Recorded so the next lane
touching crdt-gated code does not rediscover it at CI.
**Why it is worth a bounded interruption:** a mis-scoped property can **NINE registry rows moved on this lane** — R7, R6, U6, U14, U15, U16,
now randomly redden `main`, and the dispatched run proved it. **It does U17, U18 and U19. *(This heading said "four" while listing more; corrected.)*
NOT reorder the roadmap** — GUI arc 1b remains the next product lane
per `COHERENCE.md` §20's priority order. - **R7's eleventh and TWELFTH occurrences** — the eleventh is the
green/red pair whose heads differ by one markdown file, which excludes
the SOURCE TREE and nothing more; an earlier write-up of mine narrowed
the cause to three gate-state candidates and that overstatement is
withdrawn in the row. **The row's numbering was wrong twice over**: it
carried TWO blocks labelled "fourth" (D3 2026-08-11, TMPDIR
2026-08-13), **and** two full-fragment occurrences of 2026-08-15 sat
in this file marked "owed to the registry" and were never absorbed.
Renumbered by date with both fixed, and the row's summary now states
the total and which `attach.rs` line each group reports;
- **U6 went from one occurrence to five** — four on 2026-08-30, two out
of gate and two in. Its first reproduction ever. **A direction claim I
made here ("runs the OPPOSITE way to R7", resting on four green
`04-lib-crdt` stages) was falsified by the next gate run and is
withdrawn in the row;**
- **U14, new** — four selectors across **four** unrelated subsystems
(async runtime, optimistic orchestration, editor composition, LSP
dispatch) red in one gate run, spread over three stages;
- **U15, new** — the rotated cluster 40 minutes later. It carries a
single `/proc/loadavg` reading of **34.04**, which makes severe
unrelated load a **measured presence contemporaneous with a multi-red
run — not a measured cause.** The reading is one point taken after the
fact and the margins are not monotonic
(`composition_overhead` ran 1.182x, 1.592x, 1.527x), so no
dose-response is claimed. **It is the first contemporaneous load
reading for a U6 occurrence, and a second data point beside the one
U7 has carried since 2026-08-09** — not this registry's first. Two
earlier versions of that write-up overreached: one said a load average
of 34 "explains it without any help", the other that U6 and U7 had
both wanted a number since August. Both are corrected in place.
- **U16, new** — a `git` invocation in `packages::fetcher` found its
working directory **deleted**. Not a budget: the only row in the
registry that arrives with a **named candidate mechanism inside the
test suite**, and the load-bearing step is **child inheritance**.
`src/file_io.rs:434` mutates process-global cwd; concurrently
`run_git` calls `run_git_inner(None, …)`, which sets `current_dir`
only when `cwd` is `Some` (`fetcher.rs:329`–`:330`), so the spawned
`git` **inherits** the temp cwd. **Restoring the parent's cwd does
nothing for that child**, and the `TempDir` then drops underneath it.
Candidate, not a demonstrated chain — 8 full parallel `--lib` runs did
not reproduce it, which establishes **intermittence and nothing
more**. The controls that would settle it are in the row and **none is
run here**; note that a serial guard around `set_current_dir` tests is
*not* among them, since the child outlives the guard. The structural
fix belongs to whoever owns `file_io`, not to a CRDT invariant lane.
- **R6's SECOND occurrence** — 26 days after the first, macOS `lua54`,
a **full three-condition match** including both required fragments.
**The log was read before anything was rerun**, which is U3's lesson
and U8's fourth-violation warning finally honoured on a macOS job. A
**merge-base control was dispatched** at `aae5b35` rather than
arguing from an unrelated diff. **Not the `workflow_dispatch` key's
first use** — #245's own D2/D3 witnesses dispatched three runs right
after it merged — but **the first use for a live merge-base
control**, which is the case U11 motivated it for. It came back **green on the macOS legs**, so the
inference it could have supplied is **unavailable**; recorded as a
null result, as R1's row had to record its own;
- **U16 REPRODUCED** — second occurrence 2026-08-31, same step, same
fragments. It settles the earlier withdrawal in the right direction:
"the window is narrow" was wrong to claim from eight green runs, and
the failure returned within the day. The child-inheritance chain
stays a candidate; this occurrence demonstrates it no more than the
first did. **It then passed in all three stages of the next gate
run** — two reds and many greens **all on 2026-08-31**, intermittent at
a rate nothing has measured.
- **U19, new** — `terminal_bell_baseline_…` timed out on a **5-second**
poll, in the same run as U16's second. The evidence is that **no bell
was observed within five seconds** — not that one never came, and not
that scheduling cannot explain it. A much slacker deadline than the
budget family's (5000× the 1ms budget, but only **25×** the 200ms
one), so the distinction is of degree rather than kind. Like R1, **its
assertion reports no elapsed value**, so **this** margin is gone for
good; adding it later would make future occurrences comparable **to
each other**, not to this one. **It passed in all three stages of the
next gate run**, so it is intermittent.
- **U18, new** — `Test (ubuntu-latest / luajit)` died in **toolchain
setup**, before any `cargo` command ran: `go install gopls@v0.16.2`
hit `INTERNAL_ERROR` from `sum.golang.org` while verifying
`x/telemetry`. A new class for the registry — every other row is a
test that failed; this is infrastructure the workflow depends on
failing to answer, and it presents as a red check indistinguishable
from a real one. No attribution to the branch is possible: the step
precedes compilation, and the other 13 checks passed on the same head.
**Not rerun** — a transient network error is expected to pass on
retry, which would establish nothing.
- **U17, new** — that same control run **redded `Test (crdt)` on `main`
at `aae5b35`**: `read_dir_supersede_cancels_in_flight_predecessor`,
`first read_dir must be superseded; got ok`. It fails the **opposite**
way to R1 and R5 — not a missed deadline. What `got ok` proves is
narrow: the predecessor **completed successfully before cancellation
took effect**, which does not say when the supersede arrived. The job
runs `--test-threads=1`, which serializes test **functions within one
executable** — **not** the test-**binary** concurrency U9's control
named, and cargo runs binaries serially anyway. A PR run could show
this failure too; what only a `main`-side run establishes is that it
fails **on `main`**, with no observing branch to suspect.
U14 and U15 are two rows rather than one because the second run's
selector set had **rotated**, and this registry matches on the exact
set — recording it as a second U14 occurrence was a matching-rule
violation, caught in review.
**Two claims THIS BLOCK made are corrected by measurement:**
- it said the fixture had verified that *"replicas stay converged — the
op IS broadcast"*. **That was inspection of the call sites, not
execution.** Nothing had ever replayed the op on a replica, and text
equality alone cannot detect a lost version advance — the drop-the-op
mutant leaves the text identical, and the failure that catches it
reads `version vector diverged undo`. C3 establishes convergence
properly, by seeding a replica with the forward ops first;
- it said the buffer-end range location was *"genuinely arbitrary either
way"*. **The §4 census rules it**: five consumers inert, three
permitted, none harmed — and for `TextView`, the one whose cost
depends on the location, the buffer end is the **cheapest** rebuild.
**What the four review rounds caught, none of it by me.** Revision 1
posed the decision instead of answering it, and its C3 passed its own
drop-op mutant. Revision 2's C4 contradicted the implementation
(`mark_stale` is unconditional and range-independent) and named two
consumers out of six. Revision 3's C4 claimed guard mutations that
**survive** — at the buffer end, deleting the fold or style guard
changes nothing — and its C9 guarded a file set and a count, which a
same-file substitution walks straight through.
**15 mutation checks were run, each on a clean tree and reverted.** All
behaved as the framing predicted, including the two asymmetries the
framing states rather than assumes: C2b is **masked** for the Insert and
Delete forms by their defensive early returns (`buffer.rs:1177`,
`:1192`) and **dies** for Replace, which has none; and C4b **survives**
the style-guard deletion, which is why C4c injects an INTERIOR empty
edit where the fragmenting is reachable.
**Deliberately not done:** the proptest regression seed is NOT
committed. It duplicates a deterministic fixture and would make a
disputed assertion fail permanently rather than occasionally.
**It does NOT reorder the roadmap.** GUI arc 1b remains the next product
lane per `COHERENCE.md` §20.
### Superseded lane state, kept for the record ### Superseded lane state, kept for the record
@ -428,7 +576,7 @@ from #171 and #215.
U13 is not this lane's and is left as recorded, but the pattern is U13 is not this lane's and is left as recorded, but the pattern is
now a pattern rather than an oversight, and each occurrence costs a now a pattern rather than an oversight, and each occurrence costs a
review round to establish nothing. review round to establish nothing.
- **R7 gained its sixth and seventh occurrences here**, on a branch - **R7 gained its ninth and tenth occurrences here** (recorded at the time as its sixth and seventh; the row was renumbered on 2026-08-31 after two duplicate ordinals and two unabsorbed 2026-08-15 occurrences were found), on a branch
touching no `pmacs-gpu` file, and the registry gained a **bounded touching no `pmacs-gpu` file, and the registry gained a **bounded
observation window** so the in-gate/out-of-gate ratio cannot drift observation window** so the in-gate/out-of-gate ratio cannot drift
with review activity — plus a correction: seventeen green with review activity — plus a correction: seventeen green
@ -518,10 +666,14 @@ from #171 and #215.
the full 36-test binary both passed immediately afterwards — the full 36-test binary both passed immediately afterwards —
intermittence only. This lane changes neither the gate script nor intermittence only. This lane changes neither the gate script nor
that acceptance binary; diagnostic hardening is a separate lane. that acceptance binary; diagnostic hardening is a separate lane.
- **Still owed, separately:** `workflow_dispatch` on `ci.yml`, and U9's - **Still owed, separately:** `workflow_dispatch` on `ci.yml` (**landed
discriminating control — pin test-binary concurrency to 1, then load as #245**), and U9's discriminating control — named since 2026-08-09,
a lone `--lib` binary — which has been named since 2026-08-09 and never run, and now **VOID**: its premise that `cargo test --workspace`
never run. runs many test binaries at once is false, cargo runs test targets
serially, so "pin test-binary concurrency to 1" pins something already
1. See the correction on U9 in `docs/ci-red-signatures.md`. **A
replacement control has to be designed**; the budget family no longer
has one written down.
## Panel-pointer replay (parent acceptance 48) — MERGED as #243 (`6c9bae6`) ## Panel-pointer replay (parent acceptance 48) — MERGED as #243 (`6c9bae6`)
@ -1227,8 +1379,12 @@ from #171 and #215.
branches' entries "merged **without a conflict**, producing duplicate branches' entries "merged **without a conflict**, producing duplicate
ids across four sites". **The rows below are owed to the registry by ids across four sites". **The rows below are owed to the registry by
whichever branch merges second**, numbered after the other's. whichever branch merges second**, numbered after the other's.
- **R7, two occurrences on this branch** (2026-08-15, `gpu` step, - **R7, two occurrences on this branch — ABSORBED 2026-08-31 as the
logs `20260815T095532Z` and `20260815T100719Z`). Fragments verified row's seventh and eighth** (2026-08-15, `gpu` step,
logs `20260815T095532Z` and `20260815T100719Z`). **They sat here
unabsorbed for sixteen days** while both branches merged, which is
why R7's count read two low; the deferral was reasonable, not
discharging it was not. Fragments verified
both times: `transient sequence must attach: Attach(Handshake(Io(Os both times: `transient sequence must attach: Attach(Handshake(Io(Os
{ code: 32, kind: BrokenPipe, message: "Broken pipe" })))` at { code: 32, kind: BrokenPipe, message: "Broken pipe" })))` at
`pmacs-gpu/src/attach.rs:1728`. One machine, one day, one branch, `pmacs-gpu/src/attach.rs:1728`. One machine, one day, one branch,
@ -1407,9 +1563,15 @@ from #171 and #215.
load average **14.02 → 28.35**, from an unrelated `turso` test load average **14.02 → 28.35**, from an unrelated `turso` test
suite on the same machine (`./verify_task_state.sh suite on the same machine (`./verify_task_state.sh
turso-without-rowid`, target dir `/opt/target`, one test binary at turso-without-rowid`, target dir `/opt/target`, one test binary at
**693% CPU**). It is not a controlled experiment, but it is the **693% CPU**). It is not a controlled experiment, and **an earlier
same evidence U9's synthetic-load control was meant to produce, and version of this bullet called it "the same evidence U9's
it points at load. U9 stays owed; its value is now lower. synthetic-load control was meant to produce" and said "U9 stays
owed". Both are withdrawn.** Uncontrolled foreign load is *not* the
same evidence as an experiment that applies load deliberately, and
U9's control is now **VOID** in its concurrency arm and a
non-discriminator in its load arm — see the correction on U9 in
`docs/ci-red-signatures.md`. What this reading is: a **named
confound**, recorded, pointing at load without establishing it.
- **Two process traps this cost, both worth carrying forward.** The - **Two process traps this cost, both worth carrying forward.** The
Bash tool caps a command at 10 minutes and SIGTERMs it, which the Bash tool caps a command at 10 minutes and SIGTERMs it, which the
gate reports as `FAILED (exit 143)` on whatever stage was running — gate reports as `FAILED (exit 143)` on whatever stage was running —
@ -1427,9 +1589,14 @@ from #171 and #215.
only: no mechanism is claimed, and the standing leaked-daemon only: no mechanism is claimed, and the standing leaked-daemon
confound is uncontrolled as always. confound is uncontrolled as always.
- **Cost, stated plainly:** four `--protocol` gate runs on one commit, - **Cost, stated plainly:** four `--protocol` gate runs on one commit,
three of them lost to these two signatures. U9's synthetic-load three of them lost to these two signatures. **This sentence used to
control remains unrun and is the cheapest thing that would either add that U9's synthetic-load control "would either implicate load or
implicate load or clear it. clear it". It would not.** With cargo running test targets serially,
U9's concurrency arm is void and there is no second arm to compare
against: a red under synthetic load shows load is **sufficient**, and
a green shows nothing — non-reproduction never clears anything under
this file's own rerun rule. See the correction on U9 in
`docs/ci-red-signatures.md`.
- **Rustdoc split, FOUR occurrences on this branch** (`screen_size`, - **Rustdoc split, FOUR occurrences on this branch** (`screen_size`,
`peer_may_send_panel_events`, `send_panel_pointer`, and `peer_may_send_panel_events`, `send_panel_pointer`, and
`SemanticRenderState`). Always the same mechanism: inserting an item `SemanticRenderState`). Always the same mechanism: inserting an item

View File

@ -239,10 +239,70 @@ happened and had a signature, which is the entire bar for a row.
| **selector** | `--test bottom_panel_stage1_acceptance acc28_child_input_and_the_c_c_escape_work_unchanged_in_a_panel` | | **selector** | `--test bottom_panel_stage1_acceptance acc28_child_input_and_the_c_c_escape_work_unchanged_in_a_panel` |
| **job / flavor** | macOS / lua54 | | **job / flavor** | macOS / lua54 |
| **required fragments** | `timed out waiting for` **and** `/ready` | | **required fragments** | `timed out waiting for` **and** `/ready` |
| **causal status** | **UNRESOLVED — no diagnosis** | | **causal status** | **UNRESOLVED — no diagnosis. SECOND OCCURRENCE 2026-08-31**, 26 days after the first |
| **evidence** | #217 [run 31023651701](https://github.com/levineuwirth/pmacs/actions/runs/31023651701), 2026-08-05 | | **evidence** | #217 [run 31023651701](https://github.com/levineuwirth/pmacs/actions/runs/31023651701), 2026-08-05. Second: PR #246 [job 99431791766](https://github.com/levineuwirth/pmacs/actions/runs/33374169011/job/99431791766), head `e87d227` |
| **retirement** | the readiness helpers are audited and reconciled, with a witness. **Never a green rerun** — the next push was green and that retires nothing. | | **retirement** | the readiness helpers are audited and reconciled, with a witness. **Never a green rerun** — the next push was green and that retires nothing. |
**Second occurrence — the CRDT identity-undo lane, 2026-08-31,
`Test (macos-latest / lua54)` on PR #246.** **A full three-condition
match**, which this file requires and which is worth spelling out
because the last occurrence of this selector could not be matched at
all:
1. **selector** — `acc28_child_input_and_the_c_c_escape_work_unchanged_in_a_panel`;
2. **job / flavor** — macOS / `lua54`, the same leg;
3. **both required fragments** — `timed out waiting for /var/folders/df/djsxfhc17x95674wsm_g8s980000gn/T/.tmpyBcHeZ/ready`.
Panic at `tests/bottom_panel_stage1_acceptance.rs:2454`,
`46 passed; 1 failed`.
**The log was read BEFORE anything was rerun.** U3 named that lesson,
U8 recorded its fourth violation, and this is the first occurrence in
this file's history where the rule was followed on a macOS job at the
moment it mattered. The fragments above exist because of it.
**Not attributed to the observing lane, and a merge-base control was
dispatched rather than argued.** The branch's whole diff is
`src/buffer.rs`, `src/rope.rs`, `src/overlay.rs`, `src/view.rs` (tests
and doc comments) plus three docs — **no file under `tests/`, and
nothing in the panel, process or terminal paths**. But "my diff looks
unrelated" is not evidence, so
[run 33375945966](https://github.com/levineuwirth/pmacs/actions/runs/33375945966)
was dispatched at `aae5b35`, **the branch's exact merge base**, via the
`workflow_dispatch` key #245 landed for precisely this.
**It is NOT that key's first use, and an earlier version of this block
said so.** #245's own owed witnesses D2 and D3 dispatched three runs
(`33307137965`, `33308891808`, `33308921103`) immediately after it
merged, and the first of those already found a red on `main`. What this
is: **the first use for a live merge-base CONTROL** — a contemporaneous
`main`-side run obtained to answer a specific branch-side red, which is
the case U11 motivated the key for.
**THE CONTROL LANDED GREEN on the macOS legs**, and the meaning was
pre-registered above before the result was seen:
`Test (macos-latest / lua54)` **succeeded** at `aae5b35`. So the
inference this control could have supplied — that the branch did not
introduce the failure — **is unavailable**. What is established is only
that the merge base can pass the same job in the same hour. That is
exactly what R1's row had to record about its own green control, and it
is recorded the same way here: **a null result, not an exculpation.**
**The control run was not otherwise clean, and that is its own finding.**
`Test (crdt)` **failed on `main` at `aae5b35`** — see **U17**. **A PR
run can show the identical red**, and an earlier version of this block
denied it; what only the `main` dispatch establishes is that the failure
occurred **on the merge base**, with no observing branch to suspect.
**Circumstantial alignment with U8, deliberately NOT a merge.** U8 has
the same selector, panicking at the **same line** `:2454` with the
**same** `46 passed; 1 failed`, on macOS `luajit` at base `0190102`.
That is suggestive. It is also unconfirmable: **U8's fragments were
destroyed**, and a row with no fragments cannot be matched — which is
exactly what U8's own entry says it is for. The alignment is recorded
here so a future reader sees it; U8 stays a separate row, and the
inference stays unavailable.
**A THIRD copy of the readiness helper.** R4's disposition already **A THIRD copy of the readiness helper.** R4's disposition already
recorded that the empty-file predicate lived in a second helper recorded that the empty-file predicate lived in a second helper
(`wait_for_published_file`) and warned that leaving it would let the (`wait_for_published_file`) and warned that leaving it would let the
@ -535,13 +595,96 @@ Stage 4; the lane touches no `pmacs-gpu` code at all.
| **selector** | `-p pmacs-gpu attach::tests::managed_retry_survives_transients_and_uses_the_successful_stream` | | **selector** | `-p pmacs-gpu attach::tests::managed_retry_survives_transients_and_uses_the_successful_stream` |
| **job / flavor** | local (Linux), `cargo test --workspace --features crdt --no-fail-fast`, i.e. under full-sweep load | | **job / flavor** | local (Linux), `cargo test --workspace --features crdt --no-fail-fast`, i.e. under full-sweep load |
| **required fragments** | `transient sequence must attach` + `Handshake(Io(` + `BrokenPipe` (or `code: 32`) | | **required fragments** | `transient sequence must attach` + `Handshake(Io(` + `BrokenPipe` (or `code: 32`) |
| **status** | **SEVENTH OCCURRENCE 2026-08-29 — causal status still UNRESOLVED.** The sixth and seventh came back to back on one lane and are written up together below; the fifth carries the strongest tree exclusion this row has had, a **documentation-only diff** | | **status** | **TWELFTH OCCURRENCE 2026-08-31 — causal status still UNRESOLVED.** The eleventh carries the strongest tree exclusion this row has had, and it supersedes the sixth's: two consecutive gate runs on ONE worktree whose heads differ by a single markdown file, the first all-green and the second red. **NOTE: the numbering was wrong twice over.** The row carried TWO blocks labelled "fourth" (D3 on 2026-08-11, TMPDIR isolation on 2026-08-13), and **two further full-fragment occurrences of 2026-08-15 sat in `docs/active-work.md` marked "owed to the registry" and were never absorbed** (logs `20260815T095532Z`, `20260815T100719Z`). Renumbered by date with both defects fixed; the count below is the total |
| **what IS established** | **three** occurrences at `pmacs-gpu/src/attach.rs:1680`, the second and third with all three fragments **verified** rather than inferred; the test drives a scripted transient-then-success sequence over a real socket pair. **The added GPU test is not the mechanism** — see the third-occurrence control below | | **what IS established** | **TWELVE occurrences.** The line moves as `attach.rs` changes and this row treats a `:LINE` suffix as occurrence-specific: occurrences **one through four** report `pmacs-gpu/src/attach.rs:1680`; the **fifth records no line at all** — its block never captured one, and it is marked unrecorded rather than guessed; **six through eight** report `:1728`; **nine through twelve** report `:1889`. The second and third carry all three fragments **verified** rather than inferred; so do the seventh and eighth. The test drives a scripted transient-then-success sequence over a real socket pair. **The added GPU test is not the mechanism** — see the third-occurrence control below |
| **what is NOT** | whether the broken pipe is the *fixture's* writer closing early or a real retry-path defect. **This row is not a claim that it is harmless** | | **what is NOT** | whether the broken pipe is the *fixture's* writer closing early or a real retry-path defect. **This row is not a claim that it is harmless** |
| **rerun evidence** | occurrence 1: 6 isolated runs green, plus a full `--workspace --features crdt` sweep green (113 targets). Occurrence 2: **30 green on the observing branch** (15 isolated selector, 15 full `-p pmacs-gpu`) **plus a 15-run merge-base control, also green**. Occurrence 3: 5 isolated selector runs green, 10 full `-p pmacs-gpu` runs green **with** the added test, and **1 failure in 10 with the added test `#[ignore]`d** — the first rerun in this row's history that reproduced anything. Per the rerun rule the green runs establish intermittence only; the red control run is what carries the exclusion | | **rerun evidence** | occurrence 1: 6 isolated runs green, plus a full `--workspace --features crdt` sweep green (113 targets). Occurrence 2: **30 green on the observing branch** (15 isolated selector, 15 full `-p pmacs-gpu`) **plus a 15-run merge-base control, also green**. Occurrence 3: 5 isolated selector runs green, 10 full `-p pmacs-gpu` runs green **with** the added test, and **1 failure in 10 with the added test `#[ignore]`d** — the first rerun in this row's history that reproduced anything. Per the rerun rule the green runs establish intermittence only; the red control run is what carries the exclusion |
| **retirement** | hardening that removes the named mechanism plus a discriminating witness — or a diagnosis showing the fixture, not the code, closes the pipe | | **retirement** | hardening that removes the named mechanism plus a discriminating witness — or a diagnosis showing the fixture, not the code, closes the pipe |
**Sixth occurrence — the parse-budget diagnosability lane, 2026-08-29, **Twelfth occurrence — the same lane, 2026-08-31, local (Linux), `gpu`
step**, log `20260831T141818Z-2974002`. All three required fragments,
same selector, same `pmacs-gpu/src/attach.rs:1889`, same
`283 passed; 1 failed`. The other seven stages were green.
**Recorded, and it adds nothing but a count — deliberately.** The
eleventh occurrence's method note says the remaining candidates have to be
varied **inside** the gate, one per run; that has not been done here and
this lane is not the place to start. `/proc/loadavg` at the failure read
`3.35 8.88 5.93` — a condition, recorded because U15 made load a
measured quantity in this file, **not** a cause, and R7 is not a budget
row. The observing commit is documentation only.
**Eleventh occurrence — the CRDT identity-undo lane, 2026-08-30, local
(Linux), `gpu` step.** All three required fragments present in the
durable log
(`pmacs-fdccc423/gate-logs/20260830T155621Z-3005460/06-gpu.log`):
```
transient sequence must attach: Attach(Handshake(Io(Os { code: 32,
kind: BrokenPipe, message: "Broken pipe" })))
```
at `pmacs-gpu/src/attach.rs:1889`, `283 passed; 1 failed`.
**This occurrence discriminates tree from runner more sharply than any
before it, and the reason is the pair, not the diff.** Two consecutive
`scripts/gate` runs, same worktree, minutes apart:
| run | head | delta from the previous run | result |
|---|---|---|---|
| `20260830T154827Z-2907414` | `db24ae3` | — | **all 8 stages green** |
| `20260830T155621Z-3005460` | `96bf2c3` | **one commit, touching one file: `docs/active-work.md`** | **`gpu` and `sweep` FAILED** |
The sixth occurrence excluded the observing tree *relative to `main`*
by having a documentation-only diff. This pair excludes it relative to
**the immediately preceding green run of the same gate on the same
worktree**, where the entire delta is a markdown file that no Rust
target reads. Whatever varies between those two runs, it is not the
source tree.
**`sweep` failed with the SAME single test**, so this run is one failure
surfacing in two stages, not two failures. Both stage logs name
`attach::tests::managed_retry_survives_transients_and_uses_the_successful_stream`
and nothing else: `283 passed; 1 failed` in each.
* **Rerun: isolated selector green five times** (`1 passed`, 0.00s
each). Per this file's rerun rule that establishes **intermittence
only** — and per the tenth occurrence's correction, running the
selector outside the gate excludes nothing at all, because nothing
outside the gate has ever reproduced this failure.
* **No ratio is claimed from this occurrence.** It is one in-gate
failure following one in-gate pass, on a lane whose gate runs exist to
verify a head rather than to observe this row. Folding them into the
2026-08-29 window would be exactly the drift that window was bounded
to prevent.
* **The observing lane touches no `pmacs-gpu` file**: its whole diff is
`src/buffer.rs`, `src/rope.rs`, `src/overlay.rs`, `src/view.rs` — tests
and doc comments — plus three docs.
* **The NEXT in-gate run was green**, all eight stages
(`20260830T160242Z-3095339`), one commit later. Recorded because
omitting it would be selective, not because it resolves anything: per
the rerun rule a green run establishes intermittence only, and the
tenth occurrence already falsified "in-gate always fails".
**What this changes about the method, stated at the strength it
carries.** The tenth occurrence's narrowing said the remaining
candidates must be varied INSIDE the gate, one per run. This pair
sharpens **one** exclusion and nothing else: **the Rust source tree is
not the variable.**
It does **not** narrow the cause to three things. The gate's per-run
state — its ambient root, its fresh `TMPDIR`, and process state carried
across stage boundaries — remains the set of candidates this project can
actually vary one at a time, which is why they are the ones to try. But
**they are not an exhaustive causal set**, and an earlier version of
this paragraph said they were. Everything unrelated to the repository
also differed between the two runs: scheduler load and CPU contention,
kernel and socket timing, page cache and memory pressure, and whatever
else the machine was doing at 15:48 versus 15:56. A socket handshake
racing a `BrokenPipe` is exactly the kind of failure those can drive,
and holding the tree fixed says nothing about any of them.
**Ninth occurrence — the parse-budget diagnosability lane, 2026-08-29,
local (Linux), `gpu` step.** All three required fragments present in the local (Linux), `gpu` step.** All three required fragments present in the
durable log durable log
(`pmacs-parse-budget-9c27ecfe/gate-logs/20260829T144541Z-350549/06-gpu.log`): (`pmacs-parse-budget-9c27ecfe/gate-logs/20260829T144541Z-350549/06-gpu.log`):
@ -553,7 +696,7 @@ kind: BrokenPipe, message: "Broken pipe" })))
at `pmacs-gpu/src/attach.rs:1889`, `283 passed; 1 failed`. at `pmacs-gpu/src/attach.rs:1889`, `283 passed; 1 failed`.
* **The tree exclusion is as strong as the fifth's.** The observing * **The tree exclusion is as strong as the sixth's.** (Occurrences seven and eight, below, are on that same lane's branch.) The observing
lane's entire diff is `src/async_runtime.rs`, lane's entire diff is `src/async_runtime.rs`,
`tests/m4_acceptance.rs` and three docs — **no `pmacs-gpu` file, and no `tests/m4_acceptance.rs` and three docs — **no `pmacs-gpu` file, and no
file `pmacs-gpu` links against beyond the workspace it always did.** file `pmacs-gpu` links against beyond the workspace it always did.**
@ -565,17 +708,22 @@ at `pmacs-gpu/src/attach.rs:1889`, `283 passed; 1 failed`.
`07-sweep.log` ends in `Terminated`. That stage's absence says `07-sweep.log` ends in `Terminated`. That stage's absence says
nothing, and the run as a whole is **not** a gate result. Only the nothing, and the run as a whole is **not** a gate result. Only the
`gpu` stage's failure is, because it completed and reported. `gpu` stage's failure is, because it completed and reported.
**A SEVENTH OCCURRENCE FOLLOWED IMMEDIATELY**, on the next gate run of **A TENTH OCCURRENCE FOLLOWED IMMEDIATELY**, on the next gate run of
the same worktree at head `45d438c` the same worktree at head `45d438c`
(`20260829T150011Z-429115/06-gpu.log`), same selector, same three (`20260829T150011Z-429115/06-gpu.log`), same selector, same three
fragments, `283 passed; 1 failed`. **That run's other seven stages were fragments, `283 passed; 1 failed`. **That run's other seven stages were
green**, `sweep` included and complete — 121 result lines, none with a green**, `sweep` included and complete — 121 result lines, none with a
failure — so this pair is not confounded by a truncation the way the failure — so this pair is not confounded by a truncation the way the
sixth was. ninth was.
**Two consecutive in-gate failures is new for this row**, whose prior **Two consecutive in-gate failures is new for this row**, whose prior
five were spread across lanes and months. It prompted a narrowing, and **eight** were spread across lanes and weeks — **though not evenly**:
the narrowing is the useful part. three of the eight fall on 2026-08-15, on one branch and one machine.
*(This sentence said "prior five … across lanes and months" until the
renumbering of 2026-08-31; it was written before the 2026-08-15 pair was
absorbed and before the duplicate "fourth" was found, and it was wrong
about the count and the spread.)* It prompted a narrowing, and the
narrowing is the useful part.
**A THIRD IN-GATE RUN WAS GREEN** (head `68a16f9`, log **A THIRD IN-GATE RUN WAS GREEN** (head `68a16f9`, log
`20260829T152024Z-563254`, all eight stages, zero failures anywhere). `20260829T152024Z-563254`, all eight stages, zero failures anywhere).
@ -626,7 +774,31 @@ lanes and trees, and these locate the asymmetry in the *runner* while
showing that the obvious way to probe it — reproducing gate conditions showing that the obvious way to probe it — reproducing gate conditions
outside the gate — cannot work. outside the gate — cannot work.
**Fifth occurrence — panel cell-mapping generation (§5b) framing, **Seventh and eighth occurrences — the same §5b branch, later the same
day, 2026-08-15, local (Linux), `gpu` step.** Logs
`20260815T095532Z` and `20260815T100719Z`. **All three fragments
verified both times** —
`transient sequence must attach: Attach(Handshake(Io(Os { code: 32,
kind: BrokenPipe, message: "Broken pipe" })))` at
`pmacs-gpu/src/attach.rs:1728`, the same line as the sixth. One machine,
one day, one branch, **with a green full-gate run between them**.
Isolated reruns green.
**These sat unabsorbed for sixteen days, and that is the finding worth
keeping.** `docs/active-work.md` recorded them under a heading saying
they were "**owed to the registry by whichever branch merges second**",
deliberately held back to avoid inventing a row id against an unseen
neighbour — a real hazard this file has been bitten by, when two
branches' entries merged without a conflict and produced duplicate ids
across four sites. **The deferral was reasonable; not discharging it was
not.** Both branches merged, and nothing carried them across, so R7's
count read two low until 2026-08-31.
*The lesson is narrower than "absorb faster": an entry parked under
"owed to the registry" needs an owner named in the same sentence, or it
belongs to nobody.*
**Sixth occurrence — panel cell-mapping generation (§5b) framing,
2026-08-15, local (Linux).** The `scripts/gate` **`gpu` step** again, 2026-08-15, local (Linux).** The `scripts/gate` **`gpu` step** again,
the same flavor as occurrence 2, inside a `--protocol` run the same flavor as occurrence 2, inside a `--protocol` run
(log `20260815T072601Z-2230169`). (log `20260815T072601Z-2230169`).
@ -647,9 +819,11 @@ the same flavor as occurrence 2, inside a `--protocol` run
not exonerate the tree — though here there is no tree change to not exonerate the tree — though here there is no tree change to
exonerate. exonerate.
**What five occurrences now support, stated carefully:** the failure is **What the first six occurrences supported, stated carefully** — and it
**not lane-correlated**. It has appeared under three flavors across is unchanged by the six that followed: the failure is **not
five unrelated lanes, once on a diff that touches no code whatsoever. lane-correlated**. It has appeared under three flavors across six
unrelated lanes by this point, once on a diff that touches no code
whatsoever.
That is evidence about *where the cause is not*, and still says nothing That is evidence about *where the cause is not*, and still says nothing
about what it is. **The retirement condition is unchanged.** about what it is. **The retirement condition is unchanged.**
@ -688,7 +862,7 @@ one-second deadline. Contention is a plausible mechanism for a
lands, **run the control with the added test removed** rather than at the lands, **run the control with the added test removed** rather than at the
merge base — that is the discriminating comparison this one was not. merge base — that is the discriminating comparison this one was not.
**Fourth occurrence — the `scripts/gate` TMPDIR isolation lane, **Fifth occurrence — the `scripts/gate` TMPDIR isolation lane,
2026-08-13, local (Linux). Same selector, same `gpu`-step flavor 2026-08-13, local (Linux). Same selector, same `gpu`-step flavor
(`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`), all three fragments (`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`), all three fragments
verified** against the durable gate log verified** against the durable gate log
@ -725,7 +899,7 @@ with no new mechanism.** What it adds is the corroboration above. Three
isolated re-runs on the current tree were green, which by this file's isolated re-runs on the current tree were green, which by this file's
own rule establishes intermittence only. own rule establishes intermittence only.
**The discriminating comparison for a fifth occurrence** remains the **The discriminating comparison for a SIXTH occurrence** remains the
one the third occurrence prescribed. One occurrence, with no supported one the third occurrence prescribed. One occurrence, with no supported
mechanism, is not grounds to reverse a fix that closes two observed mechanism, is not grounds to reverse a fix that closes two observed
hazards. hazards.
@ -763,7 +937,7 @@ the lane's only `pmacs-gpu` addition is the arm that went red.
The next agent to touch this row should reproduce at 1-in-10 and The next agent to touch this row should reproduce at 1-in-10 and
instrument which side closes the pipe, rather than re-running for green. instrument which side closes the pipe, rather than re-running for green.
**Fourth occurrence — D3 file-watch scheduler (PR #235), 2026-08-11, **Fourth occurrence by date — D3 file-watch scheduler (PR #235), 2026-08-11,
local (Linux), at the gate's SWEEP step** (`cargo test --workspace local (Linux), at the gate's SWEEP step** (`cargo test --workspace
--no-fail-fast`, default features — U3's flavor, this time with the --no-fail-fast`, default features — U3's flavor, this time with the
fragments captured). All three required fragments verified against the fragments captured). All three required fragments verified against the
@ -855,11 +1029,54 @@ was lost.
| **selector** | `--lib --features crdt optimistic::tests::criterion_1_end_of_line_typing_completes_sub_frame_per_keystroke` **and** `editor::tests::composition_overhead_under_ten_percent`, failing in the same run | | **selector** | `--lib --features crdt optimistic::tests::criterion_1_end_of_line_typing_completes_sub_frame_per_keystroke` **and** `editor::tests::composition_overhead_under_ten_percent`, failing in the same run |
| **job / flavor** | local (Linux), `scripts/gate` step `04-lib-crdt`, with sibling worktrees building concurrently | | **job / flavor** | local (Linux), `scripts/gate` step `04-lib-crdt`, with sibling worktrees building concurrently |
| **required fragments** | `criterion 1: per-keystroke orchestrator time` + `exceeds 1ms`; and `composition machinery added more than 10% overhead` | | **required fragments** | `criterion 1: per-keystroke orchestrator time` + `exceeds 1ms`; and `composition machinery added more than 10% overhead` |
| **status** | **new incident, one occurrence, not reproduced** | | **status** | **FIVE OCCURRENCES — one on 2026-08-09 and FOUR on 2026-08-30**, the first time this row has ever reproduced. Two of the four out of gate, two in gate (`20260830T171941Z`, `T175657Z`). Still no mechanism; see the block below |
| **what IS established** | both are **wall-clock budget assertions** — 1.264ms against a 1ms budget, and 1.297× against a 1.10× budget — so both are load-sensitive by construction. Both green in an isolated rerun of exactly those two selectors, and both green in the next full gate run of the same command (2105 passed) | | **what IS established** | both are **wall-clock budget assertions** — 1.264ms against a 1ms budget, and 1.297× against a 1.10× budget — so both are load-sensitive by construction. Both green in an isolated rerun of exactly those two selectors, and both green in the next full gate run of the same command (2105 passed) |
| **what is NOT** | whether the machine's concurrent load caused it. The confound is real (this machine runs one shared `CARGO_TARGET_DIR` and several worktrees) but **was not measured**, so it is a rival explanation, not a finding | | **what is NOT** | whether the machine's concurrent load caused it. The confound is real (this machine runs one shared `CARGO_TARGET_DIR` and several worktrees) but **was not measured**, so it is a rival explanation, not a finding |
| **rival explanation not excluded** | a genuine regression in either path. Nothing in the observing diff touches the optimistic-echo orchestrator or the composition pipeline, but "my diff looks unrelated" is not evidence, and this row does not treat it as such | | **rival explanation not excluded** | a genuine regression in either path. Nothing in the observing diff touches the optimistic-echo orchestrator or the composition pipeline, but "my diff looks unrelated" is not evidence, and this row does not treat it as such |
**Second occurrence — the CRDT identity-undo lane, 2026-08-30, local
(Linux).** Both required fragments captured, both selectors, one run:
```
criterion 1: per-keystroke orchestrator time 1.343883ms exceeds 1ms
composition machinery added more than 10% overhead: 1.182
```
**It reproduced on the immediately following run**, which is new for
this row — the first occurrence explicitly recorded "not reproduced".
Margins are recorded because U11 taught this registry what their absence
costs. Here: **1.343883ms against a 1ms budget** (1.34×) and **1.182×
against a 1.10× budget**; at the first occurrence, 1.264ms and 1.297×.
So the composition margin grew and the keystroke margin grew, but
neither by an order that separates load from regression. Both selectors
were green in isolated single-selector reruns.
**Fourth and fifth occurrences — both IN gate, both `04-lib-crdt`,
both the exact pair.** `20260830T171941Z-3509751` (`1.689259ms` against
1ms; `1.592×` against 1.10×) and `20260830T175657Z-3881334`
(`2.269247ms`; `1.527×`). Those two runs also redded other selectors in
other steps; **those clusters are U14 and U15 respectively**, and only
the `04-lib-crdt` pair belongs to this row.
**A DIRECTION CLAIM WAS MADE HERE AND IS WITHDRAWN, within the hour.**
This block first said the asymmetry "runs the OPPOSITE way to R7":
both failures were out of gate, while `04-lib-crdt` was green in all
four of this lane's gate runs to that point (`20260830T154827Z`,
`T155621Z`, `T160242Z`, `T160824Z`). **The very next gate run redded
`04-lib-crdt` with this exact pair.** So U6 fails **both** in and out of
gate, the four green stages were a run of four and not a property, and
the only honest reading is the one the first occurrence already gave:
these are wall-clock budget assertions.
**No mechanism is claimed.** The load confound was unmeasured at the
second and third occurrences and at the fourth; **U15 records a single
`/proc/loadavg` reading of `34.04` taken after the fifth**, which makes
severe unrelated load a measured presence rather than a measured cause —
see that row for why the difference matters. What is worth having here
is that **this row is now reproducible under some condition**, which the
first occurrence could not say.
**Two budget tests failing in one run and neither in the next is the **Two budget tests failing in one run and neither in the next is the
signature worth matching**, more than either name alone: a real signature worth matching**, more than either name alone: a real
regression in two unrelated subsystems at once is far less likely than regression in two unrelated subsystems at once is far less likely than
@ -1013,7 +1230,7 @@ claim is the one a later reader would otherwise reach for.*
| **status** | **one occurrence; INTERMITTENT — the identical sweep command on the same tree was green (118 targets, 1928 passed, exit 0)** | | **status** | **one occurrence; INTERMITTENT — the identical sweep command on the same tree was green (118 targets, 1928 passed, exit 0)** |
| **what IS established** | intermittence, with the strongest available exclusion of the tree: green in two earlier steps of the **same run**, green isolated afterwards (`2 passed`, 1.70 s), green on a full sweep rerun. Both assertions are **timing-sensitive by construction** — one reads collected child output within a deadline, the other measures wall-clock composition overhead (observed 1.613× against a 1.10× budget; 61.3% dispatch and 124.6% realistic overhead) | | **what IS established** | intermittence, with the strongest available exclusion of the tree: green in two earlier steps of the **same run**, green isolated afterwards (`2 passed`, 1.70 s), green on a full sweep rerun. Both assertions are **timing-sensitive by construction** — one reads collected child output within a deadline, the other measures wall-clock composition overhead (observed 1.613× against a 1.10× budget; 61.3% dispatch and 124.6% realistic overhead) |
| **what is NOT** | cause, and the load confound is **partially measured but NOT controlled**. The failing sweep ran inside a full gate; the green rerun started at load average 1.98 with the 5-minute figure still at 8.03 from that gate. Different conditions is not a measurement of the mechanism, and this row does not treat it as one | | **what is NOT** | cause, and the load confound is **partially measured but NOT controlled**. The failing sweep ran inside a full gate; the green rerun started at load average 1.98 with the 5-minute figure still at 8.03 from that gate. Different conditions is not a measurement of the mechanism, and this row does not treat it as one |
| **the structural difference worth testing next** | `cargo test --workspace` runs **many test binaries concurrently**; `--lib` runs **one**. That is a difference in kind between the passing steps and the failing one, not merely a difference in load average — and it is the first candidate this family has had that is checkable rather than atmospheric. **Discriminating control:** rerun the sweep with test-binary concurrency pinned to 1, and separately run the `--lib` binary alone under synthetic load. A red under synthetic load at low sweep concurrency implicates load; a red at high concurrency and low load implicates the concurrency itself | | **the structural difference worth testing next — PREMISE FALSIFIED 2026-08-31** | This cell claimed `cargo test --workspace` runs **many test binaries concurrently** while `--lib` runs one, and derived a control from it: "pin test-binary concurrency to 1". **Cargo runs test targets SERIALLY**, one executable at a time, so that concurrency is already 1 and the control pins nothing. Measured in this project's own logs: in `20260831T093655Z-857818/07-sweep.log` the **119 ordinary targets** each report before the next starts — **zero** overlapping starts — and the two trailing result lines (numbers 120 and 121 of 121) are the `Doc-tests` groups, not targets. *An earlier version of this cell said "alternates strictly, 119 to 121", which is the very claim the paragraph below retracts.* `--test-threads=1` is a *different* knob — it serializes test functions **within** one executable — and does not stand in for the control either. **The real difference between the steps is which binaries run and how long the whole step takes, not how many run at once.** A replacement control has to be designed; this row no longer has one |
| **relation to U2 — a NEAR MISS, do not match it there** | the PTY fragment is U2's exact family (`stty -a output was: ""`), but U2's selector field names only `m6_1_pty_raw_mode_disables_kernel_echo`. U2's occurrence 2 saw raw **and** canonical fail together; here **canonical redded alone and raw passed**, which U2's evidence has never shown. It is recorded here rather than folded into U2 so that the "canonical alone" case stays visible | | **relation to U2 — a NEAR MISS, do not match it there** | the PTY fragment is U2's exact family (`stty -a output was: ""`), but U2's selector field names only `m6_1_pty_raw_mode_disables_kernel_echo`. U2's occurrence 2 saw raw **and** canonical fail together; here **canonical redded alone and raw passed**, which U2's evidence has never shown. It is recorded here rather than folded into U2 so that the "canonical alone" case stays visible |
| **relation to U6 — its own instruction, honoured** | `composition_overhead_under_ten_percent` is one of U6's two selectors, and U6 says plainly: "If a future run reds **one** of these without the other, that is a different incident and should be judged as one." It redded without `criterion_1_end_of_line_typing…`, in a different step, at a far larger margin (1.613× here against U6's 1.297×). Judged as a different incident, as instructed | | **relation to U6 — its own instruction, honoured** | `composition_overhead_under_ten_percent` is one of U6's two selectors, and U6 says plainly: "If a future run reds **one** of these without the other, that is a different incident and should be judged as one." It redded without `criterion_1_end_of_line_typing…`, in a different step, at a far larger margin (1.613× here against U6's 1.297×). Judged as a different incident, as instructed |
| **what this row does NOT assert** | that the two selectors share a mechanism. They failed together once; they belong to different subsystems; and U7 already refused this exact merge for U6. The **co-failure inside one step with an in-run green control** is the signature — not either name, and not a shared cause | | **what this row does NOT assert** | that the two selectors share a mechanism. They failed together once; they belong to different subsystems; and U7 already refused this exact merge for U6. The **co-failure inside one step with an in-run green control** is the signature — not either name, and not a shared cause |
@ -1042,15 +1259,54 @@ green in the other run**.
| **relation to U6** | run B's selector is one of U6's two, redding **without** `composition_overhead_under_ten_percent`. U6 instructs that one-without-the-other is a different incident; honoured here | | **relation to U6** | run B's selector is one of U6's two, redding **without** `composition_overhead_under_ten_percent`. U6 instructs that one-without-the-other is a different incident; honoured here |
| **what this row does NOT assert** | a shared mechanism between the two rows, or any mechanism at all. **The signature is the rotation across an identical commit** — not either name | | **what this row does NOT assert** | a shared mechanism between the two rows, or any mechanism at all. **The signature is the rotation across an identical commit** — not either name |
**Why this family keeps recurring, stated plainly.** Every row in it is **Why this family keeps recurring — with its stated premise CORRECTED,
a wall-clock budget asserted **inside a workspace-wide parallel test because it was false.** Every row in it is a wall-clock budget asserted
run**. `cargo test --workspace` starts many test binaries at once, so inside a workspace-wide test run. This paragraph used to add that
each budget competes with the rest of the sweep in **every** run, "`cargo test --workspace` starts many test binaries at once". **It does
including the ones that pass. A 4.5% overshoot on a 1ms budget is not a not. Cargo runs test targets SERIALLY, one executable at a time**, and
signal about the code. **U9 already named the discriminating control** this project's own gate logs measure it. In
— pin test-binary concurrency to 1 and separately load a lone `--lib` `20260831T093655Z-857818/07-sweep.log` (and reproduced on
binary — and it remains unrun. Until it runs, this family should not `20260831T130742Z-2805186`): **119 ordinary targets, each of which
consume another review round. reports its result before the next one starts** — zero cases of one
`Running` line following another. There are **121 result lines in total**, and the last two —
**numbers 120 and 121** — are not targets: they belong to the doc-test
groups `Doc-tests pmacs` and `Doc-tests pmacs_protocol`, which cargo
labels differently and runs after everything else. So the alternation is
119 `Running`/result pairs, then two doc-test results.
**Two earlier versions of this paragraph got the arithmetic wrong**,
which is worth leaving on the record in a file about not trusting
unverified numbers: the first called the whole thing "strictly
`RTRTRT…`" with 119 and 121, which cannot be strict; the second called
the doc-test results the 121st and 122nd, when there is no 122nd.
So the budgets do **not** compete with the rest of the sweep in the way
this family assumed.
**And the first replacement for that premise did not describe U9
either.** It said a budget "runs at an arbitrary point in a multi-minute
step". **U9's two selectors are both in the root lib target** —
`m6_1_pty_canonical_mode_keeps_kernel_echo` (`src/process.rs:3967`) and
`composition_overhead_under_ten_percent` (`src/editor.rs:9717`) — and
**note the selector**: U9's row names the CANONICAL test, not
`m6_1_pty_raw_mode_disables_kernel_echo` (`:3945`), and an earlier
version of this paragraph named the raw one. U9's own "relation to U2"
cell turns on exactly that distinction — canonical redded alone while
raw passed — so getting it backwards would have undercut the row it was
trying to correct.
the sweep runs that target **first**, finishing it in about 12 seconds
of a multi-minute step. The sweep's later minutes cannot reach them.
What survives is narrower still: **the sweep re-runs the lib target late
in the overall gate invocation**, after `03-lib` and `04-lib-crdt` have
already run it, under machine state nobody measured. A 4.5% overshoot on
a 1ms budget remains not a signal about the code.
**And U9's named control does not discriminate what it claimed** —
"pin test-binary concurrency to 1" pins something that is *already* 1.
See the correction on U9 itself. This family still should not consume
another review round, but it now needs a control someone has to design,
not one already written down.
**Widening a budget is not the fix**, and R1 already rejected it. **Widening a budget is not the fix**, and R1 already rejected it.
@ -1110,11 +1366,424 @@ PTY selector.
| **relation to U6** | its selector, alone again, in U6's own step. U6's instruction to judge that separately is honoured for the second time — see U9, which did the same | | **relation to U6** | its selector, alone again, in U6's own step. U6's instruction to judge that separately is honoured for the second time — see U9, which did the same |
| **relation to U9** | the same budget-plus-PTY co-failure, in `04-lib-crdt` rather than `11-sweep`, with `setsid_escapee…` where U9 had `m6_1_pty_raw_mode…` | | **relation to U9** | the same budget-plus-PTY co-failure, in `04-lib-crdt` rather than `11-sweep`, with `setsid_escapee…` where U9 had `m6_1_pty_raw_mode…` |
**This family has now produced U6, U9, U10 and U12, and the **This family has now produced U6, U9, U10 and U12 — and the control
discriminating control U9 named is STILL UNRUN**: pin test-binary U9 named no longer exists to run.** It had two halves. *Pin test-binary
concurrency to 1, and separately load a lone `--lib` binary. Four concurrency to 1* is **VOID**: cargo already runs targets serially, so
incidents is enough evidence that the family will keep costing review it pins nothing (see U9). *Separately load a lone `--lib` binary under
rounds until someone runs it. synthetic load* survives as an experiment but is **not a discriminator**
— with concurrency fixed at 1 there is no second arm to compare against,
so a red would show load is **sufficient** to produce one, and a green
would show nothing at all. **It could never "clear" load**, and this
file's own rerun rule says why.
Four incidents is enough evidence that the family will keep costing
review rounds. What it needs is a control someone designs, not the one
written down.
### U14 — FOUR selectors red in ONE gate run, across three stages
Recorded on the CRDT identity-undo lane, 2026-08-30, local (Linux),
`scripts/gate` log `20260830T171941Z-3509751`. **The co-occurrence is
the signature**, as it is for U6, U9 and U12: four selectors in **four
unrelated subsystems** — the async runtime, the optimistic-echo
orchestrator, editor composition, and the LSP dispatch seam — failing in
one run is less likely than one loaded machine, and no single selector
reds twice within the run. **U6's own row treats its two selectors as
unrelated subsystems**, so the `04-lib-crdt` pair is two of the four
here, not one.
| field | value |
|---|---|
| **selectors** | **four.** `03-lib`: `async_runtime::tests::grep_supersede_cancels_predecessor_within_50ms`. `04-lib-crdt`: `optimistic::tests::criterion_1_end_of_line_typing_completes_sub_frame_per_keystroke` **and** `editor::tests::composition_overhead_under_ten_percent` (U6's pair — see below). `07-sweep`: `lsp_dispatch_seams_acceptance::acc34_purge_reaches_a_server_that_is_in_no_attachment` |
| **job / flavor** | local (Linux), one `scripts/gate` run, three different steps |
| **required fragments** | `grep supersede did not cancel within 50ms` + an `elapsed:` value; `criterion 1: per-keystroke orchestrator time` + `exceeds 1ms`; `composition machinery added more than 10% overhead`; `is not ready for requests (state: initializing)` |
| **status** | **new incident, ONE occurrence** |
| **what IS established** | all four fragments captured from the durable stage logs. Margins: `52.44341ms` against 50ms (4.9% over); `1.689259ms` against 1ms; `1.592×` against 1.10×. The `07-sweep` failure is **not** a budget — an LSP server was asked for a request while still `initializing` |
| **what is NOT** | any shared mechanism, and **any load measurement**: no `/proc/loadavg` reading was taken during or after this run. Three stages, **four subsystems**, and one of the four selectors is a readiness race rather than a clock |
| **the observing tree** | the lane's revision-5 commits: an enumeration in a `#[cfg(test)]` predicate and documentation. It touches `async_runtime`, `optimistic`, `editor` and the LSP dispatch seam **not at all** |
**An earlier version of this row claimed a SECOND occurrence, and that
was a matching-rule violation.** The run 40 minutes later
(`20260830T175657Z-3881334`) redded a **different selector set** —
`full_buffer_summary_flatten_scales_on_large_grammar_file` and
`dired_renders_10k_entries_within_200ms` in place of
`grep_supersede…` and `acc34_purge…`. Under "How a row matches" above,
the exact selectors must match; a rotated set is a **new incident**.
It is now **U15**, and the `04-lib-crdt` pair the two runs do share is
recorded where it belongs, as U6's own occurrence.
**Relation to R1, and it is NOT a match.** The `03-lib` failure carries
R1's required fragment `supersede did not cancel within 50ms`, but R1's
selector is `supersede_cancels_in_flight_job_within_50ms` and this is
`grep_supersede_cancels_predecessor_within_50ms` — **a different test**.
This registry matches on selector *and* fragments, and U6's own
instruction ("one without the other is a different incident") points the
same way. Recorded as a sibling, not an occurrence.
**One thing the sibling shows for free, and R1 should have it.** R1's
row records that its assertion "still omits its measurement —
`started.elapsed()` is in hand at the panic and the message reports none
of it, so this occurrence's margin is as unrecoverable as every prior
one's." **The sibling test already reports it**: `(elapsed:
52.44341ms)`, which is how the 4.9% margin above is known at all. The
measurement-design question R1 defers to the async-runtime lane is
untouched by this — but the cheap half of it is demonstrably already
written, next door in the same module.
**Reruns: all four selectors green in isolation** — `grep_supersede…`
`1 passed`, U6's two `1 passed` each, and the whole
`lsp_dispatch_seams_acceptance` binary `15 passed`. Per this file's
rerun rule that establishes **intermittence only**; it exonerates
nothing, and in particular it does not show the tree is innocent, only
that the failures do not reproduce alone.
### U15 — a rotated multi-red cluster, with a contemporaneous load reading
Recorded on the CRDT identity-undo lane, 2026-08-30, local (Linux),
`scripts/gate` log `20260830T175657Z-3881334` — 40 minutes after U14's
run, on the same tree. **A new incident rather than a U14 occurrence**,
because the selectors rotated and this file matches on the exact set.
| field | value |
|---|---|
| **selectors** | `03-lib` (**default features**): `editor::tests::composition_overhead_under_ten_percent` **and** `semantic_render::tests::full_buffer_summary_flatten_scales_on_large_grammar_file`. `07-sweep`: `dired_acceptance::dired_renders_10k_entries_within_200ms` |
| **job / flavor** | local (Linux), one `scripts/gate` run, steps `03-lib` and `07-sweep` |
| **required fragments** | `composition machinery added more than 10% overhead`; `full-buffer flatten took` + `the event sweep must stay ~linear`; `10K entries must render within 200ms; took ` |
| **status** | **new incident, one occurrence** |
| **what IS established** | margins `1.450×` against 1.10×, `1.274901136s` against a ~linear expectation, and `221.459827ms` against 200ms (10.7% over). **`/proc/loadavg` read immediately after the run: `34.04 38.45 26.29`**, with the CPU saturated by unrelated `lean` processes — nothing this session started, and no `cargo`, `rustc` or `scripts/gate` process of mine left running |
| **what is NOT** | that the load caused any of it. See below |
**Two selectors here belong to other rows and are deliberately NOT
claimed as their occurrences.** `composition_overhead_under_ten_percent`
is one of U6's two, and it redded in `03-lib` **without** its partner
and under **default features**, not U6's `04-lib-crdt`/`crdt` flavor —
U6's own instruction is that one-without-the-other is a different
incident. `dired_renders_10k_entries_within_200ms` is one of U7's three,
and this is the **second** time that same selector has redded — U7's own
instruction is that a repeat of one selector is a different incident.
Both instructions are honoured rather than quoted and ignored.
*(The same run's `04-lib-crdt` step redded U6's pair together, in U6's
flavor and step. That IS a U6 occurrence and is recorded there.)*
**What the load number establishes, stated at its real strength — and
it is NOT this registry's first.** **U7 has carried a load average since
2026-08-09** (`12.9 / 23.9`, in its job/flavor field); what U7 records as
unmeasured is something narrower, whether the shared
`CARGO_TARGET_DIR` and its sibling worktree builds *produced* that load.
An earlier version of this block said U6 and U7 had both wanted a number
since 2026-08-09. Half of that was wrong.
What `34.04` is: **the first contemporaneous load reading for a U6
occurrence** — U6's row has said since 2026-08-09 that its confound "was
not measured" — and **a new reading alongside a recurring U7 selector**,
not U7's first. That makes load a **measured confound present
contemporaneously with a multi-red run**. It does not make it the cause,
and three specific things stop it short:
* **the reading is a single point, taken after the fact.** No
`/proc/loadavg` was captured during U14's run or the two out-of-gate
runs, so there is no series to correlate margins against;
* **the margins are not monotonic.**
`composition_overhead_under_ten_percent` went `1.182×` → `1.592×` →
`1.527×` across the three runs, and in this run alone it reports
`1.450×` in `03-lib` and `1.527×` in `04-lib-crdt`. Only
`criterion_1`'s three points rise monotonically (`1.343883ms`,
`1.689259ms`, `2.269247ms`), and three points with one load reading
is not a dose-response;
* **an earlier version of this write-up said "a load average of 34
explains it without any help."** That overstates it. Severe unrelated
load was present; whether it produced these particular margins is
unmeasured.
**What it does change:** for U6, "one loaded machine" stops being a
hypothesis offered in good faith and becomes a **quantity on the
record** — a second data point for the correlation U7's reading started.
It retires nothing: **U15 itself**, U6, U7, U14 and R1 all keep their
dispositions, and the budgets remain wall-clock assertions whose
measurement design nobody has defended.
**Reruns: green in isolation** — `composition_overhead…` `1 passed`,
and the `dired_acceptance` selector had already been shown green in
isolation by U7. Intermittence only, per the rerun rule.
### U16 — a git invocation finds its working directory deleted
Recorded on the CRDT identity-undo lane, 2026-08-31, local (Linux),
`scripts/gate` log `20260831T083021Z-272257`, step `07-sweep`, **and
again the same day** — see the second occurrence below. **Not a budget
row** — nothing here is a clock. It is the only row in this file that
arrives with a **named candidate mechanism inside the test suite**,
which is why it was worth recording before it had reproduced.
| field | value |
|---|---|
| **selector** | `--lib packages::fetcher::tests::cache_survives_across_fetcher_instances` |
| **job / flavor** | local (Linux), `scripts/gate` step `07-sweep` (`cargo test --workspace --no-fail-fast`) |
| **required fragments** | `Unable to read current working directory: No such file or directory` + `remote did not send all necessary objects` |
| **status** | **SECOND OCCURRENCE 2026-08-31 — it reproduced, in the same step, with the same fragments** |
| **what IS established** | the fragments, captured from the durable stage log at `src/packages/fetcher.rs:929`. `1989 passed; 1 failed`. The test spawns `git` against a `file://` remote in a temp dir |
| **what is NOT** | that the mechanism below is what happened. It is a candidate with a citation, not a demonstrated chain |
| **the observing tree** | the lane's docs-only commit. It touches `src/packages/` not at all |
**The candidate mechanism, and the load-bearing step is CHILD
INHERITANCE.** An earlier version of this row stopped at "the window
exists", which misses why restoring the cwd does not close it:
1. `src/file_io.rs:434` — `bare_filename_saves_in_cwd` — calls
`std::env::set_current_dir(dir.path())`, mutating **process-global**
state and pointing it at a `TempDir`;
2. concurrently, `cache_survives_across_fetcher_instances` reaches
`f1.fetch(&url)` (`fetcher.rs:929`), which clones via `run_git`
(`:305`). `run_git` calls `run_git_inner(None, …)`, and
`run_git_inner` (`:322`) sets `cmd.current_dir` **only when `cwd` is
`Some`** — `if let Some(d) = cwd { cmd.current_dir(d); }`,
`:329`–`:330`. With `None`, **the spawned `git` INHERITS the
parent's cwd** — the temp directory;
3. the parent then restores its own cwd. **That does nothing for the
child**, which already has its working directory;
4. the `TempDir` drops. `git` is now a live process whose cwd is a
**deleted directory**, and `fatal: Unable to read current working
directory` is exactly what that produces.
So the restore in `bare_filename_saves_in_cwd` is not merely
insufficiently early — it is **irrelevant to the child**, which is why
care inside that one test cannot close this.
**What would settle it**, and neither has been run:
* run the two selectors concurrently in a tight loop until the failure
reproduces, which converts the candidate into a demonstration;
* or make the hazard structural rather than probabilistic. **A serial
guard around `set_current_dir` tests is NOT one of the options**, and
an earlier version of this row offered it: the child outlives the
guard, so any unguarded test that spawns a process inheriting the cwd
is still exposed. What does work:
* **remove the process-global mutation** — `bare_filename_saves_in_cwd`
exists to check that a bare filename resolves against the cwd, and
`save_atomic` could take the directory rather than inheriting it;
* **run that test in a subprocess**, so its cwd is its own;
* **serialize the whole lib-test binary** (`--test-threads=1`), which
removes the concurrency the race needs — at the cost of the whole
binary's wall clock, and note U17, where that same flag is a
candidate for causing a different failure.
**Reruns: green in three isolated runs of the selector, and in EIGHT
full parallel `cargo test --lib` runs** (1990 passed each). Per this
file's rerun rule that establishes **intermittence, and nothing more**.
An earlier version added "and here it also says the window is narrow" —
**it does not**. Non-reproduction over eight runs says the failure did
not recur in eight runs. It says nothing about the width of *this
candidate's* window, which no measurement here has sized.
**SECOND OCCURRENCE, 2026-08-31, log `20260831T174104Z-3438184`, step
`07-sweep`** — same selector, same panic site `fetcher.rs:929`, both
required fragments, `1988 passed; 2 failed`. **This is the first time
the row has reproduced, and it settles the point above in the right
direction**: withdrawing "the window is narrow" was correct, because
eight green runs had not measured it, and the failure returned within
the day. The candidate mechanism in §"child inheritance" above is
unchanged and still a candidate — nothing in this occurrence
demonstrates the chain either.
**And it passed again immediately after**, in all three stages of the
next gate run — `03-lib`, `04-lib-crdt` and the same `07-sweep` context
— at `ea786a2`, log `20260831T174716Z-3535694`. Two reds and many greens
**all on 2026-08-31**: **intermittent**, at a rate nothing here has
measured. *(An earlier version said "across two days"; every run cited
by this row is the same day, as the sentences above it already said.)*
**Not folded into U14 or U15.** Different selector, different fragments,
different step, and a different kind of failure: those are wall-clock
budgets under load, this is a race over process-global state. U14's
`acc34_purge` readiness failure is the nearest relative in kind, and even
that is a different mechanism.
### U17 — a supersede race lost the OTHER way, on `main`, single-threaded
Surfaced 2026-08-31 by the **merge-base control dispatched for R6's
second occurrence** — so it is a red on `main` at `aae5b35`, on no
branch at all.
**An earlier version said "no PR run can show this." That is wrong**: a
PR run exercises the same test and could fail it identically. What only
a `main`-side run can establish is that it fails **on `main`** — that
there is no observing branch to suspect — and that is the distinction
the dispatch actually bought.
| field | value |
|---|---|
| **selector** | `--test m8_1_acceptance read_dir_supersede_cancels_in_flight_predecessor` |
| **job / flavor** | GitHub Actions, `Test (crdt)`: `cargo test --all-targets --no-default-features --features luajit,crdt -- --test-threads=1` |
| **required fragments** | `first read_dir must be superseded; got ok` |
| **status** | **new incident, one occurrence, ON `main`** |
| **what IS established** | `9 passed; 1 failed`, panic at `tests/m8_1_acceptance.rs:278`, [run 33375945966](https://github.com/levineuwirth/pmacs/actions/runs/33375945966) job 99437344558, head `aae5b35` |
| **what is NOT** | any mechanism. The candidate below is a reading of the assertion, not a diagnosis |
| **attribution** | **none available, and none needed** — `aae5b35` is `main`. There is no observing branch to suspect |
**It fails the OPPOSITE way to R1 and R5, and that is the interesting
part.** Both of those are **deadline** failures — a cancellation that
did not arrive in time (`supersede did not cancel within 50ms`, `async
pump deadline exceeded`). This one reports `got ok`: the first
`read_dir` **completed successfully** rather than reporting cancellation.
**What `got ok` proves, precisely:** the predecessor **completed
successfully before the cancellation took effect**. It does **not**
establish when the supersede arrived — an earlier version of this row
said "it arrived after there was nothing left to supersede", which
assumes a late arrival the assertion cannot see. A supersede that
arrived in time and whose cancellation simply did not take effect first
produces the identical message.
**Candidate mechanism, stated as one — and stated smaller than an
earlier version had it.** The job runs **`--test-threads=1`**, which
serializes the **test functions inside one libtest executable**. A test
that supersedes a job "in flight" depends on the predecessor still being
in flight, and removing sibling test functions from the same process
**removes one source of contention** for it.
That is all it supports. The earlier wording said the predecessor is at
its *fastest* and the window at its *narrowest*; neither follows. Other
contention remains — the rest of the machine, the CI runner's own load,
and every other process — and nothing here measured the predecessor's
duration with the flag on versus off. It is **not** a diagnosis, and
nothing rules out a real supersede defect.
**Worth noting for U9 — and NOT as an instance of its control.** An
earlier version said this job "already does" what U9's control asks. It
does not, and the distinction is the whole point of U9's premise:
**`--test-threads=1` serializes test FUNCTIONS within one executable; it
does not pin test-BINARY concurrency.** Those are different knobs. See
the correction recorded against U9 and U12 below, which is larger than
this note.
**Not R1 and not R5**, on this file's own matching rule: different
selector, different module, different assertion. R5's row draws exactly
this distinction against R1 and the same reasoning applies again —
sharing a subject is not sharing a signature.
**No rerun was performed.** U3's lesson and R6's "never a green rerun"
disposition both apply, and there is no branch here whose merge this
would gate.
### U18 — a Go module checksum fetch fails before anything is built
Recorded on the CRDT identity-undo lane, 2026-08-31,
`Test (ubuntu-latest / luajit)` on PR #246 at `a7c4b3a`
([job 99499800716](https://github.com/levineuwirth/pmacs/actions/runs/33395769472/job/99499800716)).
**A new class for this file: nothing was built and no test ran.** The
job died in its toolchain-setup step.
| field | value |
|---|---|
| **selector** | none — this is not a test. The failing step is `go install golang.org/x/tools/gopls@v0.16.2`, part of the LSP fixture setup |
| **job / flavor** | GitHub Actions, `Test (ubuntu-latest / luajit)` |
| **required fragments** | `sum.golang.org/tile/` + `stream error` + `INTERNAL_ERROR; received from peer`, while `verifying module: golang.org/x/telemetry` |
| **status** | **new incident, one occurrence** |
| **what IS established** | the failure is a **checksum-database read over HTTP/2**: `reading https://sum.golang.org/tile/8/0/x114/644: stream error: stream ID 41; INTERNAL_ERROR; received from peer`. `gopls@v0.16.2` and the `x/telemetry` pin both downloaded successfully first; only the sum-database verification failed. Job duration 1m45s, exit code 1 |
| **what is NOT** | anything about this repository. **No `cargo` command ran**, no test executed, and the pinned versions are the point — the workflow comments say the pin exists so "CI behaviour" does not "drift with upstream releases" |
| **attribution** | **none to the branch.** The step runs before any pmacs code is compiled, and the other 13 checks passed on the same head |
**Why it gets a row at all.** Every other row here is a test that
failed. This is **infrastructure the workflow depends on failing to
answer**, and it presents as a red check indistinguishable from a real
one at a glance. A future occurrence should be recognisable as this
rather than investigated as a product defect, which is the whole
purpose of a signature.
**It is genuinely external, and that is a claim with a limit.** The
fragments name `sum.golang.org` — Google's checksum database — returning
an HTTP/2 stream error. Nothing in this repository can produce that.
What this repository *does* control is whether a transient upstream
outage fails a whole matrix leg, and that is a real question this row
does not answer.
**An earlier version of this paragraph listed three options, and all
three were wrong.** They are corrected here rather than deleted:
* **`GONOSUMCHECK` is not a Go environment variable.** It was invented
by that sentence;
* **`GOFLAGS=-mod=mod` does not bypass checksum-database
authentication.** It selects the module *update* mode, which is a
different thing;
* **vendoring does not follow from "a vendored `gopls`".** The step is a
version-suffixed `go install …@v0.16.2`, and that form **ignores
vendor directories**, so pinning that way needs a different
installation path entirely.
**`GONOSUMDB` was thrown out with them, and should not have been.** It
is real, and `go help environment` on the toolchain in this checkout
documents `GOPRIVATE, GONOPROXY, GONOSUMDB` as glob patterns of module
prefixes "that should always be fetched directly **or that should not be
compared against the checksum database**" — precisely the step that
failed here. So it is **technically applicable**. Whether exempting a
dependency from checksum verification is an acceptable authentication
tradeoff to buy CI stability is a real question, and a different one
from whether the knob exists. The same page notes `GOINSECURE` does
**not** disable that validation, which is the kind of near-miss that
made the original sentence wrong.
**The options that stand, then:** `GONOSUMDB`/`GOPRIVATE` with the
tradeoff stated, or **retrying the step**. Choosing between them is not
this lane's work. Both directions of this paragraph's history are
recorded because both were errors: **inventing an environment variable
to fill a sentence**, and then **discarding a real one while correcting
it**.
**No rerun was performed**, and deliberately: U3's lesson is to read the
log first, and the log is now read and quoted above. Whether a rerun
would pass is uninteresting — a transient network error is *expected* to
pass on retry, and doing so would establish nothing while destroying
nothing either. It is left for whoever next pushes to this branch.
### U19 — a terminal bell is not observed within a 5s poll
Recorded on the CRDT identity-undo lane, 2026-08-31, local (Linux),
`scripts/gate` step `07-sweep`, log `20260831T174104Z-3438184` — the
**same run** that produced U16's second occurrence, and recorded
separately because the selectors and fragments differ.
| field | value |
|---|---|
| **selector** | `--lib daemon::tests::terminal_bell_baseline_suppresses_history_and_delivers_each_new_bell_once` |
| **job / flavor** | local (Linux), `scripts/gate` step `07-sweep` (`cargo test --workspace --no-fail-fast`) |
| **required fragments** | `initial terminal bell timed out` |
| **status** | **new incident, one occurrence** |
| **what IS established** | panic at `src/daemon.rs:5296`, `1988 passed; 2 failed`. The assertion is a **5-second poll**: `while bell_count(buffer_id) != Some(1) { tick_processes(); assert!(Instant::now() < deadline); sleep(10ms) }` (`src/daemon.rs:5293`–`:5298`) |
| **what is NOT** | whether the bell never arrived or arrived late. The loop cannot tell those apart, and **the panic reports no elapsed value** — R1's complaint about its own assertion, in a second place |
| **the observing tree** | a documentation-only commit |
**It is a deadline, but a much slacker one than the budget family's.**
U6, U7, U9, U10, U12 and U15 assert that work finishes *fast* — 1ms,
1.10×, 200ms. This one allows **five seconds** for an event to be
observed at all: 5000× the 1ms budget, but only **25×** the 200ms one.
*(An earlier version said "three orders of magnitude", which does not
hold against the 200ms row it was comparing to.)* Folding this into that
family would still blur a real distinction, but the distinction is one
of degree, not of kind.
**What the evidence supports is narrower than the earlier wording.** It
shows that **no bell was observed within five seconds** — not that the
bell "never comes", and **not that scheduling cannot explain it**. Five
seconds is a long time on an idle machine and a short one on a loaded
one, and nothing here measured which this was.
**What it shares with R1 is the missing measurement.** `Instant::now()`
is in hand at the panic and the message reports none of it, so **this**
occurrence's margin is unrecoverable — exactly what R1's row records
about itself, and what U11 cost this project once already. **Adding the
elapsed value would not make a future occurrence comparable to this
one** — that margin is gone for good. It would make future occurrences
comparable **to each other**, which is the whole of what the change buys
and is still worth having.
**Reruns: it PASSED in all three stages of the next gate run** — `03-lib`,
`04-lib-crdt` and the exact `07-sweep` context it failed in — at
`ea786a2`, log `20260831T174716Z-3535694`. **So it is intermittent.**
An earlier version of this row said "no rerun was performed"; the very
next gate run was one, and the row said otherwise until review caught
it. Per this file's rerun rule, three green runs establish
**intermittence only**.
### U13 — gate prune-reporting row receives empty child stdout in `sweep` ### U13 — gate prune-reporting row receives empty child stdout in `sweep`

View File

@ -0,0 +1,343 @@
# Identity-replace undo — a CRDT-version delta is not a text delta
**Status: revision 5 — APPROVED at revision 4 and IMPLEMENTED**
(PR #246, branch `crdt-identity-undo`). Revision 5 is a correction pass
answering implementation review; it changes the invariant's shape, not
its ruling.
Revision 5 answers three findings against the implementation:
1. **the predicate conflated an empty TEXT delta with a version
delta.** It called every empty-range/zero-insertion edit
`version_only` and then accepted `(History, empty, None)` through a
wildcard arm — which contradicts this framing's own "the op must
survive". The rule is now a full enumeration over three independent
axes (§1a), and C5 asserts all four empty-delta quadrants rather
than two;
2. **the public `Edit` doc was factually false**, saying forward
`apply_edit` never produces the empty-delta shape while C2b proves
all three forward empty forms do. The shape is now named an **empty
text delta**, reachable on both paths, with `crdt_op` as the
discriminator;
3. **R7's write-up overstated what the paired gate runs exclude** — see
`docs/ci-red-signatures.md`; the pair excludes the source tree and
nothing else.
Revision 4 answered review of 3, which found one substantive gap: **C9
guarded the census by file set and count, which a same-file substitution
walks straight through.** C9 now asserts the exact
`(file, impl target)` pairs, and its claim is scoped to in-tree
implementations.
Revision 3 answered review of 2 by completing §4's census: it closes by
construction, and two of its results changed the framing's own claims.
## 1. The decision, ANSWERED — and it is about PROVENANCE, not shape
**A visible TEXT delta and a CRDT-VERSION delta are INDEPENDENT
dimensions of `Edit`.** An `Edit` may legitimately carry
`crdt_op = Some(_)` with `range.is_empty() && inserted_len == 0`.
**Revision 2 stated that without qualification, and review 2 showed why
that is too loose:** an `Edit` carries no provenance marker, so if the
shape alone were legitimate the invariant would have nothing left to
assert. The precise answer:
> **An empty TEXT delta carrying a CRDT op is legitimate when the
> `Edit` came from `undo`/`redo`, and REQUIRED there. On the FORWARD
> path the same shape carrying an op is a bug, and stays asserted.**
That is a real narrowing, not a repeal, and it is what makes C5
testable at all.
### 1a. The three axes, enumerated
Revision 4 wrote this as one predicate with a default, and the
implementation inherited the gap: `(History, empty delta, None)` fell
through a wildcard and was accepted. The axes are independent — that is
the lane's whole claim — so the rule is a full enumeration:
| provenance | text delta | `crdt_op` | verdict |
|---|---|---|---|
| forward | empty | `None` | **valid** — a syntactic no-op |
| forward | empty | `Some` | **invalid** — the original bug |
| forward | real | `Some` | valid |
| forward | real | `None` | invalid |
| history | empty | `Some` | **valid** — a version-only edit |
| history | empty | `None` | **invalid** — the version advance is gone |
| history | real | `Some` | valid |
| history | real | `None` | invalid |
**An empty text delta is a SHAPE, not a verdict.** Both paths reach it.
`crdt_op` is what separates them, and each direction of that separation
is asserted.
**Why this answer:**
- The invariant it contradicts was written for `is_no_op_edit`
(`src/buffer.rs:1836`), a **pre-check on the `EditOp`** reached only
from `run_rope_edit_and_broadcast` (`:1256`). **`undo_crdt_mode` and
`redo_crdt_mode` never reach it** — they diff two ropes via
`derive_replacement_edit` (`:1440`, `:1525`) and attach the op
`crdt.undo()` produced (`:1454`), so identical ropes yield an empty
range describing a real operation.
- **Forward edits reach the empty-delta shape routinely** — each of
the three syntactically empty `EditOp` forms produces exactly it, as
C2b asserts. What is unreachable forward is the shape **carrying an
op**: an empty form short-circuits to `(None, None)`, and a
real-delta form is not empty. So forward "empty range and zero
insertion" implies `crdt_op == None`, still — which is what lets the
invariant keep its full strength there.
- The op must survive. Dropping it would lose a version advance the
replicas need — which is what C3 now actually tests, and revision 1's
C3 did not.
- **The codebase already assumes this, in two places written for other
reasons.** `FoldStore::translate` (`src/fold.rs:211`–`:213`) and
`BufferStyleSpanTranslator::on_edit` (`src/overlay.rs:261`–`:263`)
both return early on `old_len == 0 && new_len == 0`, and both say so
in a comment — *"Buffers broadcast no-op edits; nothing moved."* This
lane is not introducing a doctrine; it is naming one that consumers
were already written against.
- The public contract has room for it. `src/rope.rs:301`–`:303`
enumerates pure insert, pure delete and replace, and **has no fourth
case**; the `crdt_op` field doc (`src/rope.rs:316`) goes further and
asserts the conflation outright ("`None` … for no-op edits in CRDT
mode"). Both are updated by this lane.
**The empty range's LOCATION is settled by §4's census, not deferred.**
It stays at the buffer end. No consumer is harmed there, and for the
one consumer whose cost depends on it, the buffer end is the *cheapest*
choice — see §4.
## 2. What is already known — and precisely how well
`src/buffer.rs:3044` carries a deterministic fixture,
`crdt_undo_of_an_identity_replace_reports_a_no_op_edit_carrying_an_op`,
`#[ignore]`d at `:3042` and documented from `:3005`. It reduces this
exact case: replacing bytes with **identical bytes** is a textual no-op
but a real CRDT delete-plus-insert, so undoing it advances the CRDT
version while leaving text unchanged.
**Its evidence is narrower than revision 1 claimed, and narrower in one
more place than revision 2 admitted:**
| claim | how it is established | strength |
|---|---|---|
| content stays correct | **asserted** in the fixture — rope and CRDT projection agree | direct |
| the op reaches broadcast consumers | **by INSPECTION of the call sites** | reasoning, not execution |
| the cursor does not jump | **by INSPECTION** — `EditorCore::undo` only clamps to length | reasoning, not execution |
**The cursor row was marked "direct" in revision 2. It is not.** The
fixture body (`buffer.rs:3044`–`:3093`) contains **no reference to
`EditorCore` and none to a cursor**; it exercises `Buffer` alone. The
cursor claim is inspection of a different function than the one the
fixture runs.
**Nothing here replays the op on a remote replica or witnesses
convergence.** Revision 1 said "replicas stay converged" as though it
were established. It is not. **That is exactly what C3 must newly
establish**, and it is the main new evidence this lane produces.
The CI red that prompted the lane is a randomly sampled recurrence of
this fixture, not a new defect.
## 3. Terminology, because revision 1's contradicted itself
**An identity replace IS a forward textual no-op**, and it *does*
produce an operation. So "forward textual no-ops produce no operation"
is false, and revision 1 asserted it while §2 said the opposite.
The correct statement names a **syntactic** category:
> The three **syntactically empty `EditOp` forms** — `Insert` with
> empty bytes, `Delete` with an empty range, and `Replace` with both
> empty — produce no CRDT operation.
That is what `is_no_op_edit` tests, and it stays true.
## 4. The consumer census — COMPLETE
Revision 2 listed `broadcast_on_edit` as a row reading *"every attached
view — not enumerated, owes."* **That is a dispatcher, not a consumer,
and review 2 was right that it cannot stand.** Here is the enumeration.
Both `undo_crdt_mode` (`buffer.rs:1456`) and `redo_crdt_mode`
(`:1537`) do broadcast, so this path is real.
### 4a. How the census closes
Three measurements bound it, so it is complete by construction rather
than by search effort — **for this tree**; see §4d on why no in-tree
measurement can reach further:
1. **The `View` trait's `on_edit` default is `Ok(())`**
(`src/view.rs:450`–`:452`). Every impl that does not override it is
**structurally inert** — it never reads the range.
2. **Exactly four non-test impls override `on_edit`**: `ParseView`
(`syntax.rs:1637`), `TextView` (`text_view.rs:521`),
`FoldStoreTranslator` (`fold.rs:274`), `BufferStyleSpanTranslator`
(`overlay.rs:248`). The other twelve inherit the default.
3. **Exactly four production `Buffer::attach_view` call sites exist**
— `fold.rs:341`, `lua_bindings/mod.rs:3963`, `:4008`, `:8137`.
Measured over the 50 occurrences of `attach_view` outside its own
definition: **38 sit inside `#[cfg(test)]`**, and of the 12
remaining, **8 are doc comments or a different API** (the Lua
`pmacs.diag._attach_view` name, and `SyntaxRegistry::attach_view` at
`lua/mod.rs:8138`, which registers a handle rather than a buffer
view).
### 4b. Broadcast consumers, classified
| attached view | site | reads range? | verdict |
|---|---|---|---|
| `FoldStoreTranslator` | `fold.rs:341` | via `FoldStore::translate` | **INERT** — explicit `old_len == 0 && new_len == 0` early return at `fold.rs:211`–`:213` |
| `BufferStyleSpanTranslator` | `lua/mod.rs:4008` | yes | **INERT** — same explicit early return, `overlay.rs:261`–`:263` |
| `ParseView` | `lua/mod.rs:8137` | yes | **PERMITTED, justified below** |
| `LuaInterceptView` | `lua/mod.rs:3963` | — | **INERT** — overrides `intercept_edit` only (`lua/mod.rs:2132`); inherits the `Ok(())` default |
**`ParseView` is the one permitted effect.** At 0→0 its splice
(`syntax.rs:1656`) is `source.splice(n..n, [])` — the source mirror is
**unchanged** — and it pushes one `InputEdit` with
`start_byte == old_end_byte == new_end_byte` and all three `Point`s
equal (`:1661`–`:1668`). **Why that is acceptable:** a degenerate
`InputEdit` describes no change, so the incremental parse it feeds must
produce an identical tree. **C4 asserts that rather than assuming it**,
and also asserts the pending queue drains, since an effect that
accumulates per undo would not be acceptable.
### 4c. Direct (non-broadcast) consumers
**Revision 2 filed `TextView` under broadcast. It is not attached to
any buffer** — it lives on the window (`win.text_view`) and
`EditorCore::undo` calls it directly at `editor_core.rs:2846`.
| consumer | reads range? | verdict |
|---|---|---|
| `Buffer::adjust_marks_for_edit` (def. `buffer.rs:1609`; called `:1442` undo, `:1527` redo) | yes | **INERT, arithmetically** — with `start == end` and `inserted_len == 0`, every branch is identity: `pos < start` → `pos`; `pos > end` → `pos - 0 + 0`; `pos == start` → `start` under both gravities (`:1617`–`:1629`) |
| `EditorCore::undo` → `TextView::on_edit` (`editor_core.rs:2846`, body `text_view.rs:521`) | yes | **PERMITTED** — `rebuild_lines_from(buf, line_at_offset(range.start))`. Text is unchanged, so the rebuild is **output-identical**; the cost is the tail of the buffer from `range.start`. **The buffer-end location makes this the CHEAPEST possible rebuild** — moving the range to the edit site would rebuild strictly more |
| `search_invalidate_for_edit` → `mark_stale` (`editor_core.rs:1974`) | **no** | **PERMITTED** — unconditional and range-independent. Search matches are marked stale on an edit that changed no text. Acceptable (correctness is preserved; a re-search is redundant, not wrong), and **moving the range would not change it** |
| `search_invalidate_for_edit` → `translate_search_origin` (`editor_core.rs:1984`) | yes | **INERT, arithmetically** — with `start == end` and `inserted_len == 0`: `pos < start` → `pos`; `pos > end` → `pos - 0 + 0`; else `start + 0`, reachable only at `pos == start` (`:1994`–`:2000`) |
### 4d. The disposition
**Five inert, three permitted, none harmed. The range does not move,**
and that conclusion now rests on measurement rather than on a deferral.
| | inert | permitted |
|---|---|---|
| broadcast (§4b) | `FoldStoreTranslator`, `BufferStyleSpanTranslator`, `LuaInterceptView` | `ParseView` |
| direct (§4c) | `adjust_marks_for_edit`, `translate_search_origin` | `TextView`, `mark_stale` |
*(Revision 3 said four and three. Miscount, corrected.)*
The two permitted effects with a cost — `TextView`'s rebuild and
`mark_stale` — are both **strictly cheaper or equal at the buffer end**
than at the edit site, so the location the fixture called arbitrary is
not merely harmless but weakly preferable.
**This census is a point-in-time measurement of THIS TREE**, valid at
the commit the lane branches from. Both `View` (`src/view.rs:419`) and
`Buffer::attach_view` (`src/buffer.rs:674`) are **public**, so a
downstream crate may implement `on_edit` and attach it, and no in-tree
measurement can enumerate that. The census, and C9 with it, are scoped
to in-tree implementations; the public contract §5's C7 updates is what
speaks to anyone outside. Revision 3 claimed C4 would guard it against a new
override or attach site; **it cannot — executing three consumers says
nothing about a fourth, and that claim is withdrawn.** C9 is the guard
that actually holds, and it holds the one condition that matters: if
the set of `on_edit` overrides is unchanged, then every attach site,
new or old, attaches a view that is either the inert trait default or
one of the four already classified.
## 5. Acceptance
| # | contract | witness | mutation |
|---|---|---|---|
| C1 | the fixture runs, and is not silently re-ignored | un-ignore it; **plus a structural assertion** that no `#[ignore]` attribute precedes the fixture's `fn` (via `include_str!` on the file), **plus** the run's `1 passed; 0 ignored` line recorded as gate evidence | restore `#[ignore]` → the structural assertion fires **and** the recorded line reads `0 passed; 1 ignored`. Without one of these, re-ignoring is a green suite |
| C2a | `is_no_op_edit` classifies all three **syntactically empty forms** as no-ops | assert `is_no_op_edit` **directly** for `Insert{bytes:[]}`, `Delete{range:empty}`, `Replace{range:empty,bytes:[]}` | flip **any one** arm (`buffer.rs:1838`–`:1840`) → C2a fires. Nothing sits between the assertion and the classifier, so this mutant **cannot be masked** |
| C2b | end-to-end, each empty form still yields `crdt_op == None` | apply each form through `apply_edit` on a CRDT buffer | **compound mutant, and it must be**: flip the arm **and delete that variant's defensive early return** — `buffer.rs:1177`–`:1182` (Insert) or `:1192`–`:1194` (Delete). See below |
| C3 | an empty-text history op **replays convergently on a REMOTE replica**, **for both `undo` and `redo`** | seed replica B with the **forward** ops, apply the history op to B, assert **(a)** identical materialized text **and (b)** identical CRDT version/frontier; then apply a **causally dependent** op and assert both still agree | **drop the history op before replay** → text still matches, so only the version/frontier assertion catches it |
| C4a | the history edit is **broadcast at all**, for both `undo` and `redo` | attach a counting view (the `RecorderView` shape, `buffer.rs:2218`) and assert **exactly one** `on_edit` per history op | **delete `self.broadcast_on_edit(&inverse_edit)?`** at `buffer.rs:1456` (undo) or `:1537` (redo) → the count is 0 → C4a fires |
| C4b | the classified consumers are unchanged by the real history edit | attach `FoldStoreTranslator`, `BufferStyleSpanTranslator` and `ParseView`; run the identity-replace op; assert fold store unchanged, span vector unchanged, **parse tree identical**, and `pending_edit_count()` returns to 0 after the drain (`syntax.rs:712`, `:737`) | see the note below — **C4b claims no guard mutation**, and C4a is what makes it non-vacuous |
| C4c | the style-span guard's own contract, pinned where it can fire | call `BufferStyleSpanTranslator::on_edit` with a **synthetic INTERIOR 0→0 `Edit`** whose position falls strictly inside an existing span, and assert the span vector is **byte-identical** — not merely equal in coverage | delete `overlay.rs:261`–`:263` → the span splits into two adjacent fragments and the vector differs → C4c fires |
| C5 | the invariant is keyed on **provenance**, and covers **all four** empty-text-delta quadrants of §1a | preserve the `GenOp` classification (`buffer.rs:3101`, where `op` is moved before it can be classified) as an operation class; extract the shape check to take `(class, &Edit)`; then **inject** all four: `(Forward, empty, None)` accepted, `(Forward, empty, Some)` rejected, `(History, empty, Some)` accepted, `(History, empty, None)` rejected | widen the forward rule → C5 fires; accept `(History, empty, None)` → C5 fires, and **revision 4's two-assertion C5 did not**. **The proptest alone catches neither**, because no generated input reaches either row — which is why C5 is a directed injection, not a property |
| C6 | the executed history-case set **is** `{Undo, Redo}` | after the parameterized loop, assert the collected set of cases actually run equals the literal `{Undo, Redo}`; a `match` over the case enum keeps a future variant from being added silently | drop `Redo` from the case list → the **set assertion** fires. Without it the suite simply runs one case and stays green, which is why revision 3's C6 was a zero-execution witness |
| C7 | the public contract names the **empty text delta** and says which path produces which `crdt_op` | the `Edit` doc (from `src/rope.rs:292`) gains the empty-delta shape **as reachable on BOTH paths** — `None` forward, `Some` from history — and the `crdt_op` field doc stops asserting that no-op edits have no op | leave the doc → it contradicts the code the lane just blessed. **An earlier version of this row said forward `apply_edit` never produces the shape; C2b proves all three forward empty forms do**, so the doc it produced was false and is corrected in revision 5 |
| C8 | **the fixture's own doc comment is corrected**, not just its attribute | rewrite `buffer.rs:3005`–`:3040`: convergence is **established by C3**, not "verified" (`:3023`–`:3026`); the buffer-end range is **ruled and weakly preferable** per §4, not "genuinely arbitrary" (`:3034`–`:3036`); "**The open question**" (`:3030`) becomes the ruling; and the `#[ignore]` reason string (`:3042`–`:3043`) goes with the attribute | leave the comment → the repository's most-read record of this defect still says the decision is open and that convergence was already checked, contradicting §1, §2 and C3 |
| C9 | §4's census stays closed **for in-tree implementations** | walk `CARGO_MANIFEST_DIR/src` and assert the set of **`(file, impl target)` pairs** carrying a non-`#[cfg(test)]` `fn on_edit` override is exactly `{(syntax.rs, ParseView), (text_view.rs, TextView), (fold.rs, FoldStoreTranslator), (overlay.rs, BufferStyleSpanTranslator)}` — pairs, not file set and count, and by name rather than line number | **replace `ParseView`'s override with an unclassified type in the SAME file** → file set and count are both unchanged, and only the pair set catches it. Adding a fifth override anywhere under `src/` fires it too, naming the file and the type |
**Why C4 was rebuilt.** Revision 3's C4 claimed that deleting the fold
or style guard would make an assertion fire. **Both mutants survive**,
and the arithmetic says why: the history edit sits at the buffer end,
so with `old_start == old_end == len` and `old_len == new_len == 0`,
`BufferStyleSpanTranslator` emits a left fragment `[s, min(e, len))`
for every span within the buffer and no right fragment — the vector is
unchanged with or without the guard (`overlay.rs:269`–`:285`). The fold
store's remaining arithmetic is identity for the same reason. **At this
location the fold guard is an optimization, not a behaviour
discriminator, and no mutation is claimed for it.** What discriminates
is whether the broadcast happens at all (C4a) and whether the style
guard holds where the fragmenting is reachable (C4c).
*(A span of zero width at exactly `len` would be dropped without the
guard and kept with it. That is not used as a witness: whether such a
span is constructible is unestablished, and a witness resting on a
degenerate value is a worse instrument than the interior injection.)*
**C2b's mutant is compound because two of three variants mask it, and
the asymmetry is measured.** `apply_to_crdt_then_normalize_bytes`
returns `(None, None)` early for an empty `Insert` (`:1177`–`:1182`)
and an empty `Delete` (`:1192`–`:1194`), so flipping those arms alone
still yields `crdt_op == None` and the simple mutant survives. The
empty `Replace` has **no** early return — `:1208` skips the delete,
`:1211` skips the insert, and control falls through to the
unconditional `Some(crdt_op)` at `:1231`–`:1236` — so there, and only
there, the simple mutant dies. C2a exists so this asymmetry cannot hide
a classifier regression.
**C3's mutation is the point of C3.** Revision 1's version asserted
text equality alone, and an identity-replace history op leaves text
unchanged — so **dropping the op passed it**. Version/frontier equality
is what discriminates; the causally dependent op is corroboration on
top.
**C5 is the second half of §1's answer.** §1 makes the shape legitimate
*for history ops*; without a provenance-keyed check there is no
remaining assertion for the forward path, and the invariant would have
been repealed rather than narrowed.
**C1, C6 and C9 exist because a green suite is not evidence that a
suite RAN.** Re-ignoring a fixture, dropping a parameter, and adding an
unclassified consumer are all silent under ordinary assertions. Each
gets a witness that fails on absence rather than reporting it.
## 6. Coherence impact (`COHERENCE.md` §20)
Under the resolution the census confirms — invariant narrowed to
provenance, behaviour unchanged:
- **Journey steps touched: NONE.** No product behaviour changes; the
work is a test contract, a census, and public documentation.
- **Interaction islands: none added.**
- **Config registry: no entry.**
- **Background work: none started.**
Revision 2 made this section conditional on a census that had not run.
**It has now run, and no consumer is harmed, so the section is
unconditional.**
## 7. What this does NOT do
- **It does not commit the proptest regression seed.** That duplicates
a deterministic fixture and would make a disputed assertion fail
permanently rather than occasionally.
- **It does not re-verify content correctness**, which §2 records as
directly asserted. It *does* newly establish remote convergence,
which §2 records as only inspected.
- **It does not move the empty range**, and after §4 that is a measured
result rather than a deferral.
- **It does not audit `intercept_edit`**, a different stage with a
different contract. The census covers `on_edit` and the direct
consumers of the history `Edit`.
- **It does not reorder the roadmap.** GUI arc 1b remains the next
product lane.

View File

@ -2962,6 +2962,115 @@ mod tests {
] ]
} }
/// Provenance of the `Edit` a [`GenOp`] produced.
///
/// The `crdt_op` shape invariant is keyed on THIS, not on the
/// `Edit`'s shape alone. An `Edit` carries no provenance
/// marker, so the classification has to be taken from the
/// operation *before* it is applied — see the call site, where
/// `op` is moved into `apply_capturing`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OperationClass {
/// `apply_edit` — `Insert` / `Delete` / `Replace`.
Forward,
/// `undo` / `redo`.
History,
}
impl OperationClass {
fn of(op: &GenOp) -> Self {
match op {
GenOp::Insert(..) | GenOp::Delete(..) | GenOp::Replace(..) => Self::Forward,
GenOp::Undo | GenOp::Redo => Self::History,
}
}
}
/// The `crdt_op` shape invariant, over three independent axes.
///
/// The axes are **provenance** (forward vs. history), the
/// **text delta** (empty vs. real), and whether a **CRDT op** is
/// carried. They are independent, which is the whole point of
/// this lane, so the rule is a full enumeration rather than a
/// default with exceptions:
///
/// | provenance | text delta | `crdt_op` | verdict |
/// |---|---|---|---|
/// | forward | empty | `None` | **valid** — a syntactic no-op |
/// | forward | empty | `Some` | **invalid** |
/// | forward | real | `Some` | **valid** |
/// | forward | real | `None` | **invalid** |
/// | history | empty | `Some` | **valid** — a version-only edit |
/// | history | empty | `None` | **invalid** |
/// | history | real | `Some` | **valid** |
/// | history | real | `None` | **invalid** |
///
/// An **empty text delta** — `range.is_empty() && inserted_len
/// == 0` — is a SHAPE, and forward edits reach it routinely:
/// each of the three syntactically empty `EditOp` forms produces
/// exactly this shape. What separates the two empty-delta cases
/// is the op. Forward, `is_no_op_edit` short-circuits before the
/// CRDT path exists, so there is nothing to carry. History
/// diffs two ropes; an identity replace makes them equal, so the
/// op IS the content of the edit and **dropping it loses the
/// version advance**. That is why the history row demands
/// `Some` rather than merely tolerating it.
///
/// A present op is separately required to carry the buffer's
/// peer id and non-empty wire bytes.
///
/// Returns `Err(reason)` rather than asserting, so the same
/// predicate serves the proptest (over generated sequences) and
/// a directed injection. The injection is not optional: the
/// `(forward, empty, Some)` row is **unreachable from any
/// generated forward input**, because a forward empty form
/// short-circuits and a forward real-delta form is not empty.
// The enumeration IS the contract. `match_same_arms` would have
// the three `Ok(())` rows collapsed into one alternation, which
// is exactly the conflation this lane exists to remove: it would
// stop the table from showing that `(forward, empty, None)` and
// `(history, empty, Some)` are valid for OPPOSITE reasons, and a
// future reader would have no way to see which quadrant a change
// moved.
#[allow(clippy::match_same_arms)]
fn check_crdt_op_shape(
class: OperationClass,
edit: &Edit,
expected_peer_id: u64,
) -> Result<(), String> {
if let Some(op) = edit.crdt_op.as_ref() {
if op.peer_id != expected_peer_id {
return Err(format!(
"peer_id must thread from CrdtState: got {}, want {expected_peer_id}",
op.peer_id
));
}
if op.bytes.is_empty() {
return Err("wire bytes must be non-empty".to_owned());
}
}
let empty_text_delta = edit.range.is_empty() && edit.inserted_len == 0;
match (class, empty_text_delta, edit.crdt_op.is_some()) {
(OperationClass::Forward, true, false) => Ok(()),
(OperationClass::Forward, true, true) => Err(
"a FORWARD edit with an empty text delta must have crdt_op = None: the \
three syntactically empty EditOp forms short-circuit at is_no_op_edit"
.to_owned(),
),
(OperationClass::History, true, true) => Ok(()),
(OperationClass::History, true, false) => Err(
"a HISTORY edit with an empty text delta must have crdt_op = Some: the \
op is the version advance, and without it the edit carries nothing"
.to_owned(),
),
(_, false, true) => Ok(()),
(_, false, false) => Err(
"an edit with a real text delta must have crdt_op = Some in CRDT mode"
.to_owned(),
),
}
}
// T M10.2 Day 3 helper: applies a `GenOp` and returns the // T M10.2 Day 3 helper: applies a `GenOp` and returns the
// resulting Edit so the proptest can assert per-op shape. // resulting Edit so the proptest can assert per-op shape.
// Each op is best-effort: out-of-range positions are clamped // Each op is best-effort: out-of-range positions are clamped
@ -3012,35 +3121,43 @@ mod tests {
/// version while leaving the materialized text unchanged, so /// version while leaving the materialized text unchanged, so
/// `undo_crdt_mode` derives an EMPTY replacement edit — and /// `undo_crdt_mode` derives an EMPTY replacement edit — and
/// still attaches the `crdt_op` that `crdt.undo()` produced. /// still attaches the `crdt_op` that `crdt.undo()` produced.
/// That trips the proptest's `crdt_op` shape invariant, "a
/// no-op edit must have `crdt_op = None`".
/// ///
/// **What was verified about the consequences**, so the next /// **The ruling** (`docs/crdt-identity-undo-framing.md`, and
/// reader does not have to redo it: /// this is no longer an open question): a visible TEXT delta
/// and a CRDT-VERSION delta are INDEPENDENT dimensions of
/// `Edit`, so the behavior is right and the *invariant* was
/// mis-scoped. It was written for [`is_no_op_edit`], a
/// pre-check on the forward `EditOp` that returns before the
/// CRDT path exists; `undo_crdt_mode` and `redo_crdt_mode`
/// never reach it. The invariant is now keyed on **provenance**
/// — see `check_crdt_op_shape` — and still rejects this shape
/// on the forward path, where it remains unreachable.
///
/// **What is established, and by what:**
/// ///
/// * content stays correct — rope and CRDT projection agree /// * content stays correct — rope and CRDT projection agree
/// before and after (asserted below); /// before and after (asserted below);
/// * replicas stay converged — both `crdt_op` consumers /// * replicas stay converged — established by
/// (`EditorCore::queue_daemon_origin_crdt_op` and the remote-op /// `identity_replace_history_op_replays_convergently_on_a_remote_replica`,
/// path) read `edit.crdt_op` unconditionally and do **not** /// which seeds a second replica with the forward ops and then
/// short-circuit on an empty range, so the op is broadcast; /// replays the history op, asserting the materialized text
/// * the cursor does not jump — `EditorCore::undo` only clamps /// **and** the version vector. Before that witness existed
/// to buffer length and never seeks `edit.range.start`. /// this comment asserted convergence from call-site
/// inspection alone, which cannot see a lost version advance:
/// dropping the op leaves the text identical;
/// * every consumer of the resulting `Edit` is classified inert
/// or permitted — the census is §4 of the framing, and
/// `identity_replace_history_op_leaves_classified_consumers_unchanged`
/// executes it.
/// ///
/// **The open question** is therefore whether the *invariant* is /// **The empty range's location is ruled, not arbitrary.**
/// simply mis-scoped rather than the behavior being wrong. It /// `derive_replacement_edit` reports it at the buffer END. No
/// was written for the FORWARD `apply_edit` short-circuit, which /// consumer is harmed there, and for the one consumer whose
/// returns before ever producing an op; CRDT-mode undo/redo /// cost depends on it — `TextView::on_edit`, which rebuilds
/// never reach that path. One artifact is genuinely arbitrary /// from `line_at_offset(range.start)` — the buffer end is the
/// either way: `derive_replacement_edit` reports the empty range /// cheapest possible choice. An earlier version of this comment
/// at the buffer END rather than at the edit site. /// called it genuinely arbitrary; it is weakly preferable.
///
/// Ignored, not deleted: it documents a real, reproducible
/// asymmetry that nothing else on `main` records, and un-ignoring
/// it is the first step of whichever resolution wins.
#[test] #[test]
#[ignore = "known pre-existing main behavior; see the doc comment \
for the verified consequences and the open question"]
fn crdt_undo_of_an_identity_replace_reports_a_no_op_edit_carrying_an_op() { fn crdt_undo_of_an_identity_replace_reports_a_no_op_edit_carrying_an_op() {
let mut buffer = let mut buffer =
Buffer::new_with_crdt(BufferId::next(), "*identity-undo*", 1).expect("crdt"); Buffer::new_with_crdt(BufferId::next(), "*identity-undo*", 1).expect("crdt");
@ -3084,6 +3201,484 @@ mod tests {
); );
} }
/// C1: the fixture above must not be silently re-ignored.
///
/// A restored `#[ignore]` is invisible to a green suite — the
/// run simply reports one fewer test. This reads the source and
/// asserts the attribute's ABSENCE, which is the only form that
/// fails rather than quietly reporting.
#[test]
fn the_identity_replace_fixture_carries_no_ignore_attribute() {
const SOURCE: &str = include_str!("buffer.rs");
const FIXTURE: &str =
"fn crdt_undo_of_an_identity_replace_reports_a_no_op_edit_carrying_an_op";
let at = SOURCE
.find(FIXTURE)
.expect("the fixture is present by name");
let attrs: Vec<&str> = SOURCE[..at]
.lines()
.rev()
.map(str::trim)
.skip_while(|l| l.is_empty())
.take_while(|l| l.starts_with("#["))
.collect();
assert!(
attrs.iter().any(|a| a.starts_with("#[test]")),
"the fixture should still be a #[test]: {attrs:?}"
);
assert!(
!attrs.iter().any(|a| a.starts_with("#[ignore")),
"C1: the identity-replace fixture is ignored again — {attrs:?}"
);
}
/// C2a: the classifier itself, with nothing between the
/// assertion and it.
///
/// Two of the three end-to-end paths mask a classifier
/// regression (see C2b), so this row exists to be unmaskable.
#[test]
fn is_no_op_edit_classifies_all_three_syntactically_empty_forms() {
assert!(is_no_op_edit(&EditOp::Insert { pos: 0, bytes: b"" }));
assert!(is_no_op_edit(&EditOp::Delete {
range: Range::new(0, 0)
}));
assert!(is_no_op_edit(&EditOp::Replace {
range: Range::new(0, 0),
bytes: b"",
}));
// …and does not over-classify: each form with any content
// is a real edit.
assert!(!is_no_op_edit(&EditOp::Insert {
pos: 0,
bytes: b"x"
}));
assert!(!is_no_op_edit(&EditOp::Delete {
range: Range::new(0, 1)
}));
assert!(!is_no_op_edit(&EditOp::Replace {
range: Range::new(0, 1),
bytes: b"",
}));
}
/// C2b: end to end, each syntactically empty form still
/// produces no CRDT op.
///
/// **This witness is masked for two of the three forms**, which
/// is why C2a exists. `apply_to_crdt_then_normalize_bytes`
/// returns `(None, None)` early for an empty `Insert` and an
/// empty `Delete`, so flipping `is_no_op_edit`'s arm for either
/// leaves `crdt_op == None` and this test still passes. Killing
/// it there needs a compound mutant: flip the arm AND delete
/// that variant's defensive early return. The empty `Replace`
/// has no such return and falls through to the unconditional
/// `Some(crdt_op)`, so there the simple mutant does die here.
#[test]
fn each_syntactically_empty_form_yields_no_crdt_op_end_to_end() {
let mut b = Buffer::new_with_crdt(BufferId::next(), "*empty-forms*", 1).expect("crdt");
b.apply_edit(EditOp::Insert {
pos: 0,
bytes: b"seed",
})
.expect("seed");
for (label, op) in [
("Insert{bytes:[]}", EditOp::Insert { pos: 0, bytes: b"" }),
(
"Delete{range:empty}",
EditOp::Delete {
range: Range::new(1, 1),
},
),
(
"Replace{range:empty,bytes:[]}",
EditOp::Replace {
range: Range::new(1, 1),
bytes: b"",
},
),
] {
let edit = b.apply_edit(op).expect("empty form applies");
assert!(
edit.crdt_op.is_none(),
"C2b: {label} must produce no CRDT op"
);
assert!(
edit.range.is_empty() && edit.inserted_len == 0,
"C2b: {label} must be version-only in shape too"
);
}
}
/// C5: the invariant is keyed on PROVENANCE, and covers all
/// four empty-text-delta quadrants.
///
/// Two of these must be a directed injection rather than a
/// property. `(forward, empty, Some)` is **unreachable from any
/// generated forward input** — an empty form short-circuits
/// before the CRDT path, and a real-delta form is not empty — so
/// the proptest alone cannot tell a narrowed rule from a deleted
/// one. `(history, empty, None)` is equally unreachable, because
/// `undo_crdt_mode` and `redo_crdt_mode` always attach the op;
/// it is asserted so that a future change which stops attaching
/// it fails here rather than silently losing version advances.
#[test]
fn the_shape_invariant_covers_all_four_empty_text_delta_quadrants() {
let with_op = |op: Option<Box<crate::rope::CrdtOp>>| Edit {
new_rope: crate::rope::Rope::from_bytes(b"hello"),
range: Range::new(5, 5),
inserted_len: 0,
crdt_op: op,
};
let carrying = || {
Some(Box::new(crate::rope::CrdtOp {
peer_id: 1,
bytes: vec![0xAB],
}))
};
// Forward + empty delta + None: a syntactic no-op. Valid.
assert!(
check_crdt_op_shape(OperationClass::Forward, &with_op(None), 1).is_ok(),
"C5: a forward syntactic no-op carries no op, and that is correct"
);
// Forward + empty delta + Some: the original bug.
assert!(
check_crdt_op_shape(OperationClass::Forward, &with_op(carrying()), 1).is_err(),
"C5: a forward edit with an empty text delta must not carry an op"
);
// History + empty delta + Some: a version-only edit. Valid.
assert!(
check_crdt_op_shape(OperationClass::History, &with_op(carrying()), 1).is_ok(),
"C5: the same shape from undo/redo is a legitimate version advance"
);
// History + empty delta + None: the version advance is gone.
assert!(
check_crdt_op_shape(OperationClass::History, &with_op(None), 1).is_err(),
"C5: a history edit with an empty text delta and no op carries nothing at all"
);
}
/// The two history operations every history witness must cover.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum HistoryCase {
Undo,
Redo,
}
impl HistoryCase {
const ALL: [HistoryCase; 2] = [HistoryCase::Undo, HistoryCase::Redo];
/// The `match` is the growth guard: a new variant fails to
/// compile here rather than going silently unexercised.
fn label(self) -> &'static str {
match self {
Self::Undo => "undo",
Self::Redo => "redo",
}
}
}
/// C6: assert the executed case set IS `{Undo, Redo}`.
///
/// Narrowing a parameterized loop from two cases to one
/// ordinarily leaves a passing test — the suite just runs less,
/// which no assertion inside the loop can notice. The expected
/// set is spelled out literally rather than derived from
/// `HistoryCase::ALL`, which would make the check circular.
fn assert_executed_both_history_cases(executed: &[HistoryCase]) {
let mut got = executed.to_vec();
got.sort();
got.dedup();
assert_eq!(
got,
vec![HistoryCase::Undo, HistoryCase::Redo],
"C6: the history witnesses must execute BOTH cases"
);
}
/// A buffer holding "hello" whose last edit was an identity
/// replace — the shape whose undo and redo are version-only.
fn seeded_identity_replace_buffer() -> Buffer {
let mut b =
Buffer::new_with_crdt(BufferId::next(), "*identity-history*", 1).expect("crdt");
b.apply_edit(EditOp::Insert {
pos: 0,
bytes: b"hello",
})
.expect("seed insert");
b.apply_edit(EditOp::Replace {
range: Range::new(1, 2),
bytes: b"e",
})
.expect("identity replace");
b
}
/// Run `case`'s history operation, asserting it is version-only.
fn take_history_edit(b: &mut Buffer, case: HistoryCase) -> Edit {
let edit = match case {
HistoryCase::Undo => b.undo().expect("undo"),
HistoryCase::Redo => b.redo().expect("redo"),
};
assert!(
edit.range.is_empty() && edit.inserted_len == 0,
"{}: expected a version-only edit, got {:?}/{}",
case.label(),
edit.range,
edit.inserted_len
);
edit
}
fn assert_converged(a: &Buffer, replica: &crate::crdt::CrdtState, when: &str) {
let doc = a.crdt_state().expect("crdt");
assert_eq!(
doc.materialize_string(),
replica.materialize_string(),
"text diverged {when}"
);
// The VERSION is the discriminator, and `version_scalar` is
// documented as unusable for exactly this comparison
// (equal scalars do not imply equal states across
// replicas). `VersionVector`'s `PartialEq` compares logical
// content.
assert_eq!(
doc.version(),
replica.version(),
"version vector diverged {when}"
);
}
/// C3: an empty-text history op replays convergently on a
/// REMOTE replica, for both `undo` and `redo`.
///
/// The existing round-trip proptest deliberately excludes
/// history ops, because replaying one onto a replica that never
/// saw the forward history is ill-posed. Seeding the replica
/// with the forward ops first is what makes this case well
/// posed — and is the shape the wire protocol actually uses.
///
/// **Text equality alone does not discriminate.** Dropping the
/// history op leaves the replica's text identical, because the
/// op advances the version without changing bytes. The version
/// vector is what catches it.
#[test]
fn identity_replace_history_op_replays_convergently_on_a_remote_replica() {
let mut executed = Vec::new();
for case in HistoryCase::ALL {
let mut a = Buffer::new_with_crdt(BufferId::next(), "*replay*", 1).expect("crdt");
let replica = crate::crdt::CrdtState::new(2).expect("replica");
for op in [
EditOp::Insert {
pos: 0,
bytes: b"hello",
},
EditOp::Replace {
range: Range::new(1, 2),
bytes: b"e",
},
] {
let edit = a.apply_edit(op).expect("forward edit");
let carried = edit.crdt_op.as_ref().expect("a forward edit carries an op");
replica
.import_updates(&carried.bytes)
.expect("seed the replica");
}
assert_converged(&a, &replica, "after seeding the forward ops");
// Redo needs an undo first — and that undo is itself a
// version-only edit, so it is replayed the same way.
if case == HistoryCase::Redo {
let undone = take_history_edit(&mut a, HistoryCase::Undo);
replica
.import_updates(&undone.crdt_op.as_ref().expect("op").bytes)
.expect("replay the preparatory undo");
assert_converged(&a, &replica, "after the preparatory undo");
}
let history = take_history_edit(&mut a, case);
let carried = history
.crdt_op
.as_ref()
.expect("the history op carries a version advance");
replica
.import_updates(&carried.bytes)
.expect("replay the history op");
assert_converged(&a, &replica, case.label());
// Corroboration: a causally dependent op still lands.
let follow = a
.apply_edit(EditOp::Insert {
pos: a.len(),
bytes: b"!",
})
.expect("dependent edit");
replica
.import_updates(&follow.crdt_op.as_ref().expect("op").bytes)
.expect("replay the dependent op");
assert_converged(&a, &replica, "after a causally dependent op");
executed.push(case);
}
assert_executed_both_history_cases(&executed);
}
/// C4a: the history edit is BROADCAST at all.
///
/// This is what makes C4b non-vacuous. C4b asserts that the
/// classified consumers are unchanged, and "unchanged" is also
/// what a missing broadcast produces — so the census's whole
/// broadcast branch rests on this count.
#[test]
fn identity_replace_history_op_is_broadcast_to_attached_views() {
let mut executed = Vec::new();
for case in HistoryCase::ALL {
let mut b = seeded_identity_replace_buffer();
let events = std::sync::Arc::new(Mutex::new(Vec::new()));
b.attach_view(Box::new(RecorderView {
events: std::sync::Arc::clone(&events),
}));
if case == HistoryCase::Redo {
take_history_edit(&mut b, HistoryCase::Undo);
}
events.lock().unwrap().clear();
take_history_edit(&mut b, case);
let broadcasts = events
.lock()
.unwrap()
.iter()
.filter(|e| matches!(e, RecorderEvent::OnEdit { .. }))
.count();
assert_eq!(
broadcasts,
1,
"C4a: {} must broadcast exactly one on_edit",
case.label()
);
executed.push(case);
}
assert_executed_both_history_cases(&executed);
}
/// C4b: §4's classification, executed.
///
/// The three production views that override `on_edit` are
/// attached to one buffer, the identity-replace history op runs,
/// and each consumer's classified outcome is asserted: the fold
/// store and the span vector unchanged (INERT), and `ParseView`
/// left with an identical parse and a queue that drains
/// (PERMITTED — one degenerate `InputEdit`, describing no
/// change).
#[test]
fn identity_replace_history_op_leaves_classified_consumers_unchanged() {
use crate::overlay::{
BufferStyleSpan, BufferStyleSpanTranslator, SharedBufferStyleSpans,
};
use pmacs_protocol::ByteRange;
let registry = crate::syntax::SyntaxRegistry::new();
let Some(language) = registry.language("rust") else {
panic!("the rust grammar must load for C4b");
};
let mut executed = Vec::new();
for case in HistoryCase::ALL {
let mut b = seeded_identity_replace_buffer();
let folds = crate::fold::FoldRegistry::default();
let store = folds.store_or_attach(&mut b);
assert!(
store.lock().unwrap().insert(ByteRange { start: 1, end: 4 }),
"a fold to observe"
);
let spans: SharedBufferStyleSpans =
std::sync::Arc::new(Mutex::new(vec![BufferStyleSpan {
start: 1,
end: 4,
style: crate::cell::Style::default(),
}]));
b.attach_view(Box::new(BufferStyleSpanTranslator::new(
std::sync::Arc::clone(&spans),
)));
let parse_view = crate::syntax::ParseView::new(&b, language.clone(), "rust".into());
let handle = parse_view.handle();
b.attach_view(Box::new(parse_view));
if case == HistoryCase::Redo {
take_history_edit(&mut b, HistoryCase::Undo);
}
// Baselines, taken after any preparatory op so the
// comparison is against the state the op under test
// actually starts from.
let folds_before = store.lock().unwrap().folds();
let spans_before = spans.lock().unwrap().clone();
let source_before = handle.source_snapshot();
let baseline = std::sync::Arc::new(
crate::syntax::run_parse(handle.make_request()).expect("baseline parse"),
);
handle.install(std::sync::Arc::clone(&baseline));
let tree_before = baseline.root_tree().root_node().to_sexp();
assert_eq!(
handle.pending_edit_count(),
0,
"the baseline parse drains the queue"
);
take_history_edit(&mut b, case);
assert_eq!(
store.lock().unwrap().folds(),
folds_before,
"C4b: FoldStoreTranslator is INERT for {}",
case.label()
);
assert_eq!(
*spans.lock().unwrap(),
spans_before,
"C4b: BufferStyleSpanTranslator is INERT for {}",
case.label()
);
assert_eq!(
handle.source_snapshot(),
source_before,
"C4b: ParseView's source mirror is unchanged for {}",
case.label()
);
// The permitted effect, bounded: exactly one degenerate
// InputEdit is queued, and the next request drains it.
assert_eq!(
handle.pending_edit_count(),
1,
"C4b: one degenerate InputEdit for {}",
case.label()
);
let after = crate::syntax::run_parse(handle.make_request()).expect("parse again");
assert_eq!(
handle.pending_edit_count(),
0,
"C4b: the queue drains for {}",
case.label()
);
assert_eq!(
after.root_tree().root_node().to_sexp(),
tree_before,
"C4b: the parse is identical for {}",
case.label()
);
executed.push(case);
}
assert_executed_both_history_cases(&executed);
}
proptest! { proptest! {
// Smaller proptest case count than the default (64) to keep // Smaller proptest case count than the default (64) to keep
// CI overhead modest; the per-op invariant check is the // CI overhead modest; the per-op invariant check is the
@ -3098,6 +3693,10 @@ mod tests {
.expect("crdt construction"); .expect("crdt construction");
for op in ops { for op in ops {
let op_repr = format!("{op:?}"); let op_repr = format!("{op:?}");
// C5: classify BEFORE the move. This is the only
// point at which forward and history are
// distinguishable; the resulting `Edit` is not.
let class = OperationClass::of(&op);
let edit = apply_capturing(&mut b, op); let edit = apply_capturing(&mut b, op);
// Per-op invariant check: catches drift the moment // Per-op invariant check: catches drift the moment
// it happens, with the failing op visible in the // it happens, with the failing op visible in the
@ -3109,37 +3708,13 @@ mod tests {
"invariant violated after op {}: rope={:?} crdt={:?}", "invariant violated after op {}: rope={:?} crdt={:?}",
op_repr, rope, crdt op_repr, rope, crdt
); );
// Day 3: crdt_op shape invariant. // Day 3: crdt_op shape invariant, now keyed on
// - real edits in CRDT mode populate crdt_op // provenance rather than on the Edit's shape alone.
// - no-op short-circuits leave crdt_op = None // A history-stack-empty error returns no Edit.
// - history-stack-empty errors return None Edit if let Some(edit) = edit
if let Some(edit) = edit { && let Err(why) = check_crdt_op_shape(class, &edit, 1)
let is_no_op_edit_result = {
edit.range.is_empty() && edit.inserted_len == 0; prop_assert!(false, "{} ({})", why, op_repr);
if is_no_op_edit_result {
prop_assert!(
edit.crdt_op.is_none(),
"no-op edit must have crdt_op = None ({})",
op_repr
);
} else {
prop_assert!(
edit.crdt_op.is_some(),
"non-no-op CRDT-mode edit must have crdt_op = Some ({})",
op_repr
);
let crdt_op = edit.crdt_op.as_ref().unwrap();
prop_assert_eq!(
crdt_op.peer_id, 1,
"peer_id must thread from CrdtState ({})",
op_repr
);
prop_assert!(
!crdt_op.bytes.is_empty(),
"wire bytes must be non-empty ({})",
op_repr
);
}
} }
} }
} }

View File

@ -753,6 +753,52 @@ mod tests {
virt.render(&buf, viewport(1, 5), &mut grid); virt.render(&buf, viewport(1, 5), &mut grid);
} }
/// C4c: the empty/empty guard's own contract, pinned where it
/// can actually fire.
///
/// **The history edit this guard was written for cannot test it.**
/// That edit sits at the buffer END, and with
/// `old_start == old_end == len` the loop below emits a left
/// fragment `[s, min(e, len)) == [s, e)` for every span within the
/// buffer and no right fragment — the vector is unchanged with or
/// without the guard, so deleting the guard is a surviving mutant
/// there. An INTERIOR empty edit is where the fragmenting the
/// guard prevents is reachable: a span straddling the position
/// splits into two adjacent fragments covering the same bytes.
///
/// So the assertion is on the span VECTOR, not on coverage.
#[test]
fn an_interior_empty_edit_does_not_fragment_a_straddled_span() {
let buf = Buffer::from_bytes(BufferId::next(), "t", b"abcdef");
let store: SharedBufferStyleSpans = Arc::new(Mutex::new(vec![BufferStyleSpan {
start: 1,
end: 5,
style: red(),
}]));
let before = store.lock().unwrap().clone();
let mut translator = BufferStyleSpanTranslator::new(Arc::clone(&store));
// A synthetic 0→0 edit strictly inside the span. Built by hand:
// no forward EditOp produces this shape, and the history edit
// that does produce it lands at the buffer end.
let interior = crate::rope::Edit {
new_rope: buf.snapshot_rope(),
range: crate::rope::Range::new(3, 3),
inserted_len: 0,
crdt_op: None,
};
translator
.on_edit(&buf, &interior)
.expect("the translator accepts the edit");
assert_eq!(
*store.lock().unwrap(),
before,
"C4c: an interior no-op edit must leave the span vector \
byte-identical, not split it into adjacent fragments"
);
}
fn red() -> Style { fn red() -> Style {
Style { Style {
fg: crate::cell::Color::Indexed(1), fg: crate::cell::Color::Indexed(1),

View File

@ -301,6 +301,27 @@ impl<'a> Iterator for Chunks<'a> {
/// A pure insert has `range.start == range.end` and `inserted_len > 0`. /// A pure insert has `range.start == range.end` and `inserted_len > 0`.
/// A pure delete has `range.start < range.end` and `inserted_len == 0`. /// A pure delete has `range.start < range.end` and `inserted_len == 0`.
/// A replace has both nonzero. /// A replace has both nonzero.
/// An **empty text delta** has `range.start == range.end` and
/// `inserted_len == 0` — no bytes changed at all.
///
/// That last shape is produced on BOTH paths, and `crdt_op` is what
/// tells them apart:
///
/// * **forward** `apply_edit` reaches it whenever the `EditOp` is one of
/// the three syntactically empty forms (an empty insert, an
/// empty-range delete, an empty-range/empty-bytes replace). Those
/// short-circuit before the CRDT path exists, so `crdt_op` is `None`
/// — nothing happened;
/// * **CRDT-mode `undo`/`redo`** reach it when the operation being
/// inverted was itself a textual no-op (replacing bytes with
/// identical bytes). Here `crdt_op` is `Some`, and **must be**: a CRDT
/// VERSION delta is a separate dimension from a TEXT delta, and the
/// op is the whole content of such an edit.
///
/// The history case's `range` sits at the buffer end, which is where
/// `derive_replacement_edit` reports a no-difference diff; see
/// `docs/crdt-identity-undo-framing.md` for the consumer census that
/// ruled that location harmless.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Edit { pub struct Edit {
/// The rope after the edit. `Send + Sync`; safe to hand to a worker. /// The rope after the edit. `Send + Sync`; safe to hand to a worker.
@ -312,9 +333,17 @@ pub struct Edit {
/// T M10.2 Day 3: optional CRDT-op metadata. /// T M10.2 Day 3: optional CRDT-op metadata.
/// ///
/// `Some` when this Edit was produced by a CRDT-backed Buffer's /// `Some` when this Edit was produced by a CRDT-backed Buffer's
/// edit path (`apply_edit` / `undo` / `redo`); `None` otherwise — both /// edit path (`apply_edit` / `undo` / `redo`); `None` otherwise —
/// in v0.1 mode (no CRDT) and for no-op edits in CRDT mode (an /// in v0.1 mode (no CRDT), and in CRDT mode for the three
/// empty insert at an empty range produces no CRDT op). /// syntactically empty `EditOp` forms, which `is_no_op_edit`
/// short-circuits before the CRDT path runs.
///
/// An empty text delta is therefore NOT by itself a `None` signal:
/// forward, it means the edit short-circuited and `crdt_op` is
/// `None`; from `undo`/`redo` it means an identity operation was
/// inverted, and `crdt_op` is `Some` — and must be, or the version
/// advance the replicas need is lost. See the shape list on
/// [`Edit`] above.
/// ///
/// `Box` indirection: keeps Edit's None-case cost to 8 bytes /// `Box` indirection: keeps Edit's None-case cost to 8 bytes
/// (Box has a niche-optimized None) rather than the ~32 bytes /// (Box has a niche-optimized None) rather than the ~32 bytes

View File

@ -597,4 +597,119 @@ mod tests {
assert_eq!(vp.row_offset_of(0, 3), None, "hidden lines have no row"); assert_eq!(vp.row_offset_of(0, 3), None, "hidden lines have no row");
assert_eq!(vp.row_offset_of(0, 5), Some(2), "rows below shift up"); assert_eq!(vp.row_offset_of(0, 5), Some(2), "rows below shift up");
} }
/// C9: §4 of `docs/crdt-identity-undo-framing.md` enumerates every
/// in-tree `on_edit` override so that the consumers of a
/// version-only history `Edit` are a closed set. This asserts the
/// set is still what the census measured.
///
/// **It asserts PAIRS, not a file set and a count.** Replacing
/// `ParseView`'s override with an unclassified type in the same
/// file leaves both the file set and the total unchanged, and only
/// the pair set catches it.
///
/// **Its reach is in-tree, and that is a real limit.** [`View`] and
/// `Buffer::attach_view` are both public, so a downstream crate may
/// implement `on_edit` and attach it; no in-tree measurement can
/// enumerate that. What speaks to those implementors is the
/// documented contract on `Edit` itself.
///
/// It also guards only the census's CLOSURE condition — that the
/// override set is unchanged — not the classifications inside it.
/// Those are executed by
/// `identity_replace_history_op_leaves_classified_consumers_unchanged`.
#[test]
fn every_in_tree_on_edit_override_is_one_the_census_classified() {
/// `(file, impl target)`, as measured by the census.
const CLASSIFIED: [(&str, &str); 4] = [
("fold.rs", "FoldStoreTranslator"),
("overlay.rs", "BufferStyleSpanTranslator"),
("syntax.rs", "ParseView"),
("text_view.rs", "TextView"),
];
fn rs_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
for entry in std::fs::read_dir(dir).expect("src is readable") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
rs_files(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
let mut files = Vec::new();
rs_files(
&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
&mut files,
);
files.sort();
let mut found: Vec<(String, String)> = Vec::new();
for path in &files {
let name = path
.file_name()
.expect("file name")
.to_string_lossy()
.into_owned();
let text = std::fs::read_to_string(path).expect("source is readable");
let lines: Vec<&str> = text.lines().collect();
// The test module boundary: the first `#[cfg(test)]` that
// introduces a `mod`. Overrides below it are test fixtures
// and are out of scope, as the census says.
let boundary = lines.iter().enumerate().find_map(|(i, l)| {
(l.trim() == "#[cfg(test)]"
&& lines
.get(i + 1)
.is_some_and(|n| n.trim_start().starts_with("mod ")))
.then_some(i)
});
for (i, line) in lines.iter().enumerate() {
if boundary.is_some_and(|b| i > b) {
break;
}
if !line.contains("fn on_edit") {
continue;
}
// Walk back to the enclosing `impl … for <Type>`. A hit
// on a trait declaration first means this is the
// trait's own default, which is not an override.
for j in (0..=i).rev() {
let l = lines[j].trim_start();
if let Some(rest) = l.strip_prefix("impl")
&& let Some(after) = rest.split(" for ").nth(1)
{
let target = after
.split_whitespace()
.next()
.unwrap_or("")
.trim_end_matches('{')
.rsplit("::")
.next()
.unwrap_or("")
.to_owned();
found.push((name.clone(), target));
break;
}
if l.starts_with("trait ") || l.starts_with("pub trait ") {
break;
}
}
}
}
found.sort();
let expected: Vec<(String, String)> = CLASSIFIED
.iter()
.map(|(f, t)| ((*f).to_owned(), (*t).to_owned()))
.collect();
assert_eq!(
found, expected,
"C9: the in-tree `on_edit` override set no longer matches the \
census in docs/crdt-identity-undo-framing.md §4. Every \
override is a consumer of the version-only history `Edit` \
and needs classifying there."
);
}
} }