Commit Graph

5 Commits

Author SHA1 Message Date
Levi Neuwirth c7ad4f491f 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.
2026-07-28 20:55:01 -04:00
Levi Neuwirth 46b3c8db1a docs(framing): revision 4 of the resource-op delete guard
Review round 3 accepted the core --- pre-filesystem refusal, four-phase
ordering, the #171 split, Q#RD10 --- and raised four P1s and a P2. All
accepted.

The external-claim audit added in revision 3 certified this document as
clean while Q#RD3, the normative decision, still asserted the very LSP
claim the audit's own row 4 marked WITHDRAWN. It did so because it
checked each claim where it was DEFINED and never at the sites that
CONSUMED it, so a withdrawal recorded in one section coexisted with the
same claim doing load-bearing work three sections away. That is a defect
in the audit procedure rather than a missed line, and since the table is
now an artifact readers trust, section 1.15 records both the failure and
the corrected procedure: grep the whole document per row and check every
hit.

P1, lane state. Re-measured: `git rev-list --left-right --count
fd7ae37...7586905` returns `13  2`, so #171 is two commits behind, not
zero, and those two are exactly PR #189's COHERENCE change. Revision 3's
"0 behind" was taken against `ad41cf1` and reported in present tense
after main had moved. Both ledger lane entries are corrected in place
--- #186's revision and base, and #171's superseded `ab42a79` /
153-behind / merge-base-`c8ec8f3` ground truth --- rather than having a
correction layered above stale text, which is what made the ledger
self-contradicting. Every count now appears as pasted command output.

P1, withdrawn claim surviving. Q#RD3 called partial application
`FailureHandlingKind.Abort`, "the strategy the spec itself assigns to
any edit containing resource changes", and section 1.11 called `Abort`
the default "by omission". The spec establishes no default for a client
advertising no strategy. Both sites now say only that verified pmacs
behaviour resembles abort-style application, resting on the reproduction
in section 1.6.

P1, acceptance 15. `WorkspaceEditResponse::from_lsp_value` returns
`Self`, its doc says a shapeless result yields an empty response, and
the binding's only `?` is `lua_to_json` over a value that arrived
through `json_to_lua` --- so no server payload can make the parse fail
and the criterion could not fail. Decision, new Q#RD11: keep the wrap,
drive the test with an explicit throwing stub, label it defensive.
Q#RD7's promise narrows to "always attempts a response while the
response channel remains live", since `send_response` is itself under an
ignored `pcall`.

P1, absent-plus-ignore. `pmacs.fs.stat` dispatches async and
`canonicalize` resolves symlinks and returns nil for a dangling one, so
it disagrees with the primitive's `symlink_metadata` on exactly the
input this query turns on. New Q#RD12 specifies a structured Rust-backed
verdict --- no-op / clear / conflict --- evaluated with the same
`symlink_metadata` call, with an error contract that fails toward
refusal. New criteria 11a and 11b supply the missing opposite direction.

P2. Criterion 14's "fails in both directions" was false: with both
duplicates clean the setup cannot distinguish first-match validation
from full validation. The claim is fixed rather than the setup, because
criterion 6 already pins validation breadth. Section 8's touch table and
section 7's gate list are reconciled.

Sweep for corrections applied at one site while a dependent site kept
the old claim: count 4 --- the two `Abort` sites above, and the stale
#171 count in both the revision history and section 1.12. Nine other
withdrawn or revised claims were checked at every consuming site and
found clean.

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
2026-07-28 20:31:36 -04:00
Levi Neuwirth aea5097890 docs(framing): revision 3 of the resource-op delete guard
Review round 2 confirmed everything central from round 1 as fixed and
raised four P1s. All four accepted; both requested sweeps run. Rebased
onto `7586905`.

P1-1, the ownership boundary was stale. Revision 2 described PR #171 as
"OPEN, STALE, 153 commits behind, under re-scout" claiming the rename
side only. Re-checked directly: #171 is at revision 7, `fd7ae37`,
merge-base `ad41cf1`, zero commits behind, and its revision 6 had
assigned rename AND delete to Stage 2a with the opposite policy --- file
deleted, modified buffer kept alive and orphaned, that orphaning named
as accepted residue. Two lanes, opposite answers, same event. The
settled split is now carried verbatim in the framing and the ledger, and
#171 revision 7 has adopted it from its side.

P1-2, the LSP failure-handling claim was wrong. The spec does not assign
`Abort` to resource-op-bearing edits. Recovery is described by the
client's advertised `workspace.workspaceEdit.failureHandling`; `Abort`
is one of four strategies, `Transactional` covers all operations, `Undo`
attempts rollback, and only `TextOnlyTransactional` degrades to abort
when resource changes are present. pmacs advertises none, so the spec
assigns pmacs no strategy at all. Section 1.7 and B2 now rest on
verified repository behaviour --- the reproduced partial batch --- not
on borrowed protocol authority.

P1-3, Q#RD7 had no implementable seam. Three gaps confirmed by reading:
`_parse_workspace_edit` is called one line above `apply_workspace_edit`
and is fallible, so the proposed wrap left "always answers" untrue;
`append_to_errors_buffer` is private and a Lua preflight rejection never
reaches Rust, so the promised logging was not implementable from where
it was promised; and acceptance 13 tested the response but not the
trace. Q#RD7 is rewritten around one seam at the server-request
boundary, and of the two options offered this revision picks wrapping
parse-plus-apply rather than narrowing the claim.

P1-4, clean duplicate reconciliation is now Q#RD10: validate every
match, reconcile today's first exact-path match only. Widening would
enlarge the parked lifecycle defect Q#RD5 exists to contain; the
surviving clean duplicate is named as residue handed to #171.

External-claim sweep. Every non-repo claim is now listed in a new
section 1.15 with its evidence. One was a paraphrase standing in for a
quote: "in Emacs `kill-buffer` on a modified file-visiting buffer
prompts". It is true, but the gate is `INTERACTIVE`, defined as
`(NILP (Vexecuting_kbd_macro) && !noninteractive)` --- keyboard present,
not `call-interactively` --- so eglot's programmatic kill does prompt in
a normal session and does not in batch or during a keyboard macro. The
revision 2 sentence was right for a reason it never established and
false in two environments it never considered.

Cross-lane sweep. Q#RD8 said mode (d) needs its own lane; it has one now
--- #171's `reconcile_delete` composes both removal phases. Q#RD6 claims
the shared walk query explicitly so the duplicate resolves in one
direction. And `pmacs.fs.remove`, guarded by neither lane and verified
to have zero production callers, is named as explicitly out of scope
with its owner rather than left to read as covered.

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
2026-07-28 19:01:19 -04:00
Levi Neuwirth e1e9b44154 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
2026-07-28 18:49:24 -04:00
Levi Neuwirth aae113ba76 docs(framing): guard the resource-op delete arm against data loss
`pmacs.buffer.apply_resource_op`'s delete arm removes the file from
disk and then unconditionally drops any buffer bound to that path.
There is no dirty check at any link in the chain --- not in the arm,
not in `remove_buffer_and_fire`, and not in `BufferRegistry::remove`,
whose only guard is `editing_in_progress`. A buffer with unsaved edits
is destroyed and the file that would have held them is already gone.
Reachable today through any language server's `WorkspaceEdit`.

Reproduced four ways against `ad41cf1`, by throwaway tests run in this
worktree and removed before commit:

  a. the reported bug --- op returns `Ok(())`, file gone, buffer gone;
  b. `ignore_if_not_exists = true` does zero filesystem work and still
     destroys the buffer (the `create` arm's early return was never
     applied to delete);
  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`: windows are left bound to a removed
     `BufferId` and the registry can be driven to empty.

Recommends refusing before touching disk, at two layers: the Rust
primitive reconciles the registry first so the guard is expressible at
all, and the applier's existing URI preflight gains a second
precondition so the whole batch aborts with its documented
`nil, message` contract rather than half-applying.

Prompting is rejected on evidence: prompts are callback-continuations
resumed by a later keystroke, so a synchronous Rust binding cannot
issue one; the alternative is a seventh dispatcher shadow; and the
server-initiated path must answer `workspace/applyEdit` synchronously
with no user turn available. Backing up is rejected because removing
the buffer purges its autosave recovery file. Emacs prior art (eglot's
`do-delete`) kills the buffer before deleting the file and confirms
server-initiated edits as a whole-batch decision taken before any
mutation --- the same shape, in the phase pmacs already has.

Framing only. No runtime code. PROPOSED --- needs explicit user
approval before implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 18:49:24 -04:00