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:
parent
46b3c8db1a
commit
c7ad4f491f
|
|
@ -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 5–7
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 1–14 and 16) and **`lsp_dispatch_seams_acceptance`**
|
||||
the home of criteria 1–14 including 11a–11d, 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 1–14, 16 |
|
||||
| `tests/m4_acceptance.rs` | criteria 1–14 including 11a–11d, 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`;
|
||||
|
|
|
|||
Loading…
Reference in New Issue