Commit Graph

5 Commits

Author SHA1 Message Date
Levi Neuwirth 8ddc8a7a5f docs: dired Stage 2 framing rev 5 — re-scout against main @ 6bee09d
Rev 4 was scouted at c8ec8f3, which is dired Stage 1's own merge commit.
Canonical main has moved 153 commits since. This is that re-scout. Every
claim below was read on the tree at 6bee09d, not inferred.

Four dependencies arrived that rev 4 could not have known about:

- #178 landed `Buffer::set_generated_contents`, and dired has NOT
  adopted it — the handoff §4 and COHERENCE §14 both name
  `dired.lua:371` in the non-adopter inventory. Dired pairs an erroring
  intercept with a bypass_intercept write over a still-writable rope, so
  `M-x buffer.undo` empties a listing today, with no keybinding needed.
  Stage 2 writes that buffer on every mark. New decision Q#DR25 adopts
  the primitive at the head of 2b, with acceptance items 48-49.
- #182 (Journey Stage 1a) demoted dired to a replaceable slot
  (`pmacs.path.directory_handler`, deliberately not a hook subscriber)
  and rewrote `open_directory` around `pmacs.window.commit_to`, whose
  scope REFUSES an await. That constrains §9's serialize-and-await batch.
- `tests/journey_acceptance.rs` is a declared ratchet (24 tests, "stages
  add rows, none removes them"), seven rows assert on dired, and #183
  put the GPU row in `gpu_invocation_acceptance.rs` instead — so the
  ratchet is split across two files and both are gates now.
- #179/#181's typed-edit chain: dired participates in neither it nor
  `buffer.after-edit`, and `set_generated_contents` fires no hook, so
  Q#DR25 does not drag dired onto the chain. The chain's lessons still
  bind §5's two NEW hooks.

Seven of rev 4's own claims about pmacs were wrong:

- W1 (load-bearing): §5 named `drain_external_cancelled` and cited
  `lsp.rs:1596`. Those are two different functions.
  `drain_external_cancelled` (:1561) is the unconditional server-scoped
  drain and is the right precedent; `drain_cancelled_externals` (:1596)
  is a per-tick token/timeout sweep. A rename flips no token, so
  following the line number yields a `forget_uri` whose drain half is a
  silent no-op and the awaiting coroutine hangs forever — the exact
  failure step 2 exists to prevent.
- W2: there is no `fn restart`. The teardown is `start_generation`
  (:1307-1345), and there is a second precedent rev 4 never named,
  `LspManager::forget` (:3015-3042). Neither clears the 14 result
  stores, so rev 4's "surprising" note now holds twice.
- W3: `ResponseRoute` has 15 variants, 14 URI-bearing; there are 16
  insert sites, 15 URI-bearing. Rev 4's fifteen was a correct count of
  URI-bearing inserts and an incorrect count of sites. The purge
  predicate must retain `WorkspaceSymbol { query }` explicitly.
- W4: `rec.uri` is read at 57 lines in lsp.lua, not "~20".
- W5: the path-owner census has grown to SIX. `lean.lua`'s
  `M.file_progress` is a URI-keyed Lua module table in no Rust store, so
  `forget_uri` cannot reach it — independent evidence that the hook, not
  the Rust method, is the mechanism that scales.
- W6: the ledger note named PR #169, which merged. #185 holds the
  durable records now.
- W7: the C1 seam is at mod.rs:7104-7115, and mod.rs defines three
  `_tick` bindings in different classes.

The 2a/2b/2c/Stage-3 cut was re-examined and holds unchanged; §16 adds
the ownership warning that 2a overlaps editor_core.rs, lsp.lua and the
URI-keyed LSP state with Journey Stage 1b and must not run concurrently
with it. §0.5 now cites COHERENCE §20 Priority 1 and §14 by number, and
records that #182 put dired on journey step 3 — a claim rev 4 could not
make.

Status is stated honestly: PROPOSED, never formally approved, and
needing explicit framing approval before any implementation branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
2026-07-28 17:18:25 -04:00
Levi Neuwirth ab42a7991c docs: dired Stage 2 framing rev 4 — review round 3
Three design blockers, four cleanups, and the staging call taken. Round
3's theme: rev 3 named the right seams but sized two of them from a
partial inventory, and one promise was still stronger than its mechanism.
All three verified against c93f9ee.

H1 — the modified-buffer delete check races the syscall. Rev 3's
"immediately before each syscall" was wrong about where the boundary is:
pmacs.fs.remove DISPATCHES A WORKER, so the interval to remove_blocking's
remove_file is wide open, and acceptance 20 (edit before y) could never
have detected it. NARROWED to a TOCTOU-bounded pre-dispatch check, the
same honest framing G6 forced on R, rather than inventing a reservation
primitive inside a dired stage. The residue is stated precisely: the
buffer survives with its contents (that half IS robust — it runs at drain
time), the file does not. So the orphan deferral rev 3 scoped to the LSP
path now covers dired too, as one deferral rather than two. Acceptance 20
says outright that the interval has no test because it is not closed.

H2 — the LSP teardown inventory was a third of the real one. LspManager
holds FOURTEEN URI-bearing store families (lsp.rs:741-819), not five, plus
the `documents` text map didChange diffs against — a stale entry there is
a correctness problem, not a leak — plus pending_routes, whose
ResponseRoute variants CARRY THE URI at fifteen insert sites, so an
in-flight response repopulates the old key AFTER any clear. Rev 4 gives
the full table and one manager-level forget_uri(sid, uri) that purges
routes, drain-cancels the matching awaiters (the existing contract at
:799-803 already requires that wherever routes are purged), and clears all
fourteen plus documents — handling locations_store's kind key and
symbol_store's scope key specially. Modelled on the server-scoped
teardown at :1316-1331. Also records the surprise found on the way: that
teardown clears routes and documents but NOT the fourteen stores.

H3 — the diagnostic-view seam is now chosen, not either/or. Verified the
constraints: DiagnosticView.uri is private and immutable, View has no
downcast, and _attach_view takes active_window_mut() and ERRORS otherwise,
so it reaches one window and cannot drive a per-window loop from Lua; and
a remove-and-re-push loses composition order in an ordered
Vec<Box<dyn View>>. The seam: a View::rename_resource default-no-op hook,
joining overlay_identity and clone_for_split — the family #113 round 6
added for exactly this class — swept over core.windows.values_mut() the
way overlay disposal already is (mod.rs:2016-2019). In-place mutation, so
order is preserved by construction, the field stays private, and future
URI-bearing overlays opt in by overriding rather than growing a special
case. Acceptance 30 now needs TWO windows and an order assertion; a new
item 31 pins the store inventory and the in-flight repopulation.

Staging: TOOK THE FURTHER CUT as directed. Three PRs — 2a the
reconciliation transaction with no dired surface, 2b marks and operations,
2c the new fs primitives. 2a leads with the two defects it closes on main
today (an LSP-authored delete that destroys unsaved work; a workspace-edit
phantom buffer), neither of which needs dired to be worth fixing. Named
for the substrate per #161's precedent. §10 states the cost: three review
cycles, and 2a ships nothing visible.

Cleanups: item 35→40 (now 41), acceptance 27→30 and 28→32 (now 33), and
the §10 table's obsolete rename-only-Rust description, replaced by a
per-PR breakdown of what each actually carries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
2026-07-25 21:22:51 -04:00
Levi Neuwirth 9227860e08 docs: dired Stage 2 framing rev 3 — review round 2
Four blocking, two high, four cleanups. Round 2's real finding: rev 2
widened the rename fix into a resource transaction, and four of the
consumers it named were not actually reachable by it. All six
substantive claims verified against c8ec8f3.

G1 — acceptance 29 was unimplementable. apply_workspace_edit captures
origin as a STRING (active_buffer_path is pmacs.editor.file_path,
lsp.lua:471-473), so no transaction reaches it and the phantom survives.
The applier itself changes: capture the buffer handle, restore with
switch_buffer, and no path fallback — restoring nothing beats inventing
a file that does not exist.

G2 — the dired subscriber could not rename its own buffer. dired.lua's
module doc says there is no pmacs.buffer.set_name, which is exactly why
Stage 1 chose buffer-per-directory. Rev 3 adds the setter (Q#DR21):
Buffer::set_name already exists and already documents itself as for
"rename operations", §5 needs it anyway for the Buffer.name half, and the
alternative — kill/recreate plus window replacement — loses placement,
cursor, intercept, round-trip input, and mode.

G3 — rec.uri was not the last LSP owner. DiagnosticView captures its URI
at construction and its own field doc anticipates this ("M5 may add
re-rooting if a buffer is renamed", diag.rs:455-457); five more stores
are URI-keyed. §5 now carries the ordered contract: flush pending
didChange, didClose, drop all five stores, re-run ensure_server, didOpen,
re-root the view per window.

G4 — Q#DR18 had no seam and was racy across the prompt. apply_resource_op
kills via find_by_path: raw path, first match, no descendants, no
modified check — it destroys unsaved work today. Rev 3 defines one shared
reconcile_delete called by both paths, harvests remove in the drain like
rename (so fire-and-forget reconciles too), and rechecks modified state
immediately before each syscall, since another frontend can edit while
the prompt is open. The policy stays asymmetric on purpose: dired refuses
the entry, an LSP-authored delete still removes the file but no longer
destroys the buffer.

G5 — w had no surface and the wrong semantics. push_entry is local and
copy() requires a region. Adds pmacs.killring.push (Q#DR22) with copy()'s
own semantics including breaking the kill chain, and makes w SET-BASED:
the parent approved the binding and Emacs copies marked filenames, so
rev 2's point-only narrowing was an unapproved change of its own. R is
now the only point-based operation.

G6 — R's no-clobber was only a preflight. rename_blocking calls plain
std::fs::rename, which silently replaces. The claim is narrowed to a
TOCTOU-bounded preflight refusal, acceptance 12 reworded to promise only
that, and a no-replace primitive named as deferred.

G7 — lsp_multi_root added to the gates, the §13/§7 slips fixed, and the
"2a's only Rust is the rename rebind" line corrected: it is now a rename
and delete reconciliation, two hooks, two new public surfaces, an LSP
teardown contract, and an applier change. §10 says so, and names the
further cut if that is now too large for one PR.

Acceptance renumbered flat (46 items) and the bite obligations are now a
table of eleven item/mutation pairs, three of them round-2 additions
where rev 2's design would have passed a weaker test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
2026-07-25 19:41:08 -04:00
Levi Neuwirth eec731b8f4 docs: dired Stage 2 framing rev 2 — review round 1
Seven findings, two blocking. Every checkable claim was verified against
c8ec8f3 before being acted on; all seven held.

F1 (blocking) — the rename contract reached one path owner. Verified the
other four: Buffer::set_name documents itself as for "rename operations"
and set_buffer_path never calls it; rec.uri is cached per LSP attachment
and read at ~20 sites; dired's buffers are PATHLESS so no buffer-keyed
rebind can reach them; and the workspace-edit origin restore does not
fail gracefully — find_or_open on a renamed-away path hits
resolve_target_buffer's NotFound arm, which creates an empty path-backed
buffer, so it materializes a phantom at the obsolete path and selects it.

Rev 2 replaces the rebind with EditorCore::reconcile_rename — whole
registry, equality-or-path-component prefix, updates file_path AND name,
called by BOTH the async drain and apply_resource_op so the two cannot
drift — plus a new resource.renamed(old, new) hook so path-keyed Lua
consumers reconcile. lsp.lua recomputes rec.uri, issues didClose/didOpen,
and re-runs ensure_server because #161 keys affinity on project root, so
a cross-root move needs a different server. dired.lua follows its
handles. Verified the ordering the design needs already holds:
_async.tick calls _tick() before resuming any coroutine.

F2 (blocking) — deletion of visited paths had no policy. New §6 decides
all four cases. An unmodified visited buffer is killed; a MODIFIED one
refuses that entry, deliberately diverging from Emacs, because an
orphaned buffer is indistinguishable from a normal one and the next
C-x C-s silently resurrects the file. The check runs before the confirm
so the prompt states the skip. Adds a symmetric resource.deleted hook.

F3 (high) — the key table silently changed approved scope. The parent
lists `w` and contains no `M`. Restored `w` (Q#DR20); `M` is now an
explicit new-scope decision (Q#DR19) that REFUSES symlinks, since the
parent already ruled that the fixture's symlink-perms rejection "carries
over unchanged" and rev 1's warn-after-the-fact contradicted it.

F4 (high) — Q#DR13 contradicted the R contract. Narrowed to three
classes: set-based (D, M, C), flag-based (x), point-based (R, w).

F5 (high) — five falsifying acceptance items added, and the bite matrix
now names six mutations including "dispatch-all-then-await" and "add a
completion source to confirm".

F6 (medium) — take_settled_renames was underspecified. Took the
reviewer's preferred shape: tick returns a structured TickOutcome so
settle identity and rename metadata stay in one transaction.

F7 (2b) — defined the full C command flow with an up-front collision
scan and one confirm (declining copies the non-colliding entries), pinned
remove_dir_all's lstat safety at the primitive, and stated
dired.recursive-deletes as boolean/default false.

Not done, and said so: R is not widened to the marked set. Multi-file
rename needs a target-directory concept that does not exist, so R stays
point-based and is NAMED as a class rather than left an unstated
exception.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
2026-07-25 18:31:11 -04:00
Levi Neuwirth c2dfe1a89d docs: framing for dired Stage 2 (marks and operations)
Continues docs/dired-framing.md, whose §§6-7 carry the approved shape of
marks and operations. This re-verifies every claim in them against
main @ c8ec8f3 — Stage 1 changed three of the files Stage 2 leans on
most — and adds what the parent did not decide: the batch-execution
contract, the confirmation surface, the staging cut, and acceptance.
Decisions continue the Q#DR scheme from Q#DR12.

Five corrections to the parent, one load-bearing:

- The rename rebind belongs in the drain (the parent's decision, kept)
  but CANNOT be implemented in `AsyncRuntime::tick`: AsyncRuntime has no
  buffer registry and no core. The seam is `pmacs._async._tick`, one
  layer up, which already has `lua` in scope. AsyncRuntime harvests, the
  binding rebinds.
- Line references drifted (tick 991 -> 1003, the FsUnit arm 1022 ->
  1046, apply_resource_op's raw lookup 3248 -> 3249).
- The frozen fixture has NO mark-and-operate layer — eight commands, two
  keys, and its "marks" are wdired text-position marks. So Stage 2 has
  no in-repo reference implementation, which the parent's "45 tests pin
  dired/wdired behavior" reads as implying it does.
- No y_or_n exists anywhere, and there is no runtime minibuffer.lua at
  all — pmacs.minibuffer is Rust-only.
- `remove_blocking` already deletes files AND empty directories, so
  `remove_dir_all` is needed only for non-empty ones. This is what makes
  the staging cut possible.

Also carries a verified pre-existing defect, confirmed by probe rather
than inferred: a fire-and-forget non-stream job leaks its pending entry
forever (only stream eviction and take_result remove entries, and the
Lua handle has no __gc). Named as a deferral. The same probe establishes
that a settled job IS still readable at drain time, which is what makes
the rebind design sound.

Recommends splitting Stage 2 at the "needs a new Rust primitive" line:
2a is the mark layer plus d/x/D/R/M on the five ops that already exist,
plus the rename correctness fix; 2b adds mkdir/copy/remove_dir_all and
+/C and recursive delete. The reasoning is that 2a's only Rust is the
rename rebind, whose design is subtle enough to deserve a reviewer's
whole attention.

Coherence impact per COHERENCE.md §20 is stated in §0.5, including the
honest part: Stage 2 must add a `rename_paths` field to `PendingJob`,
which is a one-off where §9 wants a general owner/purpose — though a
side map, the alternative, is worse by §9's own diagnosis of the
parse-job link. 2b grows the closed JobKind enum 12 -> 15.

Touches only this file: docs/active-work.md and docs/agent-handoff.md
are held by the open docs PR #169.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
2026-07-25 17:55:14 -04:00