docs(framing): revision 2 of the resource-op delete guard
Review round 1 approved the refusal strategy in principle and rejected
revision 1 as written. Six blocking points, all accepted, plus three
further overclaims found by the requested sweep. Q#RD1 and Q#RD5 are
settled yes; Q#RD9 is settled no and is withdrawn.
Q#RD2 conflated inspection with removal. Revision 1 removed the buffer
before the filesystem call, which fires arbitrary Lua `on_removed`
callbacks while the file still exists and accepts losing the buffer if
the deletion then fails. The sequence is now `stat/no-op -> enumerate
and validate -> mutate filesystem -> reconcile`. Validation inspects
`is_modified` and `editing_in_progress` without removing anything, so a
failed deletion leaves buffers intact automatically and `on_removed`
still observes the path already gone.
Q#RD3 overclaimed whole-batch atomicity. `documentChanges` are
sequential, so an earlier edit can dirty a clean buffer and an earlier
rename can move a modified buffer into a later delete's subtree, after
the snapshot. LSP 3.18 assigns `FailureHandlingKind.Abort` to any edit
containing resource changes --- "all operations executed before the
failing operation stay executed" --- so the protocol itself declines to
promise what revision 1 claimed. The preflight is now described as an
early conflict check, with robustness coming from per-op `pcall`, an
always-sent server response, and best-effort origin restore.
The lookup cannot be `EditorCore::find_buffer_for_path`: it normalizes
but delegates to the first-match-only `find_by_path`, and
`pmacs.buffer.from_file` creates path-bound buffers with no dedup, so a
clean first match can hide a modified second. Q#RD6 now requires a full
scan with component-aware `Path::starts_with`.
Recursive deletion now inspects the tree but reconciles only the exact
path, so the parked lifecycle defect stays exact-path rather than
becoming tree-wide. Q#RD4 holds at both layers, so the preflight cannot
reject an absent path the primitive treats as a no-op.
The prompt argument was overclaimed and that was my error.
`pmacs.lsp.send_response` takes `request_id` as an ordinary value, so a
`workspace/applyEdit` can be answered on a later tick, and a callback
continuation would reuse the existing minibuffer shadow rather than add
a seventh dispatcher rung. Prompting is expensive and separately
scoped, not impossible; the section now claims only what the evidence
carries.
Sweep found three more of the same defect class --- an absence or a
guarantee asserted rather than established:
* a durable error surface does exist (`append_to_errors_buffer` ->
`*errors*`), so Q#RD7 now records the refusal there as well;
* no caller reliably surfaces a raise, because the async path routes
uncaught coroutine errors through the undefined `pmacs.error`;
* pmacs advertises no `workspace.workspaceEdit` capability at all ---
no `documentChanges`, no `resourceOperations`, no
`failureHandling`.
Adds seven acceptance pins with their bite obligations, adds the
`docs/active-work.md` lane the ledger requires for every open PR, and
drops the two-PR plan: #186 is revised in place and becomes the
implementation PR.
Still PROPOSED. No runtime code. Implementation begins only after
explicit user approval.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
This commit is contained in:
parent
aae113ba76
commit
e1e9b44154
|
|
@ -459,6 +459,78 @@ has **no branch and no framing yet**.
|
|||
`FrontendView.fold_projection` to `true` for semantic frontends, which
|
||||
Stage 2 deliberately left `false` (Q#FD21).
|
||||
|
||||
## Resource-op delete guard lane — PR #186 OPEN, PROPOSED, DO NOT MERGE
|
||||
|
||||
- Portable branch: `githubsucks/resource-op-delete-guard`; worktree
|
||||
`../pmacs-resource-op-delete`. **PR #186**, base `main`, forked from
|
||||
`ad41cf1` with **no drift** (`main` is still `ad41cf1`). Currently
|
||||
framing only — `docs/resource-op-delete-guard-framing.md`, revision 2
|
||||
— plus this lane entry. No runtime code yet.
|
||||
- **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
|
||||
commits land on this same branch **only after explicit user
|
||||
approval**.
|
||||
- **Live data-loss bug, reproduced four ways against `ad41cf1`.**
|
||||
`pmacs.buffer.apply_resource_op`'s delete arm removes the path from
|
||||
disk and *then* drops any buffer bound to it, with no dirty check at
|
||||
any link — not the arm, not `remove_buffer_and_fire`, and not
|
||||
`BufferRegistry::remove`, whose only guard is `editing_in_progress`.
|
||||
Reachable through any language server's `WorkspaceEdit`. The four
|
||||
modes: (a) the plain case returns `Ok(())` with file and buffer both
|
||||
gone; (b) `ignore_if_not_exists = true` does **zero** filesystem work
|
||||
and still destroys the buffer; (c) `recursive = true` reconciles
|
||||
**nothing**, so a whole tree leaves orphaned buffers — the most
|
||||
destructive arm does the least reconciliation, and it bypasses any
|
||||
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 1 rejected.
|
||||
Settled: refuse unconditionally; take delete-side prefix-awareness now
|
||||
rather than waiting on #171. Withdrawn: rev 1's buffer-first ordering.
|
||||
The design is now `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.
|
||||
- **Four facts a re-scout should not have to rediscover**, all verified
|
||||
at `ad41cf1`:
|
||||
- **No caller reliably surfaces a raise.** The server pump runs under
|
||||
`pcall(handle_server_requests)` (`builtin/runtime/lsp.lua:1892`), so
|
||||
a raise unwinds past the `send_response` and the server is never
|
||||
answered; and the two user-initiated paths route uncaught coroutine
|
||||
errors through `pmacs.error`, which is **undefined** (11 call sites
|
||||
in `builtin/`, zero definitions). Refusals must travel as values.
|
||||
- **A partial batch is already the status quo** — verified: two delete
|
||||
ops, the second raises, the first stayed applied. LSP 3.18 says so
|
||||
too: resource-op-bearing edits get `FailureHandlingKind.Abort`,
|
||||
"all operations executed before the failing operation stay
|
||||
executed". Any framing claiming batch atomicity here is wrong.
|
||||
- **`find_by_path` is singular and duplicates are reachable.**
|
||||
`BufferRegistry::find_by_path` returns the first match in insertion
|
||||
order, `EditorCore::find_buffer_for_path` inherits that, and
|
||||
`pmacs.buffer.from_file` creates path-bound buffers with **no
|
||||
dedup** — so a clean first match can hide a modified second. The
|
||||
guard needs a full scan with component-aware `Path::starts_with`.
|
||||
- **pmacs advertises no `workspace.workspaceEdit` capability at all** —
|
||||
`"applyEdit": true` but no `documentChanges`, no
|
||||
`resourceOperations`, no `failureHandling`; `grep -rn
|
||||
failureHandling` returns 0. Parked, not fixed here.
|
||||
- **Ownership claim, per the dired lane's own warning below:** dired
|
||||
Stage 2a is "rename/delete reconciliation substrate" and overlaps
|
||||
`builtin/runtime/lsp.lua`. **This lane claims the delete half of that
|
||||
substrate and the `apply_workspace_edit` applier for its duration**;
|
||||
dired Stage 2 keeps the rename half. Whichever lands second adopts the
|
||||
first's shared lookup helper. Do not run the two concurrently over
|
||||
`builtin/runtime/lsp.lua` without re-splitting that claim.
|
||||
- Files the implementation will touch: `src/lua_bindings/mod.rs`,
|
||||
`builtin/runtime/lsp.lua`, `tests/m4_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.
|
||||
- Recovery from a clean checkout:
|
||||
`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, STALE, DO NOT MERGE AS-IS
|
||||
|
||||
- Portable branch: `githubsucks/dired-stage2-framing` (head `ab42a79`,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue