pmacs/builtin/runtime
Levi Neuwirth 1873be6141 feat(lsp): refuse a resource-op delete that would destroy unsaved work
Implements the framing merged as #186. On `main` today,
`pmacs.buffer.apply_resource_op`'s delete arm removes a file and then
removes any buffer bound to it, with no dirty check at any link in the
chain — so a server-driven delete destroys unsaved edits, and the
`ignore_if_not_exists` arm destroys them having done no filesystem work
at all.

Layer 1 — the primitive. The delete arm becomes four ordered phases:
stat/no-op decision, enumerate and validate, mutate the filesystem,
reconcile the registry. Validation inspects and removes nothing, so a
filesystem failure leaves every buffer intact automatically rather than
by compensation, and `on_removed` still observes the path already gone
because reconciliation stays last.

`delete_verdict` is the single shared query. It scans *every*
path-bound buffer rather than the first match, because `find_by_path`
is first-match-only and `pmacs.buffer.from_file` makes duplicates
reachable — a clean first match could otherwise hide a modified second.
It normalizes both sides before comparing and uses component-aware
`starts_with`, so `/tree` does not match `/tree-sibling`. It stats with
`symlink_metadata`, not `canonicalize`, which reports a dangling
symlink as absent and would disagree with the primitive on exactly the
input `ignore_if_not_exists` turns on.

Layer 2 — the applier and the server-request boundary.
`apply_workspace_edit` gains a plan-time delete precondition check
driven by the same Rust helper, so the two layers cannot drift. It is a
filter, not a transaction, and the code says so: `documentChanges` are
sequential, so an earlier edit can dirty a buffer a later op deletes.
The applier is now total — every failure becomes `nil, message`, and
the origin buffer is restored on the failure path as well as the
success path. At the boundary, parse *and* apply are wrapped:
`_parse_workspace_edit` sits one line above the applier and is
fallible, so a parse failure previously escaped, was swallowed by
`pcall(handle_server_requests)`, and left the server unanswered — the
defect being fixed, one line out of scope. Failures now also append one
labelled record to `*errors*`.

Scope, stated plainly rather than implied by what is present:

  * Acceptance criteria 1-10, 14 and 16 land here — 11 tests driving
    the primitive directly. Criteria 11, 11a-11d, 12, 13 and 15 do
    NOT: they exercise Layer 2 through a real server pump and need
    `pmacs_fake_lsp` modes that do not exist yet. Criterion 13
    explicitly rejects a direct-call test as insufficient, so the
    Layer 2 code currently has no production-path pin. That is a real
    gap and the reason this is not the whole lane.

  * The framing's §8 branch plan said the implementation would land on
    #186 itself. #186 merged as framing-only, so it gets its own
    branch and PR. No decision changes.

  * Criterion 3's stated bite in the framing is wrong. It claims to
    fail against buffer-first ordering; it does not, because the
    deleted path is a directory no buffer is bound to, so the
    reordering never fires on that input. It does fail against
    validation that removes rather than inspects. Checked by mutation
    rather than trusted, and the test comment carries the correction.

Bite: criteria 1, 5, 6, 8 and 10 fail against `githubsucks/main` under
`scripts/bite`. Criteria 3 and 4 pin phase ordering against designs
never committed, so `main` cannot falsify them; both were verified by
hand mutation instead. Criteria 2, 7, 9 and 14 assert preserved or
deliberately-unchanged behaviour and pass against `main` by design —
2 is criterion 1's opposite direction, 9 pins today's imperfect
orphaning so widening cannot happen silently.

Gates: fmt; clippy -D warnings; --lib 1863; --lib --features crdt
2048; m4_acceptance 132; lsp_dispatch_seams_acceptance 15;
dired_acceptance 25 and autosave_acceptance 29 (the framing's watch
items); PMACS_REQUIRE_GPU=1 -p pmacs-gpu 202; git diff --check clean.

No protocol change.
2026-07-29 09:47:35 -04:00
..
async.lua feat(journey): open a directory, on one path 2026-07-26 16:39:59 -04:00
autosave.lua feat(config): typed configuration registry with buffer-local scope 2026-07-21 18:29:18 -04:00
comment.lua feat(lean4): editing surface + Stage 1 acceptance (Q#LN5, LN6, LN17) 2026-07-25 09:59:48 -04:00
compile.lua fix(terminal): implement the Q#BP7 growth re-arm and pin it honestly 2026-07-24 18:32:23 -04:00
completion.lua fix(completion): address TUI-validation findings (LSP query gaps, scoping, prefix keys, window scope) 2026-07-07 16:54:14 -04:00
desktop.lua fix(persistence): reliable daemon gate, unarm, per-pane after-load 2026-07-08 22:27:24 -04:00
dired.lua fix(journey): honor the captured window, not the selected one 2026-07-26 18:20:02 -04:00
editops.lua fix(config): reject wrongly-typed spec fields; make trim-on-save buffer-aware 2026-07-21 18:29:18 -04:00
fold.lua feat(fold): Arc 6 Stage 1 — instance fold engine (headless) 2026-07-23 12:14:00 -04:00
fs.lua Merge remote-tracking branch 'githubsucks/main' into lean4-stage3a-seams 2026-07-26 09:25:25 -04:00
indent.lua fix(edit): PR #109 round 1 — shared search invalidation, daemon anchor clear, bounded indent scan 2026-07-10 15:46:36 -04:00
killring.lua feat(edit): editing-conveniences pack (editops) 2026-07-12 15:33:17 +01:00
lean.lua fix(lsp): close round-six Lean fallback gaps 2026-07-25 22:32:48 -04:00
lean_abbrev.lua feat(lean4): the Unicode input method (Arc 8 Stage 4b) 2026-07-26 16:24:06 -04:00
lean_input.lua fix(lean4): count fan-outs where a claim cannot skip the count 2026-07-26 17:15:24 -04:00
listview.lua fix(window): wire the side-window split guard and scope the divider drag 2026-07-24 15:07:35 -04:00
lsp.lua feat(lsp): refuse a resource-op delete that would destroy unsaved work 2026-07-29 09:47:35 -04:00
mcp.lua M9 ship gate 2026-05-09 15:04:23 -04:00
pair.lua feat(typed-edit): the typed-edit consumer chain (Arc 8 Stage 4a) 2026-07-26 13:00:03 -04:00
recentf.lua fix(persistence): symlink confinement, real test-inertness, view_top restore 2026-07-08 18:21:46 -04:00
saveplace.lua fix(persistence): symlink confinement, real test-inertness, view_top restore 2026-07-08 18:21:46 -04:00
syntax.lua feat(lean4): editing surface + Stage 1 acceptance (Q#LN5, LN6, LN17) 2026-07-25 09:59:48 -04:00
terminal.lua fix(buffer): fan out generated writes, and clear the history that exists 2026-07-26 13:43:22 -04:00
typed_edit.lua fix(typed-edit): close round-8 review on the consumer chain 2026-07-26 13:39:33 -04:00
window.lua feat(window): adopter placement opt-in and the Stage 1 acceptance suite 2026-07-24 14:10:45 -04:00