docs: revise generated-buffer immutability framing
Close review round four by preserving applied edits through the borrow-free fan-out, making the transaction criteria discriminating, removing the unproven unlock capability, requiring the CRDT divergence fault seam, and aligning Stage 2 with owner-local identity routing. Update the active-work lane to carry revision 5 and its cross-lane facts.
This commit is contained in:
parent
d98d0b3994
commit
238fd043bf
|
|
@ -465,10 +465,10 @@ has **no branch and no framing yet**.
|
||||||
`../pmacs-generated-immutability`. **PR #188**, base `main`, forked from
|
`../pmacs-generated-immutability`. **PR #188**, base `main`, forked from
|
||||||
`githubsucks/main` @ `ad41cf1`, **integrated to `7586905`** (#189,
|
`githubsucks/main` @ `ad41cf1`, **integrated to `7586905`** (#189,
|
||||||
`COHERENCE.md` only; clean merge, no conflict). Framing only —
|
`COHERENCE.md` only; clean merge, no conflict). Framing only —
|
||||||
`docs/generated-buffer-immutability-framing.md`, revision 4, plus this
|
`docs/generated-buffer-immutability-framing.md`, revision 5, plus this
|
||||||
lane. **No runtime code, no protocol change.**
|
lane. **No runtime code, no protocol change.**
|
||||||
- **PROPOSED — three review rounds closed (fifteen findings, nine P1, six
|
- **PROPOSED — four review rounds closed (twenty findings, thirteen P1,
|
||||||
P2). Not approved. Do not implement, do not merge.**
|
seven P2). Not approved. Do not implement, do not merge.**
|
||||||
- **What it frames.** The class-wide half of the `set_generated_contents`
|
- **What it frames.** The class-wide half of the `set_generated_contents`
|
||||||
invariant that `docs/agent-handoff.md` §4 and `COHERENCE.md` §14 both
|
invariant that `docs/agent-handoff.md` §4 and `COHERENCE.md` §14 both
|
||||||
record as unfinished: `Buffer::undo` gates on `ensure_writable()`
|
record as unfinished: `Buffer::undo` gates on `ensure_writable()`
|
||||||
|
|
@ -495,15 +495,19 @@ has **no branch and no framing yet**.
|
||||||
(`:1140-1163`), so it was neither cleaned nor detected; and the
|
(`:1140-1163`), so it was neither cleaned nor detected; and the
|
||||||
unconditional relock **locked a fresh buffer that was never
|
unconditional relock **locked a fresh buffer that was never
|
||||||
successfully written**. `NoOp` clears, `Rejected` restores the entry
|
successfully written**. `NoOp` clears, `Rejected` restores the entry
|
||||||
lock state, `Diverged` clears nothing and surfaces.
|
lock state, `Diverged` clears nothing and surfaces. **Revision 5 keeps
|
||||||
|
the five outcomes but preserves the `Edit` in
|
||||||
|
`AppliedThenFailed { edit, error }`: the borrow-free Lua finisher fans
|
||||||
|
it out to window caches and replica mirrors before returning the
|
||||||
|
error.** Collapsing to `Result` inside `Buffer` was too early.
|
||||||
- **Two stages, two PRs.** Stage 1 — listview ownership fix **plus its
|
- **Two stages, two PRs.** Stage 1 — listview ownership fix **plus its
|
||||||
identity-routing fix in the same PR**, dired and listview adopting the
|
identity-routing fix in the same PR**, dired and listview adopting the
|
||||||
shipped primitive, the window-coordinate clamp, and the fold decision.
|
shipped primitive, the window-coordinate clamp, and the fold decision.
|
||||||
Stage 2 — the new primitive, compile's nine write sites, the search
|
Stage 2 — the new primitive, compile's nine write sites, the search
|
||||||
panel's four, compile/search ownership + routing, the path-backed
|
panel's four, compile/search ownership + routing, the path-backed
|
||||||
refusal plus `mark_clean`, the `identity_protected` field, and
|
refusal plus `mark_clean`, and the terminal-only
|
||||||
the bounded `unlock_generated`.
|
`identity_protected` guard. **No Lua unlock ships.**
|
||||||
- **Six facts from this lane that other lanes need before it merges:**
|
- **Nine facts from this lane that other lanes need before it merges:**
|
||||||
- **`bypass_intercept` is the wrong inventory key.** It misses
|
- **`bypass_intercept` is the wrong inventory key.** It misses
|
||||||
`*buffer-list*`, `*help*` and `*workers*`, which are generated with
|
`*buffer-list*`, `*help*` and `*workers*`, which are generated with
|
||||||
plain writes and no intercept at all. `docs/agent-handoff.md` §4's
|
plain writes and no intercept at all. `docs/agent-handoff.md` §4's
|
||||||
|
|
@ -545,10 +549,11 @@ has **no branch and no framing yet**.
|
||||||
identity buffer.** It does `self.read_only = false` unconditionally
|
identity buffer.** It does `self.read_only = false` unconditionally
|
||||||
(`src/buffer.rs:546`), so it lifts a lock it did not install, writes,
|
(`src/buffer.rs:546`), so it lifts a lock it did not install, writes,
|
||||||
and re-locks. Present on `main`, untested, unframed anywhere before
|
and re-locks. Present on `main`, untested, unframed anywhere before
|
||||||
this revision. Bounded in Stage 2 by the `identity_protected` field —
|
revision 4. Refused in Stage 2 by the `identity_protected` field —
|
||||||
an **intrinsic** flag set once by `TerminalSession::open`, never
|
an **intrinsic** flag marked once by a crate-private monotonic
|
||||||
written by `set_read_only`. Revision 3 tried to infer this from the
|
`mark_identity_protected()` in `TerminalSession::open`, never written
|
||||||
lock's provenance instead; that broke the lift-and-restore idiom at
|
by `set_read_only`. Revision 3 tried to infer this from the lock's
|
||||||
|
provenance instead; that broke the lift-and-restore idiom at
|
||||||
`tests/terminal_copy_mode_acceptance.rs:578-584`, and the general
|
`tests/terminal_copy_mode_acceptance.rs:578-584`, and the general
|
||||||
lesson is that a **derived** fact must be maintained by every
|
lesson is that a **derived** fact must be maintained by every
|
||||||
mutation of what it derives from — and `set_read_only` is `pub`.
|
mutation of what it derives from — and `set_read_only` is `pub`.
|
||||||
|
|
@ -558,6 +563,13 @@ has **no branch and no framing yet**.
|
||||||
compiles it, so a green run of that suite proves nothing about the
|
compiles it, so a green run of that suite proves nothing about the
|
||||||
seam. Any lane touching `read_only` semantics must run it with the
|
seam. Any lane touching `read_only` semantics must run it with the
|
||||||
feature and confirm `acc16e` is in the count.
|
feature and confirm `acc16e` is in the count.
|
||||||
|
- **`identity_protected` is not generated-lock provenance.** Revision
|
||||||
|
4 tried to use “not a terminal identity buffer” as proof that the
|
||||||
|
generated primitive installed the lock; it is not. Revision 5
|
||||||
|
therefore removes `pmacs.buffer.unlock_generated` from the arc
|
||||||
|
entirely. Wdired's future generated→editable transition remains
|
||||||
|
dired Stage 3 work and must be owner-specific or use the eventual
|
||||||
|
lock-policy enum.
|
||||||
- **The CRDT `Replace` mid-transaction divergence is real and
|
- **The CRDT `Replace` mid-transaction divergence is real and
|
||||||
unowned.** `crdt.delete` then `crdt.insert` (`src/buffer.rs:1140-1163`);
|
unowned.** `crdt.delete` then `crdt.insert` (`src/buffer.rs:1140-1163`);
|
||||||
if the first succeeds and the second fails, the code's own comment
|
if the first succeeds and the second fails, the code's own comment
|
||||||
|
|
@ -565,7 +577,11 @@ has **no branch and no framing yet**.
|
||||||
violation." It reaches `apply_edit` and `apply_edit_skip_intercepts`
|
violation." It reaches `apply_edit` and `apply_edit_skip_intercepts`
|
||||||
today and is reported as an ordinary `CrdtRejected`, so nothing
|
today and is reported as an ordinary `CrdtRejected`, so nothing
|
||||||
distinguishes it. This lane names and contains it; **repair is
|
distinguishes it. This lane names and contains it; **repair is
|
||||||
deferred and unowned.**
|
deferred and unowned.** Revision 5 makes the classifier mandatory:
|
||||||
|
a private delete→insert helper is fault-injected under
|
||||||
|
`cargo test --lib --features crdt`; there is no four-variant fallback
|
||||||
|
that maps divergence to `Rejected` and leaves a fresh buffer
|
||||||
|
writable.
|
||||||
- **Overlap warning.** Stage 2 touches `src/lua_bindings/mod.rs`'s buffer
|
- **Overlap warning.** Stage 2 touches `src/lua_bindings/mod.rs`'s buffer
|
||||||
mutator bindings and `src/buffer.rs`. Do not run it concurrently with
|
mutator bindings and `src/buffer.rs`. Do not run it concurrently with
|
||||||
the `apply_resource_op` lane or the bottom-panel 2B work without
|
the `apply_resource_op` lane or the bottom-panel 2B work without
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,41 @@
|
||||||
**PROPOSED — needs explicit user approval before implementation. DO NOT
|
**PROPOSED — needs explicit user approval before implementation. DO NOT
|
||||||
implement, DO NOT merge.**
|
implement, DO NOT merge.**
|
||||||
|
|
||||||
**Revision 4 — scouted against canonical `githubsucks/main` @ `7586905`,
|
**Revision 5 — scouted against canonical `githubsucks/main` @ `7586905`,
|
||||||
2026-07-28.** Every count in this document was re-measured at this
|
2026-07-28.** Every count in this document was re-measured at this
|
||||||
revision; the command output is in the revision-4 block below.
|
base in revision 4; revision 5 changes only the framed design and
|
||||||
|
acceptance, not a counted tree surface.
|
||||||
|
|
||||||
|
## Revision 5
|
||||||
|
|
||||||
|
**Answers review round 4 on `d98d0b3` — four P1, one P2. All five
|
||||||
|
findings are confirmed. This revision removes one proposed public
|
||||||
|
capability, makes one transaction outcome larger, and makes the CRDT
|
||||||
|
fault seam mandatory rather than leaving a fallback for implementation
|
||||||
|
time.**
|
||||||
|
|
||||||
|
| finding | the decision |
|
||||||
|
|---|---|
|
||||||
|
| **P1-1** — `AppliedThenFailed` loses the `Edit`, so the binding cannot fan out a mutation before returning `Err` | Confirmed. `AppliedThenFailed` becomes `{ edit: Edit, error: BufferError }`; `apply_generated_edit` returns the outcome rather than collapsing it to `Result` inside `Buffer`. After the registry borrow drops, the binding fans out `Applied`, `NoOp`, **and `AppliedThenFailed`**. The last arm then surfaces its error. That preserves the window-cache and replica-mirror invariant even when a `View::on_edit` failure occurs after the rope swap. The Rust return type of `set_generated_contents` changes with the internal transaction; its Lua name and call signature do not. New criterion 15a pins both the window and CRDT directions. |
|
||||||
|
| **P1-2** — criterion 15 probes the edit flag while `read_only` masks it | Confirmed. `begin_edit` calls `ensure_writable` before it checks `editing_in_progress`, while `AppliedThenFailed` deliberately relocks. The old follow-up therefore returned `ReadOnly` for both the correct and broken implementations. Criterion 15 now performs the same Rust-side lift already used by criteria 4, 16b and 17 before issuing the ordinary edit; only then can its outcome distinguish the cleared flag from `ConcurrentEdit`. |
|
||||||
|
| **P1-3** — the four-variant fallback maps divergence to `Rejected` and restores a fresh buffer to writable | Confirmed. The fallback is withdrawn. A CRDT delete-success/insert-failure must be `Diverged`, must leave the buffer locked, and must surface distinctly. Criterion 16c is reclassified as a `crdt`-only fault-injection unit test: Stage 2 extracts a private delete→insert classifier whose production closures call loro and whose test closures force delete `Ok` / insert `Err`. There is no implementation-time choice to weaken containment because staging is inconvenient. |
|
||||||
|
| **P1-4** — `identity_protected` is not generated-lock provenance | Confirmed. Q#GB7 now chooses the fallback it previously named: **this arc ships no `unlock_generated` at all.** `identity_protected` remains, but only as a monotonic terminal-identity guard in the write direction; it is never described as proof that the generated primitive installed a lock. The proposed setter becomes a crate-private, one-way `mark_identity_protected()` so “set once” is enforced rather than documentary. A future wdired door stays with dired Stage 3 and must frame the provenance/ownership transition it actually needs. |
|
||||||
|
| **P2-5** — Stage 2 still instructs implementation to build revision 3's rejected shared registry | Confirmed. The staging checklist now matches Q#GB18 exactly: `compile.lua` answers from `slot_for_buffer`, `default.lua` answers from `search_panel_owns`, and each capture site consults the other through a guarded optional predicate. No owner registers buffers with the other, and no teardown work is introduced. |
|
||||||
|
|
||||||
|
**Reachability recheck for the changed criteria.** Criterion 15 now
|
||||||
|
crosses the post-failure lock only by a Rust-side lift, so the next edit
|
||||||
|
can actually reach the flag it asserts. Criterion 15a drives the same
|
||||||
|
valid `FailingView` mutation through the production Lua finisher and
|
||||||
|
observes its window and replica consumers. Criterion 16c drives the
|
||||||
|
otherwise-unconstructible second-op failure through the private
|
||||||
|
delete→insert classifier. Criterion 13 is deliberately structural
|
||||||
|
because revision 5's decision is the absence of a capability. None
|
||||||
|
passes merely by failing before the mechanism under test.
|
||||||
|
|
||||||
|
Revision 4's measurements and its answers to review round 3 remain
|
||||||
|
below as history. Where its live design said “bounded unlock” or offered
|
||||||
|
the four-variant CRDT fallback, revision 5 supersedes it explicitly
|
||||||
|
rather than silently smoothing the reversal over.
|
||||||
|
|
||||||
## Revision 4
|
## Revision 4
|
||||||
|
|
||||||
|
|
@ -245,10 +277,11 @@ Two things the arc turns out NOT to be, both discovered by measurement:
|
||||||
says it prevents. Q#GB18.
|
says it prevents. Q#GB18.
|
||||||
- **Added in revision 3:** `read_only` is **not** this arc's flag. It
|
- **Added in revision 3:** `read_only` is **not** this arc's flag. It
|
||||||
carries three unrelated authorities (§2.11), so a capability defined
|
carries three unrelated authorities (§2.11), so a capability defined
|
||||||
over it reaches all three — which is why the unlock needed provenance
|
over it reaches all three — which is why revision 5 removes the unlock
|
||||||
(Q#GB15), why the lock silently disables fold creation (Q#GB16), and
|
rather than pretending `identity_protected` is provenance (Q#GB7),
|
||||||
why the *shipped* primitive can already overwrite a live terminal's
|
why the lock silently disables fold creation (Q#GB16), and why the
|
||||||
identity buffer.
|
*shipped* primitive can already overwrite a live terminal's identity
|
||||||
|
buffer (Q#GB15).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -947,7 +980,7 @@ are the two consequences.
|
||||||
|
|
||||||
### 3.1 Recommendation
|
### 3.1 Recommendation
|
||||||
|
|
||||||
**`Buffer::apply_generated_edit(op: EditOp) -> Result<Edit, BufferError>`
|
**`Buffer::apply_generated_edit(op: EditOp) -> GeneratedOutcome`
|
||||||
— one authorized op at a time — exposed to Lua as a new option key on
|
— one authorized op at a time — exposed to Lua as a new option key on
|
||||||
the mutators that already exist:**
|
the mutators that already exist:**
|
||||||
|
|
||||||
|
|
@ -960,15 +993,23 @@ buf:replace(s, e, text, { generated = true })
|
||||||
Semantics, per call, entirely inside one `with_registry_mut` and one
|
Semantics, per call, entirely inside one `with_registry_mut` and one
|
||||||
`&mut Buffer` method — the exact ordering, including every error path, is
|
`&mut Buffer` method — the exact ordering, including every error path, is
|
||||||
§3.4, which revision 3 adds because review P1-1 showed revision 2 had no
|
§3.4, which revision 3 adds because review P1-1 showed revision 2 had no
|
||||||
workable one. The binding then fans the `Edit` out through the
|
workable one. The binding then fans every outcome that carries an
|
||||||
`notify_buffer_edit_to_windows` call it **already makes**
|
`Edit` out through `notify_buffer_edit_to_windows` after the borrow has
|
||||||
(`src/lua_bindings/mod.rs:1291`, `:1302`, `:1322`), after the borrow has
|
dropped: `Applied` and `NoOp` before returning success, and
|
||||||
dropped.
|
`AppliedThenFailed` **before returning its error**. Revision 5 makes that
|
||||||
|
third arm explicit because collapsing the outcome to `Result` inside
|
||||||
|
`Buffer` discarded the only value capable of updating window caches and
|
||||||
|
replica mirrors.
|
||||||
|
|
||||||
`Buffer::set_generated_contents(bytes)` is reimplemented as
|
`Buffer::set_generated_contents(bytes)` is reimplemented as
|
||||||
`apply_generated_edit(Replace { range: 0..len, bytes })`. **It keeps its
|
`apply_generated_edit(Replace { range: 0..len, bytes })`. **Its Lua name
|
||||||
name, its signature, its doc comment and its tests** — it becomes the
|
and call signature stay fixed; its Rust return changes from
|
||||||
whole-buffer spelling of one primitive rather than a second primitive.
|
`Result<Edit, BufferError>` to `GeneratedOutcome`**, and its direct Rust
|
||||||
|
tests match on the outcome. The change is required: a wrapper that
|
||||||
|
collapses `AppliedThenFailed` to `Err` before the binding sees it loses
|
||||||
|
the applied `Edit` and cannot satisfy the fan-out contract. It becomes
|
||||||
|
the whole-buffer spelling of one primitive rather than a second
|
||||||
|
primitive.
|
||||||
|
|
||||||
**One sentence for why it wins: it is the only candidate in which the
|
**One sentence for why it wins: it is the only candidate in which the
|
||||||
buffer is never observably unlocked, because the lift and the re-assert
|
buffer is never observably unlocked, because the lift and the re-assert
|
||||||
|
|
@ -1036,12 +1077,16 @@ in Q#GB8's deferral, not adopted here.
|
||||||
|
|
||||||
### 3.3 The four questions the recommendation must answer
|
### 3.3 The four questions the recommendation must answer
|
||||||
|
|
||||||
**How many `Edit`s are fanned out, and when?** One per generated op,
|
**How many `Edit`s are fanned out, and when?** One per generated op that
|
||||||
immediately, by the binding that already does it. `compile.lua`'s
|
reaches an `Edit`, immediately, by the binding. That includes
|
||||||
|
`AppliedThenFailed`: the rope and CRDT have already changed, so the
|
||||||
|
binding fans its carried `Edit` out and only then returns the carried
|
||||||
|
error. `compile.lua`'s
|
||||||
`emit_text` fast path emits one insert for a whole output batch, so a
|
`emit_text` fast path emits one insert for a whole output batch, so a
|
||||||
typical `feed_bytes` produces one to three ops; a CR-heavy progress bar
|
typical `feed_bytes` produces one to three ops; a CR-heavy progress bar
|
||||||
produces more. This is exactly today's fan-out count — the conversion
|
produces more. Successful calls retain today's fan-out cardinality; the
|
||||||
changes authority, not cardinality.
|
new failure arm adds the notification that today's early `Err` loses,
|
||||||
|
because that error can follow a real mutation.
|
||||||
|
|
||||||
**Per-op or per-scope history clearing?** Per op, and it is cheap by
|
**Per-op or per-scope history clearing?** Per op, and it is cheap by
|
||||||
construction: because `read_only` is re-asserted immediately, **at most
|
construction: because `read_only` is re-asserted immediately, **at most
|
||||||
|
|
@ -1056,19 +1101,46 @@ against the existing compile-mode timings in both configurations. If it
|
||||||
does, the escape hatch is to suppress recording rather than clear it —
|
does, the escape hatch is to suppress recording rather than clear it —
|
||||||
recorded as a named deferral rather than designed speculatively.
|
recorded as a named deferral rather than designed speculatively.
|
||||||
|
|
||||||
**CRDT-mode behaviour?** Identical to `set_generated_contents` today. The
|
**CRDT-mode behaviour?** Identical to `set_generated_contents` today on
|
||||||
`Edit` carries `crdt_op` when the buffer is CRDT-backed, and
|
success, and corrected on the post-apply error path. The `Edit` carries
|
||||||
|
`crdt_op` when the buffer is CRDT-backed, and
|
||||||
`notify_buffer_edit_to_windows` queues it via
|
`notify_buffer_edit_to_windows` queues it via
|
||||||
`queue_daemon_origin_crdt_op` (`src/lua_bindings/mod.rs:1582`) so replica
|
`queue_daemon_origin_crdt_op` (`src/lua_bindings/mod.rs:1582`) so replica
|
||||||
mirrors import the owner's write. History clearing goes to loro's
|
mirrors import the owner's write. `AppliedThenFailed` must carry that
|
||||||
`UndoManager`. Nothing new.
|
same `Edit`; returning its error before notification would leave the
|
||||||
|
authoritative CRDT changed while every replica missed the op. History
|
||||||
|
clearing goes to loro's `UndoManager`.
|
||||||
|
|
||||||
**How do the returned edits reach the fan-out without a live registry
|
**How do the returned edits reach the fan-out without a live registry
|
||||||
borrow?** By construction, unchanged since #178: `run_bypass_edit`
|
borrow?** By construction, extending #178's shape: `run_bypass_edit`
|
||||||
(`src/lua_bindings/mod.rs:1445`) closes its `with_registry_mut` before
|
(`src/lua_bindings/mod.rs:1445`) closes its `with_registry_mut` before
|
||||||
returning, and the mutator bindings call
|
returning, and the mutator bindings call
|
||||||
`notify_buffer_edit_to_windows` afterwards. `run_generated_edit` (§3.4)
|
`notify_buffer_edit_to_windows` afterwards. `run_generated_edit` (§3.4)
|
||||||
occupies the same position and closes its borrow the same way.
|
occupies the same position and closes its borrow the same way, but
|
||||||
|
returns the whole `GeneratedOutcome`. A binding-level finisher handles
|
||||||
|
it:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
match run_generated_edit(lua, id, op) {
|
||||||
|
Applied(edit) | NoOp(edit) => {
|
||||||
|
notify_buffer_edit_to_windows(lua, id, &edit);
|
||||||
|
Ok(edit)
|
||||||
|
}
|
||||||
|
AppliedThenFailed { edit, error } => {
|
||||||
|
notify_buffer_edit_to_windows(lua, id, &edit);
|
||||||
|
Err(error.into())
|
||||||
|
}
|
||||||
|
Rejected(error) => Err(error.into()),
|
||||||
|
Diverged(error) => {
|
||||||
|
surface_divergence(lua, &error);
|
||||||
|
Err(error.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper is shared by the three mutators and the
|
||||||
|
`set_generated_contents` binding so one of the four cannot forget the
|
||||||
|
failure fan-out.
|
||||||
|
|
||||||
### 3.4 The transaction, and why revision 2 had none (review P1-1)
|
### 3.4 The transaction, and why revision 2 had none (review P1-1)
|
||||||
|
|
||||||
|
|
@ -1159,7 +1231,7 @@ for the v0.1 undo stack and does not extend past it.** Three failures:
|
||||||
the no-op arm (`src/buffer.rs:1245-1253`), returns `Ok`, never bumps
|
the no-op arm (`src/buffer.rs:1245-1253`), returns `Ok`, never bumps
|
||||||
`revision`, so revision 3 skipped **both** the clear and
|
`revision`, so revision 3 skipped **both** the clear and
|
||||||
`mark_clean`: the buffer ends locked, **modified**, and carrying
|
`mark_clean`: the buffer ends locked, **modified**, and carrying
|
||||||
poppable history that `unlock_generated` re-exposes. Shipped
|
poppable history that a later Rust-side lift can re-expose. Shipped
|
||||||
`set_generated_contents` clears unconditionally (`:551-553`), so
|
`set_generated_contents` clears unconditionally (`:551-553`), so
|
||||||
revision 3's predicate was a **regression of an existing contract**,
|
revision 3's predicate was a **regression of an existing contract**,
|
||||||
not a refinement of one. And it lands on a path this document
|
not a refinement of one. And it lands on a path this document
|
||||||
|
|
@ -1192,34 +1264,53 @@ signature change and not a free one — see the cost note below.
|
||||||
```rust
|
```rust
|
||||||
/// What a generated write did, reported by the apply rather than
|
/// What a generated write did, reported by the apply rather than
|
||||||
/// inferred by its caller. The variant, not `revision`, selects cleanup.
|
/// inferred by its caller. The variant, not `revision`, selects cleanup.
|
||||||
enum GeneratedOutcome {
|
pub enum GeneratedOutcome {
|
||||||
/// The rope changed and an undo entry exists.
|
/// The rope changed and an undo entry exists.
|
||||||
Applied(Edit),
|
Applied(Edit),
|
||||||
/// A semantic no-op: rope unchanged, no undo entry pushed. The call
|
/// A semantic no-op: rope unchanged, no undo entry pushed. The call
|
||||||
/// SUCCEEDED, so the buffer is now a generated buffer.
|
/// SUCCEEDED, so the buffer is now a generated buffer.
|
||||||
NoOp(Edit),
|
NoOp(Edit),
|
||||||
/// Refused before anything mutated: rope, CRDT and history are all
|
/// Failed with rope, CRDT and history exactly as they were. This also
|
||||||
/// exactly as they were.
|
/// includes a deliberate no-op whose `on_edit` notification failed:
|
||||||
|
/// view side effects may have run, but no buffer/CRDT mutation exists
|
||||||
|
/// to fan out.
|
||||||
Rejected(BufferError),
|
Rejected(BufferError),
|
||||||
/// The rope changed and a later stage failed. An undo entry exists
|
/// The rope changed and a later stage failed. The Edit must survive so
|
||||||
/// over contents no caller can see.
|
/// windows and replica mirrors observe the mutation before the error.
|
||||||
AppliedThenFailed(BufferError),
|
AppliedThenFailed { edit: Edit, error: BufferError },
|
||||||
/// CRDT only: the CRDT was partially mutated and the rope was not.
|
/// CRDT only: the CRDT was partially mutated and the rope was not.
|
||||||
/// `rope ≡ CRDT projection` no longer holds.
|
/// `rope ≡ CRDT projection` no longer holds.
|
||||||
Diverged(BufferError),
|
Diverged(BufferError),
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`run_rope_edit_and_broadcast` returns this richer outcome.
|
||||||
|
`apply_edit` and `apply_edit_skip_intercepts` map it back to their
|
||||||
|
existing `Result<Edit, BufferError>` API, preserving ordinary callers'
|
||||||
|
surface; the generated path retains it through cleanup and the
|
||||||
|
borrow-free binding finisher. The enum is public because the existing
|
||||||
|
public `set_generated_contents` method and the new
|
||||||
|
`apply_generated_edit` method return it; their Rust docs state that a
|
||||||
|
higher layer that owns window or replica state must fan out every
|
||||||
|
edit-bearing variant before handling its success/error.
|
||||||
|
|
||||||
|
That mapping deliberately does **not** claim to repair the same
|
||||||
|
post-apply notification loss for every ordinary Rust edit API. Their
|
||||||
|
public `Result<Edit, BufferError>` surface cannot carry both the edit and
|
||||||
|
the later view error, and changing all of those callers is broader than
|
||||||
|
generated-buffer immutability. The pre-existing ordinary path is named
|
||||||
|
in §8 rather than hidden by the helper refactor.
|
||||||
|
|
||||||
**The cleanup each variant triggers.** `entry` is the `read_only` value
|
**The cleanup each variant triggers.** `entry` is the `read_only` value
|
||||||
observed on entry.
|
observed on entry.
|
||||||
|
|
||||||
| outcome | history | `mark_clean` | `read_only` after | returns |
|
| outcome | history | `mark_clean` | `read_only` after | binding action |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `Applied` | **cleared** | yes | `true` | `Ok(Edit)` |
|
| `Applied` | **cleared** | yes | `true` | fan out `Edit`, then `Ok` |
|
||||||
| `NoOp` | **cleared** | yes | `true` | `Ok(Edit)` |
|
| `NoOp` | **cleared** | yes | `true` | fan out `Edit`, then `Ok` |
|
||||||
| `Rejected` | untouched | no | **restored to `entry`** | `Err` |
|
| `Rejected` | untouched | no | **restored to `entry`** | `Err`, no fan-out |
|
||||||
| `AppliedThenFailed` | **cleared** | **no** | `true` | `Err` |
|
| `AppliedThenFailed` | **cleared** | **no** | `true` | **fan out carried `Edit`, then `Err`** |
|
||||||
| `Diverged` | **untouched** | no | `true` | `Err`, and it must **surface** |
|
| `Diverged` | **untouched** | no | `true` | distinct surfaced `Err`, no rope `Edit` |
|
||||||
|
|
||||||
`editing_in_progress` is cleared on **all five**, unconditionally.
|
`editing_in_progress` is cleared on **all five**, unconditionally.
|
||||||
|
|
||||||
|
|
@ -1247,9 +1338,11 @@ that **this arc cannot fix it**:
|
||||||
- The rope is intact and the CRDT is not. Nothing local reconstructs the
|
- The rope is intact and the CRDT is not. Nothing local reconstructs the
|
||||||
deleted range — loro exposes no rollback at this seam, and the
|
deleted range — loro exposes no rollback at this seam, and the
|
||||||
`export_updates_since` that would name the delta runs after both ops.
|
`export_updates_since` that would name the delta runs after both ops.
|
||||||
- **Clearing history would destroy the last local record of the
|
- The rope still contains the pre-edit bytes, but the CRDT UndoManager is
|
||||||
pre-edit rope**, which is the only material anything could later
|
the only existing **CRDT-native inverse record** for the successful
|
||||||
reconcile from. So `Diverged` clears nothing.
|
delete. Clearing it would discard the one operation a later repair
|
||||||
|
lane might use to reconcile the document. So `Diverged` clears
|
||||||
|
nothing; this arc does not invoke that undo automatically.
|
||||||
- Locking is the strongest available *containment*: it stops further ops
|
- Locking is the strongest available *containment*: it stops further ops
|
||||||
compounding a divergence that already exists. So `read_only = true`,
|
compounding a divergence that already exists. So `read_only = true`,
|
||||||
and this is the one place where locking a buffer on an error path is
|
and this is the one place where locking a buffer on an error path is
|
||||||
|
|
@ -1270,18 +1363,24 @@ lane.** Splitting a CRDT `Replace` into a single transactional op, or
|
||||||
reconciling the two, is loro-level work with no bearing on generated
|
reconciling the two, is loro-level work with no bearing on generated
|
||||||
buffers specifically.
|
buffers specifically.
|
||||||
|
|
||||||
**The ordering, with every exit path named.**
|
**The ordering, with every exit path named.** The method returns the
|
||||||
|
outcome intact; only the borrow-free binding finisher above converts it
|
||||||
|
to Lua success/error after performing any required fan-out.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub fn apply_generated_edit(&mut self, op: EditOp<'_>) -> Result<Edit, BufferError> {
|
pub fn apply_generated_edit(&mut self, op: EditOp<'_>) -> GeneratedOutcome {
|
||||||
// (1) Q#GB10: path-backed refusal. Before any state change.
|
// (1) Q#GB10: path-backed refusal. Before any state change.
|
||||||
if self.file_path.is_some() { return Err(GeneratedWriteOnFileBuffer { .. }); }
|
if self.file_path.is_some() {
|
||||||
|
return Rejected(GeneratedWriteOnFileBuffer { .. });
|
||||||
|
}
|
||||||
// (2) Q#GB15: this buffer's read_only is an identity protection.
|
// (2) Q#GB15: this buffer's read_only is an identity protection.
|
||||||
if self.identity_protected { return Err(ReadOnly { .. }); }
|
if self.identity_protected { return Rejected(ReadOnly { .. }); }
|
||||||
// (3) re-entrancy gate — begin_edit's SECOND check, not its first.
|
// (3) re-entrancy gate — begin_edit's SECOND check, not its first.
|
||||||
if self.editing_in_progress { return Err(ConcurrentEdit { .. }); }
|
if self.editing_in_progress { return Rejected(ConcurrentEdit { .. }); }
|
||||||
// (4) bounds pre-validation, so an invalid range costs nothing.
|
// (4) bounds pre-validation, so an invalid range costs nothing.
|
||||||
self.validate_op_bounds(&op)?;
|
if let Err(error) = self.validate_op_bounds(&op) {
|
||||||
|
return Rejected(error);
|
||||||
|
}
|
||||||
|
|
||||||
let entry_read_only = self.read_only;
|
let entry_read_only = self.read_only;
|
||||||
self.editing_in_progress = true;
|
self.editing_in_progress = true;
|
||||||
|
|
@ -1293,12 +1392,15 @@ pub fn apply_generated_edit(&mut self, op: EditOp<'_>) -> Result<Edit, BufferErr
|
||||||
self.clear_history();
|
self.clear_history();
|
||||||
self.mark_clean();
|
self.mark_clean();
|
||||||
}
|
}
|
||||||
AppliedThenFailed(_) => { self.read_only = true; self.clear_history(); }
|
AppliedThenFailed { .. } => {
|
||||||
|
self.read_only = true;
|
||||||
|
self.clear_history();
|
||||||
|
}
|
||||||
Diverged(_) => { self.read_only = true; }
|
Diverged(_) => { self.read_only = true; }
|
||||||
Rejected(_) => { self.read_only = entry_read_only; }
|
Rejected(_) => { self.read_only = entry_read_only; }
|
||||||
}
|
}
|
||||||
self.editing_in_progress = false; // (6) unconditional, all paths
|
self.editing_in_progress = false; // (6) unconditional, all paths
|
||||||
outcome.into_result()
|
outcome // binding owns fan-out + conversion
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -1309,7 +1411,7 @@ pub fn apply_generated_edit(&mut self, op: EditOp<'_>) -> Result<Edit, BufferErr
|
||||||
| (3) | re-entrant on the same buffer | unchanged | unchanged (`true`, the outer edit's) | **untouched** | untouched |
|
| (3) | re-entrant on the same buffer | unchanged | unchanged (`true`, the outer edit's) | **untouched** | untouched |
|
||||||
| (4) | range out of bounds | unchanged | unchanged (`false`) | **untouched** | untouched |
|
| (4) | range out of bounds | unchanged | unchanged (`false`) | **untouched** | untouched |
|
||||||
| `Rejected` | mid-codepoint position, CRDT | **restored to entry** | `false` | **untouched** | untouched |
|
| `Rejected` | mid-codepoint position, CRDT | **restored to entry** | `false` | **untouched** | untouched |
|
||||||
| `AppliedThenFailed` | a view rejected `on_edit` | `true` | `false` | **cleared** | **mutated** |
|
| `AppliedThenFailed` | a view rejected `on_edit` | `true` | `false` | **cleared** | **mutated; carried `Edit` must fan out** |
|
||||||
| `Diverged` | CRDT delete ok, insert failed | `true` | `false` | **untouched** | rope untouched, **CRDT diverged** |
|
| `Diverged` | CRDT delete ok, insert failed | `true` | `false` | **untouched** | rope untouched, **CRDT diverged** |
|
||||||
| `NoOp` | empty write over an empty rope | `true` | `false` | **cleared** | unchanged |
|
| `NoOp` | empty write over an empty rope | `true` | `false` | **cleared** | unchanged |
|
||||||
| `Applied` | the ordinary case | `true` | `false` | **cleared** | replaced |
|
| `Applied` | the ordinary case | `true` | `false` | **cleared** | replaced |
|
||||||
|
|
@ -1342,11 +1444,19 @@ them means that function tracking whether its `delete` succeeded before
|
||||||
its `insert` failed. That is a real change to a shipped CRDT path, it is
|
its `insert` failed. That is a real change to a shipped CRDT path, it is
|
||||||
in Stage 2's scope, and it is the reason `Diverged` is a *named* variant
|
in Stage 2's scope, and it is the reason `Diverged` is a *named* variant
|
||||||
rather than a note — a variant nothing can construct is not a design.
|
rather than a note — a variant nothing can construct is not a design.
|
||||||
**If the user prefers a smaller Stage 2**, the fallback is to ship four
|
|
||||||
variants, fold `Diverged` into `Rejected`, and accept that a
|
**Revision 5 withdraws revision 4's four-variant fallback.** Folding
|
||||||
CRDT-mid-transaction failure is indistinguishable from a clean refusal —
|
`Diverged` into `Rejected` would apply `Rejected`'s cleanup: on a fresh
|
||||||
which is the status quo, and should be a stated trade rather than a
|
writable buffer it restores `entry_read_only = false`, leaving a buffer
|
||||||
silent one.
|
whose CRDT and rope already disagree open to further writes. That
|
||||||
|
directly contradicts the containment argument above. Stage 2 therefore
|
||||||
|
extracts a private delete→insert classifier that accepts the two
|
||||||
|
operations as closures: production supplies the loro calls; a
|
||||||
|
`#[cfg(feature = "crdt")]` unit test supplies delete `Ok` followed by
|
||||||
|
insert `Err`. The test makes `Diverged` constructible without adding a
|
||||||
|
public fault-injection API, and `cargo test --lib --features crdt` is its
|
||||||
|
gate. If that extraction proves larger than expected, Stage 2 stops for
|
||||||
|
review; it does not silently weaken the approved cleanup table.
|
||||||
|
|
||||||
`validate_op_bounds` is a new
|
`validate_op_bounds` is a new
|
||||||
private helper duplicating the bounds arithmetic `Rope::insert` /
|
private helper duplicating the bounds arithmetic `Rope::insert` /
|
||||||
|
|
@ -1368,7 +1478,9 @@ numbering is chronological; the placement is topical.*
|
||||||
**Q#GB1 — The streaming primitive.** `Buffer::apply_generated_edit(op)`,
|
**Q#GB1 — The streaming primitive.** `Buffer::apply_generated_edit(op)`,
|
||||||
exposed as `{ generated = true }` on the three Lua mutators.
|
exposed as `{ generated = true }` on the three Lua mutators.
|
||||||
`set_generated_contents` becomes its whole-buffer wrapper, keeping name,
|
`set_generated_contents` becomes its whole-buffer wrapper, keeping name,
|
||||||
signature and tests. Rationale and rejected alternatives: §3.
|
**Lua** signature and behavioral tests; its Rust return becomes
|
||||||
|
`GeneratedOutcome` so a failed-but-applied edit survives to the fan-out.
|
||||||
|
Rationale and rejected alternatives: §3.
|
||||||
|
|
||||||
**Q#GB2 — `generated` is additive; `bypass_intercept` stays.** Seven
|
**Q#GB2 — `generated` is additive; `bypass_intercept` stays.** Seven
|
||||||
call sites outside `builtin/` depend on `bypass_intercept`, including
|
call sites outside `builtin/` depend on `bypass_intercept`, including
|
||||||
|
|
@ -1386,20 +1498,35 @@ Revision 2 said "a generated write goes through `run_buffer_edit`'s
|
||||||
bypass arm". **That is unimplementable** — the bypass arm is
|
bypass arm". **That is unimplementable** — the bypass arm is
|
||||||
`run_bypass_edit`, which calls `begin_edit`, which calls
|
`run_bypass_edit`, which calls `begin_edit`, which calls
|
||||||
`ensure_writable` first (§3.4). `run_buffer_edit`
|
`ensure_writable` first (§3.4). `run_buffer_edit`
|
||||||
(`src/lua_bindings/mod.rs:1353-1374`) grows a third arm:
|
(`src/lua_bindings/mod.rs:1353-1374`) grows a third arm **and becomes the
|
||||||
|
single owner of post-borrow fan-out**. The three mutator bodies remove
|
||||||
|
their separate `notify_buffer_edit_to_windows` calls, preventing the
|
||||||
|
generated success arm from notifying twice:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
if generated {
|
if generated {
|
||||||
unfold_before_interactive_lua_edit(lua, id, edit_start_of(&op));
|
unfold_before_interactive_lua_edit(lua, id, edit_start_of(&op));
|
||||||
run_generated_edit(lua, id, op) // no begin_edit; §3.4
|
let outcome = run_generated_edit(lua, id, op); // no begin_edit; §3.4
|
||||||
|
finish_generated_outcome(lua, id, outcome) // fan-out, then Ok/Err
|
||||||
} else if bypass_intercept {
|
} else if bypass_intercept {
|
||||||
unfold_before_interactive_lua_edit(lua, id, edit_start_of(&op));
|
unfold_before_interactive_lua_edit(lua, id, edit_start_of(&op));
|
||||||
run_bypass_edit(lua, id, op)
|
let edit = run_bypass_edit(lua, id, op)?;
|
||||||
|
notify_buffer_edit_to_windows(lua, id, &edit);
|
||||||
|
Ok(edit)
|
||||||
} else {
|
} else {
|
||||||
run_managed_edit(lua, id, op)
|
let edit = run_managed_edit(lua, id, op)?;
|
||||||
|
notify_buffer_edit_to_windows(lua, id, &edit);
|
||||||
|
Ok(edit)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`run_generated_edit` mirrors `run_bypass_edit`'s borrow shape but returns
|
||||||
|
the single transaction's whole outcome rather than calling `begin_edit`
|
||||||
|
+ `apply_edit_skip_intercepts`. `finish_generated_outcome` is the match
|
||||||
|
in §3.3 and is also used by the `set_generated_contents` binding.
|
||||||
|
`begin_edit` stays byte-identical, so no ordinary edit's error precedence
|
||||||
|
changes.
|
||||||
|
|
||||||
**What revision 2 got right and revision 3 keeps: the unfold seam.** The
|
**What revision 2 got right and revision 3 keeps: the unfold seam.** The
|
||||||
generated arm still calls `unfold_before_interactive_lua_edit` at the
|
generated arm still calls `unfold_before_interactive_lua_edit` at the
|
||||||
same point the bypass arm does, and for the same reason — the guard
|
same point the bypass arm does, and for the same reason — the guard
|
||||||
|
|
@ -1501,9 +1628,8 @@ panels constantly and because it fixes terminal copy mode retroactively.
|
||||||
Alternative if the user prefers a narrower Stage 1: its own lane, in
|
Alternative if the user prefers a narrower Stage 1: its own lane, in
|
||||||
which case Stage 1 must say so out loud rather than inherit it silently.
|
which case Stage 1 must say so out loud rather than inherit it silently.
|
||||||
|
|
||||||
**Q#GB7 — The unlock survives ONLY bounded by lock provenance, and it
|
**Q#GB7 — No unlock ships in this arc. Revision 5 chooses the fallback
|
||||||
moves to Stage 2. Revision 3 withdraws revision 2's recommendation
|
revision 4 named (review round 4, P1-4).**
|
||||||
(review P1-3).**
|
|
||||||
|
|
||||||
Revision 2 recommended `pmacs.buffer.unlock_generated(buf)` — a one-way
|
Revision 2 recommended `pmacs.buffer.unlock_generated(buf)` — a one-way
|
||||||
clear of `read_only`, shipped in Stage 1 — on the strength of sweep B's
|
clear of `read_only`, shipped in Stage 1 — on the strength of sweep B's
|
||||||
|
|
@ -1535,35 +1661,35 @@ returns writability to a buffer whose data is gone. The recovery
|
||||||
scenario that justified moving this from a deferral to Stage 1 work was
|
scenario that justified moving this from a deferral to Stage 1 work was
|
||||||
never a recovery.
|
never a recovery.
|
||||||
|
|
||||||
**The decision.** The capability survives, bounded by **lock provenance**
|
**Revision 4's proposed bound was not provenance.** It refused only
|
||||||
(Q#GB15), and:
|
when `identity_protected` was true. That proves “this is not a declared
|
||||||
|
terminal identity buffer”; it does **not** prove “the generated-write
|
||||||
|
primitive installed this lock.” As revision 4 itself admitted,
|
||||||
|
`unlock_generated` could therefore release any non-identity-protected
|
||||||
|
Rust lock, including one a future owner installed for an unrelated
|
||||||
|
reason. The live Q#GB7 text simultaneously promised to refuse exactly
|
||||||
|
those locks. Both statements cannot be the approved contract.
|
||||||
|
|
||||||
- **`pmacs.buffer.unlock_generated(buf)` refuses any buffer whose lock
|
**The decision: remove the capability, not the promise.** Neither stage
|
||||||
this arc's primitive did not install** — terminal identity buffers, and
|
adds `pmacs.buffer.unlock_generated`, and Q#GB15's field is used only to
|
||||||
anything a future Rust owner locks for its own reasons. It is not a
|
refuse generated writes to intrinsic identity buffers. There is no Lua
|
||||||
clear of `read_only`; it is the *inverse of `apply_generated_edit`'s
|
clear of `read_only`.
|
||||||
lock*, and it can undo only what the same public API did.
|
|
||||||
- **It moves to Stage 2**, with Q#GB15, because it is meaningless without
|
|
||||||
the provenance the same field provides and because Stage 2 is where the
|
|
||||||
lock capability actually widens.
|
|
||||||
- **Its claim narrows.** It is the **closure of the capability
|
|
||||||
`{ generated = true }` adds** — not a recovery mechanism. The honest
|
|
||||||
statement of what it buys: after a mistaken generated write, the buffer
|
|
||||||
becomes writable again; its former contents do not come back.
|
|
||||||
|
|
||||||
**On the standing asymmetry the review asks this to address directly**
|
This leaves the standing asymmetry visible rather than pretending to
|
||||||
(`remove_intercept` is exposed; `set_read_only` deliberately is not,
|
close it: `remove_intercept` is exposed while `set_read_only` is not
|
||||||
`src/lua_bindings/mod.rs:3072-3078` and `docs/agent-handoff.md` §4). A
|
(`src/lua_bindings/mod.rs:3072-3078` and
|
||||||
provenance-bounded `unlock_generated` **does not breach that policy**,
|
`docs/agent-handoff.md` §4). That asymmetry is deliberate today because
|
||||||
and the reason is precise rather than rhetorical: it can only reach a
|
the rope lock protects more than generated buffers. A safe inverse needs
|
||||||
lock that a public Lua call installed, so it adds **no reachable state
|
a lock-kind/provenance representation, or an owner-specific transition
|
||||||
that `{ generated = true }` did not already make reachable**. A
|
whose preconditions prove the caller owns the state. Neither is required
|
||||||
`set_read_only(buf, true)` would add the "lock with no door" state the
|
to stop undo destroying generated output, and neither should be smuggled
|
||||||
invariant exists to forbid; a `set_read_only(buf, false)` would reach
|
into this arc for an escape that cannot restore the overwritten data.
|
||||||
locks Lua never set. This reaches neither. If that argument does not
|
|
||||||
persuade, the fallback is to ship no unlock at all and let dired Stage 3
|
The future concrete consumer remains wdired in dired Stage 3. That lane
|
||||||
frame its own door — which costs this arc nothing, because Stage 3 is
|
must frame the transition it needs—generated listing to editable rename
|
||||||
not built.
|
surface—against its own owner handle and lifecycle. It may choose the
|
||||||
|
eventual `read_only` policy enum; it does not inherit a general-purpose
|
||||||
|
unlock from here.
|
||||||
|
|
||||||
**What Stage 1 loses, and why that is correct.** Nothing. Stage 1 adopts
|
**What Stage 1 loses, and why that is correct.** Nothing. Stage 1 adopts
|
||||||
`set_generated_contents`, which is **already public on `main`**, on two
|
`set_generated_contents`, which is **already public on `main`**, on two
|
||||||
|
|
@ -1574,8 +1700,9 @@ the shipped primitive, not about Stage 1. The `*scratch*` exposure sweep
|
||||||
B found is real, and it is real **today**; it is recorded in §8 as a
|
B found is real, and it is real **today**; it is recorded in §8 as a
|
||||||
pre-existing hazard this arc neither creates nor closes.
|
pre-existing hazard this arc neither creates nor closes.
|
||||||
|
|
||||||
**Q#GB15 — `read_only` gains a provenance companion. New in revision 3
|
**Q#GB15 — `read_only` gains an intrinsic-identity guard, not a
|
||||||
(review P1-3, sweep C).**
|
provenance companion. New in revision 3, narrowed in revision 5
|
||||||
|
(review P1-3, sweep C; review round 4 P1-4).**
|
||||||
|
|
||||||
**Revision 4 replaces revision 3's `generated_lock` with an intrinsic
|
**Revision 4 replaces revision 3's `generated_lock` with an intrinsic
|
||||||
`identity_protected`, because review round 3's P1-3 showed provenance
|
`identity_protected`, because review round 3's P1-3 showed provenance
|
||||||
|
|
@ -1609,7 +1736,7 @@ is a property of **what the buffer is**, not of who last locked it:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
/// Whether this buffer's read-only state is an intrinsic identity
|
/// Whether this buffer's read-only state is an intrinsic identity
|
||||||
/// protection rather than an ordinary lock. Set once, at construction,
|
/// protection rather than an ordinary lock. Marked once during owner setup,
|
||||||
/// by an owner that means "the host may not edit this at all"; never
|
/// by an owner that means "the host may not edit this at all"; never
|
||||||
/// derived from `read_only` and never changed by `set_read_only`.
|
/// derived from `read_only` and never changed by `set_read_only`.
|
||||||
identity_protected: bool,
|
identity_protected: bool,
|
||||||
|
|
@ -1617,13 +1744,13 @@ identity_protected: bool,
|
||||||
|
|
||||||
Two rules maintain it, and `set_read_only` is not one of them:
|
Two rules maintain it, and `set_read_only` is not one of them:
|
||||||
|
|
||||||
1. `Buffer::set_identity_protected(true)` — a new `pub` method, called
|
1. `Buffer::mark_identity_protected()` — a new **crate-private,
|
||||||
**once** by `TerminalSession::open` beside its existing
|
monotonic** method that can only set the field to `true`, called once
|
||||||
`set_read_only(true)` (`src/terminal/session.rs:305`). That is the
|
by `TerminalSession::open` beside its existing `set_read_only(true)`
|
||||||
only production caller.
|
(`src/terminal/session.rs:305`). There is no “false” operation and no
|
||||||
2. `apply_generated_edit` refuses iff `identity_protected` (§3.4 exit 2);
|
Lua surface, so “set once” is enforced rather than a caller convention.
|
||||||
`unlock_generated` refuses iff `identity_protected`. Neither ever
|
2. `apply_generated_edit` refuses iff `identity_protected` (§3.4 exit 2).
|
||||||
writes the field.
|
It never writes the field.
|
||||||
|
|
||||||
**The lift-and-restore seam is now unaffected**, because
|
**The lift-and-restore seam is now unaffected**, because
|
||||||
`identity_protected` is `false` for a snapshot buffer and stays `false`
|
`identity_protected` is `false` for a snapshot buffer and stays `false`
|
||||||
|
|
@ -1633,20 +1760,12 @@ right.
|
||||||
|
|
||||||
**Why declaration beats inference, stated as a rule rather than as a
|
**Why declaration beats inference, stated as a rule rather than as a
|
||||||
patch.** Inference required every mutation of `read_only` to maintain a
|
patch.** Inference required every mutation of `read_only` to maintain a
|
||||||
derived fact; P1-3 is the proof that it does not. Declaration puts the
|
derived fact; P1-3 is the proof that it does not. Declaration is sound
|
||||||
burden on the locker: *if you lock a buffer and Lua must not unlock it,
|
for the one question this field now answers: *may an owner-authorized
|
||||||
mark it identity-protected.* One flag, one meaning, one writer, and a
|
generated write ever edit this buffer?* Terminal identity says no,
|
||||||
future Rust owner that wants the same protection opts in with one line
|
independently of temporary `read_only` lifts. It is deliberately **not**
|
||||||
instead of relying on an inference chain staying intact.
|
used to answer who installed an ordinary lock; Q#GB7 no longer asks it
|
||||||
|
to.
|
||||||
**What this costs relative to revision 3.** It is strictly narrower in
|
|
||||||
one respect and that is worth naming: `unlock_generated` can now release
|
|
||||||
**any** non-identity-protected `read_only` buffer, including one a future
|
|
||||||
Rust owner locked without declaring itself. Revision 3's version would
|
|
||||||
have refused that by inference. The trade is deliberate — an inference
|
|
||||||
that is wrong on a shipped seam is worse than a contract that has to be
|
|
||||||
opted into — and the contract is documented on `set_read_only` so the
|
|
||||||
next locker reads it at the point of use.
|
|
||||||
|
|
||||||
**Rule 1's refusal is the half revision 2 did not have, and it closes a
|
**Rule 1's refusal is the half revision 2 did not have, and it closes a
|
||||||
hole in the SHIPPED primitive** (sweep C item 1).
|
hole in the SHIPPED primitive** (sweep C item 1).
|
||||||
|
|
@ -1655,16 +1774,15 @@ unconditionally (`src/buffer.rs:546`), so
|
||||||
`pmacs.buffer.set_generated_contents(term_buf, "junk")` on a live
|
`pmacs.buffer.set_generated_contents(term_buf, "junk")` on a live
|
||||||
terminal identity buffer overwrites its contents and re-locks it as
|
terminal identity buffer overwrites its contents and re-locks it as
|
||||||
though the primitive owned it. Nothing in the tree refuses that, and no
|
though the primitive owned it. Nothing in the tree refuses that, and no
|
||||||
test covers it. It is why the field earns its cost in **both**
|
test covers it. That write-direction hole is the field's sole purpose.
|
||||||
directions rather than existing only to make Q#GB7 safe.
|
|
||||||
|
|
||||||
**The alternatives, and why not.**
|
**The alternatives, and why not.**
|
||||||
|
|
||||||
- *A registry-side set of generated-locked ids, held as Lua app-data.* A
|
- *A registry-side set of generated-locked ids, held as Lua app-data.*
|
||||||
second source of truth that can drift from the flag, plus a pruning
|
This would exist only to resurrect Q#GB7: a second source of truth that
|
||||||
obligation on buffer removal — the shape the terminal-config lane
|
can drift from the flag, plus a pruning obligation on buffer removal —
|
||||||
records as "`prune` **reacts** to buffer removal". A field on the
|
the shape the terminal-config lane records as "`prune` **reacts** to
|
||||||
buffer cannot drift from the buffer.
|
buffer removal". Rejected along with the unlock.
|
||||||
- *Replacing `read_only: bool` with an enum.* Cleaner in principle,
|
- *Replacing `read_only: bool` with an enum.* Cleaner in principle,
|
||||||
and it would let §2.11's third policy (`document_bytes`) ask the
|
and it would let §2.11's third policy (`document_bytes`) ask the
|
||||||
question it actually means. It also churns `ensure_writable`, all six
|
question it actually means. It also churns `ensure_writable`, all six
|
||||||
|
|
@ -1672,11 +1790,12 @@ directions rather than existing only to make Q#GB7 safe.
|
||||||
`set_read_only` signature — a refactor this arc would be smuggling.
|
`set_read_only` signature — a refactor this arc would be smuggling.
|
||||||
Named as the right eventual shape in §8, not adopted.
|
Named as the right eventual shape in §8, not adopted.
|
||||||
|
|
||||||
**Cost, stated:** one bool per buffer; one new `pub` method with exactly
|
**Cost, stated:** one bool per buffer; one new crate-private monotonic
|
||||||
one production caller; **no invariant to maintain**, because the field is
|
method with exactly one production caller; **no invariant to maintain
|
||||||
never derived from `read_only`; and one new refusal that changes shipped
|
against `set_read_only`**, because the field is never derived from it;
|
||||||
`set_generated_contents` behaviour, so it lands in Stage 2 with the rest
|
and one new refusal that changes shipped `set_generated_contents`
|
||||||
of Q#GB10's changes to that function, not in Stage 1.
|
behaviour, so it lands in Stage 2 with the rest of Q#GB10's changes to
|
||||||
|
that function, not in Stage 1.
|
||||||
|
|
||||||
**Q#GB16 — The lock silently disables fold creation on every buffer it
|
**Q#GB16 — The lock silently disables fold creation on every buffer it
|
||||||
touches. New in revision 3 (sweep C item 2).**
|
touches. New in revision 3 (sweep C item 2).**
|
||||||
|
|
@ -1710,8 +1829,9 @@ here.** Three options, with the recommendation being (a):
|
||||||
document buffer". Cheap, honest, and it converts a silent behaviour
|
document buffer". Cheap, honest, and it converts a silent behaviour
|
||||||
change into a stated one.
|
change into a stated one.
|
||||||
- **(b) Preserve foldability** by changing the guard to
|
- **(b) Preserve foldability** by changing the guard to
|
||||||
`read_only && !identity_protected`. Available once Q#GB15 lands, but it
|
the intrinsic `identity_protected` predicate rather than the broad
|
||||||
edits a pinned Q#FD11 seam for a use case nobody has asked for.
|
`read_only` predicate. Available once Q#GB15 lands, but it edits a
|
||||||
|
pinned Q#FD11 seam for a use case nobody has asked for.
|
||||||
- **(c) Do nothing and say nothing.** Rejected: this is exactly the
|
- **(c) Do nothing and say nothing.** Rejected: this is exactly the
|
||||||
defect class the review's findings 1 and 3 are instances of.
|
defect class the review's findings 1 and 3 are instances of.
|
||||||
|
|
||||||
|
|
@ -1721,7 +1841,7 @@ families get locked.
|
||||||
|
|
||||||
**Q#GB17 — The transaction shape.** §3.4. One `&mut Buffer` method, its
|
**Q#GB17 — The transaction shape.** §3.4. One `&mut Buffer` method, its
|
||||||
own `run_buffer_edit` arm, `begin_edit` untouched, eight named exits, and
|
own `run_buffer_edit` arm, `begin_edit` untouched, eight named exits, and
|
||||||
and cleanup driven by an explicit `GeneratedOutcome` — **not** by
|
cleanup driven by an explicit `GeneratedOutcome` — **not** by
|
||||||
inferring one from `revision`, which revision 3 did and which was wrong
|
inferring one from `revision`, which revision 3 did and which was wrong
|
||||||
in three directions (§3.4). New in revision 3 (review
|
in three directions (§3.4). New in revision 3 (review
|
||||||
P1-1).
|
P1-1).
|
||||||
|
|
@ -2015,13 +2135,15 @@ it is not the obvious one.
|
||||||
`fold.rs:68` status string, and the criterion that makes the change
|
`fold.rs:68` status string, and the criterion that makes the change
|
||||||
stated rather than silent.
|
stated rather than silent.
|
||||||
|
|
||||||
**Revision 3 removes `unlock_generated` from Stage 1** (Q#GB7). Revision
|
**Revision 3 removed `unlock_generated` from Stage 1; revision 5 removes
|
||||||
2 put it here as "the escape from a bricked buffer"; the escape does not
|
it from the arc** (Q#GB7). Revision 2 put it here as "the escape from a
|
||||||
recover anything (the history is already cleared), and the brick it
|
bricked buffer"; the escape does not recover anything (the history is
|
||||||
escapes is one `main` already ships, since `set_generated_contents` is
|
already cleared), and the brick it escapes is one `main` already ships,
|
||||||
already public. Stage 1 adds no lock capability that does not already
|
since `set_generated_contents` is already public. Stage 1 adds no lock
|
||||||
exist, so it needs no door. The capability re-appears in Stage 2, bounded
|
capability that does not already exist, so it needs no door. Revision
|
||||||
by Q#GB15's provenance.
|
4's attempt to restore the capability in Stage 2 used
|
||||||
|
`identity_protected` as though “not terminal identity” proved “generated
|
||||||
|
lock”; it does not. No stage adds the binding.
|
||||||
|
|
||||||
**Revision 2 grew Stage 1 by two prerequisites and one reversal.** The
|
**Revision 2 grew Stage 1 by two prerequisites and one reversal.** The
|
||||||
ownership rule is load-bearing for the lock rather than adjacent to it,
|
ownership rule is load-bearing for the lock rather than adjacent to it,
|
||||||
|
|
@ -2041,7 +2163,7 @@ reachable without `M-x`.
|
||||||
|
|
||||||
**Why the cut is safe under every candidate primitive:** a whole-buffer
|
**Why the cut is safe under every candidate primitive:** a whole-buffer
|
||||||
replace is expressible in all of A–C, and under the recommendation
|
replace is expressible in all of A–C, and under the recommendation
|
||||||
`set_generated_contents` keeps its name and signature as
|
`set_generated_contents` keeps its **Lua** name and signature as
|
||||||
`apply_generated_edit`'s wrapper. Stage 1 is therefore not rework under
|
`apply_generated_edit`'s wrapper. Stage 1 is therefore not rework under
|
||||||
any Q#GB1 outcome — which is the decisive argument for cutting here
|
any Q#GB1 outcome — which is the decisive argument for cutting here
|
||||||
rather than shipping one large PR.
|
rather than shipping one large PR.
|
||||||
|
|
@ -2059,18 +2181,22 @@ diff at `dired.lua:371` and `listview.lua:60-61` is written once.
|
||||||
shape as Stage 1's listview fix.
|
shape as Stage 1's listview fix.
|
||||||
- **In the SAME PR as that disambiguation (Q#GB18):**
|
- **In the SAME PR as that disambiguation (Q#GB18):**
|
||||||
`pmacs.compile.is_generated_buffer` (`compile.lua:212-217`) stops
|
`pmacs.compile.is_generated_buffer` (`compile.lua:212-217`) stops
|
||||||
comparing names and scans an owner-registered id list, with
|
comparing names and answers only
|
||||||
registration in `ensure_slot` and `ensure_search_panel` and
|
`slot_for_buffer(buf) ~= nil`; `default.lua` adds local
|
||||||
unregistration in the `on_removed` callbacks both already have.
|
`search_panel_owns(buf)` and exposes
|
||||||
|
`pmacs.project._is_search_panel`. Each capture site ORs its own
|
||||||
|
predicate with the other module's predicate through the guarded
|
||||||
|
optional call shape Q#GB18 specifies. **No owner registers ids with
|
||||||
|
the other and no new removal callback work exists.**
|
||||||
- `Buffer::apply_generated_edit` (§3.4) + the `{ generated = true }`
|
- `Buffer::apply_generated_edit` (§3.4) + the `{ generated = true }`
|
||||||
option + its own `run_buffer_edit` arm + `set_generated_contents`
|
option + its own `run_buffer_edit` arm + `set_generated_contents`
|
||||||
reimplemented over it (Q#GB17, Q#GB3).
|
reimplemented over it (Q#GB17, Q#GB3).
|
||||||
- Q#GB10's path-backed refusal **and** `mark_clean` — one rule, both
|
- Q#GB10's path-backed refusal **and** `mark_clean` — one rule, both
|
||||||
halves, since the refusal is what makes the flag change safe.
|
halves, since the refusal is what makes the flag change safe.
|
||||||
- **Q#GB15's `identity_protected` field**, its write-direction refusal, and
|
- **Q#GB15's `identity_protected` field** and its write-direction
|
||||||
`pmacs.buffer.unlock_generated` bounded by it (Q#GB7). All three edit
|
refusal. `mark_identity_protected()` is crate-private and monotonic;
|
||||||
`set_generated_contents` or the flag it sets, so they belong with the
|
`TerminalSession::open` is its only production caller. Q#GB7 adds no
|
||||||
reimplementation.
|
unlock surface.
|
||||||
- Conversion of all 13 remaining write sites (`compile.lua` 9,
|
- Conversion of all 13 remaining write sites (`compile.lua` 9,
|
||||||
`builtin/commands/default.lua` 4).
|
`builtin/commands/default.lua` 4).
|
||||||
- Q#GB5's `ensure_slot` lock, which is only placeable once ownership
|
- Q#GB5's `ensure_slot` lock, which is only placeable once ownership
|
||||||
|
|
@ -2089,13 +2215,13 @@ the cut.
|
||||||
2. **Q#GB18 (identity routing) rides in the same PR as Q#GB13 for the
|
2. **Q#GB18 (identity routing) rides in the same PR as Q#GB13 for the
|
||||||
same writer**, per the ordering constraint under Q#GB13. New in
|
same writer**, per the ordering constraint under Q#GB13. New in
|
||||||
revision 3.
|
revision 3.
|
||||||
3. **Q#GB7 (unlock) moves to Stage 2, bounded by Q#GB15.** Revision 1
|
3. **Q#GB7 (unlock) is removed from both stages.** Revision 1 deferred
|
||||||
deferred it; revision 2 built it in Stage 1 on an argument review
|
it; revision 2 built it in Stage 1; revision 3 moved it to Stage 2;
|
||||||
P1-3 falsified; revision 3 lands it in Stage 2 with the provenance
|
revision 4 replaced its provenance with an identity exclusion and
|
||||||
that makes it a bounded capability rather than a general one. The
|
thereby made it general again. Revision 5 chooses no unlock. The
|
||||||
two reversals are recorded rather than smoothed over because the
|
reversals are recorded rather than smoothed over because the reason
|
||||||
*reason* moved twice and the next reader needs to know which reason
|
moved three times and the next reader needs to know which decision is
|
||||||
is live.
|
live.
|
||||||
4. **Q#GB10 (path refusal + `mark_clean`) lands in Stage 2**, because it
|
4. **Q#GB10 (path refusal + `mark_clean`) lands in Stage 2**, because it
|
||||||
edits `set_generated_contents` itself and therefore changes the
|
edits `set_generated_contents` itself and therefore changes the
|
||||||
already-shipped terminal snapshot. **Stage 1 is not pure Lua**
|
already-shipped terminal snapshot. **Stage 1 is not pure Lua**
|
||||||
|
|
@ -2309,10 +2435,11 @@ intercept refuses it either way.
|
||||||
not catch a misrouted consumer, which is why 11 and 12 assert through
|
not catch a misrouted consumer, which is why 11 and 12 assert through
|
||||||
`dispatch_key`.
|
`dispatch_key`.
|
||||||
|
|
||||||
**Moved out of Stage 1 in revision 3:** the unlock criterion. Revision
|
**Moved out of Stage 1 in revision 3 and removed in revision 5:** the
|
||||||
2's Stage 1 criterion 11 pinned `unlock_generated`; Q#GB7 moves the
|
unlock criterion. Revision 2's Stage 1 criterion 11 pinned
|
||||||
capability to Stage 2, so the criterion moves with it (Stage 2 criterion
|
`unlock_generated`; revision 3 moved it to Stage 2, revision 4's
|
||||||
13) and grows the negative terminal-identity half review P1-3 asks for.
|
identity exclusion failed to bound it by generated-lock provenance, and
|
||||||
|
Q#GB7 now ships no binding in either stage.
|
||||||
|
|
||||||
### Stage 2
|
### Stage 2
|
||||||
|
|
||||||
|
|
@ -2410,7 +2537,7 @@ capability to Stage 2, so the criterion moves with it (Stage 2 criterion
|
||||||
on `main` today for the intercept half alone.
|
on `main` today for the intercept half alone.
|
||||||
10. **Coverage, not a criterion: both configurations** — default and
|
10. **Coverage, not a criterion: both configurations** — default and
|
||||||
`--features crdt` — for criteria 1–5 and for the §3.4 transaction
|
`--features crdt` — for criteria 1–5 and for the §3.4 transaction
|
||||||
criteria 15, 16 (first half), 16b and 18. CI never enables the
|
criteria 15, 15a, 16 (first half), 16b and 18. CI never enables the
|
||||||
feature, so CRDT must not be the only home of any of them.
|
feature, so CRDT must not be the only home of any of them.
|
||||||
|
|
||||||
**Three are irreducibly `crdt`-only and must say so rather than be
|
**Three are irreducibly `crdt`-only and must say so rather than be
|
||||||
|
|
@ -2437,31 +2564,17 @@ capability to Stage 2, so the criterion moves with it (Stage 2 criterion
|
||||||
conversion that drops the intruder edit entirely leaves the desync
|
conversion that drops the intruder edit entirely leaves the desync
|
||||||
machinery unpinned while the suite stays green.
|
machinery unpinned while the suite stays green.
|
||||||
|
|
||||||
**New in revision 3 — the transaction, the provenance, and the bounded
|
**New in revision 3 — the transaction and the identity guard. Revision
|
||||||
unlock.**
|
5 removes the attempted bounded unlock.**
|
||||||
|
|
||||||
13. **[`main`] The unlock is real, is narrow, and refuses a lock it did
|
13. **[structural] No Lua unlock surface is added (Q#GB7).** There is no
|
||||||
not install (Q#GB7 + Q#GB15; review P1-3).** Three halves, and the
|
`pmacs.buffer.unlock_generated`, no exposed `set_read_only`, and no
|
||||||
third is the one revision 2 lacked.
|
binding that clears `read_only` without performing an
|
||||||
- *Real:* on a plain pathless buffer with no intercept, a generated
|
owner-authorized generated write. *Bite:* adding the revision 4
|
||||||
write locks it (a `bypass_intercept` write raises),
|
binding fails this structural assertion even if it refuses terminal
|
||||||
`unlock_generated` releases it (a bypass write lands), and an
|
identity buffers; `identity_protected == false` is not proof that the
|
||||||
ordinary edit then lands too.
|
generated primitive installed the lock.
|
||||||
- *Narrow:* on a listview panel, after `unlock_generated` an ordinary
|
14. **[`main`] A generated write REFUSES a terminal identity buffer
|
||||||
edit is still refused **by the intercept**, asserted on the full
|
|
||||||
message text per Stage 1 criterion 5.
|
|
||||||
- *Negative terminal identity, the criterion review P1-3 asks for:*
|
|
||||||
open a real terminal, take its identity buffer id, and require
|
|
||||||
`pmacs.buffer.unlock_generated(term_buf)` to **error**, with
|
|
||||||
`Buffer::is_read_only()` still `true` afterwards and an ordinary
|
|
||||||
edit still refused. Assert the post-state, not the error alone.
|
|
||||||
|
|
||||||
*Bite:* a no-op unlock fails the first half; an unlock that also
|
|
||||||
tears down the intercept — "unprotect" rather than "unlock" — fails
|
|
||||||
the second; and **revision 2's `unlock_generated` as written passes
|
|
||||||
the first two and fails the third**, which is the whole finding.
|
|
||||||
Falsify the third by deleting the `identity_protected` check.
|
|
||||||
14. **[`main`] A generated write REFUSES a buffer someone else locked
|
|
||||||
(Q#GB15; sweep C item 1).** Open a real terminal; call
|
(Q#GB15; sweep C item 1).** Open a real terminal; call
|
||||||
`pmacs.buffer.set_generated_contents(term_buf, "junk")` and each of
|
`pmacs.buffer.set_generated_contents(term_buf, "junk")` and each of
|
||||||
the three `{ generated = true }` mutators against it. Every one must
|
the three `{ generated = true }` mutators against it. Every one must
|
||||||
|
|
@ -2476,7 +2589,7 @@ unlock.**
|
||||||
round 3, P1-2, is right and the diagnosis is worth stating because it is
|
round 3, P1-2, is right and the diagnosis is worth stating because it is
|
||||||
the second time this arc has shipped a criterion that passes with the bug
|
the second time this arc has shipped a criterion that passes with the bug
|
||||||
restored.** Both used an out-of-bounds op. Bounds validation is §3.4 exit
|
restored.** Both used an out-of-bounds op. Bounds validation is §3.4 exit
|
||||||
4 — **before** `editing_in_progress` is set and **before** the unlock —
|
4 — **before** `editing_in_progress` is set and **before** the internal lift —
|
||||||
so the operation never enters the transaction the criteria claim to test,
|
so the operation never enters the transaction the criteria claim to test,
|
||||||
and an implementation that omits *both* the flag clear and the error-path
|
and an implementation that omits *both* the flag clear and the error-path
|
||||||
relock passes both. Worse, revision 3 argued *in the same document* that
|
relock passes both. Worse, revision 3 argued *in the same document* that
|
||||||
|
|
@ -2493,16 +2606,36 @@ test.** Every criterion below names its exit.
|
||||||
`pub`, so a test crate can implement one whose `on_edit` returns
|
`pub`, so a test crate can implement one whose `on_edit` returns
|
||||||
`Err(BufferError::Intercepted { .. })` — then perform a **valid**
|
`Err(BufferError::Intercepted { .. })` — then perform a **valid**
|
||||||
generated write. It fails at the broadcast, *after* the rope swap.
|
generated write. It fails at the broadcast, *after* the rope swap.
|
||||||
Then require an **ordinary** edit on the same buffer to report the
|
Then **lift `read_only` Rust-side** and require an ordinary edit on
|
||||||
intercept's message, **not** `is already being edited`.
|
the same buffer to report the `FailingView`'s message, **not**
|
||||||
|
`is already being edited`.
|
||||||
*Bite:* an implementation that returns from the `match` without
|
*Bite:* an implementation that returns from the `match` without
|
||||||
reaching §3.4's line (6) leaves the flag set, and `begin_edit`
|
reaching §3.4's line (6) leaves the flag set, and `begin_edit`
|
||||||
(`:726-731`) and `apply_edit` (`:774-779`) then refuse **every**
|
(`:726-731`) and `apply_edit` (`:774-779`) then refuse **every**
|
||||||
later edit to that buffer for the rest of the session. Falsify by
|
later writable edit to that buffer for the rest of the session.
|
||||||
|
Without the lift, `ensure_writable` runs before the flag check and
|
||||||
|
both the correct and broken implementations report `ReadOnly`, which
|
||||||
|
was revision 4's non-discriminating form. Falsify by
|
||||||
moving the flag clear inside the `Applied | NoOp` arm. Assert the
|
moving the flag clear inside the `Applied | NoOp` arm. Assert the
|
||||||
*next* edit's outcome, not the failing call's — the failing call
|
*next* edit's outcome, not the failing call's — the failing call
|
||||||
reports the same error either way, which is the whole reason this
|
reports the same error either way, which is the whole reason this
|
||||||
criterion is about the buffer's state afterwards.
|
criterion is about the buffer's state afterwards. Restore
|
||||||
|
`read_only` after the probe so criterion 16 begins from the specified
|
||||||
|
post-failure state.
|
||||||
|
15a. **[mutation; both configurations] `AppliedThenFailed` fans out its
|
||||||
|
carried `Edit` before surfacing the error (Q#GB17; review round 4,
|
||||||
|
P1-1).** Display the test buffer in a real window, attach the same
|
||||||
|
`FailingView`, and issue a valid shrinking generated replace through
|
||||||
|
the Lua binding. The call returns the view's error, but painting the
|
||||||
|
window must show the new shorter contents with no stale-line panic.
|
||||||
|
Under `--features crdt`, attach a replica mirror before the call and
|
||||||
|
require it to import the owner's `crdt_op` despite the Lua error.
|
||||||
|
*Bite:* deleting only the `AppliedThenFailed` arm's
|
||||||
|
`notify_buffer_edit_to_windows` call leaves criteria 15 and 16 green
|
||||||
|
— buffer cleanup is correct — while the window keeps stale line
|
||||||
|
ranges and the replica never receives an operation that already
|
||||||
|
changed the authoritative CRDT. This criterion fails that mutation
|
||||||
|
in both directions.
|
||||||
16. **[`main`] A generated write relocks on that same failure, and does
|
16. **[`main`] A generated write relocks on that same failure, and does
|
||||||
NOT lock on a refusal (Q#GB17). Two halves, because §3.4 gives them
|
NOT lock on a refusal (Q#GB17). Two halves, because §3.4 gives them
|
||||||
opposite answers and revision 3 gave them the same one.**
|
opposite answers and revision 3 gave them the same one.**
|
||||||
|
|
@ -2533,24 +2666,20 @@ test.** Every criterion below names its exit.
|
||||||
locked, modified buffer with poppable history. Falsify by restoring
|
locked, modified buffer with poppable history. Falsify by restoring
|
||||||
`if self.revision() != rev_before`. This is not a hypothetical path:
|
`if self.revision() != rev_before`. This is not a hypothetical path:
|
||||||
Q#GB5 prescribes exactly this call in `ensure_slot`.
|
Q#GB5 prescribes exactly this call in `ensure_slot`.
|
||||||
16c. **[`main`, `crdt`-only] A CRDT mid-transaction failure is
|
16c. **[fault-injection, `crdt`-only] A CRDT mid-transaction failure is
|
||||||
distinguishable and surfaces (Q#GB17; §3.4 `Diverged`). New in
|
distinguishable and contained (Q#GB17; §3.4 `Diverged`). New in
|
||||||
revision 4 (P1-1 direction B).** Drive an `EditOp::Replace` whose
|
revision 4, made stageable and mandatory in revision 5.** In a
|
||||||
CRDT `delete` succeeds and whose `insert` fails
|
`src/buffer.rs` unit test, drive the private delete→insert classifier
|
||||||
(`src/buffer.rs:1140-1163`), and require: a **distinct** error
|
with a delete closure that succeeds and an insert closure that
|
||||||
variant, not `CrdtRejected`; the buffer left `read_only`; and undo
|
returns the same error shape as loro. Require `Diverged`, then drive
|
||||||
history **not** cleared. *Bite:* revision 3's predicate did nothing
|
that outcome through generated cleanup and require: a **distinct**
|
||||||
at all here — `revision` never advanced — so it neither cleaned nor
|
error variant, not `CrdtRejected`; the buffer left `read_only`; and
|
||||||
reported, in the one case the code's own comment calls "an invariant
|
undo history **not** cleared. *Bite:* folding the variant into
|
||||||
violation". Falsify by folding the variant back into `Rejected`.
|
`Rejected` restores a fresh buffer's `entry_read_only = false`; the
|
||||||
**Honest caveat on stageability:** unlike 15, 16 and 16b, this
|
test must assert the lock post-state so that mutation fails, not only
|
||||||
criterion has **no staging recipe verified in this document** —
|
the error discriminant. `cargo test --lib --features crdt` is the
|
||||||
loro's `insert` is expected to succeed when the position is valid,
|
explicit gate. No public fault-injection API is added, and there is
|
||||||
which it is by construction here, so provoking the failure may need a
|
no four-variant fallback.
|
||||||
fault-injection seam rather than an input. If Stage 2 cannot stage
|
|
||||||
it, the correct outcome is the four-variant fallback named at the end
|
|
||||||
of §3.4 — **not** a criterion that passes by never reaching its
|
|
||||||
path, which is precisely what P1-2 caught.
|
|
||||||
17. **[`main`] An invalid-range generated write does NOT destroy undo
|
17. **[`main`] An invalid-range generated write does NOT destroy undo
|
||||||
history (Q#GB17).** On a pathless buffer with two ordinary edits
|
history (Q#GB17).** On a pathless buffer with two ordinary edits
|
||||||
already on the stack, call `b:delete(0, b:len() + 1000,
|
already on the stack, call `b:delete(0, b:len() + 1000,
|
||||||
|
|
@ -2628,10 +2757,11 @@ test.** Every criterion below names its exit.
|
||||||
prefers no duplication, the fallback is to accept the shipped
|
prefers no duplication, the fallback is to accept the shipped
|
||||||
unconditional clear and **drop criterion 17**, which should be a
|
unconditional clear and **drop criterion 17**, which should be a
|
||||||
stated trade rather than a silent one.
|
stated trade rather than a silent one.
|
||||||
- **That one extra `bool` on `Buffer` is the right size for lock
|
- **That one extra `bool` on `Buffer` is the right size for terminal
|
||||||
provenance** (Q#GB15), rather than the enum §2.11's three policies
|
identity protection** (Q#GB15), rather than the enum §2.11's three
|
||||||
really want. The bet is that the enum is a separable refactor; if it
|
policies really want. It is deliberately not lock provenance and
|
||||||
is not, the field becomes churn the refactor has to undo.
|
enables no unlock. The bet is that the enum is a separable refactor;
|
||||||
|
if it is not, the field becomes churn the refactor has to undo.
|
||||||
|
|
||||||
## 8. Deferred (named)
|
## 8. Deferred (named)
|
||||||
|
|
||||||
|
|
@ -2659,12 +2789,18 @@ test.** Every criterion below names its exit.
|
||||||
measurement says the per-op clear costs anything.
|
measurement says the per-op clear costs anything.
|
||||||
- **`read_only` in `describe.buffer`** (Q#GB14) — separable, no new
|
- **`read_only` in `describe.buffer`** (Q#GB14) — separable, no new
|
||||||
capability, not required by this arc.
|
capability, not required by this arc.
|
||||||
- **Replacing `read_only: bool` with a provenance enum** (Q#GB15's
|
- **Replacing `read_only: bool` with a policy/provenance enum**
|
||||||
rejected alternative). It is the shape §2.11's three policies actually
|
(Q#GB15's rejected alternative). It is the shape §2.11's three
|
||||||
want, and it would let `document_bytes` ask the question it means
|
policies actually want, and it would let `document_bytes` ask the
|
||||||
instead of the question the flag happens to answer (Q#GB16). Rejected
|
question it means instead of the question the flag happens to answer
|
||||||
here as a refactor this arc would be smuggling; named as the right
|
(Q#GB16). Rejected here as a refactor this arc would be smuggling;
|
||||||
eventual shape.
|
named as the right eventual shape.
|
||||||
|
- **Ordinary edit fan-out after a post-apply `View::on_edit` error.**
|
||||||
|
The richer internal outcome makes the pre-existing loss explicit, but
|
||||||
|
`apply_edit` and `apply_edit_skip_intercepts` keep their public
|
||||||
|
`Result<Edit, BufferError>` surface in this arc. Generated bindings
|
||||||
|
retain and fan out the edit; generalizing that contract across every
|
||||||
|
Rust and Lua edit caller is separate work.
|
||||||
- **The five `*scratch*` find-or-create copies** (§2.10 Class 5) —
|
- **The five `*scratch*` find-or-create copies** (§2.10 Class 5) —
|
||||||
`default.lua:581`, `:1145`, `listview.lua:190`, `compile.lua:1052`,
|
`default.lua:581`, `:1145`, `listview.lua:190`, `compile.lua:1052`,
|
||||||
`dired.lua:914`. Correct only while `*scratch*` stays unowned and
|
`dired.lua:914`. Correct only while `*scratch*` stays unowned and
|
||||||
|
|
@ -2675,10 +2811,10 @@ test.** Every criterion below names its exit.
|
||||||
to ship an unlock. It is a **pre-existing** exposure:
|
to ship an unlock. It is a **pre-existing** exposure:
|
||||||
`pmacs.buffer.set_generated_contents` is already public on `main` and
|
`pmacs.buffer.set_generated_contents` is already public on `main` and
|
||||||
already locks any buffer id it is handed. This arc neither creates it
|
already locks any buffer id it is handed. This arc neither creates it
|
||||||
nor closes it — Q#GB15's provenance bounds who may *unlock*, not who
|
nor closes it, and revision 5 deliberately adds no unlock because
|
||||||
may *lock*. Recorded as a standing hazard rather than as this arc's
|
`identity_protected` cannot prove who installed the lock. Recorded as
|
||||||
work, which is the correction revision 3 makes to sweep B's
|
a standing hazard rather than as this arc's work, which is the
|
||||||
conclusion.
|
correction revision 3 began and revision 5 completes.
|
||||||
- **`docs/agent-handoff.md` §4's inventory is keyed by
|
- **`docs/agent-handoff.md` §4's inventory is keyed by
|
||||||
`bypass_intercept`** and therefore misses Class C, and its headline
|
`bypass_intercept`** and therefore misses Class C, and its headline
|
||||||
"four writer mechanisms" is **five** once `src/help.rs:354` is counted
|
"four writer mechanisms" is **five** once `src/help.rs:354` is counted
|
||||||
|
|
@ -2687,10 +2823,12 @@ test.** Every criterion below names its exit.
|
||||||
- **Removed from this list in revision 3: `COHERENCE.md` §14's listview
|
- **Removed from this list in revision 3: `COHERENCE.md` §14's listview
|
||||||
consumer list.** PR #189 landed the correction (§1.5). A merged
|
consumer list.** PR #189 landed the correction (§1.5). A merged
|
||||||
correction is removed, not relabelled.
|
correction is removed, not relabelled.
|
||||||
- **Returned to this list in revision 3: wdired's unlock.** Revision 1
|
- **Wdired's generated→editable transition.** Revision 1 deferred it,
|
||||||
deferred it, revision 2 made it Stage 1 work, and revision 3 lands the
|
revision 2 made a general unlock Stage 1 work, revision 3 moved that
|
||||||
*capability* in Stage 2 (Q#GB7 + Q#GB15) while the **wdired consumer**
|
capability to Stage 2, and revision 5 removes it. The **wdired
|
||||||
itself stays deferred to dired Stage 3, which is not framed.
|
consumer** stays deferred to dired Stage 3, which must frame an
|
||||||
|
owner-specific transition or the eventual policy enum rather than
|
||||||
|
inheriting a Lua clear of `read_only`.
|
||||||
|
|
||||||
## 9. Coherence impact (`COHERENCE.md` §20)
|
## 9. Coherence impact (`COHERENCE.md` §20)
|
||||||
|
|
||||||
|
|
@ -2835,8 +2973,11 @@ Plus, per stage:
|
||||||
nothing about the seam P1-3 is about. Judge it by whether the test
|
nothing about the seam P1-3 is about. Judge it by whether the test
|
||||||
count includes `acc16e`, not by the verdict alone.
|
count includes `acc16e`, not by the verdict alone.
|
||||||
- **Stage 2 additionally needs a `crdt` run of whatever suite hosts the
|
- **Stage 2 additionally needs a `crdt` run of whatever suite hosts the
|
||||||
§3.4 transaction criteria**, for criterion 16's second half and 16c.
|
§3.4 transaction criteria**, for criterion 15a's replica half and
|
||||||
Same reasoning, same failure mode.
|
criterion 16's second half. Criterion 16c lives in
|
||||||
|
`cargo test --lib --features crdt` because its private
|
||||||
|
delete-success/insert-failure classifier is a unit-test fault seam,
|
||||||
|
not a public acceptance input. Same reasoning, same failure mode.
|
||||||
- **Run `scripts/bite` on every criterion expressible as a test today.**
|
- **Run `scripts/bite` on every criterion expressible as a test today.**
|
||||||
Stage 1 criteria 1–3, 8, 9 and Stage 2 criteria 1, 7, 8, 9, 14 have
|
Stage 1 criteria 1–3, 8, 9 and Stage 2 criteria 1, 7, 8, 9, 14 have
|
||||||
`main` pre-images and can be falsified by revert; the rest are
|
`main` pre-images and can be falsified by revert; the rest are
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue