From 5fedd15a3deaab58b72b3a481cc679e322b67682 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 17:41:16 +0200 Subject: [PATCH 01/23] docs: frame the identity-replace undo lane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A CRDT-version delta and a text delta are independent dimensions of `Edit`. The invariant that says otherwise was written for `is_no_op_edit`, a forward pre-check that `undo_crdt_mode` and `redo_crdt_mode` never reach. Approved at revision 4, after four review rounds. What the rounds changed, since the record is worth more than the conclusion: - revision 2 answered the question instead of posing it, and narrowed what the existing fixture actually established (broadcast reachability is by inspection, not replay); - revision 3 completed §4's consumer census rather than deferring it, and two of its results corrected the framing's own claims: `TextView` is not buffer-attached at all, and two consumers already carry explicit empty/empty guards written for other reasons; - revision 4 replaced C9's file-set-and-count guard, which a same-file substitution walks straight through, with an exact `(file, impl target)` assertion scoped to in-tree implementations. Three witnesses exist because a green suite is not evidence that a suite RAN: re-ignoring a fixture (C1), dropping a loop parameter (C6), and adding an unclassified consumer (C9) are all silent under ordinary assertions. --- docs/crdt-identity-undo-framing.md | 298 +++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 docs/crdt-identity-undo-framing.md diff --git a/docs/crdt-identity-undo-framing.md b/docs/crdt-identity-undo-framing.md new file mode 100644 index 0000000..0811fbd --- /dev/null +++ b/docs/crdt-identity-undo-framing.md @@ -0,0 +1,298 @@ +# Identity-replace undo — a CRDT-version delta is not a text delta + +**Status: revision 4 — AWAITING APPROVAL. Nothing implemented.** + +Revision 4 answers 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: + +> **The version-only shape is legitimate when the `Edit` came from +> `undo`/`redo`. On the FORWARD path it remains a bug, and stays +> asserted.** + +That is a real narrowing, not a repeal, and it is what makes C5 +testable at all. + +**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. +- **On the forward path the shape is unreachable**, which is what lets + the invariant keep its full strength there. A forward empty form + short-circuits to `(None, None)`; a forward non-empty form has a + non-empty range or `inserted_len > 0`. So forward "empty range and + zero insertion" implies `crdt_op == None`, still. +- 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 still rejects a version-only `Edit` on the FORWARD path | preserve the `GenOp` classification (`buffer.rs:3101`, where `op` is currently moved before it can be classified) as an operation class; extract the shape check to take `(class, &Edit)`; then **inject** `(Forward, version-only Edit)` and assert it is rejected, and `(History, version-only Edit)` and assert it is accepted | widen the `Forward` branch to permit the version-only shape → C5 fires. **The proptest alone cannot catch this**, because no forward input produces that shape — 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 admits the fourth shape | `src/rope.rs:301`–`:303` (in the `Edit` doc from `:292`) gains the empty-range/zero-inserted case, and the `crdt_op` field doc (`:316`) stops asserting that no-op edits have no op | leave the doc → it contradicts the code the lane just blessed | +| 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. From db24ae30109e3f487641245044de785c534c1674 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 17:41:31 +0200 Subject: [PATCH 02/23] fix(crdt): key the crdt_op shape invariant on provenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `undo_crdt_mode` and `redo_crdt_mode` diff two ropes. When the operation being inverted was an identity replace, those ropes are equal, so the derived edit has an empty range and zero insertion — while still carrying the `crdt_op` that `crdt.undo()` produced. The proptest read that shape as "a no-op edit with an op" and redded. The behaviour is right; the invariant was mis-scoped. It now takes `(OperationClass, &Edit)`: - a FORWARD version-only edit must carry no op, unchanged in strength and still unreachable, because the three syntactically empty `EditOp` forms short-circuit at `is_no_op_edit` before the CRDT path exists; - a HISTORY version-only edit may carry one, and must, or the version advance the replicas need is lost. Implements `docs/crdt-identity-undo-framing.md` revision 4, C1-C9. The two contracts worth naming here: C3 replays the history op on a REMOTE replica seeded with the forward ops, asserting materialized text AND version vector. Text alone does not discriminate: dropping the op leaves the text identical. The existing round-trip proptest excludes history ops precisely because replaying one onto an unseeded replica is ill-posed; seeding is what makes this well posed. C4 is three witnesses because the obvious one is vacuous. C4a counts the broadcast, since "unchanged" is also what a missing broadcast produces. C4b executes the census. C4c pins the style-span guard with a synthetic INTERIOR empty edit — at the buffer end, where the real history edit lands, deleting that guard changes nothing, so the mutant would have survived. Also updates the public `Edit` contract, which enumerated three shapes and had no fourth, and the fixture's own doc comment, which presented convergence as verified when nothing had replayed it. --- src/buffer.rs | 636 ++++++++++++++++++++++++++++++++++++++++++++----- src/overlay.rs | 46 ++++ src/rope.rs | 23 +- src/view.rs | 115 +++++++++ 4 files changed, 763 insertions(+), 57 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 3ccb5e9..3832506 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -2962,6 +2962,87 @@ 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, as a function of provenance. + /// + /// Four rules, checked in order: + /// + /// 1. a present `crdt_op` carries the buffer's peer id and + /// non-empty wire bytes; + /// 2. an edit that changed text must carry an op; + /// 3. a **forward** version-only edit (empty range, zero + /// insertion) must NOT carry one — the three syntactically + /// empty `EditOp` forms short-circuit at `is_no_op_edit` + /// before the CRDT path exists; + /// 4. a **history** version-only edit MAY carry one. Undo and + /// redo diff two ropes; an identity replace makes those + /// ropes equal, so the empty range describes a real version + /// advance rather than the absence of one. + /// + /// 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: rule 3 + /// is **unreachable from any generated forward input**, because + /// a forward empty form short-circuits and a forward non-empty + /// form has a non-empty range or a non-zero `inserted_len`. + 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 version_only = edit.range.is_empty() && edit.inserted_len == 0; + if !version_only { + if edit.crdt_op.is_none() { + return Err( + "a text-changing CRDT-mode edit must have crdt_op = Some".to_owned() + ); + } + return Ok(()); + } + match class { + OperationClass::Forward if edit.crdt_op.is_some() => { + Err("a FORWARD version-only edit must have crdt_op = None (see \ + is_no_op_edit)" + .to_owned()) + } + _ => Ok(()), + } + } + // T M10.2 Day 3 helper: applies a `GenOp` and returns the // resulting Edit so the proptest can assert per-op shape. // Each op is best-effort: out-of-range positions are clamped @@ -3012,35 +3093,43 @@ mod tests { /// version while leaving the materialized text unchanged, so /// `undo_crdt_mode` derives an EMPTY replacement edit — and /// 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 - /// reader does not have to redo it: + /// **The ruling** (`docs/crdt-identity-undo-framing.md`, and + /// 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 /// before and after (asserted below); - /// * replicas stay converged — both `crdt_op` consumers - /// (`EditorCore::queue_daemon_origin_crdt_op` and the remote-op - /// path) read `edit.crdt_op` unconditionally and do **not** - /// short-circuit on an empty range, so the op is broadcast; - /// * the cursor does not jump — `EditorCore::undo` only clamps - /// to buffer length and never seeks `edit.range.start`. + /// * replicas stay converged — established by + /// `identity_replace_history_op_replays_convergently_on_a_remote_replica`, + /// which seeds a second replica with the forward ops and then + /// replays the history op, asserting the materialized text + /// **and** the version vector. Before that witness existed + /// 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 - /// simply mis-scoped rather than the behavior being wrong. It - /// was written for the FORWARD `apply_edit` short-circuit, which - /// returns before ever producing an op; CRDT-mode undo/redo - /// never reach that path. One artifact is genuinely arbitrary - /// either way: `derive_replacement_edit` reports the empty range - /// at the buffer END rather than at the edit site. - /// - /// 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. + /// **The empty range's location is ruled, not arbitrary.** + /// `derive_replacement_edit` reports it at the buffer END. No + /// consumer is harmed there, and for the one consumer whose + /// cost depends on it — `TextView::on_edit`, which rebuilds + /// from `line_at_offset(range.start)` — the buffer end is the + /// cheapest possible choice. An earlier version of this comment + /// called it genuinely arbitrary; it is weakly preferable. #[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() { let mut buffer = Buffer::new_with_crdt(BufferId::next(), "*identity-undo*", 1).expect("crdt"); @@ -3084,6 +3173,465 @@ 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 still rejects a + /// version-only `Edit` on the forward path. + /// + /// This must be a directed injection rather than a property. + /// `check_crdt_op_shape`'s forward rule is **unreachable from + /// any generated forward input** — an empty form short-circuits + /// before the CRDT path, and a non-empty form has a non-empty + /// range or a non-zero `inserted_len` — so the proptest alone + /// cannot tell a narrowed rule from a deleted one. + #[test] + fn the_shape_invariant_rejects_a_version_only_edit_on_the_forward_path() { + let version_only = Edit { + new_rope: crate::rope::Rope::from_bytes(b"hello"), + range: Range::new(5, 5), + inserted_len: 0, + crdt_op: Some(Box::new(crate::rope::CrdtOp { + peer_id: 1, + bytes: vec![0xAB], + })), + }; + assert!( + check_crdt_op_shape(OperationClass::Forward, &version_only, 1).is_err(), + "C5: a forward version-only edit carrying an op is still a bug" + ); + assert!( + check_crdt_op_shape(OperationClass::History, &version_only, 1).is_ok(), + "C5: the same shape from undo/redo is a legitimate version advance" + ); + } + + /// 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! { // Smaller proptest case count than the default (64) to keep // CI overhead modest; the per-op invariant check is the @@ -3098,6 +3646,10 @@ mod tests { .expect("crdt construction"); for op in ops { 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); // Per-op invariant check: catches drift the moment // it happens, with the failing op visible in the @@ -3109,37 +3661,13 @@ mod tests { "invariant violated after op {}: rope={:?} crdt={:?}", op_repr, rope, crdt ); - // Day 3: crdt_op shape invariant. - // - real edits in CRDT mode populate crdt_op - // - no-op short-circuits leave crdt_op = None - // - history-stack-empty errors return None Edit - if let Some(edit) = edit { - let is_no_op_edit_result = - edit.range.is_empty() && edit.inserted_len == 0; - 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 - ); - } + // Day 3: crdt_op shape invariant, now keyed on + // provenance rather than on the Edit's shape alone. + // A history-stack-empty error returns no Edit. + if let Some(edit) = edit + && let Err(why) = check_crdt_op_shape(class, &edit, 1) + { + prop_assert!(false, "{} ({})", why, op_repr); } } } diff --git a/src/overlay.rs b/src/overlay.rs index e02dd9e..91b4b12 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -753,6 +753,52 @@ mod tests { 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 { Style { fg: crate::cell::Color::Indexed(1), diff --git a/src/rope.rs b/src/rope.rs index 3081158..bf6b9fb 100644 --- a/src/rope.rs +++ b/src/rope.rs @@ -301,6 +301,17 @@ impl<'a> Iterator for Chunks<'a> { /// 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 replace has both nonzero. +/// A **version-only** edit has `range.start == range.end` and +/// `inserted_len == 0` — no bytes changed at all. CRDT-mode `undo` and +/// `redo` produce this shape when the operation being inverted was +/// itself a textual no-op (replacing bytes with identical bytes), and +/// it still carries a [`CrdtOp`]: a CRDT VERSION delta is a separate +/// dimension from a TEXT delta. Forward `apply_edit` never produces it, +/// because the three syntactically empty `EditOp` forms short-circuit +/// before the CRDT path exists. Its `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)] pub struct Edit { /// The rope after the edit. `Send + Sync`; safe to hand to a worker. @@ -312,9 +323,15 @@ pub struct Edit { /// T M10.2 Day 3: optional CRDT-op metadata. /// /// `Some` when this Edit was produced by a CRDT-backed Buffer's - /// edit path (`apply_edit` / `undo` / `redo`); `None` otherwise — both - /// in v0.1 mode (no CRDT) and for no-op edits in CRDT mode (an - /// empty insert at an empty range produces no CRDT op). + /// edit path (`apply_edit` / `undo` / `redo`); `None` otherwise — + /// in v0.1 mode (no CRDT), and in CRDT mode for the three + /// syntactically empty `EditOp` forms, which `is_no_op_edit` + /// short-circuits before the CRDT path runs. + /// + /// A **version-only** edit is NOT one of those: an empty range with + /// zero insertion coming out of `undo`/`redo` carries `Some`, and + /// must, 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 has a niche-optimized None) rather than the ~32 bytes diff --git a/src/view.rs b/src/view.rs index 06bc7dc..2e7c658 100644 --- a/src/view.rs +++ b/src/view.rs @@ -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, 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) { + 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 `. 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." + ); + } } From 96bf2c3dfd507a8b441e5564fdb224d38ec8648f Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 17:56:05 +0200 Subject: [PATCH 03/23] docs(lane): the identity-undo lane is taken, and corrects its own block The block claimed 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 see a lost version advance. It also called the buffer-end range location "genuinely arbitrary either way"; the census rules it, and finds it weakly preferable. Both corrections are recorded in place rather than quietly dropped, along with what each review round caught and the two mutation asymmetries the framing asserts. --- docs/active-work.md | 97 +++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 52 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 6c5aa66..fc5278b 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -301,66 +301,59 @@ waits for a signal that is not coming. - **THE FIRST DISPATCH IMMEDIATELY FOUND A RED ON `main`**, which is 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 -run's red "a DETERMINISTIC red" and "not like anything else in this -registry — a property violation with a concrete witness, not a load -artefact", and proposed committing the proptest seed as the first -step. **All of that was wrong**, and it was wrong because I recorded a -finding without checking whether `main` already documented it. +**Branch `crdt-identity-undo`, based on `aae5b35`; implemented at +`db24ae3`.** Framing `docs/crdt-identity-undo-framing.md`, **APPROVED at +revision 4** after four review rounds. Gate green **head-exact**, all 8 +stages, log `20260830T154827Z-2907414`; `HEAD` and +`git status --porcelain` identical before and after. -**It is a randomly sampled recurrence of known #157 behaviour.** -`src/buffer.rs:3005` carries an `#[ignore]`d deterministic fixture, -`crdt_undo_of_an_identity_replace_reports_a_no_op_edit_carrying_an_op`, -which reduces this exact case and records its mechanism: +**The decision, ruled:** a visible TEXT delta and a CRDT-VERSION delta +are **independent dimensions** of `Edit`. The invariant is narrowed to +key on **provenance**, not shape — a forward version-only edit must +still carry no op (and still cannot produce one, because the three +syntactically empty `EditOp` forms short-circuit at `is_no_op_edit`); a +history version-only edit may carry one, and must, or the version +advance the replicas need is lost. -1. the inserts produce `aaaaa `; -2. `Replace(5, 1, " ")` replaces the trailing space **with itself** — a - textual no-op but a real CRDT delete-plus-insert; -3. `Undo` therefore emits a **version-advancing CRDT operation with no - visible text change**, and `derive_replacement_edit` yields an empty - edit that still carries its `crdt_op`. +**Two claims THIS BLOCK made are corrected by measurement:** -**Committing the seed is NOT the first step**, and proposing it was a -second error: it duplicates a deterministic fixture that already -exists, and its only effect would be to make a disputed assertion fail -permanently instead of occasionally. +- 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 fixture already verified**, so the lane does not redo it: -content stays correct (rope and CRDT projection agree), replicas stay -converged (**the op IS broadcast** — both `crdt_op` consumers read it -unconditionally and neither short-circuits on an empty range), and the -cursor does not jump. +**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. -**THE ACTUAL DECISION** is whether a **visible text delta** and a -**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**. +**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. -**What the lane owes as evidence:** +**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. -- **forward textual no-ops still produce NO operation** — whatever the - resolution, the short-circuit the invariant was actually written for - must keep holding; -- **any permitted empty-text undo operation carries valid bytes and - preserves remote replay convergence** — permitting the shape must not - become permitting a malformed op; -- **an explicit disposition of the arbitrary artifact**: - `derive_replacement_edit` reports the empty range at the **buffer - 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 -wins**, as its own doc comment says. - -**Why it is worth a bounded interruption:** a mis-scoped property can -now randomly redden `main`, and the dispatched run proved it. **It does -NOT reorder the roadmap** — GUI arc 1b remains the next product lane -per `COHERENCE.md` §20's priority order. +**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 From c597f9c3e408c1c3c8a4d39ece70316f2d11977f Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 18:02:36 +0200 Subject: [PATCH 04/23] docs(ci-reds): R7's eighth occurrence, and the pair that discriminates Two consecutive gate runs on one worktree, minutes apart. Heads differ by a single commit touching a single markdown file. The first was all eight stages green; the second redded at `gpu` with all three of R7's required fragments, and at `sweep` with the same single test. The fifth 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, which is strictly sharper --- whatever varies across that green/red boundary, it is not the source tree. No ratio is claimed from it. Folding verification gates into the 2026-08-29 window is exactly the drift that window was bounded against. Five isolated selector runs were green, which per this file's own rerun rule and the seventh occurrence's correction establishes intermittence and excludes nothing. --- docs/ci-red-signatures.md | 62 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 99a5f79..17f61c7 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -535,12 +535,72 @@ 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` | | **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`) | -| **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** | **EIGHTH OCCURRENCE 2026-08-30 — causal status still UNRESOLVED.** The eighth carries the strongest tree exclusion this row has had, and it supersedes the fifth's: two consecutive gate runs on ONE worktree whose heads differ by a single markdown file, the first all-green and the second red | | **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 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 | | **retirement** | hardening that removes the named mechanism plus a discriminating witness — or a diagnosis showing the fixture, not the code, closes the pipe | +**Eighth 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 fifth 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 seventh 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 + seventh occurrence already falsified "in-gate always fails". + +**What this changes about the method.** The seventh occurrence's +narrowing said the remaining candidates must be varied INSIDE the gate, +one per run. This pair says something stronger about which candidates +are live: the gate's own per-run state — its ambient root, its fresh +`TMPDIR`, and process state carried across stage boundaries — is now the +only place the difference can be, because the tree was held fixed to +within a markdown file across a green/red boundary. + **Sixth occurrence — the parse-budget diagnosability lane, 2026-08-29, local (Linux), `gpu` step.** All three required fragments present in the durable log From b3c90a79494ba4028b6736e7b389c2d2609db888 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 19:19:35 +0200 Subject: [PATCH 05/23] fix(crdt): enumerate the invariant over three axes, not two Review found the predicate conflating the two things this lane exists to separate. It called every empty-range/zero-insertion edit `version_only` and then accepted `(History, empty, None)` through a wildcard arm --- which contradicts the lane's own ruling that the op must survive, and contradicts the public contract's "carries Some, and must". The rule is now a full enumeration over provenance x text delta x crdt_op. An EMPTY TEXT DELTA is a shape, not a verdict: both paths reach it, and `crdt_op` is what separates them. forward + empty + None valid, a syntactic no-op forward + empty + Some invalid, the original bug history + empty + Some valid, a version-only edit history + empty + None invalid, the version advance is gone C5 asserts all four rather than two. Both new quadrants were mutation-checked and both fire; neither is caught by the proptest, because no generated input reaches either --- which is the same reason C5 was a directed injection to begin with. The public `Edit` doc was also factually false. It said forward `apply_edit` never produces the empty-delta shape while C2b proves all three forward empty forms do. It now names the shape and says which path yields which `crdt_op`. --- src/buffer.rs | 135 ++++++++++++++++++++++++++++++++------------------ src/rope.rs | 42 ++++++++++------ 2 files changed, 114 insertions(+), 63 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 3832506..232af4c 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -2986,28 +2986,45 @@ mod tests { } } - /// The `crdt_op` shape invariant, as a function of provenance. + /// The `crdt_op` shape invariant, over three independent axes. /// - /// Four rules, checked in order: + /// 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: /// - /// 1. a present `crdt_op` carries the buffer's peer id and - /// non-empty wire bytes; - /// 2. an edit that changed text must carry an op; - /// 3. a **forward** version-only edit (empty range, zero - /// insertion) must NOT carry one — the three syntactically - /// empty `EditOp` forms short-circuit at `is_no_op_edit` - /// before the CRDT path exists; - /// 4. a **history** version-only edit MAY carry one. Undo and - /// redo diff two ropes; an identity replace makes those - /// ropes equal, so the empty range describes a real version - /// advance rather than the absence of one. + /// | 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: rule 3 - /// is **unreachable from any generated forward input**, because - /// a forward empty form short-circuits and a forward non-empty - /// form has a non-empty range or a non-zero `inserted_len`. + /// 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. fn check_crdt_op_shape( class: OperationClass, edit: &Edit, @@ -3024,22 +3041,25 @@ mod tests { return Err("wire bytes must be non-empty".to_owned()); } } - let version_only = edit.range.is_empty() && edit.inserted_len == 0; - if !version_only { - if edit.crdt_op.is_none() { - return Err( - "a text-changing CRDT-mode edit must have crdt_op = Some".to_owned() - ); - } - return Ok(()); - } - match class { - OperationClass::Forward if edit.crdt_op.is_some() => { - Err("a FORWARD version-only edit must have crdt_op = None (see \ - is_no_op_edit)" - .to_owned()) - } - _ => Ok(()), + 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(), + ), } } @@ -3283,34 +3303,53 @@ mod tests { } } - /// C5: the invariant is keyed on PROVENANCE, and still rejects a - /// version-only `Edit` on the forward path. + /// C5: the invariant is keyed on PROVENANCE, and covers all + /// four empty-text-delta quadrants. /// - /// This must be a directed injection rather than a property. - /// `check_crdt_op_shape`'s forward rule is **unreachable from - /// any generated forward input** — an empty form short-circuits - /// before the CRDT path, and a non-empty form has a non-empty - /// range or a non-zero `inserted_len` — so the proptest alone - /// cannot tell a narrowed rule from a deleted one. + /// 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_rejects_a_version_only_edit_on_the_forward_path() { - let version_only = Edit { + fn the_shape_invariant_covers_all_four_empty_text_delta_quadrants() { + let with_op = |op: Option>| Edit { new_rope: crate::rope::Rope::from_bytes(b"hello"), range: Range::new(5, 5), inserted_len: 0, - crdt_op: Some(Box::new(crate::rope::CrdtOp { + 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, &version_only, 1).is_err(), - "C5: a forward version-only edit carrying an op is still a bug" + 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::History, &version_only, 1).is_ok(), + 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. diff --git a/src/rope.rs b/src/rope.rs index bf6b9fb..1b1b40e 100644 --- a/src/rope.rs +++ b/src/rope.rs @@ -301,17 +301,27 @@ impl<'a> Iterator for Chunks<'a> { /// 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 replace has both nonzero. -/// A **version-only** edit has `range.start == range.end` and -/// `inserted_len == 0` — no bytes changed at all. CRDT-mode `undo` and -/// `redo` produce this shape when the operation being inverted was -/// itself a textual no-op (replacing bytes with identical bytes), and -/// it still carries a [`CrdtOp`]: a CRDT VERSION delta is a separate -/// dimension from a TEXT delta. Forward `apply_edit` never produces it, -/// because the three syntactically empty `EditOp` forms short-circuit -/// before the CRDT path exists. Its `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. +/// 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)] pub struct Edit { /// The rope after the edit. `Send + Sync`; safe to hand to a worker. @@ -328,10 +338,12 @@ pub struct Edit { /// syntactically empty `EditOp` forms, which `is_no_op_edit` /// short-circuits before the CRDT path runs. /// - /// A **version-only** edit is NOT one of those: an empty range with - /// zero insertion coming out of `undo`/`redo` carries `Some`, and - /// must, or the version advance the replicas need is lost. See the - /// shape list on [`Edit`] above. + /// 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 has a niche-optimized None) rather than the ~32 bytes From 099b5a738318fd93d7a24042383c1d29dc83fe32 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 19:19:35 +0200 Subject: [PATCH 06/23] docs: narrow R7's causal claim, and record U6's first reproduction R7's eighth-occurrence write-up said the gate's ambient root, TMPDIR and cross-stage process state were "now the only place the difference can be". That is wrong. The paired runs exclude the SOURCE TREE and nothing else: scheduler load, kernel and socket timing, page cache pressure and whatever else the machine was doing also varied between them, and a BrokenPipe on a socket handshake is exactly what those can drive. The three remain the candidates worth varying one at a time --- because they are the ones this project can vary --- not an exhaustive causal set. U6 gained a second occurrence, and for the first time it REPRODUCED: both selectors, both fragments, two consecutive runs. Margins recorded per U11's lesson --- 1.343883ms against 1ms, and 1.182x against 1.10x. Its asymmetry runs the opposite way to R7's: both failures were out of gate, while the same command as `04-lib-crdt` was green in all four of this lane's gate runs. Whatever the two rows share, it is not a direction. Framing revision 5 and the lane block are updated to match, including the stale "AWAITING APPROVAL. Nothing implemented." header and the gate line that named a commit the branch had already moved past. --- docs/active-work.md | 48 ++++++++++++++++----- docs/ci-red-signatures.md | 60 ++++++++++++++++++++++---- docs/crdt-identity-undo-framing.md | 69 ++++++++++++++++++++++++------ 3 files changed, 146 insertions(+), 31 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index fc5278b..588a3ab 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -303,19 +303,45 @@ waits for a signal that is not coming. ## CRDT identity-replace undo — LANE TAKEN, PR #246 OPEN -**Branch `crdt-identity-undo`, based on `aae5b35`; implemented at -`db24ae3`.** Framing `docs/crdt-identity-undo-framing.md`, **APPROVED at -revision 4** after four review rounds. Gate green **head-exact**, all 8 -stages, log `20260830T154827Z-2907414`; `HEAD` and -`git status --porcelain` identical before and after. +**Branch `crdt-identity-undo`, PR #246, based on `aae5b35`.** Framing +`docs/crdt-identity-undo-framing.md`, **APPROVED at revision 4** after +four review rounds, then **revision 5** as a correction pass answering +implementation review. Gate green **head-exact at the current head**, +all 8 stages; the `db24ae3` run recorded here earlier was head-exact for +that commit only and the branch has moved since. **The decision, ruled:** a visible TEXT delta and a CRDT-VERSION delta -are **independent dimensions** of `Edit`. The invariant is narrowed to -key on **provenance**, not shape — a forward version-only edit must -still carry no op (and still cannot produce one, because the three -syntactically empty `EditOp` forms short-circuit at `is_no_op_edit`); a -history version-only edit may carry one, and must, or the version -advance the replicas need is lost. +are **independent dimensions** of `Edit`. The invariant is keyed on +**provenance**, and enumerated over three axes rather than defaulted: +an **empty text delta** is a shape both paths reach, and `crdt_op` is +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. + +**Revision 5 fixed three things review caught in the implementation:** + +- 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. + +**Two registry rows gained occurrences on this lane**: R7's eighth (the +green/red pair whose heads differ by one markdown file) and **U6's +second — the first time U6 has ever reproduced**, twice in a row, and +running the OPPOSITE way to R7: out of gate, while `04-lib-crdt` was +green in all four of this lane's gate runs. **Two claims THIS BLOCK made are corrected by measurement:** diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 17f61c7..6e5ac17 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -593,13 +593,23 @@ and nothing else: `283 passed; 1 failed` in each. the rerun rule a green run establishes intermittence only, and the seventh occurrence already falsified "in-gate always fails". -**What this changes about the method.** The seventh occurrence's -narrowing said the remaining candidates must be varied INSIDE the gate, -one per run. This pair says something stronger about which candidates -are live: the gate's own per-run state — its ambient root, its fresh -`TMPDIR`, and process state carried across stage boundaries — is now the -only place the difference can be, because the tree was held fixed to -within a markdown file across a green/red boundary. +**What this changes about the method, stated at the strength it +carries.** The seventh 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. **Sixth occurrence — the parse-budget diagnosability lane, 2026-08-29, local (Linux), `gpu` step.** All three required fragments present in the @@ -915,11 +925,45 @@ 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 | | **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` | -| **status** | **new incident, one occurrence, not reproduced** | +| **status** | **SECOND OCCURRENCE 2026-08-30, and the first time it REPRODUCED — twice in a row.** 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 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 | +**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. + +**The asymmetry runs the OPPOSITE way to R7, and that is the useful +part.** Both failures were **out of gate** — a bare +`cargo test --lib --features crdt` in the worktree — while the same +command as `scripts/gate`'s `04-lib-crdt` step was **green in all four +of this lane's gate runs** (`20260830T154827Z`, `T155621Z`, `T160242Z`, +`T160824Z`). R7 fails in-gate and has never reproduced outside it; U6 +here did the reverse. Whatever the two rows share, it is not a +direction. + +**No mechanism is claimed, and the load confound is again unmeasured.** +The machine was running the agent session's own build and test traffic; +that is a rival explanation, not a finding, exactly as the first +occurrence recorded. What is worth having 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 signature worth matching**, more than either name alone: a real regression in two unrelated subsystems at once is far less likely than diff --git a/docs/crdt-identity-undo-framing.md b/docs/crdt-identity-undo-framing.md index 0811fbd..d922a51 100644 --- a/docs/crdt-identity-undo-framing.md +++ b/docs/crdt-identity-undo-framing.md @@ -1,8 +1,29 @@ # Identity-replace undo — a CRDT-version delta is not a text delta -**Status: revision 4 — AWAITING APPROVAL. Nothing implemented.** +**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 4 answers review of 3, which found one substantive gap: **C9 +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 @@ -22,13 +43,35 @@ 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: -> **The version-only shape is legitimate when the `Edit` came from -> `undo`/`redo`. On the FORWARD path it remains a bug, and stays -> asserted.** +> **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` @@ -38,11 +81,13 @@ testable at all. `derive_replacement_edit` (`:1440`, `:1525`) and attach the op `crdt.undo()` produced (`:1454`), so identical ropes yield an empty range describing a real operation. -- **On the forward path the shape is unreachable**, which is what lets - the invariant keep its full strength there. A forward empty form - short-circuits to `(None, None)`; a forward non-empty form has a - non-empty range or `inserted_len > 0`. So forward "empty range and - zero insertion" implies `crdt_op == None`, still. +- **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. @@ -215,9 +260,9 @@ one of the four already classified. | 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 still rejects a version-only `Edit` on the FORWARD path | preserve the `GenOp` classification (`buffer.rs:3101`, where `op` is currently moved before it can be classified) as an operation class; extract the shape check to take `(class, &Edit)`; then **inject** `(Forward, version-only Edit)` and assert it is rejected, and `(History, version-only Edit)` and assert it is accepted | widen the `Forward` branch to permit the version-only shape → C5 fires. **The proptest alone cannot catch this**, because no forward input produces that shape — which is why C5 is a directed injection, not a property | +| 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 admits the fourth shape | `src/rope.rs:301`–`:303` (in the `Edit` doc from `:292`) gains the empty-range/zero-inserted case, and the `crdt_op` field doc (`:316`) stops asserting that no-op edits have no op | leave the doc → it contradicts the code the lane just blessed | +| 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 | From e0fcfe3e9bb0b8134bb37344272e66f366a37b27 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 19:28:10 +0200 Subject: [PATCH 07/23] docs(ci-reds): U14, and U6's direction claim withdrawn within the hour The gate run verifying revision 5 redded three unrelated tests in three stages: a 50ms supersede budget in `lib`, U6's pair in `lib-crdt`, and an LSP readiness race in `sweep`. Recorded as U14, because the co-occurrence is the signature --- three subsystems failing in one run is far less likely than one loaded machine, and no selector reds twice. It also falsifies something I had committed an hour earlier. U6's second-occurrence block said the row "runs the OPPOSITE way to R7", resting on both failures being out of gate while `04-lib-crdt` was green in four gate runs. The next gate run redded `04-lib-crdt` with exactly that pair. Four green stages were a run of four, not a property. The claim is withdrawn in place rather than edited away, and U6's status moves to a third occurrence: three in one afternoon, twice out of gate and once in. U14 also declines an R1 match it could have claimed. The `lib` failure carries R1's required fragment but a different selector, and this registry matches on both. Worth noting separately: the sibling test already reports the elapsed value R1's row records as missing from its own assertion --- the cheap half of what R1 defers is written next door. --- docs/ci-red-signatures.md | 71 ++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 6e5ac17..10f44c4 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -925,7 +925,7 @@ 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 | | **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` | -| **status** | **SECOND OCCURRENCE 2026-08-30, and the first time it REPRODUCED — twice in a row.** Still no mechanism; see the block below | +| **status** | **THIRD OCCURRENCE 2026-08-30 — the first time it has REPRODUCED, three times in one afternoon, twice out of gate and once in.** 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 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 | @@ -948,14 +948,17 @@ 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. -**The asymmetry runs the OPPOSITE way to R7, and that is the useful -part.** Both failures were **out of gate** — a bare -`cargo test --lib --features crdt` in the worktree — while the same -command as `scripts/gate`'s `04-lib-crdt` step was **green in all four -of this lane's gate runs** (`20260830T154827Z`, `T155621Z`, `T160242Z`, -`T160824Z`). R7 fails in-gate and has never reproduced outside it; U6 -here did the reverse. Whatever the two rows share, it is not a -direction. +**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** (`20260830T171941Z-3509751`, +margins `1.689259ms` against 1ms and `1.592×` against 1.10×) — see U14, +which records that run whole. 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 budgets on a loaded machine. **No mechanism is claimed, and the load confound is again unmeasured.** The machine was running the agent session's own build and test traffic; @@ -1220,6 +1223,56 @@ concurrency to 1, and separately load a lone `--lib` binary. Four incidents is enough evidence that the family will keep costing review rounds until someone runs it. +### U14 — three unrelated tests red in ONE gate run, in 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: three unrelated +subsystems failing in one run is far less likely than one loaded +machine, and no single selector here reds twice. + +| field | value | +|---|---| +| **selectors** | `03-lib`: `async_runtime::tests::grep_supersede_cancels_predecessor_within_50ms`. `04-lib-crdt`: U6's pair. `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. Three stages, three subsystems, and one of the three 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** | + +**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. + +**The readiness failure is the one worth watching.** A budget test on a +loaded machine is a known shape here; `server LspServerId(1) is not +ready for requests (state: initializing)` is a **race between a test and +a state machine**, and load makes it more likely without being its +cause. If it recurs it should be judged on its own, not folded into this +row's co-occurrence. + ### U13 — gate prune-reporting row receives empty child stdout in `sweep` Recorded during PR #244 review, 2026-08-29, on signed head `756c2b8`. From ff9e1cbf0bf1fd5c8ec7247f78aa7e1aabaa18c3 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 19:56:50 +0200 Subject: [PATCH 08/23] fix(crdt): allow match_same_arms, and record why the gate missed it The enumeration is the contract. Clippy's `match_same_arms` would collapse the three `Ok(())` rows 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. The lint reached CI rather than the gate because the gate's clippy step runs default features, so `#[cfg(feature = "crdt")]` code is never linted locally. Five consecutive green gate runs could not see it. The gap is recorded in the ledger; fixing it means adding a second clippy flavor to shared gate infrastructure, which is its own lane. --- docs/active-work.md | 18 ++++++++++++++++++ src/buffer.rs | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/docs/active-work.md b/docs/active-work.md index 588a3ab..ddbfa25 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -337,6 +337,24 @@ of such an edit. machine state all varied too, and a `BrokenPipe` on a socket handshake is exactly what those can drive. +**A GATE COVERAGE GAP, found the expensive way.** The local gate's +clippy step is `cargo clippy --workspace --all-targets -- -D warnings` +— **default features**, so **no `#[cfg(feature = "crdt")]` code is ever +linted locally**. CI lints it (`--no-default-features --features +luajit,crdt`), so a crdt-only lint passes eight green gate stages and +then reds `Test (crdt)`. That is what happened here: a +`clippy::match_same_arms` on the new enumeration, invisible to five +consecutive local gate runs. + +The lint itself is `#[allow]`ed with a reason — collapsing the three +`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, alongside U9's still-unrun +discriminating control. Recorded so the next lane touching +crdt-gated code does not rediscover it at CI. + **Two registry rows gained occurrences on this lane**: R7's eighth (the green/red pair whose heads differ by one markdown file) and **U6's second — the first time U6 has ever reproduced**, twice in a row, and diff --git a/src/buffer.rs b/src/buffer.rs index 232af4c..cc2af91 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -3025,6 +3025,14 @@ mod tests { /// `(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, From 2c2430332e395130328519208d85ca8767621831 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 20:04:58 +0200 Subject: [PATCH 09/23] docs(ci-reds): U14 gains the number U6 and U7 have wanted since August The gate redded again in the same three stages, with a partly rotated selector set --- one of them being a U7 selector. This time `/proc/loadavg` was read at the failure: 34.04, with the CPU saturated by an unrelated `lean` workload on this shared machine and no cargo, rustc or gate process of mine left running. U6 and U7 have each recorded, since 2026-08-09, that the load confound "was not measured, so it is a rival explanation, not a finding." It is measured now, and the margins move with it monotonically across three runs of one unchanged tree: 1.343883ms, then 1.689259ms, then 2.269247ms, against a 1ms budget. A regression does not get 69% worse between two runs of the same tree. This retires nothing. The budgets are still wall-clock assertions whose measurement design nobody has defended --- R1's disposition, applied to five more tests. What changes is that "one loaded machine" is now a measured explanation rather than a plausible one. --- docs/ci-red-signatures.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 10f44c4..bb1c928 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -1236,11 +1236,48 @@ machine, and no single selector here reds twice. | **selectors** | `03-lib`: `async_runtime::tests::grep_supersede_cancels_predecessor_within_50ms`. `04-lib-crdt`: U6's pair. `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** | +| **status** | **two occurrences, 2026-08-30, and the SECOND ONE MEASURES THE LOAD CONFOUND** that U6 and U7 have each recorded as unmeasured since 2026-08-09 | | **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. Three stages, three subsystems, and one of the three 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** | +**Second occurrence, 40 minutes later — and this one has the number.** +`scripts/gate` log `20260830T175657Z-3881334`, same lane, same three +stages red, **a partly different selector set**: `03-lib` took +`composition_overhead_under_ten_percent` *and* +`semantic_render::tests::full_buffer_summary_flatten_scales_on_large_grammar_file`; +`04-lib-crdt` took U6's pair again; `07-sweep` took +`dired_renders_10k_entries_within_200ms`, **which is one of U7's three +selectors**. + +**`/proc/loadavg` read immediately after: `34.04 38.45 26.29`.** The +CPU was saturated by `lean` processes — an unrelated workload on this +shared machine, nothing this session started, and no `cargo`, `rustc` +or `scripts/gate` process of mine was left running. + +**U6 and U7 have both said, since 2026-08-09, that the load confound +"was not measured, so it is a rival explanation, not a finding."** Here +it is measured, and the margins move with it monotonically across three +runs of the same tree: + +| run | `criterion_1` per-keystroke | `composition_overhead` | +|---|---|---| +| out of gate, ~17:05 | 1.343883ms | 1.182× | +| gate `T171941Z` | 1.689259ms | 1.592× | +| gate `T175657Z`, loadavg 34.04 | **2.269247ms** | 1.527× | + +Against a 1ms and a 1.10× budget. **A regression does not get 69% worse +between two runs of an unchanged tree; a load average of 34 explains it +without any help.** That is a finding about these budget rows, not about +the tree — and it is the first time this registry can say so with a +number rather than a suspicion. + +**What it does NOT do:** it does not retire U6, U7 or this row. The +budgets remain wall-clock assertions whose measurement design nobody has +defended, which is the same disposition R1 carries. What changes is that +"one loaded machine" stops being a rival explanation offered in good +faith and becomes a measured one. + **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 From 6ddce0ff29180db28b973c4464b96b389d3daf0c Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 22:19:03 +0200 Subject: [PATCH 10/23] docs(ci-reds): split U15 out of U14, and stop overstating the load Three corrections, all mine, all caught in review. U14 claimed a second occurrence for a run whose selector set had ROTATED --- `full_buffer_summary_flatten` and `dired_renders_10k_entries` in place of `grep_supersede` and `acc34_purge`. This file's own matching rule requires the exact selectors to match, so that is a new incident. It is now U15. The `04-lib-crdt` pair the two runs share is recorded where it belongs, as U6's own occurrence; U6 goes from one occurrence to five, four of them on 2026-08-30. U14 also said "three unrelated tests". There are four selectors. And the load claim went too far. `/proc/loadavg` was read once, after the second run, so there is no series to correlate against; the margins are not monotonic (`composition_overhead` ran 1.182x, 1.592x, 1.527x, and reports two different values within the second run); and an earlier version said a load average of 34 "explains it without any help". What 34.04 establishes is severe unrelated load present CONTEMPORANEOUSLY with one multi-red run --- a measured confound, not a measured cause. That is still worth more than U6 and U7 have had since August, and it is worth exactly that much. The lane block also still carried the withdrawn "opposite way to R7" claim and named `db24ae3` as the gate head. It now names `2c24303` and log 20260830T193305Z-4167110. --- docs/active-work.md | 39 ++++++++-- docs/ci-red-signatures.md | 158 +++++++++++++++++++++++--------------- 2 files changed, 127 insertions(+), 70 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index ddbfa25..141fa12 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -306,9 +306,11 @@ waits for a signal that is not coming. **Branch `crdt-identity-undo`, PR #246, based on `aae5b35`.** Framing `docs/crdt-identity-undo-framing.md`, **APPROVED at revision 4** after four review rounds, then **revision 5** as a correction pass answering -implementation review. Gate green **head-exact at the current head**, -all 8 stages; the `db24ae3` run recorded here earlier was head-exact for -that commit only and the branch has moved since. +implementation review. **Gate green head-exact at `2c24303`, all 8 +stages, log `20260830T193305Z-4167110`**, taken at loadavg 0.90 with +`HEAD` and `git status --porcelain` identical before and after; **CI +14/14 green at the same commit.** Earlier gate runs recorded here named +`db24ae3`, which the branch has long since moved past. **The decision, ruled:** a visible TEXT delta and a CRDT-VERSION delta are **independent dimensions** of `Edit`. The invariant is keyed on @@ -355,11 +357,32 @@ infrastructure and belongs in its own lane, alongside U9's still-unrun discriminating control. Recorded so the next lane touching crdt-gated code does not rediscover it at CI. -**Two registry rows gained occurrences on this lane**: R7's eighth (the -green/red pair whose heads differ by one markdown file) and **U6's -second — the first time U6 has ever reproduced**, twice in a row, and -running the OPPOSITE way to R7: out of gate, while `04-lib-crdt` was -green in all four of this lane's gate runs. +**Four registry rows moved on this lane:** + +- **R7's eighth occurrence** — the green/red pair whose heads differ by + one markdown file. It 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; +- **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 red in one gate run across three stages; +- **U15, new** — the rotated cluster 40 minutes later. It carries the + 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. An earlier version of that write-up said a + load average of 34 "explains it without any help"; it does not, and + that is corrected in place. + +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:** diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index bb1c928..b02b471 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -925,7 +925,7 @@ 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 | | **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` | -| **status** | **THIRD OCCURRENCE 2026-08-30 — the first time it has REPRODUCED, three times in one afternoon, twice out of gate and once in.** Still no mechanism; see the block below | +| **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 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 | @@ -948,24 +948,30 @@ 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** (`20260830T171941Z-3509751`, -margins `1.689259ms` against 1ms and `1.592×` against 1.10×) — see U14, -which records that run whole. 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 budgets on a loaded machine. +`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, and the load confound is again unmeasured.** -The machine was running the agent session's own build and test traffic; -that is a rival explanation, not a finding, exactly as the first -occurrence recorded. What is worth having is that **this row is now -reproducible under some condition**, which the first occurrence could -not say. +**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 signature worth matching**, more than either name alone: a real @@ -1223,60 +1229,33 @@ concurrency to 1, and separately load a lone `--lib` binary. Four incidents is enough evidence that the family will keep costing review rounds until someone runs it. -### U14 — three unrelated tests red in ONE gate run, in three stages +### 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: three unrelated -subsystems failing in one run is far less likely than one loaded -machine, and no single selector here reds twice. +the signature**, as it is for U6, U9 and U12: four selectors in three +unrelated subsystems failing in one run is less likely than one loaded +machine, and no single selector reds twice within the run. | field | value | |---|---| -| **selectors** | `03-lib`: `async_runtime::tests::grep_supersede_cancels_predecessor_within_50ms`. `04-lib-crdt`: U6's pair. `07-sweep`: `lsp_dispatch_seams_acceptance::acc34_purge_reaches_a_server_that_is_in_no_attachment` | +| **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** | **two occurrences, 2026-08-30, and the SECOND ONE MEASURES THE LOAD CONFOUND** that U6 and U7 have each recorded as unmeasured since 2026-08-09 | +| **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. Three stages, three subsystems, and one of the three is a readiness race rather than a clock | +| **what is NOT** | any shared mechanism, and **any load measurement**: no `/proc/loadavg` reading was taken during or after this run. Three stages, three subsystems, and one of the four 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** | -**Second occurrence, 40 minutes later — and this one has the number.** -`scripts/gate` log `20260830T175657Z-3881334`, same lane, same three -stages red, **a partly different selector set**: `03-lib` took -`composition_overhead_under_ten_percent` *and* -`semantic_render::tests::full_buffer_summary_flatten_scales_on_large_grammar_file`; -`04-lib-crdt` took U6's pair again; `07-sweep` took -`dired_renders_10k_entries_within_200ms`, **which is one of U7's three -selectors**. - -**`/proc/loadavg` read immediately after: `34.04 38.45 26.29`.** The -CPU was saturated by `lean` processes — an unrelated workload on this -shared machine, nothing this session started, and no `cargo`, `rustc` -or `scripts/gate` process of mine was left running. - -**U6 and U7 have both said, since 2026-08-09, that the load confound -"was not measured, so it is a rival explanation, not a finding."** Here -it is measured, and the margins move with it monotonically across three -runs of the same tree: - -| run | `criterion_1` per-keystroke | `composition_overhead` | -|---|---|---| -| out of gate, ~17:05 | 1.343883ms | 1.182× | -| gate `T171941Z` | 1.689259ms | 1.592× | -| gate `T175657Z`, loadavg 34.04 | **2.269247ms** | 1.527× | - -Against a 1ms and a 1.10× budget. **A regression does not get 69% worse -between two runs of an unchanged tree; a load average of 34 explains it -without any help.** That is a finding about these budget rows, not about -the tree — and it is the first time this registry can say so with a -number rather than a suspicion. - -**What it does NOT do:** it does not retire U6, U7 or this row. The -budgets remain wall-clock assertions whose measurement design nobody has -defended, which is the same disposition R1 carries. What changes is that -"one loaded machine" stops being a rival explanation offered in good -faith and becomes a measured one. +**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 @@ -1303,12 +1282,67 @@ 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. -**The readiness failure is the one worth watching.** A budget test on a -loaded machine is a known shape here; `server LspServerId(1) is not -ready for requests (state: initializing)` is a **race between a test and -a state machine**, and load makes it more likely without being its -cause. If it recurs it should be judged on its own, not folded into this -row's co-occurrence. +### U15 — a rotated multi-red cluster, with the load MEASURED for once + +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.** U6 +and U7 have each recorded, since 2026-08-09, that the load confound +"was not measured, so it is a rival explanation, not a finding." **It is +measured now — once, after this run.** That makes it 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:** "one loaded machine" stops being a hypothesis +offered in good faith and becomes a **quantity on the record**, which is +what a future correlation would need as its first data point. It retires +nothing — 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. ### U13 — gate prune-reporting row receives empty child stdout in `sweep` From dd3f3db83923ca61f6bc582fa44eef4733c5c63b Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 22:50:46 +0200 Subject: [PATCH 11/23] docs(lane): name every gated head, not just the last one I wrote down The gate line named `2c24303` while the branch head was `6ddce0f` --- the same staleness review caught one round earlier, when it named `db24ae3`. Naming one commit reproduces the defect on the next commit. It is a table now: one row per gated head, so the next update adds a row instead of rewriting a sentence, and the staleness is stated rather than waiting to be found again. --- docs/active-work.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 141fa12..d1f8be4 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -306,11 +306,21 @@ waits for a signal that is not coming. **Branch `crdt-identity-undo`, PR #246, based on `aae5b35`.** Framing `docs/crdt-identity-undo-framing.md`, **APPROVED at revision 4** after four review rounds, then **revision 5** as a correction pass answering -implementation review. **Gate green head-exact at `2c24303`, all 8 -stages, log `20260830T193305Z-4167110`**, taken at loadavg 0.90 with -`HEAD` and `git status --porcelain` identical before and after; **CI -14/14 green at the same commit.** Earlier gate runs recorded here named -`db24ae3`, which the branch has long since moved past. +implementation review. + +**Every head of this branch is gated head-exact**, with `HEAD` and +`git status --porcelain` captured before and after each run and +identical. The two that matter: + +| commit | what it carries | gate | CI | +|---|---|---|---| +| `2c24303` | the code, and the framing at revision 5 | all 8 green, `20260830T193305Z-4167110`, loadavg 0.90 | 14/14 | +| `6ddce0f` | the registry corrections (U14/U15 split, load claim narrowed) | all 8 green, `20260830T201908Z-84597` | 14/14 | + +**This line goes stale the moment another commit lands, which is the +defect review already caught here once** — it named `db24ae3` long after +the branch had moved past it. It is written as a table so the next +update is an added row rather than a rewrite. **The decision, ruled:** a visible TEXT delta and a CRDT-VERSION delta are **independent dimensions** of `Edit`. The invariant is keyed on From 169ea1892c31f0eb7892e39451443d0497f8f8fc Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 10:30:15 +0200 Subject: [PATCH 12/23] docs(ci-reds): four subsystems, and U7 already had a load number Two precision errors, both mine. U14 said "four selectors in three unrelated subsystems". They are four: the async runtime, the optimistic-echo orchestrator, editor composition, and the LSP dispatch seam. 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. The `what is NOT` row said three as well. U15 claimed to be the load number "U6 and U7 have each wanted since 2026-08-09". Half of that was wrong: U7 has carried a load average (12.9 / 23.9) in its job/flavor field since that date. What U7 records as unmeasured is narrower --- whether the shared `CARGO_TARGET_DIR` and its sibling builds PRODUCED that load. So 34.04 is the first contemporaneous reading for a U6 occurrence and a second data point beside U7's, not the registry's first. The row title oversold it too. U15's disposition list also omitted U15. --- docs/active-work.md | 15 ++++++++----- docs/ci-red-signatures.md | 46 ++++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index d1f8be4..1be6709 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -378,16 +378,21 @@ crdt-gated code does not rediscover it at CI. 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 red in one gate run across three stages; -- **U15, new** — the rotated cluster 40 minutes later. It carries the +- **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. An earlier version of that write-up said a - load average of 34 "explains it without any help"; it does not, and - that is corrected in place. + 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. 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 diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index b02b471..f9742fb 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -1233,9 +1233,13 @@ rounds until someone runs it. 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 three -unrelated subsystems failing in one run is less likely than one loaded -machine, and no single selector reds twice within the run. +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 | |---|---| @@ -1244,7 +1248,7 @@ machine, and no single selector reds twice within the run. | **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, three subsystems, and one of the four is a readiness race rather than a clock | +| **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 @@ -1282,7 +1286,7 @@ 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 the load MEASURED for once +### 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 @@ -1311,12 +1315,20 @@ 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.** U6 -and U7 have each recorded, since 2026-08-09, that the load confound -"was not measured, so it is a rival explanation, not a finding." **It is -measured now — once, after this run.** That makes it a **measured -confound present contemporaneously with a multi-red run**. It does not -make it the cause, and three specific things stop it short: +**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 @@ -1333,12 +1345,12 @@ make it the cause, and three specific things stop it short: load was present; whether it produced these particular margins is unmeasured. -**What it does change:** "one loaded machine" stops being a hypothesis -offered in good faith and becomes a **quantity on the record**, which is -what a future correlation would need as its first data point. It retires -nothing — U6, U7, U14 and R1 all keep their dispositions, and the -budgets remain wall-clock assertions whose measurement design nobody has -defended. +**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 From e87d22796d7894fdc952537944e11276c5a0b936 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 10:37:23 +0200 Subject: [PATCH 13/23] docs(ci-reds): U16 --- a git invocation with a deleted working directory The sweep step redded on `cache_survives_across_fetcher_instances` with `fatal: Unable to read current working directory`. Not a budget test, and not a load story. It is the only row in this file that arrives with a named candidate mechanism inside the test suite. `src/file_io.rs:434` calls `std::env::set_current_dir` --- process-global state --- inside a test running in one of libtest's parallel threads, points it at a `TempDir`, and lets that `TempDir` drop. Every other test in the binary shares that cwd for the window, and after the drop it is a deleted directory, which is exactly what git reported. Recorded as a candidate with a citation, not a demonstrated chain: 8 full parallel `--lib` runs did not reproduce it, which says the window is narrow rather than absent. The row names the two controls that would settle it and runs neither --- the structural fix is `file_io`'s, not a CRDT invariant lane's. --- docs/active-work.md | 11 ++++++++ docs/ci-red-signatures.md | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/docs/active-work.md b/docs/active-work.md index 1be6709..082f64d 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -394,6 +394,17 @@ crdt-gated code does not rediscover it at CI. 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**. `src/file_io.rs:434` mutates process-global cwd inside a + parallel test and points it at a `TempDir` that then drops, and + libtest runs tests in threads of one process. Candidate, not a + demonstrated chain — 8 full parallel `--lib` runs did not reproduce + it. Two controls that would settle it are written into the row and + **neither is run here**; the structural fix belongs to whoever owns + `file_io`, not to a CRDT invariant lane. + 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 diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index f9742fb..843fc80 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -1356,6 +1356,59 @@ measurement design nobody has defended. 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`. **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 is worth more than its one occurrence. + +| 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** | **new incident, one occurrence, not reproduced** | +| **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 it is specific.** +`src/file_io.rs:434` — `bare_filename_saves_in_cwd` — calls +`std::env::set_current_dir(dir.path())`, which mutates **process-global** +state, points it at a `TempDir`, and lets that `TempDir` drop at end of +test. The libtest harness runs tests **in parallel threads within one +process**, so during that window every other test in the binary shares +the mutated cwd, and after the drop that cwd is a **deleted directory**. +`fatal: Unable to read current working directory` is exactly what a +process with a deleted cwd gets from `git`. + +The test restores the cwd before its assertions, deliberately and with a +comment saying so — **the hazard is not the restore, it is that the +window exists at all**, and no amount of care inside one test closes a +window that is process-wide. + +**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 every `set_current_dir` test, or removing the + process-global mutation from `bare_filename_saves_in_cwd` entirely + (`save_atomic` could take the directory rather than inheriting it). + +**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 only** — and here it +also says the window is narrow, not that it is absent. + +**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. + ### U13 — gate prune-reporting row receives empty child stdout in `sweep` Recorded during PR #244 review, 2026-08-29, on signed head `756c2b8`. From 4aa3853ebe8d8e4c9598f1294e412068ac2bb81d Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 11:28:11 +0200 Subject: [PATCH 14/23] docs(ci-reds): R6 recurs with fragments, and its control finds U17 on main The macOS lua54 leg redded on PR #246 with `acc28_child_input_and_the_c_c_escape_work_unchanged_in_a_panel`. It is a full three-condition match for R6 --- selector, flavor, and BOTH required fragments (`timed out waiting for` + `/ready`) --- 26 days after the first occurrence. The log was read BEFORE anything was rerun. U3 named that lesson and U8 recorded its fourth violation; this is the first time it was followed on a macOS job at the moment it mattered, and the fragments exist because of it. Rather than argue from an unrelated diff, a merge-base control was dispatched at `aae5b35` --- the first real use of the `workflow_dispatch` key #245 landed, and exactly the case U11 motivated it for. The macOS legs came back GREEN, so the inference the control could have supplied is unavailable. Recorded as a null result, the way R1's row had to record its own. What each outcome would mean was written down before the result was seen. The control was not otherwise clean: `Test (crdt)` failed on `main`, which is U17. It fails the opposite way to R1 and R5 --- not a missed deadline but a predecessor that had already completed --- and the job runs `--test-threads=1`, the condition U9's still-unrun control names. A red on the merge base is invisible to any PR run. --- docs/active-work.md | 18 +++++++ docs/ci-red-signatures.md | 107 +++++++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 082f64d..ba9aa43 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -405,6 +405,24 @@ crdt-gated code does not rediscover it at CI. **neither is run here**; 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 — **the first real use of the + `workflow_dispatch` key #245 landed**, and exactly 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; +- **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, but a predecessor that had + already finished. The job runs `--test-threads=1`, which is the + condition **U9's still-unrun control names**. A red on the merge base + is invisible to any PR run; it took the dispatch to see it. + 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 diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 843fc80..3af49a3 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -239,10 +239,63 @@ 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` | | **job / flavor** | macOS / lua54 | | **required fragments** | `timed out waiting for` **and** `/ready` | -| **causal status** | **UNRESOLVED — no diagnosis** | -| **evidence** | #217 [run 31023651701](https://github.com/levineuwirth/pmacs/actions/runs/31023651701), 2026-08-05 | +| **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. 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. | +**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. **This is that +key's first real use**, and U11 — the row that motivated it — is why it +exists. + +**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 red on +the merge base is not something a PR run can show you; it took a +dispatch on `main` to see it at all. + +**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 recorded that the empty-file predicate lived in a second helper (`wait_for_published_file`) and warned that leaving it would let the @@ -1409,6 +1462,56 @@ 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. **No PR run can show this**; it took a `workflow_dispatch` +on `main` to see it. + +| 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** instead of being cancelled. The +supersede did not arrive late; it arrived after there was nothing left +to supersede. + +**Candidate mechanism, stated as one.** The job runs +**`--test-threads=1`**. A test that dispatches a job and then supersedes +it "in flight" depends on the predecessor still being in flight; with no +other test competing for the runtime, the predecessor is at its +*fastest*, and the window in which it can be superseded is at its +narrowest. That is a reading of the assertion and the flag together — it +is **not** a diagnosis, and nothing here rules out a real supersede +defect. + +**Worth noting for U9.** U9's still-unrun discriminating control is +"pin test-binary concurrency to 1". This job **already does that**, in +CI, on every run. That does not run U9's control — different binary, +different selectors — but it does mean the single-threaded condition is +not hypothetical in this project, and a row now exists where it may be +load-bearing in the opposite direction from every budget row here. + +**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. + ### U13 — gate prune-reporting row receives empty child stdout in `sweep` Recorded during PR #244 review, 2026-08-29, on signed head `756c2b8`. From 088f24e1bba9f795e27447f44bd9f279f03cc596 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 11:36:49 +0200 Subject: [PATCH 15/23] docs(ci-reds): U16's real mechanism is child inheritance; narrow U17 Five corrections, all mine. U16 stopped at "the window exists", which misses why restoring the cwd does not close it. `run_git` calls `run_git_inner(None, ...)`, and that sets `current_dir` only when `cwd` is `Some` (fetcher.rs:329-330), so the spawned git INHERITS the parent's temporary cwd. The parent then restores its own --- which does nothing for a child that already has its working directory --- and the TempDir drops underneath it. The restore is not merely too early; it is irrelevant to the child. U16 also offered a serial guard around `set_current_dir` tests as a structural control. That does not protect an unguarded test that spawns a child, because the child outlives the guard. The options that work are removing the cwd mutation, running that test in a subprocess, or serializing the whole lib-test binary. And U16 said 8 green runs showed the window was narrow. They do not. Non-reproduction establishes intermittence and nothing else; nothing here has sized this candidate's window. U17 claimed no PR run can show its failure. A PR run exercises the same test and could fail identically; what only a main-side run establishes is that it fails ON MAIN, with no observing branch to suspect. And its `got ok` does not prove the supersede arrived late --- it proves the predecessor completed successfully before cancellation took effect, which a timely supersede whose cancellation lost the race produces identically. --- docs/active-work.md | 30 +++++++++------ docs/ci-red-signatures.md | 80 +++++++++++++++++++++++++++------------ 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index ba9aa43..01727a1 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -397,13 +397,18 @@ crdt-gated code does not rediscover it at CI. - **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**. `src/file_io.rs:434` mutates process-global cwd inside a - parallel test and points it at a `TempDir` that then drops, and - libtest runs tests in threads of one process. Candidate, not a - demonstrated chain — 8 full parallel `--lib` runs did not reproduce - it. Two controls that would settle it are written into the row and - **neither is run here**; the structural fix belongs to whoever owns - `file_io`, not to a CRDT invariant lane. + 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. @@ -418,10 +423,13 @@ crdt-gated code does not rediscover it at CI. - **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, but a predecessor that had - already finished. The job runs `--test-threads=1`, which is the - condition **U9's still-unrun control names**. A red on the merge base - is invisible to any PR run; it took the dispatch to see it. + 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`, the condition **U9's still-unrun control + names**. 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 diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 3af49a3..d3c7f3f 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -1427,34 +1427,55 @@ which is why it is worth more than its one occurrence. | **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 it is specific.** -`src/file_io.rs:434` — `bare_filename_saves_in_cwd` — calls -`std::env::set_current_dir(dir.path())`, which mutates **process-global** -state, points it at a `TempDir`, and lets that `TempDir` drop at end of -test. The libtest harness runs tests **in parallel threads within one -process**, so during that window every other test in the binary shares -the mutated cwd, and after the drop that cwd is a **deleted directory**. -`fatal: Unable to read current working directory` is exactly what a -process with a deleted cwd gets from `git`. +**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: -The test restores the cwd before its assertions, deliberately and with a -comment saying so — **the hazard is not the restore, it is that the -window exists at all**, and no amount of care inside one test closes a -window that is process-wide. +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 every `set_current_dir` test, or removing the - process-global mutation from `bare_filename_saves_in_cwd` entirely - (`save_atomic` could take the directory rather than inheriting it). +* 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 only** — and here it -also says the window is narrow, not that it is absent. +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. **Not folded into U14 or U15.** Different selector, different fragments, different step, and a different kind of failure: those are wall-clock @@ -1466,8 +1487,13 @@ that is a different mechanism. 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. **No PR run can show this**; it took a `workflow_dispatch` -on `main` to see it. +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 | |---|---| @@ -1483,9 +1509,15 @@ on `main` to see it. 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** instead of being cancelled. The -supersede did not arrive late; it arrived after there was nothing left -to supersede. +`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.** The job runs **`--test-threads=1`**. A test that dispatches a job and then supersedes From a7c4b3adec72ebfcfe9e17218514d0cdbbfb5ae6 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 15:05:36 +0200 Subject: [PATCH 16/23] docs(ci-reds): U9's control is VOID --- cargo runs test binaries serially U9's "structural difference worth testing next" claimed that `cargo test --workspace` runs many test binaries concurrently while `--lib` runs one, and derived its discriminating control from that: "pin test-binary concurrency to 1". The premise is false. 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 gate logs rather than asserted from the cargo book: `20260831T093655Z-857818/07-sweep.log` alternates `Running` and `test result:` strictly --- 119 to 121 markers, ZERO cases of one binary starting before the previous reported. The pattern is `RTRTRT`. That falsifies a premise two rows rested on, so U12's family paragraph is corrected too. What survives is smaller and still true: a sweep is a long sequence of binaries, so a budget inside it runs at an arbitrary point in a multi-minute step. The family still should not consume review rounds --- but it now needs a control someone has to design. U17 no longer claims `--test-threads=1` exercises U9's control. It is a different knob: it serializes test FUNCTIONS within one executable. Its candidate mechanism is narrowed to match --- removing sibling test functions removes ONE source of contention, which supports neither "fastest" nor "narrowest". R6's block drops two overclaims: a PR run CAN show the identical red (only the main dispatch establishes it on the merge base), and this was not the dispatch key's first use --- #245's D2/D3 dispatched three runs right after it merged. It is the first use for a live merge-base control. --- docs/active-work.md | 33 ++++++++------- docs/ci-red-signatures.md | 85 +++++++++++++++++++++++++-------------- 2 files changed, 74 insertions(+), 44 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 01727a1..274e75a 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -363,9 +363,8 @@ The lint itself is `#[allow]`ed with a reason — collapsing the three 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, alongside U9's still-unrun -discriminating control. Recorded so the next lane touching -crdt-gated code does not rediscover it at CI. +infrastructure and belongs in its own lane. Recorded so the next lane +touching crdt-gated code does not rediscover it at CI. **Four registry rows moved on this lane:** @@ -415,9 +414,10 @@ crdt-gated code does not rediscover it at CI. **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 — **the first real use of the - `workflow_dispatch` key #245 landed**, and exactly the case U11 - motivated it for. It came back **green on the macOS legs**, so the + 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; - **U17, new** — that same control run **redded `Test (crdt)` on `main` @@ -426,10 +426,11 @@ crdt-gated code does not rediscover it at CI. 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`, the condition **U9's still-unrun control - names**. 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. + 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 @@ -630,10 +631,14 @@ from #171 and #215. the full 36-test binary both passed immediately afterwards — intermittence only. This lane changes neither the gate script nor that acceptance binary; diagnostic hardening is a separate lane. -- **Still owed, separately:** `workflow_dispatch` on `ci.yml`, and U9's - discriminating control — pin test-binary concurrency to 1, then load - a lone `--lib` binary — which has been named since 2026-08-09 and - never run. +- **Still owed, separately:** `workflow_dispatch` on `ci.yml` (**landed + as #245**), and U9's discriminating control — named since 2026-08-09, + never run, and now **VOID**: its premise that `cargo test --workspace` + 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`) diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index d3c7f3f..47f2dc5 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -269,9 +269,15 @@ 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. **This is that -key's first real use**, and U11 — the row that motivated it — is why it -exists. +`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: @@ -283,9 +289,10 @@ 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 red on -the merge base is not something a PR run can show you; it took a -dispatch on `main` to see it at all. +`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 @@ -1179,7 +1186,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)** | | **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 | -| **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: `20260831T093655Z-857818/07-sweep.log` alternates `Running` and `test result:` strictly, 119 to 121, with **zero** overlapping starts. `--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 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 | @@ -1208,15 +1215,27 @@ 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 | | **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 -a wall-clock budget asserted **inside a workspace-wide parallel test -run**. `cargo test --workspace` starts many test binaries at once, so -each budget competes with the rest of the sweep in **every** run, -including the ones that pass. A 4.5% overshoot on a 1ms budget is not a -signal about the code. **U9 already named the discriminating control** -— pin test-binary concurrency to 1 and separately load a lone `--lib` -binary — and it remains unrun. Until it runs, this family should not -consume another review round. +**Why this family keeps recurring — with its stated premise CORRECTED, +because it was false.** Every row in it is a wall-clock budget asserted +inside a workspace-wide test run. This paragraph used to add that +"`cargo test --workspace` starts many test binaries at once". **It does +not. Cargo runs test targets SERIALLY, one executable at a time**, and +this project's own gate logs measure it: in +`20260831T093655Z-857818/07-sweep.log`, 119 `Running` markers and 121 +`test result:` lines alternate strictly — **zero** cases of one binary +starting before the previous one reported. The pattern is `RTRTRT…`. + +So the budgets do **not** compete with the rest of the sweep in the way +this family assumed. What is still true is smaller: a sweep is a long +sequence of binaries, so any budget inside it runs at an arbitrary point +in a multi-minute step, on whatever the machine is doing then. 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. @@ -1519,21 +1538,27 @@ 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.** The job runs -**`--test-threads=1`**. A test that dispatches a job and then supersedes -it "in flight" depends on the predecessor still being in flight; with no -other test competing for the runtime, the predecessor is at its -*fastest*, and the window in which it can be superseded is at its -narrowest. That is a reading of the assertion and the flag together — it -is **not** a diagnosis, and nothing here rules out a real supersede -defect. +**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. -**Worth noting for U9.** U9's still-unrun discriminating control is -"pin test-binary concurrency to 1". This job **already does that**, in -CI, on every run. That does not run U9's control — different binary, -different selectors — but it does mean the single-threaded condition is -not hypothetical in this project, and a row now exists where it may be -load-bearing in the opposite direction from every budget row here. +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 From 65b40897bd96687372f50bb774b348b5702976b6 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 16:17:43 +0200 Subject: [PATCH 17/23] docs(ci-reds): fix the serial-run arithmetic, U9's scope, and add U18 Three corrections to yesterday's correction, and one new row. The serial-binary measurement was stated as "119 Running and 121 result lines alternate strictly", which cannot be strict --- the count itself gave it away. Precisely: 119 ordinary targets each report before the next starts, and the two extra result lines belong to `Doc-tests pmacs` and `Doc-tests pmacs_protocol`, which cargo labels differently and runs last. The replacement premise did not describe U9 either. Both U9 selectors live in the ROOT LIB TARGET --- `m6_1_pty_raw_mode_disables_kernel_echo` (src/process.rs:3945) and `composition_overhead_under_ten_percent` (src/editor.rs:9717) --- and the sweep runs that target first, finishing it in about 12 seconds. The sweep's later minutes cannot reach them. What survives: the sweep re-runs the lib target late in the overall gate invocation, under unmeasured machine state. Two stale references to U9's void control are corrected, including the ledger's claim that a synthetic-load run would "either implicate load or clear it". It would not: with concurrency fixed at 1 there is no second arm, so a red shows load is sufficient and a green shows nothing. Non-reproduction never clears anything under this file's own rerun rule. U18 is new and a new class. `Test (ubuntu-latest / luajit)` died in toolchain setup before any cargo command ran: `go install gopls@v0.16.2` hit an HTTP/2 INTERNAL_ERROR from sum.golang.org while verifying x/telemetry. 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. --- docs/active-work.md | 21 +++++++-- docs/ci-red-signatures.md | 90 +++++++++++++++++++++++++++++++++------ 2 files changed, 95 insertions(+), 16 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 274e75a..5c1895c 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -420,6 +420,16 @@ touching crdt-gated code does not rediscover it at CI. 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; +- **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** @@ -1544,9 +1554,14 @@ from #171 and #215. only: no mechanism is claimed, and the standing leaked-daemon confound is uncontrolled as always. - **Cost, stated plainly:** four `--protocol` gate runs on one commit, - three of them lost to these two signatures. U9's synthetic-load - control remains unrun and is the cheapest thing that would either - implicate load or clear it. + three of them lost to these two signatures. **This sentence used to + add that U9's synthetic-load control "would either implicate load or + 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`, `peer_may_send_panel_events`, `send_panel_pointer`, and `SemanticRenderState`). Always the same mechanism: inserting an item diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 47f2dc5..e7762f8 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -1220,16 +1220,32 @@ because it was false.** Every row in it is a wall-clock budget asserted inside a workspace-wide test run. This paragraph used to add that "`cargo test --workspace` starts many test binaries at once". **It does not. Cargo runs test targets SERIALLY, one executable at a time**, and -this project's own gate logs measure it: in -`20260831T093655Z-857818/07-sweep.log`, 119 `Running` markers and 121 -`test result:` lines alternate strictly — **zero** cases of one binary -starting before the previous one reported. The pattern is `RTRTRT…`. +this project's own gate logs measure it. In +`20260831T093655Z-857818/07-sweep.log` (and reproduced on +`20260831T130742Z-2805186`): **119 ordinary targets, each of which +reports its result before the next one starts** — zero cases of one +`Running` line following another. **The 121st and 122nd result lines are +not targets**: they belong to the two doc-test groups, `Doc-tests pmacs` +and `Doc-tests pmacs_protocol`, which cargo labels differently and runs +after everything else. An earlier version of this paragraph called the +whole thing "strictly `RTRTRT…`" with 119 and 121, which cannot be +strict — the count itself gave it away. So the budgets do **not** compete with the rest of the sweep in the way -this family assumed. What is still true is smaller: a sweep is a long -sequence of binaries, so any budget inside it runs at an arbitrary point -in a multi-minute step, on whatever the machine is doing then. A 4.5% -overshoot on a 1ms budget remains not a signal about the code. +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_raw_mode_disables_kernel_echo` (`src/process.rs:3945`) and +`composition_overhead_under_ten_percent` (`src/editor.rs:9717`) — and +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. @@ -1295,11 +1311,19 @@ 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 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 -discriminating control U9 named is STILL UNRUN**: pin test-binary -concurrency to 1, and separately load a lone `--lib` binary. Four -incidents is enough evidence that the family will keep costing review -rounds until someone runs it. +**This family has now produced U6, U9, U10 and U12 — and the control +U9 named no longer exists to run.** It had two halves. *Pin test-binary +concurrency to 1* is **VOID**: cargo already runs targets serially, so +it pins nothing (see U9). *Separately load a lone `--lib` binary under +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 @@ -1569,6 +1593,46 @@ sharing a subject is not sharing a signature. 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: `GOFLAGS=-mod=mod`, `GONOSUMDB`/`GONOSUMCHECK`, a +vendored `gopls`, or simply retrying the step are all options with +different costs, and choosing among them is not this lane's work. + +**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. + ### U13 — gate prune-reporting row receives empty child stdout in `sweep` Recorded during PR #244 review, 2026-08-29, on signed head `756c2b8`. From 7d819902ff4a8bed621b1cb24300d6cea62518b9 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 16:25:11 +0200 Subject: [PATCH 18/23] docs(ci-reds): R7's ninth occurrence The gate verifying the previous commit redded at `gpu` with all three of R7's required fragments, same selector and same line. Other seven stages green; the observing commit is documentation only. It adds a count and nothing else, which is the honest description. The eighth occurrence's method note says the remaining candidates must be varied inside the gate, one per run, and that is not this lane's work. The loadavg reading is recorded as a condition, not a cause --- R7 is not a budget row. --- docs/ci-red-signatures.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index e7762f8..967db73 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -595,12 +595,25 @@ 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` | | **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`) | -| **status** | **EIGHTH OCCURRENCE 2026-08-30 — causal status still UNRESOLVED.** The eighth carries the strongest tree exclusion this row has had, and it supersedes the fifth's: two consecutive gate runs on ONE worktree whose heads differ by a single markdown file, the first all-green and the second red | +| **status** | **NINTH OCCURRENCE 2026-08-31 — causal status still UNRESOLVED.** The eighth carries the strongest tree exclusion this row has had, and it supersedes the fifth's: two consecutive gate runs on ONE worktree whose heads differ by a single markdown file, the first all-green and the second red | | **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 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 | | **retirement** | hardening that removes the named mechanism plus a discriminating witness — or a diagnosis showing the fixture, not the code, closes the pipe | +**Ninth 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 +eighth 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. + **Eighth occurrence — the CRDT identity-undo lane, 2026-08-30, local (Linux), `gpu` step.** All three required fragments present in the durable log From 28c2ad44639c06b78138920d57122b14e2c63043 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 17:26:47 +0200 Subject: [PATCH 19/23] docs(ci-reds): renumber R7 by date, and five corrections R7 carried TWO blocks numbered "fourth" --- D3 on 2026-08-11 and TMPDIR isolation on 2026-08-13 --- so every later ordinal was one low. The new red is R7's TENTH, not its ninth. Renumbered by date, with the duplicate recorded in the status cell rather than silently fixed. The summary cell said "three occurrences" and now states the total, keeping the `attach.rs:1680` fact as the line-specific claim it always was. U9's selector was named wrong in yesterday's correction. The row names the CANONICAL pty test (`src/process.rs:3967`), not `raw_mode_disables_kernel_echo` (`:3945`) --- and U9's own "relation to U2" cell turns on exactly that distinction, so getting it backwards would have undercut the row the paragraph was correcting. The arithmetic was still wrong in two places. There are 121 result lines and the doc-test groups are numbers 120 and 121, not 121 and 122; and U9's table cell still asserted the strict 119-to-121 alternation that the paragraph below it retracts. U18 listed three outage options and all three were wrong. `GONOSUMCHECK` is not a Go environment variable --- that sentence invented it. `GOFLAGS=-mod=mod` selects module update mode and does not bypass checksum-database authentication. And a version-suffixed `go install` ignores vendor directories, so "a vendored gopls" needs a different installation path. No replacement knob is named, because none was verified. The ledger called uncontrolled foreign load "the same evidence U9's synthetic-load control was meant to produce" and said "U9 stays owed", both of which contradict the correction below them. And its lane heading said four registry rows moved while listing eight. --- docs/active-work.md | 27 ++++++++---- docs/ci-red-signatures.md | 89 ++++++++++++++++++++++++++------------- 2 files changed, 78 insertions(+), 38 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 5c1895c..6149293 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -366,12 +366,17 @@ 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. -**Four registry rows moved on this lane:** +**EIGHT registry rows moved on this lane** — R7, R6, U6, U14, U15, U16, +U17 and U18. *(This heading said "four" while listing more; corrected.)* -- **R7's eighth occurrence** — the green/red pair whose heads differ by - one markdown file. It 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; +- **R7's ninth and TENTH occurrences** — the ninth 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 also carried TWO blocks numbered + "fourth"** — D3 on 2026-08-11 and TMPDIR isolation on 2026-08-13 — so + every later ordinal was one low. Renumbered by date, and the row's + summary now states the total rather than "three"; - **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 @@ -1534,9 +1539,15 @@ from #171 and #215. load average **14.02 → 28.35**, from an unrelated `turso` test suite on the same machine (`./verify_task_state.sh turso-without-rowid`, target dir `/opt/target`, one test binary at - **693% CPU**). It is not a controlled experiment, but it is the - same evidence U9's synthetic-load control was meant to produce, and - it points at load. U9 stays owed; its value is now lower. + **693% CPU**). It is not a controlled experiment, and **an earlier + version of this bullet called it "the same evidence U9's + 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 Bash tool caps a command at 10 minutes and SIGTERMs it, which the gate reports as `FAILED (exit 143)` on whatever stage was running — diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 967db73..55c7ac1 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -595,26 +595,26 @@ 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` | | **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`) | -| **status** | **NINTH OCCURRENCE 2026-08-31 — causal status still UNRESOLVED.** The eighth carries the strongest tree exclusion this row has had, and it supersedes the fifth's: two consecutive gate runs on ONE worktree whose heads differ by a single markdown file, the first all-green and the second red | -| **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 | +| **status** | **TENTH OCCURRENCE 2026-08-31 — causal status still UNRESOLVED.** The ninth 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: this row carried TWO blocks numbered "fourth" — D3 on 2026-08-11 and TMPDIR isolation on 2026-08-13 — so every later ordinal was one low until 2026-08-31. Renumbered by date; the count below is the total** | +| **what IS established** | **TEN occurrences**, of which the first three sit at `pmacs-gpu/src/attach.rs:1680` (the line moves as `attach.rs` changes; this row treats a `:LINE` suffix as occurrence-specific), 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 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 | | **retirement** | hardening that removes the named mechanism plus a discriminating witness — or a diagnosis showing the fixture, not the code, closes the pipe | -**Ninth occurrence — the same lane, 2026-08-31, local (Linux), `gpu` +**Tenth 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 -eighth occurrence's method note says the remaining candidates have to be +ninth 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. -**Eighth occurrence — the CRDT identity-undo lane, 2026-08-30, local +**Ninth 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`): @@ -635,7 +635,7 @@ before it, and the reason is the pair, not the diff.** Two consecutive | `20260830T154827Z-2907414` | `db24ae3` | — | **all 8 stages green** | | `20260830T155621Z-3005460` | `96bf2c3` | **one commit, touching one file: `docs/active-work.md`** | **`gpu` and `sweep` FAILED** | -The fifth occurrence excluded the observing tree *relative to `main`* +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 @@ -649,7 +649,7 @@ 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 seventh occurrence's correction, running the + only** — and per the eighth 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 @@ -664,10 +664,10 @@ and nothing else: `283 passed; 1 failed` in each. (`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 - seventh occurrence already falsified "in-gate always fails". + eighth occurrence already falsified "in-gate always fails". **What this changes about the method, stated at the strength it -carries.** The seventh occurrence's narrowing said the remaining +carries.** The eighth 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.** @@ -684,7 +684,7 @@ 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. -**Sixth occurrence — the parse-budget diagnosability lane, 2026-08-29, +**Seventh occurrence — the parse-budget diagnosability lane, 2026-08-29, local (Linux), `gpu` step.** All three required fragments present in the durable log (`pmacs-parse-budget-9c27ecfe/gate-logs/20260829T144541Z-350549/06-gpu.log`): @@ -696,7 +696,7 @@ kind: BrokenPipe, message: "Broken pipe" }))) 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.** The observing lane's entire diff is `src/async_runtime.rs`, `tests/m4_acceptance.rs` and three docs — **no `pmacs-gpu` file, and no file `pmacs-gpu` links against beyond the workspace it always did.** @@ -708,13 +708,13 @@ at `pmacs-gpu/src/attach.rs:1889`, `283 passed; 1 failed`. `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 `gpu` stage's failure is, because it completed and reported. -**A SEVENTH OCCURRENCE FOLLOWED IMMEDIATELY**, on the next gate run of +**AN EIGHTH OCCURRENCE FOLLOWED IMMEDIATELY**, on the next gate run of the same worktree at head `45d438c` (`20260829T150011Z-429115/06-gpu.log`), same selector, same three fragments, `283 passed; 1 failed`. **That run's other seven stages were green**, `sweep` included and complete — 121 result lines, none with a failure — so this pair is not confounded by a truncation the way the -sixth was. +seventh was. **Two consecutive in-gate failures is new for this row**, whose prior five were spread across lanes and months. It prompted a narrowing, and @@ -769,7 +769,7 @@ lanes and trees, and these locate the asymmetry in the *runner* while showing that the obvious way to probe it — reproducing gate conditions outside the gate — cannot work. -**Fifth occurrence — panel cell-mapping generation (§5b) framing, +**Sixth occurrence — panel cell-mapping generation (§5b) framing, 2026-08-15, local (Linux).** The `scripts/gate` **`gpu` step** again, the same flavor as occurrence 2, inside a `--protocol` run (log `20260815T072601Z-2230169`). @@ -790,9 +790,9 @@ the same flavor as occurrence 2, inside a `--protocol` run not exonerate the tree — though here there is no tree change to exonerate. -**What five occurrences now support, stated carefully:** the failure is +**What six occurrences now support, stated carefully:** the failure is **not lane-correlated**. It has appeared under three flavors across -five unrelated lanes, once on a diff that touches no code whatsoever. +six unrelated lanes, once on a diff that touches no code whatsoever. That is evidence about *where the cause is not*, and still says nothing about what it is. **The retirement condition is unchanged.** @@ -831,7 +831,7 @@ one-second deadline. Contention is a plausible mechanism for a lands, **run the control with the added test removed** rather than at the 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 (`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`), all three fragments verified** against the durable gate log @@ -868,7 +868,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 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 mechanism, is not grounds to reverse a fix that closes two observed hazards. @@ -906,7 +906,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 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 --no-fail-fast`, default features — U3's flavor, this time with the fragments captured). All three required fragments verified against the @@ -1199,7 +1199,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)** | | **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 | -| **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: `20260831T093655Z-857818/07-sweep.log` alternates `Running` and `test result:` strictly, 119 to 121, with **zero** overlapping starts. `--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 | +| **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 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 | @@ -1237,12 +1237,17 @@ this project's own gate logs measure it. In `20260831T093655Z-857818/07-sweep.log` (and reproduced on `20260831T130742Z-2805186`): **119 ordinary targets, each of which reports its result before the next one starts** — zero cases of one -`Running` line following another. **The 121st and 122nd result lines are -not targets**: they belong to the two doc-test groups, `Doc-tests pmacs` -and `Doc-tests pmacs_protocol`, which cargo labels differently and runs -after everything else. An earlier version of this paragraph called the -whole thing "strictly `RTRTRT…`" with 119 and 121, which cannot be -strict — the count itself gave it away. +`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. @@ -1250,8 +1255,14 @@ 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_raw_mode_disables_kernel_echo` (`src/process.rs:3945`) and +`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. @@ -1636,9 +1647,27 @@ 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: `GOFLAGS=-mod=mod`, `GONOSUMDB`/`GONOSUMCHECK`, a -vendored `gopls`, or simply retrying the step are all options with -different costs, and choosing among them is not this lane's work. +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. + +**No replacement knob is named, because none was verified.** The one +option above that certainly applies is **retrying the step**; anything +else requires someone to check the current toolchain's actual switches +first. Choosing among them is not this lane's work — but inventing an +environment variable to fill a sentence is the failure this file exists +to catch, so it is recorded as one. **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 From 7ce03018f82f0eb8b119bfebd0824f0289e8347e Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 19:40:57 +0200 Subject: [PATCH 20/23] docs(ci-reds): R7 has TWELVE occurrences --- absorb the two owed ones `docs/active-work.md` recorded two full-fragment R7 occurrences from 2026-08-15 (logs 20260815T095532Z and T100719Z, `attach.rs:1728`) under a heading saying they were "owed to the registry by whichever branch merges second". Both branches merged. Nothing carried them across, and they sat there for sixteen days, so R7's count read two low even after yesterday's renumbering. With those absorbed and the duplicate "fourth" fixed, the sequence is: August 29 = ninth and tenth, August 30 = eleventh, August 31 = twelfth. The parse-budget lane block, which still said sixth and seventh, is updated too. The deferral itself was reasonable --- this file has been bitten by two branches inventing the same row id --- but not discharging it was not. The lesson recorded 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. The summary cell's line-specific claim was also incomplete: occurrences one through FOUR report `attach.rs:1680`, not the first three. And U18 over-corrected. `GONOSUMDB` is a real Go variable --- `go help environment` documents `GOPRIVATE, GONOPROXY, GONOSUMDB` as module prefixes "that should not be compared against the checksum database", which is exactly the step that failed. It is technically applicable; whether the authentication tradeoff is acceptable is a different question. Discarding a real knob while correcting an invented one is its own error and is recorded as one. --- docs/active-work.md | 28 ++++++++------ docs/ci-red-signatures.md | 80 +++++++++++++++++++++++++++++---------- 2 files changed, 76 insertions(+), 32 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 6149293..fc07ad3 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -369,14 +369,16 @@ touching crdt-gated code does not rediscover it at CI. **EIGHT registry rows moved on this lane** — R7, R6, U6, U14, U15, U16, U17 and U18. *(This heading said "four" while listing more; corrected.)* -- **R7's ninth and TENTH occurrences** — the ninth 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 also carried TWO blocks numbered - "fourth"** — D3 on 2026-08-11 and TMPDIR isolation on 2026-08-13 — so - every later ordinal was one low. Renumbered by date, and the row's - summary now states the total rather than "three"; +- **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 @@ -556,7 +558,7 @@ from #171 and #215. 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 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 observation window** so the in-gate/out-of-gate ratio cannot drift with review activity — plus a correction: seventeen green @@ -1359,8 +1361,12 @@ from #171 and #215. branches' entries "merged **without a conflict**, producing duplicate ids across four sites". **The rows below are owed to the registry by whichever branch merges second**, numbered after the other's. - - **R7, two occurrences on this branch** (2026-08-15, `gpu` step, - logs `20260815T095532Z` and `20260815T100719Z`). Fragments verified + - **R7, two occurrences on this branch — ABSORBED 2026-08-31 as the + 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 { code: 32, kind: BrokenPipe, message: "Broken pipe" })))` at `pmacs-gpu/src/attach.rs:1728`. One machine, one day, one branch, diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 55c7ac1..0ddf7d2 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -595,26 +595,26 @@ 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` | | **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`) | -| **status** | **TENTH OCCURRENCE 2026-08-31 — causal status still UNRESOLVED.** The ninth 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: this row carried TWO blocks numbered "fourth" — D3 on 2026-08-11 and TMPDIR isolation on 2026-08-13 — so every later ordinal was one low until 2026-08-31. Renumbered by date; the count below is the total** | -| **what IS established** | **TEN occurrences**, of which the first three sit at `pmacs-gpu/src/attach.rs:1680` (the line moves as `attach.rs` changes; this row treats a `:LINE` suffix as occurrence-specific), 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 | +| **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** | **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 sixth through eighth `:1728`, and the ninth through twelfth `: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** | | **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 | -**Tenth occurrence — the same lane, 2026-08-31, local (Linux), `gpu` +**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 -ninth occurrence's method note says the remaining candidates have to be +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. -**Ninth occurrence — the CRDT identity-undo lane, 2026-08-30, local +**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`): @@ -649,7 +649,7 @@ 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 eighth occurrence's correction, running the + 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 @@ -664,10 +664,10 @@ and nothing else: `283 passed; 1 failed` in each. (`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 - eighth occurrence already falsified "in-gate always fails". + tenth occurrence already falsified "in-gate always fails". **What this changes about the method, stated at the strength it -carries.** The eighth occurrence's narrowing said the remaining +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.** @@ -684,7 +684,7 @@ 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. -**Seventh occurrence — the parse-budget diagnosability lane, 2026-08-29, +**Ninth occurrence — the parse-budget diagnosability lane, 2026-08-29, local (Linux), `gpu` step.** All three required fragments present in the durable log (`pmacs-parse-budget-9c27ecfe/gate-logs/20260829T144541Z-350549/06-gpu.log`): @@ -696,7 +696,7 @@ kind: BrokenPipe, message: "Broken pipe" }))) at `pmacs-gpu/src/attach.rs:1889`, `283 passed; 1 failed`. -* **The tree exclusion is as strong as the sixth'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`, `tests/m4_acceptance.rs` and three docs — **no `pmacs-gpu` file, and no file `pmacs-gpu` links against beyond the workspace it always did.** @@ -708,13 +708,13 @@ at `pmacs-gpu/src/attach.rs:1889`, `283 passed; 1 failed`. `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 `gpu` stage's failure is, because it completed and reported. -**AN EIGHTH 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` (`20260829T150011Z-429115/06-gpu.log`), same selector, same three fragments, `283 passed; 1 failed`. **That run's other seven stages were green**, `sweep` included and complete — 121 result lines, none with a failure — so this pair is not confounded by a truncation the way the -seventh was. +ninth was. **Two consecutive in-gate failures is new for this row**, whose prior five were spread across lanes and months. It prompted a narrowing, and @@ -769,6 +769,30 @@ lanes and trees, and these locate the asymmetry in the *runner* while showing that the obvious way to probe it — reproducing gate conditions outside the gate — cannot work. +**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, the same flavor as occurrence 2, inside a `--protocol` run @@ -790,9 +814,11 @@ the same flavor as occurrence 2, inside a `--protocol` run not exonerate the tree — though here there is no tree change to exonerate. -**What six occurrences now support, stated carefully:** the failure is -**not lane-correlated**. It has appeared under three flavors across -six unrelated lanes, once on a diff that touches no code whatsoever. +**What the first six occurrences supported, stated carefully** — and it +is unchanged by the six that followed: the failure is **not +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 about what it is. **The retirement condition is unchanged.** @@ -1662,12 +1688,24 @@ three were wrong.** They are corrected here rather than deleted: vendor directories**, so pinning that way needs a different installation path entirely. -**No replacement knob is named, because none was verified.** The one -option above that certainly applies is **retrying the step**; anything -else requires someone to check the current toolchain's actual switches -first. Choosing among them is not this lane's work — but inventing an -environment variable to fill a sentence is the failure this file exists -to catch, so it is recorded as one. +**`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 From ea786a2878ffe025176bb6edee8ec7fda8a77ec3 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 19:47:09 +0200 Subject: [PATCH 21/23] docs(ci-reds): U16 reproduced, and U19 is new The gate verifying the R7 renumbering redded twice in `07-sweep`. `cache_survives_across_fetcher_instances` is **U16's second occurrence** --- same selector, same panic site, both required fragments. It is the first time that row has reproduced, and it settles an earlier withdrawal in the right direction: claiming "the window is narrow" from eight green runs was wrong, and the failure came back within the day. The child-inheritance chain stays a candidate; this occurrence demonstrates it no more than the first did. `terminal_bell_baseline_suppresses_history_and_delivers_each_new_bell_once` is new, recorded as U19. It is a deadline but not the budget family's kind: those assert work finishes in 1ms or 200ms, while this asserts an event arrives at all inside FIVE SECONDS. Folding it into that family would blur the one distinction those rows have. Like R1, its assertion has `Instant::now()` in hand at the panic and reports none of it, so the margin is unrecoverable and a future occurrence will not be comparable to this one. That is the second place in this codebase where the same omission costs the same thing. --- docs/active-work.md | 17 ++++++++++++-- docs/ci-red-signatures.md | 47 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index fc07ad3..27afbc9 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -366,8 +366,8 @@ 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. -**EIGHT registry rows moved on this lane** — R7, R6, U6, U14, U15, U16, -U17 and U18. *(This heading said "four" while listing more; corrected.)* +**NINE registry rows moved on this lane** — R7, R6, U6, U14, U15, U16, +U17, U18 and U19. *(This heading said "four" while listing more; corrected.)* - **R7's eleventh and TWELFTH occurrences** — the eleventh is the green/red pair whose heads differ by one markdown file, which excludes @@ -427,6 +427,19 @@ U17 and U18. *(This heading said "four" while listing more; corrected.)* 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. +- **U19, new** — `terminal_bell_baseline_…` timed out on a **5-second** + poll for a bell that never arrived, in the same run as U16's second. + A deadline but **not** the budget family's kind — those assert work + finishes in 1ms or 200ms; this asserts an event arrives at all inside + five seconds. Like R1, **its assertion reports no elapsed value**, so + the margin is unrecoverable and a future occurrence will not be + comparable to it. - **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 diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 0ddf7d2..ae51aa6 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -1515,7 +1515,7 @@ which is why it is worth more than its one occurrence. | **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** | **new incident, one occurrence, not reproduced** | +| **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 | @@ -1570,6 +1570,16 @@ An earlier version added "and here it also says the window is narrow" — 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. + **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 @@ -1713,6 +1723,41 @@ 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 never arrives 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 not the budget family's kind.** U6, U7, U9, U10, +U12 and U15 assert that work finishes *fast* — 1ms, 1.10×, 200ms. This +asserts that an event **arrives at all** inside five seconds, which is +three orders of magnitude of slack. A 4.5% overshoot on a 1ms budget is +a scheduling story; a 5-second wait for a bell that never comes is not +the same shape, and folding it into that family would blur the one +distinction those rows have. + +**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. **A future +occurrence would be comparable to this one only if the assertion carried +its elapsed value**, and it does not. + +**No rerun was performed on this selector**, and the row does not claim +intermittence it has not observed. + ### U13 — gate prune-reporting row receives empty child stdout in `sweep` Recorded during PR #244 review, 2026-08-29, on signed head `756c2b8`. From 03204cc8afb163dcfc91bfc224d649aa95ae6bc5 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 20:17:45 +0200 Subject: [PATCH 22/23] docs(ci-reds): U19 WAS rerun, and three more overstatements withdrawn U19 said "no rerun was performed on this selector". The very next gate run was one: it passed in `03-lib`, `04-lib-crdt` and the exact `07-sweep` context it failed in, at ea786a2. So did U16's selector, after its second occurrence. Both rows now record those passes and classify as intermittent. Writing "no rerun was performed" in the same commit whose gate reran it is the kind of claim this file exists to catch. U19 also overstated three things. The evidence shows no bell was OBSERVED within five seconds --- not that one "never comes", and not that scheduling cannot explain it. "Three orders of magnitude of slack" does not hold against the 200ms budget it cited: 5s is 5000x of 1ms but only 25x of 200ms, so the distinction from the budget family is one of degree. And adding an elapsed value later cannot make a future margin comparable with THIS unmeasured one --- that margin is gone for good; it only makes future failures comparable with each other. R7 kept a sentence reconstructed before the renumbering: its "prior five spread across lanes and months" were eight, and not evenly spread --- three of them fall on one branch on 2026-08-15. The line census skipped occurrence five, whose block never captured a line; it is now marked unrecorded rather than guessed or omitted. U16's introduction still said it was "worth more than its one occurrence" while its status said second. --- docs/active-work.md | 19 ++++++----- docs/ci-red-signatures.md | 66 +++++++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 27afbc9..8a6e37f 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -432,14 +432,19 @@ U17, U18 and U19. *(This heading said "four" while listing more; corrected.)* "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. + first did. **It then passed in all three stages of the next gate + run** — two reds and many greens across two days, intermittent at a + rate nothing has measured. - **U19, new** — `terminal_bell_baseline_…` timed out on a **5-second** - poll for a bell that never arrived, in the same run as U16's second. - A deadline but **not** the budget family's kind — those assert work - finishes in 1ms or 200ms; this asserts an event arrives at all inside - five seconds. Like R1, **its assertion reports no elapsed value**, so - the margin is unrecoverable and a future occurrence will not be - comparable to it. + 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 diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index ae51aa6..2d9ce44 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -596,7 +596,7 @@ Stage 4; the lane touches no `pmacs-gpu` code at all. | **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`) | | **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** | **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 sixth through eighth `:1728`, and the ninth through twelfth `: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 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** | | **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 | @@ -717,8 +717,13 @@ failure — so this pair is not confounded by a truncation the way the ninth was. **Two consecutive in-gate failures is new for this row**, whose prior -five were spread across lanes and months. It prompted a narrowing, and -the narrowing is the useful part. +**eight** were spread across lanes and weeks — **though not evenly**: +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 `20260829T152024Z-563254`, all eight stages, zero failures anywhere). @@ -1505,10 +1510,11 @@ 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`. **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 is worth more than its one occurrence. +`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 | |---|---| @@ -1580,6 +1586,12 @@ 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 +across two days: **intermittent**, at a rate nothing here has +measured. + **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 @@ -1740,23 +1752,37 @@ separately because the selectors and fragments differ. | **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 not the budget family's kind.** U6, U7, U9, U10, -U12 and U15 assert that work finishes *fast* — 1ms, 1.10×, 200ms. This -asserts that an event **arrives at all** inside five seconds, which is -three orders of magnitude of slack. A 4.5% overshoot on a 1ms budget is -a scheduling story; a 5-second wait for a bell that never comes is not -the same shape, and folding it into that family would blur the one -distinction those rows have. +**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 +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. **A future -occurrence would be comparable to this one only if the assertion carried -its elapsed value**, and it does not. +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. -**No rerun was performed on this selector**, and the row does not claim -intermittence it has not observed. +**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` From 093d677c05380392b66c11b4442b24c530aba84c Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Mon, 31 Aug 2026 20:28:58 +0200 Subject: [PATCH 23/23] docs(ci-reds): U19's title repeated the claim its body withdraws The heading said "a terminal bell never arrives within a 5s poll" while the body two paragraphs down withdraws exactly that: the evidence shows the bell was not OBSERVED within five seconds, not that it never came. A title is the part most readers keep, so it was the worse place to leave it. Retitled to match. U16 said its reds and greens were "across two days". Every run the row cites --- both reds and every green --- is 2026-08-31, which the sentences immediately above it already said twice ("again the same day", "returned within the day"). Corrected in both files. --- docs/active-work.md | 4 ++-- docs/ci-red-signatures.md | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 8a6e37f..a48aba3 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -433,8 +433,8 @@ U17, U18 and U19. *(This heading said "four" while listing more; corrected.)* 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 across two days, intermittent at a - rate nothing has measured. + 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 diff --git a/docs/ci-red-signatures.md b/docs/ci-red-signatures.md index 2d9ce44..8546a42 100644 --- a/docs/ci-red-signatures.md +++ b/docs/ci-red-signatures.md @@ -1589,8 +1589,9 @@ 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 -across two days: **intermittent**, at a rate nothing here has -measured. +**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 @@ -1735,7 +1736,7 @@ 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 never arrives within a 5s poll +### 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