308 lines
15 KiB
Markdown
308 lines
15 KiB
Markdown
# A destination capture any async continuation can use
|
|
|
|
**Status: framing pass, revision 2. Pre-implementation. Awaiting
|
|
approval.**
|
|
|
|
**Revision 2 takes three review findings.** Q#DC-2's parameterization
|
|
was **incomplete** — a panel result does not depend on the captured
|
|
document window being live or non-dedicated either, not just on its
|
|
buffer, so the question now carries a full **preflight matrix** with
|
|
every omission testable. `tests/journey_acceptance.rs` joins dired as a
|
|
named **preservation suite and stop signal**; it carries the
|
|
`commit_to` scope, forged-userdata, preflight and restoration pins this
|
|
lane generalizes, and Journey Stage 1a's framing treats it as a
|
|
required gate. And **§5 (coherence impact) was missing entirely**,
|
|
which `CLAUDE.md` and `COHERENCE.md` §25 both require of
|
|
coherence-affecting work — this lane adds Lua API surface and
|
|
generalizes a Journey substrate, so it qualifies twice over.
|
|
|
|
**A prerequisite lane. PR #227 (git Stage 1) blocks on it**, and its
|
|
P1a review finding is the reason this exists.
|
|
|
|
---
|
|
|
|
## 1. Why, and why as its own lane
|
|
|
|
PR #227's review found that git's async completions mutate and display
|
|
UI without capturing the initiating frontend
|
|
(`builtin/runtime/git.lua:609`, `:854`), so a result can surface in
|
|
whichever frontend happens to be active when git exits. Run
|
|
`git.status` in frontend A, let frontend B become active, and A's panel
|
|
opens in B.
|
|
|
|
**The finding named the right mechanism.** `pmacs.window.commit_to`
|
|
exists for exactly this continuation boundary: Journey Stage 1a's
|
|
Q#JR14 built it because "the listing settles a tick or more later, and
|
|
by then the ambient frontend, selected window, and active buffer may
|
|
all name something else" (`src/editor.rs:1238-1240`).
|
|
|
|
**But it is not reachable from Lua outside one path**, which is why
|
|
this is a lane and not a line in #227:
|
|
|
|
- `commit_to` takes a `DirectoryDestinationLua`, **nonconstructible
|
|
from Lua** by deliberate design (`src/lua_bindings/mod.rs:4256`) —
|
|
userdata with no constructor and no setters, so a caller cannot
|
|
fabricate a plausible triple.
|
|
- The only site that mints one is inside the `path.open-directory`
|
|
listener dispatch (`src/editor.rs:1311`), from
|
|
`capture_directory_destination`, which is `pub(crate)`
|
|
(`src/editor.rs:1241`).
|
|
|
|
So any async Lua continuation that is **not** a directory open has no
|
|
way to say where its result belongs. Git is the first to need it; it
|
|
will not be the last.
|
|
|
|
Landing this inside #227 would put new Lua API surface, over another
|
|
lane's merged mechanism, inside a feature branch — the same folding
|
|
that was declined for the `scripts/gate` repair, for the same reason.
|
|
|
|
## 2. Ground truth
|
|
|
|
- **The captured data is already generic.**
|
|
`DirectoryDestination { frontend, window, buffer }`
|
|
(`src/editor_core.rs:159-166`) contains nothing directory-specific.
|
|
Only its **name** and its **capture site** are.
|
|
- **The blast radius of a rename is small**: 8 references across 4
|
|
files (`editor_core.rs`, `editor.rs`, `lua_bindings/mod.rs`,
|
|
`lua_bindings/window_panel.rs`). Checked, not estimated.
|
|
- **`commit_to`'s preflight is four checks**
|
|
(`src/lua_bindings/window_panel.rs:488-525`), in order: the
|
|
requesting frontend still has a layout; the destination window is
|
|
still live in it; **the window still shows the captured buffer**
|
|
(Q#JR14c stale intent); and the window is not dedicated (Q#JR14f).
|
|
- **`Handle:await` refuses inside a commit scope**
|
|
(`builtin/runtime/async.lua:87-90`) — yielding would restore the
|
|
scope while the coroutine is still parked. Any adopter awaits
|
|
*before* committing, as dired does.
|
|
- **Git's two continuations do not have the same shape**, and this is
|
|
the finding that shapes the design:
|
|
- `*git-status*` goes through `listview.open`, which resolves
|
|
`display` with a **`"panel"`** default
|
|
(`builtin/runtime/listview.lua:550`). It lands in the bottom
|
|
panel, **not** in a document window.
|
|
- `*git-diff*` calls `pmacs.window.display(buf, { select = true })`
|
|
— the **document** target, deliberately, "so the status panel it
|
|
was invoked from stays visible beside it"
|
|
(`builtin/runtime/git.lua:852-854`).
|
|
|
|
## 3. The tension this lane has to resolve
|
|
|
|
`DirectoryDestination.buffer` exists for one purpose, stated at its
|
|
definition: *"what that window held at capture time, so **stale intent
|
|
loses to the user**"* — a user who replaced the buffer while work was
|
|
in flight is newer information than the request.
|
|
|
|
**That predicate is right for a document replacement and wrong for a
|
|
panel.** The git status panel does not replace the captured window's
|
|
buffer; it opens in the bottom panel beside it. Refusing to show it
|
|
because the user switched files in the document window would be a
|
|
refusal with no relationship to what the continuation actually does —
|
|
the panel case would inherit a check about a window it never touches.
|
|
|
|
Meanwhile the diff case *is* a document replacement, and wants exactly
|
|
the dired semantics.
|
|
|
|
So a single one-size destination either **over-refuses** the panel case
|
|
or **under-checks** the document case. Q#DC-2 is where that gets
|
|
decided, and it is the substance of this lane.
|
|
|
|
## 4. The change, in outline
|
|
|
|
- **A Lua-reachable capture**, returning the same nonconstructible
|
|
userdata for the *current* frontend and its document window.
|
|
- **Generic naming.** `DirectoryDestination` becomes something that
|
|
does not lie about a git panel; `capture_directory_destination` and
|
|
the userdata type follow. 8 references (§2).
|
|
- **The directory path keeps behaving exactly as it does today** — this
|
|
lane generalizes the capture, it does not change Journey Stage 1a's
|
|
semantics.
|
|
- **No adopter in this lane.** Git's adoption is #227's, after this
|
|
lands. A prerequisite that also converts its first consumer makes the
|
|
two impossible to review separately.
|
|
|
|
## 5. Coherence impact (§20)
|
|
|
|
**Revision 1 omitted this section entirely, and it is required.**
|
|
`CLAUDE.md` and `COHERENCE.md` §25 both say a framing for
|
|
coherence-affecting work must cite the section it serves and state its
|
|
impact — and this lane adds **new Lua API surface** and generalizes a
|
|
Journey-substrate mechanism, which is coherence-affecting on both
|
|
counts. Recording the impacts as neutral where they are neutral is part
|
|
of the requirement, not a way around it.
|
|
|
|
- **§16 semantic frontend — the section this serves.** The defect it
|
|
removes is a continuation resolving its target from *ambient* state a
|
|
tick after the request, which is precisely the multi-frontend
|
|
correctness §16 exists to protect. A capture makes "which frontend
|
|
asked" a value rather than a guess.
|
|
- **§14 workbench primitives — indirect, and the honest framing is
|
|
*enabling*.** This does not add a primitive. It removes the reason an
|
|
async adopter would hand-roll frontend tracking, which is the
|
|
mechanism by which primitives acquire per-consumer idiosyncrasies.
|
|
- **Journey steps touched: none directly, one PROTECTED.** The golden
|
|
journey does not gain a step. But Journey Stage 1a's Q#JR14 substrate
|
|
is what this generalizes, and §7 makes `tests/journey_acceptance.rs`
|
|
a preservation suite precisely so a generalization cannot erode the
|
|
step it came from.
|
|
- **Interaction islands (§6): none added.** No key interception, no
|
|
dispatch precedence rung. `dispatch_key` is untouched.
|
|
- **Config registry: no setting.** Where a continuation lands is a
|
|
correctness property, not a preference, and a toggle would offer to
|
|
turn correctness off.
|
|
- **Background-work attribution (§9): NEUTRAL, and worth stating
|
|
precisely rather than skipping.** This lane adds no background work
|
|
and no new unattributable surface. It also does **not** improve §9 —
|
|
knowing which frontend a result belongs to is not knowing who asked
|
|
for it or why. That is the worker-identity lane's arc, and the two
|
|
should not be confused because both concern async continuations.
|
|
- **§10 extension trust — a small positive.** The capture keeps the
|
|
Q#JR14d property that a destination is **nonconstructible from Lua**,
|
|
so generalizing the mechanism does not widen what extension code can
|
|
fabricate. §7 re-asserts the forged-destination refusal after the
|
|
rename for exactly this reason.
|
|
|
|
## 6. Open questions
|
|
|
|
### Q#DC-1 — what does the capture take as arguments?
|
|
|
|
*My vote: **no arguments** — capture the acting frontend and its
|
|
document window from the ambient state at call time.* That is what the
|
|
existing `capture_directory_destination(frontend, window)` is handed by
|
|
its one caller, and a Lua-supplied frontend id would reintroduce the
|
|
fabrication hole the userdata design closes.
|
|
|
|
### Q#DC-2 — one destination shape, or a panel/document distinction? **(the substantive one)**
|
|
|
|
§3 is the problem. Three candidates:
|
|
|
|
1. **One shape, all four checks.** Simplest; over-refuses the panel
|
|
case, and the refusal reason would be about a window the panel does
|
|
not touch.
|
|
2. **One shape, preflight parameterized by the continuation** — the
|
|
caller declares whether it is replacing the captured window's
|
|
buffer, and the stale-intent check applies only then.
|
|
3. **Two capture kinds**, document and panel, with different preflights.
|
|
|
|
*My vote: **(2)***, with the profiles spelled out below rather than
|
|
left to implementation.
|
|
|
|
**Revision 1 said only "skip the stale-buffer check for a non-replacing
|
|
continuation", and that was incomplete.** Review is right: a panel
|
|
result does not depend on the captured **document window** at all. It
|
|
does not replace that window's buffer, so check 3 is irrelevant; it
|
|
does not occupy that window, so check 4 (dedicated) is irrelevant; and
|
|
it does not need that specific window to exist, so check 2 is
|
|
irrelevant. Retaining any of the three can reject `git.status` for a
|
|
document-window change that has nothing to do with where the panel
|
|
goes. But dropping them **without an explicit profile** is how document
|
|
replacement quietly loses its guarantees.
|
|
|
|
**The matrix, stated so every omission is deliberate and testable:**
|
|
|
|
| # | Precondition (`window_panel.rs:488-525`) | Document replacement | Frontend/panel scope |
|
|
|---|---|---|---|
|
|
| 1 | Requesting frontend still has a layout | **required** | **required** |
|
|
| 2 | Destination window still live in it | **required** | not applicable |
|
|
| 3 | Window still shows the captured buffer (Q#JR14c stale intent) | **required** | not applicable |
|
|
| 4 | Window is not dedicated (Q#JR14f) | **required** | not applicable |
|
|
|
|
**Check 1 is the entire panel profile**, and that is the honest reading
|
|
of what a panel continuation actually depends on: the frontend it was
|
|
launched from still exists. Everything else in the capture is document
|
|
state the panel never touches.
|
|
|
|
**Consequence for the capture, which follows and should not be
|
|
discovered later:** if the panel profile needs only the frontend, then
|
|
a frontend with **no document window** can still host a panel — so
|
|
Q#DC-4's "return `nil`" is right for the document profile and possibly
|
|
wrong for the panel one. That interaction is settled as part of
|
|
answering this, not after it.
|
|
|
|
**I hold the *choice* loosely, not the matrix.** (1) has a real
|
|
argument — a uniform rule is easier to reason about, and over-refusal
|
|
is safe — but it would refuse the git panel for reasons unrelated to
|
|
it, and "safe" refusals that users cannot explain are how a mechanism
|
|
gets worked around. If review prefers (1) or (3), the matrix above is
|
|
what changes, and **every cell marked "not applicable" must still be
|
|
tested as deliberately omitted** (§7) so a future reader cannot mistake
|
|
an omission for an oversight.
|
|
|
|
### Q#DC-3 — what is the type called?
|
|
|
|
*My vote: **`ViewDestination`***, with `pmacs.window.capture_destination()`
|
|
as the Lua entry point. It names what it is — a place in a view where a
|
|
continuation's result belongs — without claiming a directory or a
|
|
buffer kind.
|
|
|
|
The Q#JR14 doc comments should keep their references intact; a rename
|
|
that orphans the rationale is worse than a slightly stale name.
|
|
|
|
### Q#DC-4 — is the capture refused when there is no document window?
|
|
|
|
`capture_directory_destination` already returns `None` when the
|
|
frontend has no document window (`src/editor.rs:1236`). *My vote:
|
|
**return `nil`, and require every adopter to handle it***, rather than
|
|
inventing a fallback destination. A continuation with nowhere to land
|
|
should say so, and #227's adopter should degrade to today's ambient
|
|
behaviour with a status message rather than silently guessing.
|
|
|
|
## 7. Verification
|
|
|
|
- **A captured destination survives a frontend switch**: capture in A,
|
|
make B active, commit, and assert the result lands in **A**. This is
|
|
P1a's actual failure and the reason the lane exists — asserting only
|
|
that the API returns userdata would pass on a capture that does
|
|
nothing.
|
|
- **A fabricated destination is still refused** — the existing Q#JR14d
|
|
guarantee, re-asserted after the rename so the generalization cannot
|
|
quietly open the hole it was built to close.
|
|
- **Every preflight refusal is witnessed by its own case, in BOTH
|
|
profiles** (Q#DC-2's matrix): frontend gone, window gone, stale
|
|
buffer, dedicated window — each asserted to **refuse** under the
|
|
document profile, and each of the three marked "not applicable"
|
|
asserted to **NOT refuse** under the panel profile. A deliberately
|
|
omitted check that has no test is indistinguishable from a check
|
|
someone forgot, and the next reader will restore it.
|
|
- **`nil` when the frontend has no document window** (Q#DC-4) — for
|
|
the **document** profile. Whether the panel profile can capture
|
|
without one follows from Q#DC-2 and is asserted whichever way it is
|
|
answered.
|
|
- **The directory path is unchanged** — dired's existing acceptance
|
|
coverage passes untouched.
|
|
- **`tests/journey_acceptance.rs` passes UNCHANGED**, as a named
|
|
preservation suite. It carries the established contract this lane
|
|
generalizes — 27 `commit_to` references across nine named pins
|
|
including `commit_to_refuses_a_forged_destination`,
|
|
`commit_to_scopes_and_restores_on_a_normal_return`,
|
|
`commit_to_restores_when_the_callback_raises`,
|
|
`commit_to_refuses_an_await_and_restores`,
|
|
`commit_to_delivers_to_the_requesting_frontend_not_the_ambient_one`,
|
|
`a_declining_listener_cannot_redirect_the_destination`, and two
|
|
rows already named `preservation_*`. Journey Stage 1a's own framing
|
|
treats this suite as a required gate; a lane that generalizes its
|
|
substrate does not get to relax that.
|
|
- **STOP SIGNAL, for both suites.** If any existing `dired` or
|
|
`journey_acceptance` test needs editing, the generalization changed
|
|
Journey Stage 1a's semantics. That is cause to stop and report, not
|
|
to adjust the test — a suite edited to accommodate the change under
|
|
test has stopped being evidence.
|
|
- **`Handle:await` still refuses inside the scope**, including through
|
|
`pmacs.async.yield_to_next_tick` if the worker-identity lane's Q#W-7
|
|
has landed by then; if it has not, this lane does **not** add that
|
|
guard — it belongs to that lane and duplicating it would produce a
|
|
conflict for no benefit.
|
|
|
|
**What this will NOT prove:** that git surfaces in the right frontend —
|
|
that is #227's adoption, after this lands. This lane ships the
|
|
mechanism and one set of tests for the mechanism.
|
|
|
|
## 8. Not in scope
|
|
|
|
**Adopting the capture anywhere**, including git (#227 does that) and
|
|
including migrating other async continuations that have the same latent
|
|
bug — worth an audit, not this lane's work. Changing Journey Stage 1a's
|
|
directory semantics. The `commit_to` scope guard for
|
|
`yield_to_next_tick` (worker identity Q#W-7). Any protocol change —
|
|
this is entirely core + Lua bindings. Panel geometry or placement
|
|
policy, which is the bottom-panel arc's.
|