docs(framing): revision 5 of the resource-op delete guard

Review round 4 accepted the core design and the two surfaced choices,
then found two remaining contract defects and one ledger-ownership
defect. Close all three without starting runtime implementation.

Narrow the last normative "always answers" consumer to Q#RD7's honest
"always attempts while the response channel remains live" contract.
Strengthen the audit procedure to search normalized prose and term
stems so Markdown emphasis cannot hide a consuming site from a literal
phrase sweep.

Replace Q#RD12's incomplete no-op/clear/conflict table with a total
no-op/clear/refuse verdict exposed through one named internal binding.
The shared Rust helper now has an explicit mapping for missing with and
without ignore, non-NotFound stat errors, modified buffers, and
editing-in-progress buffers. Specify how both the primitive and Lua
preflight consume the same result. Add acceptance 11c for known
NotFound before earlier batch mutation and 11d for fail-closed stat
uncertainty.

Stop rewriting #171's lane from #186. Restore the sibling block exactly
to main, retain only the stable cross-lane ownership split, and record
that #171 owns its volatile state. Sequence #186 before #171 because
the urgent data-loss guard is the dependency #171 adopts, not to make a
textual conflict cheaper.

Correct #186's own ahead count to include this revision commit and add
the previously omitted lsp_dispatch_seams_acceptance file to the lane's
touch list.

Still PROPOSED. No runtime code. Implementation begins only after
explicit user approval.
This commit is contained in:
Levi Neuwirth 2026-07-28 20:55:01 -04:00
parent 46b3c8db1a
commit c7ad4f491f
2 changed files with 204 additions and 143 deletions

View File

@ -464,17 +464,17 @@ has **no branch and no framing yet**.
- Portable branch: `githubsucks/resource-op-delete-guard`; worktree
`../pmacs-resource-op-delete`. **PR #186**, base `main`. Currently
framing only — `docs/resource-op-delete-guard-framing.md`, **revision
4** — plus this lane entry. No runtime code yet.
5** — plus this lane entry. No runtime code yet.
- **Measured 2026-07-28, `main` @ `7586905`:**
```
$ git rev-list --left-right --count HEAD...githubsucks/main
3 0
5 0
```
Three commits ahead, **0 behind**. Re-measure before quoting; the
count below for #171 was wrong in three consecutive revisions of this
lane because it was carried forward instead of re-run.
Five commits ahead, **0 behind** at the pushed revision-5 head. This
count includes the revision commit itself; revision 4 recorded the
pre-commit count and was therefore one short.
- **This PR becomes the implementation PR.** Revision 2 dropped rev 1's
framing-PR-then-implementation-PR plan as a one-feature/one-branch/
one-PR violation. The framing is revised in place; implementation
@ -494,21 +494,20 @@ has **no branch and no framing yet**.
exact-path guard; (d) removal is not `kill_buffer`, so windows are
left bound to a removed `BufferId` and the registry can be driven to
**empty**.
- **Approved in principle after review round 1; revision 2 raised four
P1s; revision 3 answers them. Still PROPOSED, still not approved for
implementation.** Settled: refuse unconditionally; take the delete
side now. Withdrawn: rev 1's buffer-first ordering. The design is
`stat/no-op → enumerate and validate → mutate filesystem →
reconcile`, which keeps `on_removed`'s "path already gone" invariant
and makes a failed deletion leave buffers intact automatically.
- **The settled cross-lane split with #171 — identical wording in both
lanes, do not paraphrase:**
- **Approved in principle after review round 1; revision 5 closes round
4's two contract P1s and the ledger-ownership P1. Still PROPOSED,
still not approved for implementation.** Settled: refuse
unconditionally; take the delete side now. Withdrawn: rev 1's
buffer-first ordering. The design is `stat/no-op/refuse → enumerate
and validate → mutate filesystem → reconcile`, which keeps
`on_removed`'s "path already gone" invariant and makes a failed
deletion leave buffers intact automatically.
- **The stable cross-lane ownership split with #171:**
> #186 owns the urgent **pre-filesystem refusal** for synchronous
> `apply_resource_op`. #171 later owns **full post-delete lifecycle
> reconciliation**, including the **async race where a buffer becomes
> modified after dired dispatch**. #171's revision 7 adopts the
> refusal and stops saying LSP intentionally deletes modified files.
> modified after dired dispatch**.
#186 additionally **owns the shared walk query** (scan every
path-bound buffer, normalize once, component-aware `Path::starts_with`)
@ -516,10 +515,12 @@ has **no branch and no framing yet**.
adopts it and extends it to `reconcile_rename`. **Neither lane guards
`pmacs.fs.remove`** — zero production callers today, named out of
scope by both.
- **#171's real state — measured, not summarised.** Its own lane entry
below carries the numbers; do not duplicate them here, because two
copies is how they diverged. The one fact this lane depends on is the
policy split above, which is independent of #171's commit count.
- **#171 owns its own lane entry.** Revision 4 rewrote that sibling
block and was stale before push when #171 revision 8 landed 67 seconds
earlier. Revision 5 restores the block to `main`'s tree, so #186's
diff no longer changes it. The one fact this lane depends on is the
policy split above, which is stable through #171's pushed revision 8
and independent of its commit count.
- **Standing rule this lane learned the expensive way.** A census is a
reading, not a constant. **Do not write an ahead/behind count, a line
count, or a call-site count into this file that you have not just
@ -577,6 +578,7 @@ has **no branch and no framing yet**.
reconciles **one**, which is today's behaviour preserved on purpose.
- Files the implementation will touch: `src/lua_bindings/mod.rs`,
`builtin/runtime/lsp.lua`, `tests/m4_acceptance.rs`,
`tests/lsp_dispatch_seams_acceptance.rs`,
`src/bin/pmacs_fake_lsp.rs`. **Not** `src/daemon.rs`,
`pmacs-protocol/`, `builtin/runtime/dired.lua`,
`docs/agent-handoff.md` or `COHERENCE.md`. No protocol change.
@ -584,46 +586,24 @@ has **no branch and no framing yet**.
`git fetch githubsucks && git worktree add ../pmacs-resource-op-delete
-b resource-op-delete-guard githubsucks/resource-op-delete-guard`.
## dired Stage 2 framing lane — PR #171 OPEN, PROPOSED, DO NOT MERGE
## dired Stage 2 framing lane — PR #171 OPEN, STALE, DO NOT MERGE AS-IS
*Ground-truth lines below refreshed by the #186 lane on 2026-07-28
because they had gone stale and were contradicting the #186 entry; the
lane's own narrative and plan remain #171's to write.*
- Portable branch: `githubsucks/dired-stage2-framing` (head `fd7ae37`);
worktree `../pmacs-dired-stage1`. **PR #171**, base `main`. Framing
only — `docs/dired-stage2-framing.md`, **revision 7**, no runtime code.
- **Measured 2026-07-28, `main` @ `7586905`:**
```
$ git rev-list --left-right --count fd7ae37...7586905
13 2
$ git merge-base fd7ae37 7586905
ad41cf15c2f3905bd8b6e177af824f846b66b085
$ git show fd7ae37:docs/dired-stage2-framing.md | wc -l
2897
```
Thirteen commits ahead, **2 behind** — and those two are exactly the
COHERENCE change (`0dd0bf2`, `7586905`, PR #189), so no framing
conclusion turns on them. The re-scout that the previous entry
described as pending has **finished**: this is no longer the
"`ab42a79`, 4 ahead, 153 behind, merge base `c8ec8f3`" tree, and it is
no longer stale.
- **The commit history embodies five review rounds plus a cross-lane
reconciliation. That is not the same as approval**, and the document
still says PROPOSED — NOT APPROVED. Do not read the round count as a
green light.
- **Revision 7 reconciles with #186.** It adopts #186's pre-filesystem
refusal rather than its own rev-6 policy (which deleted the file and
kept the modified buffer orphaned), and it adopts #186's shared walk
query. See the #186 lane above for the split, quoted verbatim in both
places.
- **Its dependencies had moved materially underneath it** — the reason
the re-scout was needed. This list is **historical**: revisions 57
answered it, and the merge base is now `ad41cf1`, not `c8ec8f3`.
Retained because the three items name substrate any future revision
still stands on:
- Portable branch: `githubsucks/dired-stage2-framing` (head `ab42a79`,
four framing commits); worktree `../pmacs-dired-stage1`. **PR #171**,
base `main`. Framing only — `docs/dired-stage2-framing.md`, 1,570
lines, no runtime code.
- **Measured 2026-07-28: 4 commits ahead of `main`, 153 behind**, merge
base `c8ec8f3`. GitHub reports it mergeable, and its old CI run is
green — **both facts are about a tree nobody has looked at in 153
commits**, and the document still says PROPOSED.
- **The commit history embodies three review rounds. That is not the
same as approval**, and GitHub records no formal review or comment on
it. Do not read the round count as a green light.
- **Its dependencies moved materially underneath it**, which is the real
reason not to merge. Note that dired Stage 1 (#165) and find-file
(#162) are its *base*, not new arrivals — the merge base `c8ec8f3`
**is** #165's merge commit. Eighteen PRs landed after it, and at least
three change ground the framing stands on:
- **#178 gave generated buffers a write invariant**
(`Buffer::set_generated_contents`). Dired's listing is a generated
buffer, and dired is named in the handoff as one of the writer
@ -637,11 +617,12 @@ lane's own narrative and plan remain #171's to write.*
- **#179/#181 landed the typed-edit consumer chain**, which is the
fan-out a rename transaction has to survive.
**That re-scout is done** — revision 5 answered it, and revisions 6
and 7 followed, all on the existing branch so PR #171 keeps its
history. What remains outstanding is **explicit framing approval
before any implementation**, which the document itself still says it
has never received.
Re-scout against `6bee09d`, publish a new revision, and get explicit
framing approval before any implementation. **The re-scout is under
way** on the existing branch, so PR #171 keeps its three-round
history; the product is a revision 5, not a new document. (`main` has
since advanced to `0442d78`, but the only difference is the test-only
#174, so no re-scout conclusion turns on it.)
- **The rename problem the framing must still answer**, restated because
it is the hard part: a rename is a transaction across **five** path
owners — the buffer path, the buffer name, the URI-keyed LSP stores

View File

@ -1,15 +1,13 @@
# Framing — `apply_resource_op` delete destroys unsaved work
**Revision 4.** Status: **PROPOSED — needs explicit user approval before
**Revision 5.** Status: **PROPOSED — needs explicit user approval before
implementation. DO NOT implement, DO NOT merge.** Lane:
`resource-op-delete-guard`, worktree `../pmacs-resource-op-delete`,
based on `githubsucks/main` @ `7586905`.
Every count in this document was produced by a command run at revision
4, with its output pasted at the point of use. That is a reaction to
this lane's own record: it shipped a stale line count, then a stale
commit count, then a stale ledger citation, in three consecutive
revisions — each by carrying a measurement across a base change.
Revision 5 removes volatile sibling-branch counts from the normative
contract. A count is a reading, not a dependency; where history retains
one, it names the revision at which it was measured.
This is a live data-loss bug, reproduced four ways against `ad41cf1`
(§1.1). A language server can destroy a buffer's unsaved edits *and*
@ -24,6 +22,50 @@ this same PR (§8).
## Revision history
### Revision 4 → 5, after review round 4
Round 4 accepted the core and both surfaced decisions — the defensive
parse stub and fail-closed filesystem uncertainty — but found two
contract defects plus a ledger-ownership defect. All accepted.
**P1 — the narrowed reporting promise had one stale consumer.** Q#RD7
correctly says the unattended path always **attempts** a response while
the channel remains live, but §2.1 still said it **always answers**.
That normative consumer now uses the exact Q#RD7 promise. The revision
4 audit missed it because the literal search `always answers` did not
match Markdown's `**always** answers`. §1.15 therefore adds one more
procedural rule: search normalized prose or term stems, not only an
exact rendered phrase containing markup.
**P1 — Q#RD12 called a three-row verdict total when it was not.** It
omitted absent-without-ignore, defined `conflict` as a named modified
buffer while also assigning stat failures with no buffer to it, and did
not carry Q#RD2's `editing_in_progress` condition into the shared query.
The verdict is now `no-op` / `clear` / `refuse`, with a required message
on every refusal and an optional buffer name only for buffer-caused
refusals. The total mapping is explicit: absent-plus-ignore is `no-op`;
absent without ignore and an unanswerable stat are `refuse`; a modified
or mid-edit affected buffer is `refuse`; only a present target with a
clean, quiescent affected set is `clear`. Criteria 11c and 11d pin the
two filesystem refusal directions before any earlier batch op mutates.
**P1 — merge order was being used as ledger ownership.** Revision 4
rewrote #171's full lane entry from #186. #171 revision 8 landed 67
seconds earlier with different state, making #186's copy stale before
it was pushed; #186's own `3 / 0` count was also one short because the
revision commit itself had not yet been counted. The sibling block is
restored to `main`'s version, so #186 no longer changes it. #171 owns
its entry on its branch. This lane records only the stable split it
depends on and checks the pushed sibling framing for semantic changes,
without copying its volatile head/count/line state.
**Coordination decision.** #186 lands before #171 for a product reason,
not a merge convenience: it closes live data loss, and #171 explicitly
adopts its refusal and shared query. #171 integrates the result and
reconciles its own ledger entry. The generated-buffer lane is
independent; `journey-stage1a-directory-open` has no unmerged ledger
delta.
### Revision 3 → 4, after review round 3
Round 3 accepted the core — pre-filesystem refusal, four-phase ordering,
@ -634,36 +676,27 @@ treats the buffer as a cache of the file.
Both arms share the §1.4 lookup defects.
**Cross-lane state, re-measured at revision 4.** PR #171 (dired
Stage 2) is at **revision 7, `fd7ae37`, merge-base `ad41cf1`**:
**Cross-lane contract, rechecked at revision 5.** PR #171's pushed
revision 8 (`7ecea94`) retains revision 7's split unchanged. This
document deliberately does **not** copy its ahead/behind count, line
count, or full lane status: those are volatile state owned by #171's
branch, and revision 4 proved that a sibling copy can be false before
the copying commit is pushed. The historical correction still matters:
revision 2 described #171 as stale and rename-only, while its revision 6
had assigned rename *and* delete reconciliation to Stage 2a with the
opposite policy — `reconcile_delete` killing unmodified buffers and
keeping modified ones alive, so the file was deleted and the modified
buffer orphaned. Revision 7 withdrew that policy and established the
split below; revision 8 does not reopen it.
```
$ git rev-list --left-right --count fd7ae37...7586905
13 2
```
Thirteen ahead, **two behind** — and those two are exactly PR #189's
COHERENCE change, so no cross-lane conclusion turns on them. Revision 3
of this document said "0 commits behind"; that was measured against
`ad41cf1` and reported after `main` had moved to `7586905`, which is the
third instance in this lane of quoting a census as a constant. Revision 2 of this
document described it as "OPEN, STALE, 153 commits behind, under
re-scout" and said it claimed the rename side only; **both halves of
that were out of date**. Its revision 6 assigned rename *and* delete
reconciliation to Stage 2a with the opposite policy — `reconcile_delete`
killing unmodified buffers and keeping modified ones alive, so the file
is deleted and the modified buffer orphaned, with that orphaning named
as accepted residue.
**The settled split** (identical wording carried by both lanes):
**The stable ownership split carried by both lanes:**
> #186 owns the urgent **pre-filesystem refusal** for synchronous
> `apply_resource_op`. #171 later owns **full post-delete lifecycle
> reconciliation**, including the **async race where a buffer becomes
> modified after dired dispatch**. #171's revision 7 adopts the refusal
> and stops saying LSP intentionally deletes modified files.
> modified after dired dispatch**.
#171 revision 7 has adopted this from its side: its Q#DR18 takes this
#171 has adopted this from its side: its Q#DR18 takes this
document's Q#RD1 refusal rather than re-deciding it, and it records the
reason the refusal cannot simply be extended to cover dired — **dired
never calls `apply_resource_op`**. It calls `pmacs.fs.remove`, which
@ -798,10 +831,13 @@ contradictory. A withdrawal recorded in an audit while the claim stays
load-bearing elsewhere is worse than no withdrawal, because the audit
converts an error into a false assurance.
**So the audit procedure is, from revision 4:** for each row, grep the
whole document for the claim's terms and check every hit, not the
defining section. Revision 4 ran that and found two surviving
consumers (§1.15 is the audit; the fix is in Q#RD3 and §1.11).
**So the audit procedure is, from revision 5:** for each row, search the
whole document for the claim's terms and check every hit, not only the
defining section. Search normalized prose or multiple term stems as
well as exact phrases: revision 4's literal `always answers` search
missed §2.1's `**always** answers` because Markdown markup split the
phrase. Revision 4 found the two surviving `Abort` consumers; revision
5 found and fixed that reporting consumer.
| # | Claim | Source | Status |
|---|---|---|---|
@ -846,14 +882,15 @@ filesystem failure leaves every buffer intact automatically, and
already gone, preserving today's invariant.
**Layer 2 — the applier (early conflict check + robust reporting).**
`apply_workspace_edit`'s existing plan loop gains a modified-buffer
conflict check for delete ops and returns its existing `nil, message`.
This is a **filter, not a transaction** (§1.7): it catches the common
case cheaply, before anything is mutated, and it is honest that a
sequential batch can still refuse mid-flight. What makes mid-flight
refusal survivable is Q#RD7: each primitive call is wrapped, every
failure becomes `nil, message`, the origin buffer is restored
best-effort, and the unattended caller **always** answers the server.
`apply_workspace_edit`'s existing plan loop gains a delete-precondition
check and returns its existing `nil, message`. This is a **filter, not a
transaction** (§1.7): it catches the plan-time buffer conflict and
filesystem refusals cheaply, before anything is mutated, and it is
honest that a sequential batch can still refuse mid-flight. What makes
mid-flight refusal survivable is Q#RD7: each primitive call is wrapped,
every failure becomes `nil, message`, the origin buffer is restored
best-effort, and the unattended caller always **attempts** a response
while the response channel remains live.
Neither layer is redundant. Layer 1 alone leaves every batch failure
reported through a channel that does not work (§1.5). Layer 2 alone
@ -952,12 +989,13 @@ affected buffers → mutate the filesystem → reconcile the registry.**
### Q#RD3 — The preflight is an early conflict check, **not** a transaction — **DOWNGRADED at rev 2**
`apply_workspace_edit`'s plan loop gains a modified-buffer conflict
check for delete ops and returns its existing `nil, message`. It is
described in the code comment and here as a **filter**:
`apply_workspace_edit`'s plan loop gains a delete-precondition check and
returns its existing `nil, message`. It is described in the code comment
and here as a **filter**:
- **What it guarantees:** when the conflict is visible at plan time,
nothing in the batch is mutated at all, and the user gets one clear
- **What it guarantees:** a plan-time modified/mid-edit buffer, a known
missing target without `ignore_if_not_exists`, or an unanswerable stat
refuses before anything in the batch is mutated, with one clear
message.
- **What it does not guarantee, stated plainly:** `documentChanges` are
sequential (§1.7). An earlier text edit can dirty a clean buffer, and
@ -973,9 +1011,9 @@ described in the code comment and here as a **filter**:
- Revision 1 called this "whole-batch atomicity" and said "nothing in
the batch is mutated". **That was false and is withdrawn.**
The check needs a path-keyed modified query that Lua lacks (§1.4). It
must be **one** query shared with the primitive's validation phase, so
the two cannot drift apart.
The check needs a synchronous filesystem-and-buffer query that Lua lacks
(§1.4, Q#RD12). It must be **one** query shared with the primitive's
stat/validation phases, so the two cannot drift apart.
### Q#RD4 — `ignore_if_not_exists` short-circuits at **both** layers — **WIDENED at rev 2**
@ -1012,8 +1050,8 @@ guard is bypassed by the most destructive arm. Therefore:
The asymmetry is deliberate and is the point: **inspect widely, mutate
narrowly.**
**Boundary with dired — restated at rev 3.** The settled split (§1.12,
quoted there verbatim and carried identically by #171) is:
**Boundary with dired — restated at rev 5.** The settled split (§1.12,
also carried by #171) is:
> #186 owns the urgent **pre-filesystem refusal** for synchronous
> `apply_resource_op`. #171 later owns **full post-delete lifecycle
@ -1023,8 +1061,9 @@ quoted there verbatim and carried identically by #171) is:
**The stale justification is withdrawn.** Revision 2 supported taking
the delete side now by citing the ledger's "OPEN, STALE, 153 commits
behind, under re-scout" assessment of #171. That re-scout has finished;
#171 is at revision 7, integrated to `ad41cf1`. **The conclusion is
unchanged and rests on urgency alone** — this is a live data-loss bug
#171 has completed the re-scout and retains the settled split through
its pushed revision 8. **The conclusion is unchanged and rests on
urgency alone** — this is a live data-loss bug
with a reproduction, and a refusal that must precede the filesystem call
cannot be deferred to a lane that acts after it. It no longer rests on
any claim about #171's freshness, and it must not be re-argued from one.
@ -1050,8 +1089,8 @@ The shared query therefore:
and the Lua preflight (Q#RD3).
**This lane claims the query.** The boundary's rule is "whichever lands
first owns the query and the other adopts it", and #171 revision 7
records that this rule's four clauses are character-for-character what
first owns the query and the other adopts it", and #171's current
framing records that this rule's four clauses are character-for-character what
it had written independently for `reconcile_delete`. To stop both lanes
asserting ownership: **#186 owns and implements the shared walk**, #171
adopts it and extends it to `reconcile_rename`. If #171 lands first the
@ -1220,7 +1259,7 @@ than dressing up a reachable payload — a criterion that cannot fail is
not a pin, and pretending otherwise is the defect this decision exists
to avoid.
### Q#RD12 — The preflight needs a structured Rust-backed seam, not a registry walk alone — **NEW at rev 4**
### Q#RD12 — The preflight needs a total Rust-backed verdict, not a registry walk alone — **REWRITTEN at rev 5**
Q#RD4 requires the Lua preflight to distinguish **absent + ignore** (a
no-op the preflight must let through) from **present + ignore** (a real
@ -1240,28 +1279,51 @@ Nothing in Lua closes that gap today:
preflight that disagrees with the primitive on the one input that
matters.
**The seam: one synchronous Rust binding returning a structured verdict**,
evaluated with the *same* `symlink_metadata` call the primitive uses, so
the two layers cannot disagree by construction:
**The seam: one synchronous internal Rust binding,
`pmacs.buffer._delete_verdict(spec)`, returning a structured verdict.**
It accepts the same `path`, `recursive`, and `ignore_if_not_exists`
fields as the delete primitive and delegates to the same Rust helper,
including the same `symlink_metadata` call and affected-set walk, so the
two layers cannot disagree by construction. The Lua-visible shape is
`{ kind = "...", message = ..., buffer_name = ... }`; `message` is
required and non-empty for `refuse`, and `buffer_name` is present only
when a buffer caused the refusal:
| Verdict | Meaning |
|---|---|
| `no-op` | path absent **and** `ignore_if_not_exists` set — the op will do nothing; the preflight must not reject it |
| `clear` | the delete may proceed: no matching buffer is modified |
| `conflict` | at least one matching buffer (at the path, or beneath it for a recursive delete) is modified — refuse, with the buffer named |
| `clear` | path present, and every affected buffer is clean and not mid-edit — the delete may proceed |
| `refuse` | delete must not proceed: missing without ignore, stat uncertainty, or an affected buffer modified/mid-edit; `message` states which, and buffer-caused refusals name it |
**Error contract.** The binding is total over its inputs and does not
raise for ordinary filesystem conditions — absence is a verdict, not an
error. It raises only on argument-type violations, matching the rest of
the `pmacs.buffer` surface. A stat error that is neither success nor
`NotFound` (e.g. `EACCES` on a parent directory) yields `conflict`, not
`clear`: the preflight must never report "safe to delete" on the
strength of a question it could not answer. That asymmetry is
deliberate — the failure direction is toward refusing.
**Total mapping and error contract.**
This binding **is** the single shared query of Q#RD6: the walk is its
buffer half, the `symlink_metadata` call its filesystem half, and the
primitive's validation phase calls the same function so drift is
1. `symlink_metadata == NotFound` plus `ignore_if_not_exists` yields
`no-op`.
2. `NotFound` without ignore yields `refuse` with the ordinary delete
I/O message. Catching this deterministic failure in the plan makes
that case more atomic than today without claiming the batch is a
transaction; dynamic failures remain possible.
3. Any other stat error (for example `EACCES` or `NotADirectory`) yields
`refuse` carrying that I/O reason. The preflight never reports safe
on the strength of a question it could not answer.
4. A present path whose affected set contains a modified or
`editing_in_progress` buffer yields `refuse` naming that buffer.
5. Only a present path with a clean, quiescent affected set yields
`clear`.
The binding raises only on argument-type violations, matching the rest
of the `pmacs.buffer` surface. Ordinary filesystem conditions and buffer
refusals are values.
The callers consume the same Rust enum in different forms. The
primitive returns `Ok(())` for `no-op`, turns `refuse` into its ordinary
Lua error carrying the verdict message, and reaches filesystem mutation
only for `clear`. The Lua plan lets `no-op` and `clear` through and
returns its existing `nil, message` for `refuse`.
This helper **is** the single shared query of Q#RD6: the walk is its
buffer half, `symlink_metadata` its filesystem half, the binding
serializes its result, and the primitive consumes it directly. Drift is
impossible rather than merely discouraged.
## 4. Bets (falsifiable)
@ -1391,6 +1453,23 @@ that passes against its pre-image has no bite and is rejected.
input on which realpath and `symlink_metadata` disagree, and the
reason Q#RD12 specifies the latter.
11c. **Absent without ignore refuses in the plan, before earlier ops**
(Q#RD3, Q#RD12). A batch contains a text edit followed by a delete
of a missing target with `ignore_if_not_exists = false`. Assert
`applied = false`, a non-empty NotFound-style `failureReason`, and
that the earlier text edit was not applied.
*Bite:* fails if the verdict maps this state to `clear` and leaves
the primitive to discover it mid-batch; that implementation would
partially apply the text edit before returning the known error.
11d. **An unanswerable stat fails closed in the plan** (Q#RD12). Use a
regular file as a would-be parent and target its child, producing
`NotADirectory` on the supported CI platforms. Assert
`applied = false`, the earlier batch op did not apply, and the
`failureReason` carries the filesystem cause.
*Bite:* fails if a non-NotFound stat error is collapsed to `clear`
or if the binding raises past the value-returning boundary.
12. **Edit-then-delete and rename-into-delete still answer the server**
(Q#RD3, Q#RD7). Two batches that defeat the snapshot preflight: one
where an earlier text edit dirties the buffer a later op deletes,
@ -1505,7 +1584,8 @@ suites; `cargo test --test m4_acceptance -- --skip basedpyright`;
`PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`; `git diff --check`.
Touched suites: **`m4_acceptance`** (the resource-op home, §1.14, and
the home of criteria 114 and 16) and **`lsp_dispatch_seams_acceptance`**
the home of criteria 114 including 11a11d, and 16) and
**`lsp_dispatch_seams_acceptance`**
(criterion 15's throwing parse stub, Q#RD11). Both appear in §8's touch
table; revision 3 named the second here but omitted it there, and the
two lists are now maintained together.
@ -1532,16 +1612,16 @@ approved the implementation commits land on this same branch.
**Implementation does not begin until the user approves this revision.**
**Files the implementation will touch** — reconciled at rev 4 against
**Files the implementation will touch** — reconciled at rev 5 against
the gate list below and §5, which revision 3 left disagreeing:
| File | Why |
|---|---|
| `src/lua_bindings/mod.rs` | the delete arm's four phases (Q#RD2); the shared query binding and its structured verdict (Q#RD6, Q#RD12); the narrow `*errors*` append surface (Q#RD7) |
| `builtin/runtime/lsp.lua` | the preflight conflict check (Q#RD3); the parse-plus-apply wrap, origin restore, and boundary logging (Q#RD7) |
| `tests/m4_acceptance.rs` | criteria 114, 16 |
| `tests/m4_acceptance.rs` | criteria 114 including 11a11d, and 16 |
| `tests/lsp_dispatch_seams_acceptance.rs` | criterion 15's throwing parse stub (Q#RD11) — this file was named in the gate list but omitted from revision 3's touch list |
| `src/bin/pmacs_fake_lsp.rs` | fake modes: blocked delete; edit-then-delete; rename-into-delete; absent-plus-ignore; present-plus-ignore (11a); dangling-symlink (11b) |
| `src/bin/pmacs_fake_lsp.rs` | fake modes: blocked delete; edit-then-delete; rename-into-delete; absent-plus-ignore; present-plus-ignore (11a); dangling-symlink (11b); absent-without-ignore (11c); unanswerable-stat (11d) |
It will **not** touch `src/daemon.rs`, `pmacs-protocol/`,
`builtin/runtime/dired.lua`, `docs/agent-handoff.md` or `COHERENCE.md`.
@ -1552,8 +1632,8 @@ alongside the rest, that is a permitted simplification — but then
`lsp_dispatch_seams_acceptance` drops out of the gate list too, and the
two lists move together. Revision 3's defect was that they did not.
**Ownership note — restated at rev 3 against #171 revision 7.** The
settled split is quoted in §1.12 and Q#RD5 and is carried identically by
**Ownership note — rechecked at rev 5 against #171's pushed revision
8.** The settled split is quoted in §1.12 and Q#RD5 and is carried by
both lanes. Concretely, this lane claims for its duration:
- the **pre-filesystem refusal** inside synchronous `apply_resource_op`;