Merge pull request #203 from levineuwirth/journey-stage1b1-compile-defaults
Journey Stage 1b-1: make building discoverable (step 9)
This commit is contained in:
commit
1f290d51c3
46
COHERENCE.md
46
COHERENCE.md
|
|
@ -378,7 +378,7 @@ Full verdict table:
|
||||||
| 6 | Language intelligence | **Partial** | Rust grammar bundled and auto-attaches; rust-analyzer preconfigured (`builtin/runtime/lsp.lua:44-52`) — but a missing binary fails silently (§1.2) and highlighting masks it. No LSP status command exists to diagnose |
|
| 6 | Language intelligence | **Partial** | Rust grammar bundled and auto-attaches; rust-analyzer preconfigured (`builtin/runtime/lsp.lua:44-52`) — but a missing binary fails silently (§1.2) and highlighting masks it. No LSP status command exists to diagnose |
|
||||||
| 7 | Find symbol / file | **File: fixed (open by path merged #162; browsing #165). Symbol: works but undiscoverable** | No find-file/dired/picker existed at audit. Now `C-x C-f` opens a known path and `C-x d` / `C-x C-j` browse (flat listing, `dired` mode keymap); `M-.`/`M-?`/`C-c o` still bound but advertised nowhere and server-gated; no workspace-symbol command; `pmacs.index.*` has no UI |
|
| 7 | Find symbol / file | **File: fixed (open by path merged #162; browsing #165). Symbol: works but undiscoverable** | No find-file/dired/picker existed at audit. Now `C-x C-f` opens a known path and `C-x d` / `C-x C-j` browse (flat listing, `dired` mode keymap); `M-.`/`M-?`/`C-c o` still bound but advertised nowhere and server-gated; no workspace-symbol command; `pmacs.index.*` has no UI |
|
||||||
| 8 | Open terminal | **Works** | Full PTY with scrollback + modeline segment, bound to `C-c t` and configurable through three registered settings (`terminal.default-profile`, `terminal.scrollback-rows`, `terminal.escape-key`) plus named `pmacs.terminal.profiles` (PR #173), and searchable through `M-x terminal.copy-mode` / `C-c C-t`, which materializes the retained scrollback into an ordinary read-only buffer (Stage 2). Named limitations: `C-c t` is unreachable from *inside* a terminal window, where `C-c` is consumed as the escape — `M-x terminal` still works there; and there is still **no close/kill command**, which is the remaining half of this step's discoverability gap. *Was broken outright on the GPU frontend until the double terminal-layout sync was fixed: the child took a `SIGWINCH` storm at tick cadence, so typing into it was impossible while output still flowed.* |
|
| 8 | Open terminal | **Works** | Full PTY with scrollback + modeline segment, bound to `C-c t` and configurable through three registered settings (`terminal.default-profile`, `terminal.scrollback-rows`, `terminal.escape-key`) plus named `pmacs.terminal.profiles` (PR #173), and searchable through `M-x terminal.copy-mode` / `C-c C-t`, which materializes the retained scrollback into an ordinary read-only buffer (Stage 2). Named limitations: `C-c t` is unreachable from *inside* a terminal window, where `C-c` is consumed as the escape — `M-x terminal` still works there; and there is still **no close/kill command**, which is the remaining half of this step's discoverability gap. *Was broken outright on the GPU frontend until the double terminal-layout sync was fixed: the child took a `SIGWINCH` storm at tick cadence, so typing into it was impossible while output still flowed.* |
|
||||||
| 9 | Build / test | **Partial** | `M-x compile.run` works, defaults cwd to detected project root, parses Rust `-->` errors — but no keybinding, an **empty first prompt** (`initial = last and last.cmdline or ""`, `builtin/runtime/compile.lua:1134-1138`), and no `cargo build`/`cargo test` suggestion despite `ProjectKind::Cargo` existing (`src/project.rs:77`) |
|
| 9 | Build / test | **Partial** | `M-x compile.run` works, defaults cwd to the detected project root, and parses Rust `-->` errors — but no keybinding, an **empty first prompt** (`initial = last and last.cmdline or ""`, `builtin/runtime/compile.lua`), and no `cargo build` suggestion despite `ProjectKind::Rust` existing (`src/project.rs:78` — **not** `Cargo`, see §24). **PR #203 (open) closes all three**: `C-c c` runs `compile.run`, and the first prompt is prefilled from the detected project kind (`pmacs.compile.defaults`, seeded `rust = "cargo build"`). The prompt **captures** its directory rather than re-resolving at accept time, so the command it offers and the directory it runs in cannot drift while the minibuffer waits. Named limitation it does not fix: after `pmacs <dir>` the active buffer is dired's and pathless, so the cwd falls back to the process cwd — §8's execution-location model owns that. **This row flips to Works when #203 merges**; per §25 a grade changes only on landed evidence |
|
||||||
| 10 | Inspect error | **Partial (good once reached)** | `E:n W:n` modeline counts, underlines, `M-g n/p` + ``C-x ` `` walking a unified compile/grep/diag source, message echo, `RET` visits. Gated entirely on step 6 or 9 succeeding first |
|
| 10 | Inspect error | **Partial (good once reached)** | `E:n W:n` modeline counts, underlines, `M-g n/p` + ``C-x ` `` walking a unified compile/grep/diag source, message echo, `RET` visits. Gated entirely on step 6 or 9 succeeding first |
|
||||||
| 11 | See background work | **Works but undiscoverable** | `*workers*` view via `M-x editor.list-workers`; `C-c C-k` cancel-at-point. No keybinding, no statusline spinner/progress indicator anywhere (§9) |
|
| 11 | See background work | **Works but undiscoverable** | `*workers*` view via `M-x editor.list-workers`; `C-c C-k` cancel-at-point. No keybinding, no statusline spinner/progress indicator anywhere (§9) |
|
||||||
| 12 | Close + restore | **Partial** | Per-file cursor+scroll (saveplace), recent files, minibuffer history, autosave recovery all restore zero-config. Open-buffer set and window layout do **not**: desktop-save is opt-in (`pmacs.session.desktop_mode(true)`) *and* a documented no-op under a daemon (`src/desktop.rs:323-326`, `:353-356`, Q#DS9) |
|
| 12 | Close + restore | **Partial** | Per-file cursor+scroll (saveplace), recent files, minibuffer history, autosave recovery all restore zero-config. Open-buffer set and window layout do **not**: desktop-save is opt-in (`pmacs.session.desktop_mode(true)`) *and* a documented no-op under a daemon (`src/desktop.rs:323-326`, `:353-356`, Q#DS9) |
|
||||||
|
|
@ -388,12 +388,14 @@ A journey observation worth keeping verbatim from the audit:
|
||||||
C-M-s` opens all folds, while opening a file, opening a terminal, and
|
C-M-s` opens all folds, while opening a file, opening a terminal, and
|
||||||
running a build have no bindings at all.
|
running a build have no bindings at all.
|
||||||
|
|
||||||
Two of that observation's three examples have since been answered —
|
Two of that observation's three examples have been answered — opening a
|
||||||
opening a file by `C-x C-f` (#162) and opening a terminal by `C-c t`
|
file by `C-x C-f` (#162) and opening a terminal by `C-c t` (#173).
|
||||||
(#173). **Running a build still has no binding**, and the underlying
|
**Running a build still has no binding on `main`**; PR #203 is open and
|
||||||
inversion is a standing bias in how new work gets bound, not three
|
adds `C-c c`. **The quote stays as written either way**: it names a standing bias in how new work gets bound,
|
||||||
isolated omissions: the quote stays as written because it names the
|
not three isolated omissions, and two fixes do not retire a bias.
|
||||||
pattern, and the pattern is not retired until step 9 is.
|
**Running a build remains its uncontested golden-journey example until
|
||||||
|
#203 lands.** A new surface that ships without a binding would be further
|
||||||
|
evidence the pattern is live, and should be read that way.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1542,14 +1544,18 @@ blockers. **State: runs to step 5; thin from step 6 (§2). Mostly wiring,
|
||||||
and unusually cheap:** directory-argument handling (**done**: Journey
|
and unusually cheap:** directory-argument handling (**done**: Journey
|
||||||
Stage 1a); a find-file surface (**done**: #162 open-by-path, #165
|
Stage 1a); a find-file surface (**done**: #162 open-by-path, #165
|
||||||
browsing); surfacing the LSP spawn failure with guidance (§1.2); a
|
browsing); surfacing the LSP spawn failure with guidance (§1.2); a
|
||||||
compile keybinding + `cargo build`/`test` default from the existing
|
compile keybinding + `cargo build`/`test` default (**in flight**:
|
||||||
`ProjectKind::Cargo`; a terminal keybinding (**done**: `C-c t`, #173); a
|
Journey Stage 1b-1, PR #203, from the existing `ProjectKind::Rust` —
|
||||||
welcome buffer. The journey acceptance suite (§19) is the ratchet that
|
**not** `Cargo`, see §24); a terminal keybinding (**done**: `C-c t`,
|
||||||
|
#173); a welcome buffer. The journey acceptance suite (§19) is the ratchet that
|
||||||
keeps it fixed — it **exists now** (`tests/journey_acceptance.rs`,
|
keeps it fixed — it **exists now** (`tests/journey_acceptance.rs`,
|
||||||
Stage 1a), seeded with steps 2, 3, and 5.
|
Stage 1a), seeded with steps 2, 3, and 5.
|
||||||
|
|
||||||
Journey Stage 1b is the named remainder: the compile binding + Cargo
|
Journey Stage 1b is the named remainder, and it splits: **1b-1 — the
|
||||||
defaults, LSP spawn guidance, and the welcome buffer.
|
compile binding + project-kind defaults — is in flight as PR #203**;
|
||||||
|
1b-2 (LSP spawn guidance, step 6) and 1b-3 (the welcome buffer, step 4)
|
||||||
|
remain. The ratchet is seeded with steps 2, 3 and 5, and gains step 9
|
||||||
|
when #203 lands.
|
||||||
|
|
||||||
### Priority 2: Make workspace and location explicit
|
### Priority 2: Make workspace and location explicit
|
||||||
|
|
||||||
|
|
@ -1620,8 +1626,10 @@ implementation — this list is direction, not commitment):
|
||||||
`resolve_target_buffer` unification, the destination-scope substrate,
|
`resolve_target_buffer` unification, the destination-scope substrate,
|
||||||
and the first journey acceptance suite. It routes `pmacs .` into
|
and the first journey acceptance suite. It routes `pmacs .` into
|
||||||
#165's dired buffer rather than growing a second directory surface.
|
#165's dired buffer rather than growing a second directory surface.
|
||||||
**Stage 1b — remaining**: compile defaults, LSP-failure surfacing,
|
**Stage 1b-1 — in flight (PR #203)**: the compile binding and
|
||||||
bindings, welcome buffer.
|
project-kind defaults, with the prompt capturing its directory rather
|
||||||
|
than re-resolving it at accept time. **Stage 1b-2 / 1b-3 —
|
||||||
|
remaining**: LSP-failure surfacing, welcome buffer.
|
||||||
2. **Discovery surface** (P4): the describe/list/where-is command
|
2. **Discovery surface** (P4): the describe/list/where-is command
|
||||||
family, M-x rich rows, help unification, help prefix.
|
family, M-x rich rows, help unification, help prefix.
|
||||||
3. **Transient keymap layer** (§6): the overlay scope + lifetime
|
3. **Transient keymap layer** (§6): the overlay scope + lifetime
|
||||||
|
|
@ -1713,6 +1721,16 @@ available without being imposed.
|
||||||
Found during the audit; fix opportunistically, ideally before this
|
Found during the audit; fix opportunistically, ideally before this
|
||||||
document is wired into CLAUDE.md/AGENTS.md as required reading:
|
document is wired into CLAUDE.md/AGENTS.md as required reading:
|
||||||
|
|
||||||
|
- **This document named a `ProjectKind` variant that does not exist**,
|
||||||
|
in two places: §2's step-9 row and §20 Priority 1 both said
|
||||||
|
"`ProjectKind::Cargo` existing (`src/project.rs:77`)". Line 77 is the
|
||||||
|
*doc comment*; the variant on line 78 is **`ProjectKind::Rust`**,
|
||||||
|
produced by the `Cargo.toml` marker. The audit read the comment and
|
||||||
|
named the comment. Corrected by Journey Stage 1b-1 — which also
|
||||||
|
establishes that the Lua side never sees the variant at all:
|
||||||
|
`pmacs.project.detect` returns the **tag string** `"rust"`. Kept here
|
||||||
|
rather than silently fixed, because a wrong type name in the document
|
||||||
|
work is evaluated against costs a scout a real detour.
|
||||||
- `docs/keybindings.md` — every `src/editor.rs` line citation in §3 is
|
- `docs/keybindings.md` — every `src/editor.rs` line citation in §3 is
|
||||||
stale by ~250–1000 lines despite a "last verified @ `f8096ff`
|
stale by ~250–1000 lines despite a "last verified @ `f8096ff`
|
||||||
(2026-07-20)" stamp; its shadow list also omits the terminal `C-c`
|
(2026-07-20)" stamp; its shadow list also omits the terminal `C-c`
|
||||||
|
|
|
||||||
|
|
@ -620,6 +620,60 @@ local function daemon_working_directory()
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Where a run started right now would execute. Extracted from the
|
||||||
|
-- inline expression that used to live in `pmacs.compile.run`, because
|
||||||
|
-- the interactive command needs the SAME answer to build its prompt:
|
||||||
|
-- a suggestion computed from a different rule than the run obeys can
|
||||||
|
-- name a toolchain that is not at the directory the command executes
|
||||||
|
-- in (journey Stage 1b-1 framing §3.1).
|
||||||
|
local function resolve_cwd(explicit)
|
||||||
|
return explicit or project_root_of_active() or daemon_working_directory()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Default compile command per detected project kind, keyed by the tag
|
||||||
|
--- `pmacs.project.detect` returns. Assign into this table from
|
||||||
|
--- `init.lua` to add or override one:
|
||||||
|
---
|
||||||
|
--- pmacs.compile.defaults.go = "go build ./..."
|
||||||
|
---
|
||||||
|
--- Only `rust` ships seeded, and that is a decision rather than an
|
||||||
|
--- omission: Rust has one answer, while npm/yarn/pnpm, make/cmake, and
|
||||||
|
--- `go build` versus `go test` do not. A wrong prefill costs more than
|
||||||
|
--- an empty one — the user must delete it before typing.
|
||||||
|
pmacs.compile.defaults = { rust = "cargo build" }
|
||||||
|
|
||||||
|
-- Read `defaults[kind]` defensively. The table is public and
|
||||||
|
-- assignable, so a metatable with a throwing `__index`, a non-string
|
||||||
|
-- entry, or a non-table replacement all have to be survivable — the
|
||||||
|
-- same discipline `validated_rules` keeps for a hostile rule container
|
||||||
|
-- (round-2 finding 3). A broken `defaults` degrades to the pre-stage
|
||||||
|
-- empty prompt; it never prevents compiling.
|
||||||
|
local function default_command_for(kind)
|
||||||
|
if type(kind) ~= "string" then return nil end
|
||||||
|
local ok, value = pcall(function() return pmacs.compile.defaults[kind] end)
|
||||||
|
if ok and type(value) == "string" and #value > 0 then return value end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Where the next compile would run, and what kind of project is
|
||||||
|
--- detected *from that directory*. Public getter (per API
|
||||||
|
--- conventions): `{ cwd = string|nil, kind = string|nil }`.
|
||||||
|
---
|
||||||
|
--- `kind` is always `detect(cwd)`, one rule regardless of which branch
|
||||||
|
--- produced `cwd`. Note that is not the same as "`cwd` is a project
|
||||||
|
--- root": a Cargo workspace subdirectory contains no `Cargo.toml`, is
|
||||||
|
--- detected as `rust` by the ancestor walk, and is a perfectly good
|
||||||
|
--- place to run `cargo build`.
|
||||||
|
function pmacs.compile.context(explicit_cwd)
|
||||||
|
local cwd = resolve_cwd(explicit_cwd)
|
||||||
|
local kind = nil
|
||||||
|
if cwd then
|
||||||
|
local ok, proj = pcall(pmacs.project.detect, cwd)
|
||||||
|
if ok and proj and type(proj.kind) == "string" then kind = proj.kind end
|
||||||
|
end
|
||||||
|
return { cwd = cwd, kind = kind }
|
||||||
|
end
|
||||||
|
|
||||||
local function format_exit_marker(label, ev)
|
local function format_exit_marker(label, ev)
|
||||||
if ev.kind == "exited" then
|
if ev.kind == "exited" then
|
||||||
return string.format("\n[%s exited with code %d]\n", label, ev.code or 0)
|
return string.format("\n[%s exited with code %d]\n", label, ev.code or 0)
|
||||||
|
|
@ -762,7 +816,7 @@ local function start_run(slot, cmdline, opts)
|
||||||
if cur and not pmacs.compile.is_generated_buffer(cur) then
|
if cur and not pmacs.compile.is_generated_buffer(cur) then
|
||||||
slot.prev = cur
|
slot.prev = cur
|
||||||
end
|
end
|
||||||
local cwd = opts.cwd or project_root_of_active() or daemon_working_directory()
|
local cwd = resolve_cwd(opts.cwd)
|
||||||
|
|
||||||
-- Supersede (Q#CM9): terminate the old group and tombstone its
|
-- Supersede (Q#CM9): terminate the old group and tombstone its
|
||||||
-- pump entry; its terminal event still drives forget.
|
-- pump entry; its terminal event still drives forget.
|
||||||
|
|
@ -1132,13 +1186,27 @@ pmacs.command.define {
|
||||||
description = "Compile: run a command in a streaming *compilation* buffer (M-x compile).",
|
description = "Compile: run a command in a streaming *compilation* buffer (M-x compile).",
|
||||||
fn = function()
|
fn = function()
|
||||||
local last = pmacs.compile._last
|
local last = pmacs.compile._last
|
||||||
|
-- Captured ONCE, here. `pmacs.minibuffer.read` is asynchronous and
|
||||||
|
-- nothing freezes the active window while a prompt is open, so
|
||||||
|
-- re-resolving inside `on_accept` would let the prompt offer
|
||||||
|
-- `cargo build` for A and execute in B. Sharing the resolver is
|
||||||
|
-- necessary and not sufficient; the resolution has to be captured.
|
||||||
|
-- This is Journey Stage 1a's `commit_to` discipline on a smaller
|
||||||
|
-- seam — the mechanism differs, the failure prevented is the same.
|
||||||
|
local ctx = pmacs.compile.context()
|
||||||
pmacs.minibuffer.read {
|
pmacs.minibuffer.read {
|
||||||
prompt = "Compile command: ",
|
prompt = "Compile command: ",
|
||||||
history = "compile",
|
history = "compile",
|
||||||
initial = last and last.cmdline or "",
|
-- `_last` still wins: a user who ran `cargo test` once gets it
|
||||||
|
-- back rather than being reset to the project default.
|
||||||
|
initial = last and last.cmdline or default_command_for(ctx.kind) or "",
|
||||||
on_accept = function(cmdline)
|
on_accept = function(cmdline)
|
||||||
if cmdline == nil or cmdline == "" then return end
|
if cmdline == nil or cmdline == "" then return end
|
||||||
pmacs.compile.run(cmdline)
|
-- `ctx.cwd` passes through verbatim, INCLUDING nil: a nil cwd
|
||||||
|
-- means every resolution step failed, and the header renders
|
||||||
|
-- "(unknown)" exactly as before. Re-resolving here would
|
||||||
|
-- reintroduce the drift for the case least able to tolerate it.
|
||||||
|
pmacs.compile.run(cmdline, { cwd = ctx.cwd })
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
|
|
@ -1186,3 +1254,16 @@ pmacs.keymap.bind { scope = "global", sequence = "M-g n", command = "error.next"
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "M-g p", command = "error.previous" }
|
pmacs.keymap.bind { scope = "global", sequence = "M-g p", command = "error.previous" }
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "C-x `", command = "error.next" }
|
pmacs.keymap.bind { scope = "global", sequence = "C-x `", command = "error.next" }
|
||||||
pmacs.keymap.bind { scope = "global", sequence = "M-!", command = "shell.command" }
|
pmacs.keymap.bind { scope = "global", sequence = "M-!", command = "shell.command" }
|
||||||
|
-- Journey step 9 (COHERENCE §2): running a build had no binding at all,
|
||||||
|
-- while `C-c @ C-M-s` opened all folds. `C-c c` is free, sits under the
|
||||||
|
-- established `C-c` prefix, and does not collide with CUA copy (that is
|
||||||
|
-- `M-w`). Bound here rather than in `default.lua` because a runtime
|
||||||
|
-- module owns its own global keys — `terminal.lua` binds `C-c t`,
|
||||||
|
-- `lsp.lua` binds `C-c o`.
|
||||||
|
--
|
||||||
|
-- Two inherited reachability limits, both pre-existing: inside a
|
||||||
|
-- terminal window `C-c` is consumed as the escape key, and the repl
|
||||||
|
-- package binds `C-c` at buffer scope. `M-x compile.run` still works in
|
||||||
|
-- both. `compile.recompile` gets no global chord — `g` in
|
||||||
|
-- `*compilation*` already covers rerun.
|
||||||
|
pmacs.keymap.bind { scope = "global", sequence = "C-c c", command = "compile.run" }
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,100 @@ If it does not, stop and repair the remote/fetch configuration.
|
||||||
never been enforced. Any CI job that compiles the `crdt` targets has to
|
never been enforced. Any CI job that compiles the `crdt` targets has to
|
||||||
fix them first or it will be red on arrival.
|
fix them first or it will be red on arrival.
|
||||||
|
|
||||||
|
## Journey lane (P1) — STAGE 1a MERGED; STAGE 1b-1 IMPLEMENTED, PR OPEN
|
||||||
|
|
||||||
|
- **Branch `journey-stage1b1-compile-defaults`**, worktree
|
||||||
|
`../pmacs-journey-1b1`, based on `githubsucks/main` @ `22df6ab`.
|
||||||
|
`docs/journey-stage1b1-compile-defaults-framing.md` revision 2,
|
||||||
|
approved after one review round (two blocking, two major, all
|
||||||
|
accepted). **Implemented; PR open.**
|
||||||
|
- **ON MERGE OF #203, flip four places to landed.** The PR deliberately
|
||||||
|
ships them as *in flight*, because `COHERENCE.md` §25 says a grade
|
||||||
|
changes only on landed evidence and the PR is open: `COHERENCE.md`
|
||||||
|
§2's step-9 verdict row (Partial → **Works**), §2's post-table
|
||||||
|
keybinding-inversion paragraph (third example answered; the build is
|
||||||
|
no longer its uncontested example), §20 Priority 1 and its arc list (in
|
||||||
|
flight → done), and
|
||||||
|
`docs/agent-handoff.md` §1's arc bullet (IMPLEMENTED → LANDED).
|
||||||
|
**Recorded here because an unowned doc flip is exactly how this
|
||||||
|
ledger's drift starts** — the same rule-4 precondition that kept #176's
|
||||||
|
lane alive past its merge.
|
||||||
|
- **A lexical path expectation is wrong for anything detection touched.**
|
||||||
|
`pmacs.project.detect` canonicalizes before walking
|
||||||
|
(`canonicalize_or_passthrough`, `src/project.rs:509-511`) while the
|
||||||
|
suite's `canon()` is lexical, so the compile-directory assertions
|
||||||
|
passed on Ubuntu and **failed both macOS legs**, where `/var` is a
|
||||||
|
symlink to `/private/var`. Fixed with a `detected_root()` expectation
|
||||||
|
and pinned by a **symlinked fixture**, which reproduces the disagreement
|
||||||
|
on any platform — a Linux-only bite could not have caught it.
|
||||||
|
- **Bites found two vacuous pins of my own.** The nested-project pin
|
||||||
|
passed with the keybinding removed, because `minibuffer.contents()` is
|
||||||
|
`""` both for an empty prefill and for no minibuffer at all — `""`
|
||||||
|
compared with `""`. And the hostile-`defaults` pin called
|
||||||
|
`compile.run` directly, so it never consulted `defaults` and passed
|
||||||
|
with the guard removed. Both now assert their precondition.
|
||||||
|
- **Round 1's blocking finding is the Stage 1a lesson repeating.**
|
||||||
|
Sharing one cwd resolver between the prompt and the run is *not*
|
||||||
|
enough: `pmacs.minibuffer.read` is async, the active window can change
|
||||||
|
while the prompt is open, and `run` re-resolved at accept time — so
|
||||||
|
the prompt could offer `cargo build` for A and execute in B. The
|
||||||
|
interactive command now **captures** `context()` and passes its `cwd`
|
||||||
|
through to `run`, which is `commit_to`'s discipline on a smaller seam.
|
||||||
|
The second blocker was that no pin crossed the accept boundary at all,
|
||||||
|
so that defect passed every proposed pin.
|
||||||
|
- **Why this lane exists.** `COHERENCE.md` §20 Priority 1 names Journey
|
||||||
|
Stage 1b as the golden journey's remainder — "the compile binding +
|
||||||
|
Cargo defaults, LSP spawn guidance, and the welcome buffer" — and it
|
||||||
|
had no branch, no framing, and no lane. It is the only §20 priority
|
||||||
|
with nothing in flight.
|
||||||
|
- **What 1b-1 is.** Journey step 9 only: a global `C-c c` for
|
||||||
|
`compile.run`, and a first prompt prefilled from the detected project
|
||||||
|
kind instead of empty. Lua, tests, and docs; **no Rust change and no
|
||||||
|
protocol change**. 1b-2 (LSP spawn guidance, step 6) and 1b-3 (welcome
|
||||||
|
buffer, step 4) are separate stages — see the framing §7 for why the
|
||||||
|
three are not one PR.
|
||||||
|
- **`ProjectKind::Cargo` does not exist.** `COHERENCE.md` names it twice
|
||||||
|
(§2's step-9 row and §20 Priority 1); the variant is
|
||||||
|
`ProjectKind::Rust` (`src/project.rs:78`) and line 77 is its doc
|
||||||
|
comment. The audit read the comment. Lua matches on the **tag string**
|
||||||
|
`"rust"` that `pmacs.project.detect` returns, so no Rust primitive is
|
||||||
|
needed. The PR corrects both COHERENCE sites.
|
||||||
|
- **The suggestion and the run must share one cwd resolution.** Today
|
||||||
|
the cwd is computed *inside* `pmacs.compile.run`
|
||||||
|
(`builtin/runtime/compile.lua:765`), after the prompt has closed, so a
|
||||||
|
suggestion computed in the command's `fn` would obey a different rule
|
||||||
|
than the run. The stage extracts it and exposes
|
||||||
|
`pmacs.compile.context()`, consumed by both.
|
||||||
|
- **The trap the acceptance is designed around.** The last-resort cwd is
|
||||||
|
`std::env::current_dir()` evaluated at call time
|
||||||
|
(`pmacs-protocol/src/message.rs:1703-1706`), so in tests it is the
|
||||||
|
**test runner's cwd — the pmacs repo root, itself a Cargo project**.
|
||||||
|
`compile_mode_acceptance.rs:1721` already pins this. A pin asserting
|
||||||
|
"no Cargo suggestion" that reaches the fallback would report pmacs's
|
||||||
|
own `Cargo.toml`. Negative pins therefore use a fixture carrying a
|
||||||
|
*different* marker so the fallback is never consulted;
|
||||||
|
`set_search_boundary` does **not** help here, because it only clamps a
|
||||||
|
walk that starts below the boundary.
|
||||||
|
- **Known limitation, deliberately not fixed.** After `pmacs <dir>` the
|
||||||
|
active buffer is dired's and **pathless** (`pmacs.buffer.create` never
|
||||||
|
assigns a path; dired compensates through its own module-local
|
||||||
|
`handle_for_buffer`, `dired.lua:205-217`), so the cwd falls through to
|
||||||
|
the process cwd. Launched from elsewhere that is the wrong directory.
|
||||||
|
The fix is `COHERENCE.md` §8 (First-Class Execution Locations), a
|
||||||
|
model gap; reaching into dired's private table for one string would
|
||||||
|
add an interaction island.
|
||||||
|
- Recovery from a clean checkout — **the two-argument form does not
|
||||||
|
work** (`git worktree add <path> <remote-only-branch>` fails with
|
||||||
|
`fatal: invalid reference`, because after a bare fetch no local branch
|
||||||
|
exists):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git fetch githubsucks
|
||||||
|
git worktree add ../pmacs-journey-1b1 \
|
||||||
|
-b journey-stage1b1-compile-defaults \
|
||||||
|
githubsucks/journey-stage1b1-compile-defaults
|
||||||
|
```
|
||||||
|
|
||||||
## Generated-buffer immutability lane (Arc: workbench primitives) — STAGE 1 MERGED; STAGE 2 IS NEXT
|
## Generated-buffer immutability lane (Arc: workbench primitives) — STAGE 1 MERGED; STAGE 2 IS NEXT
|
||||||
|
|
||||||
**Framing #188 (revision 7) and Stage 1 #191 are both on `main` @
|
**Framing #188 (revision 7) and Stage 1 #191 are both on `main` @
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,51 @@ commands, read `docs/active-work.md` immediately after this file.
|
||||||
interaction islands added, config-registry adoption, background-work
|
interaction islands added, config-registry adoption, background-work
|
||||||
attribution. Its §2 grades the golden journey; **Journey Stage 1a
|
attribution. Its §2 grades the golden journey; **Journey Stage 1a
|
||||||
moved that grade off "broken at step 3"** — see the arc bullet below.
|
moved that grade off "broken at step 3"** — see the arc bullet below.
|
||||||
|
- **Journey arc (P1) — Stage 1b-1 IMPLEMENTED, PR #203 OPEN**
|
||||||
|
(`docs/journey-stage1b1-compile-defaults-framing.md`). Not landed: per
|
||||||
|
`COHERENCE.md` §25 a grade changes only on landed evidence, so §2's
|
||||||
|
step-9 row still reads **Partial** and flips at merge.
|
||||||
|
Journey step 9 will move **Partial → Works**: `C-c c` runs `compile.run`,
|
||||||
|
and the first prompt is prefilled from the detected project kind via
|
||||||
|
`pmacs.compile.defaults` (seeded `rust = "cargo build"`, extensible
|
||||||
|
from `init.lua`). Lua, tests and docs; no Rust change, no protocol
|
||||||
|
change. The ratchet now carries steps 2, 3, 5 and **9**.
|
||||||
|
- **Sharing a resolver is not capturing one.** `pmacs.minibuffer.read`
|
||||||
|
is asynchronous and nothing freezes the active window while a prompt
|
||||||
|
is open, so having the prompt and `compile.run` call the *same* cwd
|
||||||
|
resolver still let them disagree: the user could be offered
|
||||||
|
`cargo build` for A and given a run in B by clicking away
|
||||||
|
mid-prompt. The interactive command captures `pmacs.compile.context()`
|
||||||
|
once and passes its `cwd` through to the run. **This is Stage 1a's
|
||||||
|
`commit_to` lesson on a smaller seam** — capture at request time,
|
||||||
|
never re-derive when the async work lands. Review found it; the
|
||||||
|
first framing had the weaker design and said it was sufficient.
|
||||||
|
- **A pin that never crosses the accept boundary cannot see that
|
||||||
|
class of bug.** Every originally-proposed pin compared values the
|
||||||
|
prompt and the resolver already agreed on, so the defect above
|
||||||
|
passed all of them. Two pins now accept the prompt and observe a
|
||||||
|
real process.
|
||||||
|
- **`ProjectKind::Cargo` does not exist** — `COHERENCE.md` named it
|
||||||
|
twice, citing `src/project.rs:77`, which is the *doc comment*; the
|
||||||
|
variant on line 78 is `ProjectKind::Rust`. Lua never sees the
|
||||||
|
variant anyway: `pmacs.project.detect` returns the tag string
|
||||||
|
`"rust"`. Corrected in both COHERENCE sites and recorded in its §24.
|
||||||
|
- **The compile fallback cwd is the process cwd, which under `cargo
|
||||||
|
test` is the pmacs repo — itself a Cargo project.** Any pin
|
||||||
|
asserting the *absence* of a Cargo suggestion that reaches the
|
||||||
|
fallback reports pmacs's own `Cargo.toml`. Bite D confirmed it: with
|
||||||
|
the kind derived from the process cwd, the plain-fixture prefill pin
|
||||||
|
still passed and only the nested-project pin caught it.
|
||||||
|
`set_search_boundary` is no defence — it clamps only a walk starting
|
||||||
|
*below* the boundary.
|
||||||
|
- **Named limitation, deliberately unfixed:** after `pmacs <dir>` the
|
||||||
|
active buffer is dired's and **pathless** (`pmacs.buffer.create`
|
||||||
|
assigns no path; dired compensates through its own module-local
|
||||||
|
`handle_for_buffer`), so the cwd falls through to the process cwd.
|
||||||
|
Launched from elsewhere that is the wrong directory. The fix is
|
||||||
|
§8's execution-location model, not a reach into dired's private
|
||||||
|
table. What is guaranteed is that the failure stays *coherent*: the
|
||||||
|
suggestion always describes the directory the run will use.
|
||||||
- **Journey arc (P1) — Stage 1a LANDED**
|
- **Journey arc (P1) — Stage 1a LANDED**
|
||||||
(`docs/journey-stage1a-framing.md`). `pmacs .` opens a directory
|
(`docs/journey-stage1a-framing.md`). `pmacs .` opens a directory
|
||||||
instead of exiting 1, on **one** path: `resolve_target_buffer` gained a
|
instead of exiting 1, on **one** path: `resolve_target_buffer` gained a
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,12 @@ Not in 1a — Stage 1b: a compile keybinding and `cargo build`/`test`
|
||||||
defaults from the existing `ProjectKind::Cargo`, LSP spawn-failure
|
defaults from the existing `ProjectKind::Cargo`, LSP spawn-failure
|
||||||
guidance (§1.2), a welcome buffer.
|
guidance (§1.2), a welcome buffer.
|
||||||
|
|
||||||
|
*(Correction, added by Stage 1b-1: there is no `ProjectKind::Cargo`. The
|
||||||
|
variant is `ProjectKind::Rust`; this line inherited the error from
|
||||||
|
`COHERENCE.md` §20, now recorded in its §24. Left in place rather than
|
||||||
|
rewritten — this document records what was planned — but flagged so the
|
||||||
|
wrong type name does not survive by being quoted.)*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Ground truth (scouted 2026-07-26, `main` @ `d400f30`; re-verified rev 4)
|
## 2. Ground truth (scouted 2026-07-26, `main` @ `d400f30`; re-verified rev 4)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,691 @@
|
||||||
|
# Journey Stage 1b-1 — make building discoverable
|
||||||
|
|
||||||
|
**Status: framing, rev 2 — awaiting review round 2.**
|
||||||
|
**Serves `COHERENCE.md` §2 (the golden product journey, step 9), §19
|
||||||
|
(coherence acceptance tests), §20 Priority 1.**
|
||||||
|
|
||||||
|
## 0. Revision history
|
||||||
|
|
||||||
|
- rev 2 (2026-07-30) — review round 1. Two blocking, two major; all four
|
||||||
|
accepted, all four verified in the code first.
|
||||||
|
- **§3.1 did not actually prevent the drift it claimed to.**
|
||||||
|
`pmacs.minibuffer.read` is asynchronous: `on_accept` runs later, and
|
||||||
|
the active window can change while the prompt is open. `run` then
|
||||||
|
re-resolved the cwd from whatever was active *at accept time*, so the
|
||||||
|
prompt could say `cargo build` for A and execute in B. **Sharing one
|
||||||
|
resolver is not enough — the resolution has to be captured.** The
|
||||||
|
interactive `fn` now captures `context()` and passes its `cwd`
|
||||||
|
through to `run`. This is the Stage 1a lesson repeating: the
|
||||||
|
destination is captured at request time, not re-derived at commit
|
||||||
|
time (`commit_to`, §4.4 of that framing).
|
||||||
|
- **No pin accepted the prompt or observed a spawned run.** N1–N5 all
|
||||||
|
compared values that the prompt and the resolver had already agreed
|
||||||
|
on; a wrong cwd inside `on_accept` — exactly the defect above —
|
||||||
|
passed every one of them. Two pins now cross the accept boundary:
|
||||||
|
N3 (the captured directory survives a window switch) and N4 (accept
|
||||||
|
`cargo build` in a real Cargo fixture, assert the `Directory:` header
|
||||||
|
and a clean exit).
|
||||||
|
- **N3's named falsifier was not discriminating.**
|
||||||
|
`project_root_of_active()` already detects from the active file and
|
||||||
|
returns the *innermost* root, so re-detecting from that root yields
|
||||||
|
`node` again in `mixed_fixture/sub`. The mutation rev 1 named would
|
||||||
|
have left the pin green. N3 is repurposed to the prompt-to-run
|
||||||
|
handoff, and the mixed-fixture pin (now N5) states a falsifier it
|
||||||
|
actually catches.
|
||||||
|
- **§6.1 contradicted §3.1.** A Cargo *workspace subdirectory*
|
||||||
|
contains no `Cargo.toml` and correctly receives `cargo build`, so
|
||||||
|
"never offered `cargo build` for a directory with no `Cargo.toml`"
|
||||||
|
was false on the design's own terms. Reworded to *no detected Cargo
|
||||||
|
project*.
|
||||||
|
- rev 1 (2026-07-30) — first framing. Scouted against `githubsucks/main`
|
||||||
|
@ `22df6ab` (test-ambient isolation framing #201).
|
||||||
|
|
||||||
|
## 1. What this stage is, and what it is not
|
||||||
|
|
||||||
|
`COHERENCE.md` §20 Priority 1 names the remainder of the journey arc:
|
||||||
|
|
||||||
|
> Journey Stage 1b is the named remainder: the compile binding + Cargo
|
||||||
|
> defaults, LSP spawn guidance, and the welcome buffer.
|
||||||
|
|
||||||
|
Those three are unrelated in mechanism, in failure mode, and in cost.
|
||||||
|
This stage takes **only the first**: journey step 9, *build or test the
|
||||||
|
project*. LSP spawn guidance (step 6) and the welcome buffer (step 4)
|
||||||
|
get their own stages; §7 states the split and why.
|
||||||
|
|
||||||
|
Nothing here is a new Rust primitive. The stage is Lua, tests, and docs.
|
||||||
|
|
||||||
|
## 2. Ground truth
|
||||||
|
|
||||||
|
Every claim below was read in the tree at `22df6ab`, not inferred from a
|
||||||
|
name.
|
||||||
|
|
||||||
|
### 2.1 `compile.run` has no binding, and the docs say so
|
||||||
|
|
||||||
|
`compile.lua` binds four global sequences (`:1183-1188`): `M-g n`,
|
||||||
|
`M-g p`, `C-x \``, `M-!`. `bind_slot_keys` (`:220-240`) binds
|
||||||
|
`RET`/`n`/`p`/`q`/`C-c C-k`/`g` at **buffer** scope inside a generated
|
||||||
|
buffer. `builtin/keymaps/default.lua` never mentions compile. There is
|
||||||
|
no third keymap file — `builtin/keymaps/` contains `default.lua` alone.
|
||||||
|
|
||||||
|
`docs/keybindings.md:191-192` states it outright:
|
||||||
|
|
||||||
|
> `compile.run` and `compile.recompile` are available through `M-x`; no
|
||||||
|
> global key is assigned to them.
|
||||||
|
|
||||||
|
### 2.2 The first prompt is empty
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- builtin/runtime/compile.lua:1131-1144
|
||||||
|
name = "compile.run",
|
||||||
|
fn = function()
|
||||||
|
local last = pmacs.compile._last
|
||||||
|
pmacs.minibuffer.read {
|
||||||
|
prompt = "Compile command: ",
|
||||||
|
history = "compile",
|
||||||
|
initial = last and last.cmdline or "",
|
||||||
|
```
|
||||||
|
|
||||||
|
`_last` is set only by a completed `pmacs.compile.run` (`:1094`), so on a
|
||||||
|
fresh session `initial` is `""`. The user is asked what to build and
|
||||||
|
given nothing to build with.
|
||||||
|
|
||||||
|
`initial` does reach the user: `Minibuffer::begin` calls
|
||||||
|
`replace_contents(&session.initial)` (`src/minibuffer.rs:106-107`), and
|
||||||
|
`contents()` (`:129`) reads it back. The prefill mechanism works; it is
|
||||||
|
handed an empty string.
|
||||||
|
|
||||||
|
### 2.3 `ProjectKind::Cargo` does not exist — the variant is `Rust`
|
||||||
|
|
||||||
|
**`COHERENCE.md` is wrong here, in two places.** Its §2 step-9 row and
|
||||||
|
its §20 Priority 1 paragraph both say "`ProjectKind::Cargo` existing
|
||||||
|
(`src/project.rs:77`)". Line 77 is a doc comment; the variant on line 78
|
||||||
|
is `Rust`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/project.rs:77-78
|
||||||
|
/// A Cargo workspace (`Cargo.toml`).
|
||||||
|
Rust,
|
||||||
|
```
|
||||||
|
|
||||||
|
The marker that produces it is `{ name: "Cargo.toml", kind:
|
||||||
|
ProjectKind::Rust, is_directory: false }` (`:148-150`). The audit read
|
||||||
|
the comment and named the comment. This framing corrects both COHERENCE
|
||||||
|
sites (§8).
|
||||||
|
|
||||||
|
The correction is not cosmetic: it decides what the Lua side matches on.
|
||||||
|
|
||||||
|
### 2.4 Lua already sees the project kind — as a tag string
|
||||||
|
|
||||||
|
`pmacs.project.detect(path)` returns a table (`src/lua_bindings/mod.rs:11683-11694`):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
t.set("root", root.display().to_string())?;
|
||||||
|
t.set("kind", kind.tag())?;
|
||||||
|
t.set("language_id", kind.default_language_id())?;
|
||||||
|
```
|
||||||
|
|
||||||
|
`ProjectKind::Rust.tag()` is `"rust"` (`src/project.rs:101`). So the Lua
|
||||||
|
key is the string `"rust"`, and **no Rust change is needed to learn the
|
||||||
|
project kind**.
|
||||||
|
|
||||||
|
`detect` accepts a directory as well as a file: `walk_for_marker` uses
|
||||||
|
`start` unchanged when it is not a file (`src/project.rs:225-229`).
|
||||||
|
|
||||||
|
### 2.5 The cwd is resolved *inside* `run`, after the prompt has closed
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- builtin/runtime/compile.lua:765
|
||||||
|
local cwd = opts.cwd or project_root_of_active() or daemon_working_directory()
|
||||||
|
```
|
||||||
|
|
||||||
|
The interactive `fn` that builds the prompt cannot see this. Any
|
||||||
|
suggestion computed in the `fn` today would be computed from a different
|
||||||
|
rule than the one the run obeys — which is the failure this stage must
|
||||||
|
not ship. §3.1 is about closing that gap before adding the suggestion,
|
||||||
|
not after.
|
||||||
|
|
||||||
|
### 2.6 The active buffer at the journey moment is **pathless**
|
||||||
|
|
||||||
|
`project_root_of_active()` (`:600-609`) needs `buf:path()`. Directly
|
||||||
|
after `pmacs .` the active buffer is dired's, created by
|
||||||
|
`pmacs.buffer.create(name)` (`dired.lua:506`) — a name, never a path.
|
||||||
|
`buf:path()` returns `file_path()` mapped to a string
|
||||||
|
(`src/lua_bindings/mod.rs:1242-1248`), so it is `nil`.
|
||||||
|
|
||||||
|
This is not a guess. dired itself compensates, and its own helper is the
|
||||||
|
evidence:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- builtin/runtime/dired.lua:205-217
|
||||||
|
local function current_directory()
|
||||||
|
local buf = pmacs.window.buffer()
|
||||||
|
if buf ~= nil then
|
||||||
|
local ok, path = pcall(function() return buf:path() end)
|
||||||
|
if ok and path then ... end
|
||||||
|
local h = handle_for_buffer(buf) -- <-- the pathless case
|
||||||
|
if h then return h.path end
|
||||||
|
end
|
||||||
|
return canonicalize(".")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
`handle_for_buffer` is a **module-local** table. Compile cannot reach it,
|
||||||
|
and reaching for it would make compile depend on dired — a new
|
||||||
|
interaction island for one directory string. §5 decides against it and
|
||||||
|
§6 states the residual gap.
|
||||||
|
|
||||||
|
### 2.7 The last-resort cwd is the *process* cwd, evaluated at call time
|
||||||
|
|
||||||
|
`daemon_working_directory()` reads
|
||||||
|
`pmacs.instance.identity().working_directory`, which is:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// pmacs-protocol/src/message.rs:1703-1706
|
||||||
|
working_directory: std::env::current_dir()
|
||||||
|
.ok()
|
||||||
|
.map(|p| p.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
```
|
||||||
|
|
||||||
|
Two consequences, both load-bearing:
|
||||||
|
|
||||||
|
- **In production**, `pmacs .` is launched from the project directory, so
|
||||||
|
the fallback happens to be right. `pmacs ~/code/proj` launched from
|
||||||
|
`~` resolves to `~`, and the fallback is wrong. That is a pre-existing
|
||||||
|
step-9 defect this stage does not fix (§6.1).
|
||||||
|
- **In tests**, it is the *test runner's* cwd — the pmacs repo root,
|
||||||
|
**which is a Cargo project**. `compile_mode_acceptance.rs:1721-1733`
|
||||||
|
already pins exactly this (`r1f8_inherited_cwd_resolves_to_the_daemon_working_directory`),
|
||||||
|
and `:1382` already warns that "fallback would search the test
|
||||||
|
process's cwd". Any pin that asserts *no* Cargo suggestion is at the
|
||||||
|
mercy of where `cargo test` was invoked unless it is designed to never
|
||||||
|
reach the fallback. §5.2 designs for that; it is the single largest
|
||||||
|
trap in this stage.
|
||||||
|
|
||||||
|
### 2.8 `C-c c` is free, and `C-c` is not CUA copy
|
||||||
|
|
||||||
|
Global `C-c` sequences in the tree: `C-c a`/`f`/`h`/`H`/`i`/`o`/`r`/`s`/`y`
|
||||||
|
(`lsp.lua`), `C-c t` (`terminal.lua:203`), `C-c @ …` (folding).
|
||||||
|
`C-c c` is unbound. Copy is `M-w` (`builtin/keymaps/default.lua:114`) —
|
||||||
|
the CUA trio was deliberately not taken — so binding under the `C-c`
|
||||||
|
prefix collides with nothing in the default map.
|
||||||
|
|
||||||
|
`terminal.lua` binds its own `C-c t`; `lsp.lua` binds its own `C-c o`.
|
||||||
|
A runtime module owning its global binding is the established pattern,
|
||||||
|
and this stage follows it rather than editing `default.lua`.
|
||||||
|
|
||||||
|
### 2.9 dired already decided the prompt shape
|
||||||
|
|
||||||
|
`C-x d` prefills its prompt and **deliberately refuses a completion
|
||||||
|
source** (`dired.lua:749-756`): a candidate list makes RET-on-empty open
|
||||||
|
whatever sorts first, and a selected candidate shadows typed text. The
|
||||||
|
same reasoning applies to a compile command, so this stage prefills and
|
||||||
|
adds no completion source.
|
||||||
|
|
||||||
|
## 3. Design
|
||||||
|
|
||||||
|
### 3.1 One resolution, consumed twice
|
||||||
|
|
||||||
|
Extract the `:765` expression into a helper and expose a read-only view
|
||||||
|
of it:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local function resolve_cwd(explicit)
|
||||||
|
return explicit or project_root_of_active() or daemon_working_directory()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Where the next compile would run, and what kind of project is
|
||||||
|
--- detected *from that directory*. Public getter (API conventions):
|
||||||
|
--- `{ cwd = string|nil, kind = string|nil }`.
|
||||||
|
function pmacs.compile.context(explicit_cwd)
|
||||||
|
```
|
||||||
|
|
||||||
|
`run` calls `resolve_cwd(opts.cwd)`; the interactive `fn` calls
|
||||||
|
`pmacs.compile.context()`.
|
||||||
|
|
||||||
|
**Sharing the resolver is necessary and not sufficient.**
|
||||||
|
`pmacs.minibuffer.read` is asynchronous — `on_accept` runs an arbitrary
|
||||||
|
time later, and nothing freezes the active window while a prompt is
|
||||||
|
open. A mouse click, a second frontend, or a background open can change
|
||||||
|
what `project_root_of_active()` answers between the prompt and the RET.
|
||||||
|
Two calls to the same resolver at two different moments are still two
|
||||||
|
different answers, and the user is then shown a suggestion for A and
|
||||||
|
given a run in B.
|
||||||
|
|
||||||
|
So the interactive command **captures** the resolution and hands it
|
||||||
|
through:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
fn = function()
|
||||||
|
local last = pmacs.compile._last
|
||||||
|
local ctx = pmacs.compile.context() -- captured once, here
|
||||||
|
pmacs.minibuffer.read {
|
||||||
|
prompt = "Compile command: ",
|
||||||
|
history = "compile",
|
||||||
|
initial = last and last.cmdline or default_for(ctx.kind) or "",
|
||||||
|
on_accept = function(cmdline)
|
||||||
|
if cmdline == nil or cmdline == "" then return end
|
||||||
|
pmacs.compile.run(cmdline, { cwd = ctx.cwd }) -- the same ctx
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
This is Journey Stage 1a's `commit_to` lesson on a smaller seam: the
|
||||||
|
destination is captured when the request is made and revalidated at
|
||||||
|
commit, never re-derived from whatever happens to be ambient when the
|
||||||
|
async work lands. The mechanism differs — compile needs no scope
|
||||||
|
override, only the value — but the failure it prevents is the same one.
|
||||||
|
|
||||||
|
`ctx.cwd` is passed through verbatim, **including `nil`**. A `nil` cwd
|
||||||
|
means every resolution step failed, and `run`'s header renders
|
||||||
|
`(unknown)` exactly as it does today; substituting a re-resolution there
|
||||||
|
would reintroduce the drift for the one case least able to tolerate it.
|
||||||
|
|
||||||
|
**Only the interactive command captures.** `pmacs.compile.run(cmdline)`
|
||||||
|
called programmatically still resolves at call time, which is what a
|
||||||
|
caller with no prompt in between means by "here".
|
||||||
|
|
||||||
|
With both halves in place: **the suggestion is a function of the
|
||||||
|
directory the command will execute in**, and the two cannot drift —
|
||||||
|
neither across the two resolutions nor across the wait for input.
|
||||||
|
|
||||||
|
`kind` is `pmacs.project.detect(cwd).kind` — detection *from* the cwd,
|
||||||
|
which is not the same as "the cwd is the root": `opts.cwd = /proj/src`
|
||||||
|
in a Cargo workspace yields kind `rust` with root `/proj`. That is
|
||||||
|
correct (cargo works from a subdirectory) and is stated so a reader does
|
||||||
|
not read `kind` as "this directory is a project root".
|
||||||
|
|
||||||
|
`project_root_of_active()` already calls `detect` once to get a root;
|
||||||
|
`context` then calls it again on the result. The second call is
|
||||||
|
redundant in that branch and is kept anyway, because the alternative is
|
||||||
|
two different rules for where `kind` comes from depending on which
|
||||||
|
branch produced the cwd. One rule, stated as: **`kind` is always
|
||||||
|
`detect(cwd)`.**
|
||||||
|
|
||||||
|
### 3.2 The default table
|
||||||
|
|
||||||
|
```lua
|
||||||
|
--- Default compile command per detected project kind, keyed by the
|
||||||
|
--- tag `pmacs.project.detect` returns. Assign into this table from
|
||||||
|
--- `init.lua` to add or override one.
|
||||||
|
pmacs.compile.defaults = { rust = "cargo build" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Only `rust` is seeded, and that is a decision rather than an
|
||||||
|
omission.** Rust has one answer. Node does not (`npm` / `yarn` / `pnpm`
|
||||||
|
/ a `scripts.build` that may not exist); Python does not; Go's build and
|
||||||
|
test are different commands with equal claim. A wrong prefill is worse
|
||||||
|
than an empty one — the user must first delete it, then type. The table
|
||||||
|
exists so a user or a package can add the answer *they* know.
|
||||||
|
|
||||||
|
`cargo test` is reachable by editing the prefill. The prompt prefills
|
||||||
|
one string; offering both would need a candidate list, which §2.9
|
||||||
|
already ruled out for this prompt.
|
||||||
|
|
||||||
|
### 3.3 Precedence at the prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
last.cmdline -- unchanged; a session that has compiled keeps its command
|
||||||
|
or defaults[kind] -- new
|
||||||
|
or "" -- unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
`last` winning is deliberate: a user who ran `cargo test` once should get
|
||||||
|
`cargo test` back, not be reset to `cargo build`. This is a preservation
|
||||||
|
pin (§5.4, P1), not an accident of ordering.
|
||||||
|
|
||||||
|
### 3.4 The table is user-writable, so reading it is guarded
|
||||||
|
|
||||||
|
`pmacs.compile.defaults` is public and assignable, which means a
|
||||||
|
metatable with a throwing `__index`, a non-string value, or a
|
||||||
|
non-table replacement all have to be survivable. The module already
|
||||||
|
holds this discipline for a hostile rule container (`validated_rules`,
|
||||||
|
round-2 finding 3: shell-command "must neither surface compile-rule
|
||||||
|
warnings nor fail on a hostile rule container").
|
||||||
|
|
||||||
|
The lookup therefore runs under `pcall` and accepts a value only when it
|
||||||
|
is a non-empty string. Anything else yields `""` — the pre-stage
|
||||||
|
behavior. **A broken `defaults` degrades to today's prompt; it never
|
||||||
|
prevents compiling.**
|
||||||
|
|
||||||
|
### 3.5 The binding
|
||||||
|
|
||||||
|
```lua
|
||||||
|
pmacs.keymap.bind { scope = "global", sequence = "C-c c", command = "compile.run" }
|
||||||
|
```
|
||||||
|
|
||||||
|
In `compile.lua`, beside the four existing global binds (§2.8).
|
||||||
|
|
||||||
|
Two reachability limits it inherits, both pre-existing and both stated
|
||||||
|
rather than discovered later:
|
||||||
|
|
||||||
|
- Inside a **terminal** window `C-c` is consumed as the escape key, so
|
||||||
|
`C-c c` does not arrive. `COHERENCE.md` §2 already records this for
|
||||||
|
`C-c t`. `M-x compile.run` still works there.
|
||||||
|
- The repl package binds `C-c` at **buffer** scope
|
||||||
|
(`builtin/packages/repl/init.lua:300`), which shadows the global
|
||||||
|
prefix in a repl buffer. Same escape hatch.
|
||||||
|
|
||||||
|
`compile.recompile` gets no global binding: `g` in `*compilation*`
|
||||||
|
already covers rerun, and adding a second global chord for it is scope
|
||||||
|
this stage has no journey argument for.
|
||||||
|
|
||||||
|
## 4. What this changes for the journey
|
||||||
|
|
||||||
|
Walking `COHERENCE.md` §2 on a Rust project, unconfigured:
|
||||||
|
|
||||||
|
| | before | after |
|
||||||
|
|---|---|---|
|
||||||
|
| launch | `pmacs .` lists the directory (Stage 1a) | unchanged |
|
||||||
|
| open a file | `RET` visits it (Stage 1a) | unchanged |
|
||||||
|
| build | *no key exists*; `M-x compile.run` → empty prompt | `C-c c` → `Compile command: cargo build` |
|
||||||
|
| accept | — | RET runs it in the detected root |
|
||||||
|
| errors | `M-g n` walks them | unchanged |
|
||||||
|
|
||||||
|
Step 9's verdict row moves from **Partial** to **Works**; step 10 stops
|
||||||
|
being gated on the user already knowing `M-x compile.run`.
|
||||||
|
|
||||||
|
## 5. Acceptance
|
||||||
|
|
||||||
|
### 5.0 Two labels, as Stage 1a established
|
||||||
|
|
||||||
|
**N** — new behavior, must fail on full revert. **P** — preservation,
|
||||||
|
legitimately green on the pre-image, falsified only by a named targeted
|
||||||
|
mutation. Stage 1a's §6.0 is the reason the distinction is kept: an
|
||||||
|
equivalence assertion between two implementations that already agree
|
||||||
|
proves nothing.
|
||||||
|
|
||||||
|
Every pin below names the mutation that falsifies it. `scripts/bite` is
|
||||||
|
run against the suite before the PR opens.
|
||||||
|
|
||||||
|
### 5.1 Where the pins live
|
||||||
|
|
||||||
|
- **`tests/journey_acceptance.rs`** gains a step-9 section. This file is
|
||||||
|
the ratchet — *stages add rows, none removes them*. Its pins go
|
||||||
|
through the **real** entry points: `EditorState::open` on a directory,
|
||||||
|
a dispatched `RET`, a dispatched `C-c c`. Nothing calls
|
||||||
|
`pmacs.compile.context()` directly in this file.
|
||||||
|
- **`tests/compile_mode_acceptance.rs`** gains the module-contract pins:
|
||||||
|
`context()`'s shape, and the hostile-table guards.
|
||||||
|
|
||||||
|
### 5.2 The fixture problem, and its only safe shape
|
||||||
|
|
||||||
|
Per §2.7, the last-resort cwd is the test runner's cwd, and the test
|
||||||
|
runner's cwd is a Cargo project. **A pin that asserts "no `cargo build`
|
||||||
|
suggestion" and reaches the fallback will report the pmacs repo's own
|
||||||
|
`Cargo.toml` as the fixture's answer.** It would pass or fail on where
|
||||||
|
`cargo test` was invoked from.
|
||||||
|
|
||||||
|
The negative pins are therefore built so the fallback is **never
|
||||||
|
consulted**: the fixture carries a *different* project marker, so
|
||||||
|
`project_root_of_active()` resolves inside the fixture and returns
|
||||||
|
before `daemon_working_directory()` is reached.
|
||||||
|
|
||||||
|
```
|
||||||
|
rust_fixture/ Cargo.toml, main.rs
|
||||||
|
mixed_fixture/ Cargo.toml, main.rs
|
||||||
|
sub/ package.json, index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
`mixed_fixture` is what makes N5 discriminating: the file opened is
|
||||||
|
`sub/index.js`, the nearest marker is `package.json` (kind `node`, no
|
||||||
|
default), and the *outer* marker is Cargo. A suggestion computed from
|
||||||
|
anything other than the resolved cwd — the outermost marker, the launch
|
||||||
|
directory, the process cwd — produces `cargo build` here. The correct
|
||||||
|
implementation produces `""`.
|
||||||
|
|
||||||
|
`pmacs.project.set_search_boundary` is set to the fixture root in each
|
||||||
|
test that detects, so a stray marker above the tempdir (a developer's
|
||||||
|
`/tmp/.git`) cannot leak in. Note what it does **not** do: it clamps the
|
||||||
|
upward walk from a start *below* the boundary, so it is no protection at
|
||||||
|
all for the fallback path, whose start is the repo root. That is why the
|
||||||
|
fixture shape above, not the boundary call, is the actual defense.
|
||||||
|
|
||||||
|
### 5.3 New-behavior pins
|
||||||
|
|
||||||
|
**N1 — the chord reaches the command.**
|
||||||
|
Launch on `rust_fixture`, `RET` on `main.rs`, dispatch `C-c c`; assert
|
||||||
|
`pmacs.minibuffer.is_active()`.
|
||||||
|
*Falsifier:* remove the `keymap.bind` line — the chord is unbound, no
|
||||||
|
session opens.
|
||||||
|
*Why it is separate from N2:* a prefill assertion alone would stay green
|
||||||
|
if the binding were removed and the prompt were opened some other way.
|
||||||
|
The binding is the thing COHERENCE says is missing; it gets its own pin.
|
||||||
|
|
||||||
|
**N2 — the prompt is prefilled from the project kind.**
|
||||||
|
Same walk; assert `pmacs.minibuffer.contents() == "cargo build"`.
|
||||||
|
*Falsifier:* drop the `defaults[kind]` term from the precedence chain —
|
||||||
|
contents become `""`.
|
||||||
|
|
||||||
|
**N3 — the prompt's directory survives a window switch.**
|
||||||
|
Launch on `rust_fixture`, `RET` on `main.rs`, dispatch `C-c c` — the
|
||||||
|
prompt is now open and has captured A. **Then open a file in a second,
|
||||||
|
unrelated directory B**, so the active buffer's project is no longer A.
|
||||||
|
Replace the minibuffer contents with `pwd` and dispatch `RET`.
|
||||||
|
Assert `*compilation*` contains `Directory: <A>` **and** that `pwd`'s own
|
||||||
|
output is `<A>` — two independent readings of the same claim, one from
|
||||||
|
the header pmacs writes and one from the shell that actually ran.
|
||||||
|
*Falsifier:* drop `{ cwd = ctx.cwd }` from `on_accept`, i.e. rev 1's
|
||||||
|
design. `run` re-resolves at accept time, finds B, and both readings say
|
||||||
|
B.
|
||||||
|
*Why `pwd` and not `cargo build`:* the subject here is the directory, and
|
||||||
|
a cheap command keeps the pin's failure message about the directory. N4
|
||||||
|
is the pin that runs the real thing.
|
||||||
|
*Why the contents are set rather than typed:* the prompt is opened
|
||||||
|
through the real chord; only the editing of an already-open prompt is
|
||||||
|
short-circuited, which is the same split `find_file_acceptance` documents
|
||||||
|
at its head.
|
||||||
|
|
||||||
|
**N4 — the offered command runs, in the offered directory.**
|
||||||
|
A real Cargo fixture: `Cargo.toml`, `src/main.rs` with an empty `main`.
|
||||||
|
Launch on it, `RET` on `main.rs`, dispatch `C-c c`, dispatch `RET`
|
||||||
|
**without editing** — accepting exactly what was offered. Assert
|
||||||
|
`*compilation*` contains `Directory: <fixture>` and the clean-exit marker
|
||||||
|
`[compilation exited with code 0]`.
|
||||||
|
*Falsifier:* the same `{ cwd = ... }` removal as N3, and independently
|
||||||
|
any prefill that is not a runnable command.
|
||||||
|
*Why this pin has to exist:* every other pin in this stage compares
|
||||||
|
values that the prompt and the resolver already agree on. A wrong
|
||||||
|
directory inside `on_accept` — the exact defect rev 1 shipped — passes
|
||||||
|
all of them. This one crosses the accept boundary and observes a real
|
||||||
|
process.
|
||||||
|
*Operational detail:* the fixture builds into its own tempdir `target/`,
|
||||||
|
so it takes no lock the enclosing `cargo test` holds and leaves nothing
|
||||||
|
behind. The pin runs when `cargo` resolves on PATH and skips with a
|
||||||
|
message otherwise; **`PMACS_REQUIRE_CARGO_BUILD` only tolerates
|
||||||
|
absence** — presence of the binary decides execution, and the variable
|
||||||
|
makes a missing binary fatal in CI. That is the `PMACS_REQUIRE_BASH`
|
||||||
|
arming shape from #200, and it is stated here because getting the
|
||||||
|
polarity backwards is how a required pin becomes a silent skip.
|
||||||
|
*Named risk:* this is the only pin that depends on a working toolchain in
|
||||||
|
the fixture directory. If it proves flaky in CI, the fallback is to keep
|
||||||
|
the accept-and-observe shape and substitute a command with no toolchain
|
||||||
|
dependency — the prompt-to-run handoff is the claim, and `cargo build` is
|
||||||
|
the most faithful witness of it, not the only possible one.
|
||||||
|
|
||||||
|
**N5 — the suggestion follows the directory the run will use.**
|
||||||
|
Launch on `mixed_fixture`, `RET` into `sub/index.js`, dispatch `C-c c`;
|
||||||
|
assert contents are `""` and `context().kind == "node"`.
|
||||||
|
*Falsifier:* a rule that derives the kind from anything other than the
|
||||||
|
resolved cwd. The plausible one is the **launch directory** — Stage 1a
|
||||||
|
made it prominent, and `mixed_fixture` was launched on the Cargo root, so
|
||||||
|
that rule yields `cargo build` here. So does deriving it from the process
|
||||||
|
cwd.
|
||||||
|
*What it does not catch, stated because rev 1 claimed otherwise:*
|
||||||
|
re-detecting from `project_root_of_active()`'s answer. That helper
|
||||||
|
already returns the **innermost** root, so detecting from it yields
|
||||||
|
`node` again and this pin stays green. Rev 1 named that mutation as the
|
||||||
|
falsifier; it is not one.
|
||||||
|
|
||||||
|
**N6 — `context()` is total in a launched session.**
|
||||||
|
Property, not a constant, because the value is environment-dependent
|
||||||
|
(§2.7): after a launch, `context().cwd` is non-nil, and `context().kind`
|
||||||
|
equals `pmacs.project.detect(context().cwd)`'s kind (both nil, or both
|
||||||
|
the same string). Asserted with a dired buffer active — the pathless
|
||||||
|
case — so the fallback branch is the one under test.
|
||||||
|
*Falsifier:* make `resolve_cwd` return `nil` when the active buffer has
|
||||||
|
no path, i.e. drop the `daemon_working_directory()` term.
|
||||||
|
*What it deliberately does not assert:* which directory. Pinning that
|
||||||
|
would pin the test runner's cwd.
|
||||||
|
|
||||||
|
### 5.4 Preservation pins
|
||||||
|
|
||||||
|
**P1 — `_last` still outranks the kind default.**
|
||||||
|
Run a compile with an explicit cmdline that is not `cargo build`, then
|
||||||
|
open the prompt in the Rust fixture; assert the contents are the last
|
||||||
|
cmdline.
|
||||||
|
*Targeted mutation:* reorder the precedence chain to put `defaults[kind]`
|
||||||
|
first. Green on the pre-image (there was no default), red under the
|
||||||
|
mutation.
|
||||||
|
|
||||||
|
**P2 — a hostile `defaults` cannot break compiling.**
|
||||||
|
Three cases in `compile_mode_acceptance`: `defaults` replaced by a
|
||||||
|
non-table; a `__index` metatable that raises; a non-string entry for
|
||||||
|
`rust`. In all three the prompt opens with `""` and `compile.run` still
|
||||||
|
executes a typed command.
|
||||||
|
*Targeted mutation:* remove the `pcall` / type guard — the raising case
|
||||||
|
propagates out of the command and no prompt opens.
|
||||||
|
|
||||||
|
**P3 — the existing compile bindings are unchanged.**
|
||||||
|
`M-g n`, `M-g p`, `C-x \``, `M-!` still dispatch to their commands, and
|
||||||
|
`compile.run` is still reachable through `M-x`.
|
||||||
|
*Targeted mutation:* the new `bind` call written as an `unbind`+`bind`
|
||||||
|
pair over the wrong sequence.
|
||||||
|
|
||||||
|
### 5.5 Gates
|
||||||
|
|
||||||
|
The full suite from `CLAUDE.md`, plus `compile_mode_acceptance`,
|
||||||
|
`journey_acceptance`, `dired_acceptance`, and `find_file_acceptance` as
|
||||||
|
the touched suites. Local runs must control all five bootstrap-storage
|
||||||
|
variables (`XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`,
|
||||||
|
`XDG_CACHE_HOME`, `PMACS_STATE_HOME`) — the ambient-root isolation lane
|
||||||
|
(#201) is **framing only**, so the workaround is still required and
|
||||||
|
`compile_mode_acceptance` is one of the suites that goes red without it.
|
||||||
|
|
||||||
|
## 6. Named limitations — stated, not discovered later
|
||||||
|
|
||||||
|
### 6.1 `pmacs <dir>` from elsewhere still resolves the wrong cwd
|
||||||
|
|
||||||
|
Launched as `pmacs ~/code/proj` from `~`, the active buffer is dired and
|
||||||
|
pathless (§2.6), so the cwd falls through to the process cwd `~`
|
||||||
|
(§2.7). Compile would then run in `~`, and — consistently, since §3.1
|
||||||
|
ties them — suggest nothing.
|
||||||
|
|
||||||
|
**This stage does not fix it.** The fix needs a notion of "the directory
|
||||||
|
this session is working in" that is not any one module's private table,
|
||||||
|
which is `COHERENCE.md` §8 (First-Class Execution Locations) — a model
|
||||||
|
gap, not wiring. Reaching into dired's `handle_for_buffer` would make
|
||||||
|
compile depend on dired for one string and add exactly the kind of
|
||||||
|
interaction island §6 of COHERENCE is about.
|
||||||
|
|
||||||
|
What this stage does guarantee is that the failure is **coherent**: the
|
||||||
|
suggestion describes the directory the command will run in, whatever
|
||||||
|
that directory turns out to be. The user is never offered `cargo build`
|
||||||
|
for a directory with **no detected Cargo project**.
|
||||||
|
|
||||||
|
That wording is load-bearing and rev 1 got it wrong. "No `Cargo.toml`"
|
||||||
|
would have been false on this design's own terms: a Cargo *workspace
|
||||||
|
subdirectory* contains no `Cargo.toml`, is correctly detected as `rust`
|
||||||
|
by the ancestor walk, and correctly receives `cargo build` — which cargo
|
||||||
|
itself runs happily from a subdirectory. The predicate is detection, not
|
||||||
|
the presence of a file in that one directory (§3.1).
|
||||||
|
|
||||||
|
N6 pins the property; it deliberately does not pin the value.
|
||||||
|
|
||||||
|
### 6.2 The default is per-kind, not configurable through the registry
|
||||||
|
|
||||||
|
`pmacs.compile.defaults` is a plain Lua table, not a registered setting.
|
||||||
|
It cannot be one: `ConfigValue` is four scalars, so a kind→command map
|
||||||
|
is not expressible. A scalar `compile.default-command` that overrides
|
||||||
|
the table *is* expressible and is **deferred, not skipped** — it is one
|
||||||
|
more precedence step and a registry entry, and it belongs with the
|
||||||
|
config-adoption work (§20 Priority 6) rather than bolted on here.
|
||||||
|
|
||||||
|
### 6.3 One binding does not retire the inversion
|
||||||
|
|
||||||
|
`COHERENCE.md` §2 keeps a standing observation verbatim: keybinding
|
||||||
|
coverage is inverted relative to frequency, with `C-c @ C-M-s` bound
|
||||||
|
while opening a file, opening a terminal, and running a build were not.
|
||||||
|
Two of the three have been answered (#162, #173); this stage answers the
|
||||||
|
third, and §8 updates the paragraph accordingly. The *quote* stays as
|
||||||
|
written, because it names a bias in how new work gets bound rather than
|
||||||
|
three omissions.
|
||||||
|
|
||||||
|
## 7. Staging — why 1b is split
|
||||||
|
|
||||||
|
`COHERENCE.md` §20 bundles three items under "Stage 1b". They share a
|
||||||
|
priority and nothing else:
|
||||||
|
|
||||||
|
| | subsystem | shape | risk |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **1b-1** (this) | compile + project | wiring, Lua only | low |
|
||||||
|
| 1b-2 | LSP lifecycle | a failure that is currently *silent* (§1.2) must become visible without becoming noise | medium |
|
||||||
|
| 1b-3 | startup buffer | new content, plus §18's `C-h`-deletes-a-word problem | low, but touches the default keymap |
|
||||||
|
|
||||||
|
One feature, one branch, one PR. 1b-2 is the hard one — the silence
|
||||||
|
asymmetry is a design question about *when* to speak, not a wiring
|
||||||
|
question — and bundling it with a keybinding would hold the cheapest
|
||||||
|
journey fix in the tree behind the most contested one.
|
||||||
|
|
||||||
|
## 8. Coherence impact
|
||||||
|
|
||||||
|
Per `CLAUDE.md` and `COHERENCE.md` §20's standing process change.
|
||||||
|
|
||||||
|
- **Journey steps touched:** 9 directly (Partial → Works); 10 indirectly
|
||||||
|
— it was "gated entirely on step 6 or 9 succeeding first".
|
||||||
|
- **Interaction islands:** none added. The prompt is the existing
|
||||||
|
minibuffer; the binding joins the existing `C-c` prefix; the kind
|
||||||
|
comes from the existing detector. `pmacs.compile.defaults` is an
|
||||||
|
extensible table, not a new modal surface.
|
||||||
|
- **Config registry adoption:** none, deliberately — §6.2 gives the
|
||||||
|
mechanism reason and names the deferred scalar.
|
||||||
|
- **Background-work attribution:** unchanged. Compile already spawns
|
||||||
|
through the process-group machinery; this stage changes what is typed
|
||||||
|
into the prompt, not what is spawned or how it is tracked.
|
||||||
|
- **Doc updates riding this PR** (§25 requires it):
|
||||||
|
- `COHERENCE.md` §2 step-9 verdict row → Works, and the
|
||||||
|
`ProjectKind::Cargo` → `ProjectKind::Rust` correction **in both
|
||||||
|
places** (§2 row and §20 Priority 1). §24 gains the drift entry.
|
||||||
|
- `COHERENCE.md` §2's post-table paragraph: "Running a build still has
|
||||||
|
no binding" → answered, with the quote itself left intact (§6.3).
|
||||||
|
- `docs/keybindings.md`: the `C-c c` row, and the removal of the
|
||||||
|
"no global key is assigned to them" sentence at `:191`.
|
||||||
|
- `docs/agent-handoff.md` §1: the journey arc bullet gains Stage 1b-1.
|
||||||
|
|
||||||
|
## 9. Open questions for review
|
||||||
|
|
||||||
|
- **Q#J1 — is `C-c c` the right chord?** It is free, it is under the
|
||||||
|
established `C-c` prefix, and it matches what most Emacs distributions
|
||||||
|
bind compile to. The alternative worth naming is `C-c C-c`, which is
|
||||||
|
more finger-friendly but is the chord many major modes claim
|
||||||
|
buffer-locally, so a global one would be shadowed unpredictably later.
|
||||||
|
- **Q#J2 — should `rust` be the only seeded default?** §3.2 argues yes
|
||||||
|
on the grounds that a wrong prefill costs more than an empty one. The
|
||||||
|
counter-argument is that `go build` and `make` are about as
|
||||||
|
unambiguous as `cargo build`, and seeding them would make the table
|
||||||
|
read as a real registry rather than a Rust special case.
|
||||||
|
- **Q#J3 — should the prefill be selected, so typing replaces it?**
|
||||||
|
Emacs leaves the prefill unselected and the point at the end. dired's
|
||||||
|
prefill does the same. Matching them means "accept" is RET and
|
||||||
|
"replace" is a kill-line first. Changing it is a minibuffer-wide
|
||||||
|
behavior change and out of scope, but it is the ergonomic difference a
|
||||||
|
user will notice first.
|
||||||
|
|
||||||
|
## 10. Ledger
|
||||||
|
|
||||||
|
Branch `journey-stage1b1-compile-defaults`, worktree
|
||||||
|
`../pmacs-journey-1b1`, based on `githubsucks/main` @ `22df6ab`.
|
||||||
|
Framing only; no code, no PR yet.
|
||||||
|
|
||||||
|
Recovery from a clean checkout — the two-argument form of
|
||||||
|
`git worktree add` does not work for a remote-only branch (it fails with
|
||||||
|
`fatal: invalid reference`, because after a bare fetch no local branch
|
||||||
|
exists):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git fetch githubsucks
|
||||||
|
git worktree add ../pmacs-journey-1b1 \
|
||||||
|
-b journey-stage1b1-compile-defaults \
|
||||||
|
githubsucks/journey-stage1b1-compile-defaults
|
||||||
|
```
|
||||||
|
|
@ -187,9 +187,13 @@ Source: `builtin/runtime/compile.lua`.
|
||||||
| `M-g n` / `M-g p` | `error.next` / `error.previous` — compile/grep errors when that source has claimed navigation, otherwise LSP diagnostics |
|
| `M-g n` / `M-g p` | `error.next` / `error.previous` — compile/grep errors when that source has claimed navigation, otherwise LSP diagnostics |
|
||||||
| `` C-x ` `` | `error.next` |
|
| `` C-x ` `` | `error.next` |
|
||||||
| `M-!` | `shell.command` — asynchronous output in `*shell-command*` |
|
| `M-!` | `shell.command` — asynchronous output in `*shell-command*` |
|
||||||
|
| `C-c c` | `compile.run` — prompts, prefilled from the detected project kind |
|
||||||
|
|
||||||
`compile.run` and `compile.recompile` are available through `M-x`; no
|
`compile.recompile` is available through `M-x`, and through `g` inside
|
||||||
global key is assigned to them.
|
`*compilation*`; no global key is assigned to it. `C-c c` is unreachable
|
||||||
|
from inside a terminal window (`C-c` is consumed as the escape key) and
|
||||||
|
inside a repl buffer (which binds `C-c` at buffer scope); `M-x
|
||||||
|
compile.run` still works in both.
|
||||||
|
|
||||||
## 2. Buffer-local panel keymaps
|
## 2. Buffer-local panel keymaps
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2750,3 +2750,106 @@ fn r5f3_tracked_line_start_matches_the_scan_across_transitions() {
|
||||||
"every rewind lands at the tracked line start; buffer:\n{text:?}"
|
"every rewind lands at the tracked line start; buffer:\n{text:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Journey Stage 1b-1 — `pmacs.compile.defaults` is public and assignable
|
||||||
|
//
|
||||||
|
// The table is user-writable, so a metatable with a throwing `__index`, a
|
||||||
|
// non-string entry, and a non-table replacement all have to be
|
||||||
|
// survivable — the same discipline `validated_rules` keeps for a hostile
|
||||||
|
// rule container. A broken `defaults` must degrade to the pre-stage empty
|
||||||
|
// prompt; it must never prevent compiling.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Open the compile prompt and return what it offered.
|
||||||
|
fn compile_prompt_initial(s: &EditorState) -> String {
|
||||||
|
exec(s, "pmacs.command.invoke('compile.run')");
|
||||||
|
assert!(
|
||||||
|
eval::<bool>(s, "return pmacs.minibuffer.is_active()"),
|
||||||
|
"compile.run must open a prompt even with a hostile defaults table"
|
||||||
|
);
|
||||||
|
eval(s, "return pmacs.minibuffer.contents()")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_a_non_table_defaults_degrades_to_an_empty_prompt() {
|
||||||
|
let s = editor();
|
||||||
|
exec(&s, "pmacs.compile.defaults = 42");
|
||||||
|
assert_eq!(compile_prompt_initial(&s), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_a_raising_defaults_index_degrades_to_an_empty_prompt() {
|
||||||
|
let s = editor();
|
||||||
|
exec(
|
||||||
|
&s,
|
||||||
|
"pmacs.compile.defaults = setmetatable({}, {
|
||||||
|
__index = function() error('hostile') end,
|
||||||
|
})",
|
||||||
|
);
|
||||||
|
assert_eq!(compile_prompt_initial(&s), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_a_non_string_default_is_ignored() {
|
||||||
|
let s = editor();
|
||||||
|
exec(&s, "pmacs.compile.defaults = { rust = {}, }");
|
||||||
|
assert_eq!(compile_prompt_initial(&s), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_a_hostile_defaults_still_lets_a_typed_command_run() {
|
||||||
|
// The consequence that matters: degradation must not cost the user
|
||||||
|
// the ability to compile. Asserts output produced, not a property
|
||||||
|
// preserved.
|
||||||
|
let mut s = editor();
|
||||||
|
exec(
|
||||||
|
&s,
|
||||||
|
"pmacs.compile.defaults = setmetatable({}, {
|
||||||
|
__index = function() error('hostile') end,
|
||||||
|
})",
|
||||||
|
);
|
||||||
|
// Through the INTERACTIVE command, so the hostile lookup actually
|
||||||
|
// happens. Calling `pmacs.compile.run` directly never consults
|
||||||
|
// `defaults` at all, which made an earlier draft of this pin vacuous
|
||||||
|
// — it passed with the guard removed.
|
||||||
|
exec(&s, "pmacs.command.invoke('compile.run')");
|
||||||
|
assert!(
|
||||||
|
eval::<bool>(&s, "return pmacs.minibuffer.is_active()"),
|
||||||
|
"the prompt must still open"
|
||||||
|
);
|
||||||
|
exec(&s, "pmacs.minibuffer.set_contents('echo still-compiles')");
|
||||||
|
exec(&s, "pmacs.minibuffer.accept()");
|
||||||
|
assert!(
|
||||||
|
pump_until(&mut s, 10_000, |s| {
|
||||||
|
compilation_text(s).contains("still-compiles")
|
||||||
|
}),
|
||||||
|
"a broken defaults table must not prevent compiling; got:\n{}",
|
||||||
|
compilation_text(&s)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn j1b1_context_reports_the_cwd_a_run_would_use() {
|
||||||
|
// `pmacs.compile.context` and `pmacs.compile.run` must answer from
|
||||||
|
// one resolution — an explicit cwd is honoured by both, and the kind
|
||||||
|
// is detected from that same directory.
|
||||||
|
let s = editor();
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
std::fs::write(dir.path().join("Cargo.toml"), b"[package]\nname=\"x\"\n").expect("write");
|
||||||
|
exec(
|
||||||
|
&s,
|
||||||
|
&format!(
|
||||||
|
"pmacs.project.set_search_boundary({:?})",
|
||||||
|
dir.path().display().to_string()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let kind: String = eval(
|
||||||
|
&s,
|
||||||
|
&format!(
|
||||||
|
"return pmacs.compile.context({:?}).kind",
|
||||||
|
dir.path().display().to_string()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(kind, "rust", "the kind is detected from the given cwd");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1297,3 +1297,448 @@ fn preservation_display_file_still_refuses_a_directory() {
|
||||||
"a refused display_file must not create a buffer"
|
"a refused display_file must not create a buffer"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Step 9 — build or test the project (Journey Stage 1b-1)
|
||||||
|
//
|
||||||
|
// `COHERENCE.md` §2 graded this **Partial**: `M-x compile.run` worked but
|
||||||
|
// had no keybinding, an empty first prompt, and no `cargo build`
|
||||||
|
// suggestion. These rows are the ratchet for the three.
|
||||||
|
//
|
||||||
|
// The trap these fixtures are built around: compile's last-resort cwd is
|
||||||
|
// `std::env::current_dir()` evaluated at call time, which under `cargo
|
||||||
|
// test` is the **pmacs repo root — itself a Cargo project**
|
||||||
|
// (`compile_mode_acceptance.rs` pins exactly that). So any pin asserting
|
||||||
|
// the *absence* of a Cargo suggestion must be shaped so the fallback is
|
||||||
|
// never consulted: each fixture carries its own marker, and
|
||||||
|
// `project_root_of_active()` therefore answers from inside the fixture.
|
||||||
|
// `set_search_boundary` is not the defence — it clamps only a walk that
|
||||||
|
// starts below the boundary, and the fallback's walk starts at the repo.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn ctrl(s: &mut EditorState, c: char) {
|
||||||
|
s.dispatch_key(
|
||||||
|
FrontendId::LOCAL,
|
||||||
|
key(KeyCode::Char(c), KeyModifiers::CONTROL),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the compile prompt through the real `C-c c` binding.
|
||||||
|
fn press_compile_chord(s: &mut EditorState) {
|
||||||
|
ctrl(s, 'c');
|
||||||
|
type_char(s, 'c');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Cargo project a journey can plausibly be run against.
|
||||||
|
fn cargo_project() -> TempDir {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
std::fs::write(
|
||||||
|
td.path().join("Cargo.toml"),
|
||||||
|
b"[package]\nname = \"journey-fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
|
||||||
|
)
|
||||||
|
.expect("write Cargo.toml");
|
||||||
|
std::fs::create_dir_all(td.path().join("src")).expect("mkdir src");
|
||||||
|
std::fs::write(td.path().join("src/main.rs"), b"fn main() {}\n").expect("write main.rs");
|
||||||
|
td
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamp detection to the fixture so a stray marker above the tempdir
|
||||||
|
/// (a developer's `/tmp/.git`) cannot leak in.
|
||||||
|
fn bound_detection_to(s: &EditorState, dir: &Path) {
|
||||||
|
exec(
|
||||||
|
s,
|
||||||
|
&format!(
|
||||||
|
"pmacs.project.set_search_boundary({:?})",
|
||||||
|
dir.display().to_string()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The project root as **detection** reports it.
|
||||||
|
///
|
||||||
|
/// `pmacs.project.detect` canonicalizes before walking
|
||||||
|
/// (`canonicalize_or_passthrough`, `src/project.rs:509-511`), so a
|
||||||
|
/// `/var/folders/...` tempdir on macOS comes back as
|
||||||
|
/// `/private/var/folders/...`. `canon()` is *lexical* — it never
|
||||||
|
/// resolves symlinks — so it is the wrong expectation for any value
|
||||||
|
/// that has passed through detection, which is exactly what the compile
|
||||||
|
/// cwd is. Using it here failed both macOS CI legs while Ubuntu (where
|
||||||
|
/// `/tmp` is not a symlink) stayed green.
|
||||||
|
fn detected_root(path: &Path) -> String {
|
||||||
|
std::fs::canonicalize(path)
|
||||||
|
.unwrap_or_else(|_| path.to_path_buf())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minibuffer_active(s: &EditorState) -> bool {
|
||||||
|
eval(s, "return pmacs.minibuffer.is_active()")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minibuffer_contents(s: &EditorState) -> String {
|
||||||
|
eval(s, "return pmacs.minibuffer.contents()")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named_text(s: &EditorState, name: &str) -> String {
|
||||||
|
eval(
|
||||||
|
s,
|
||||||
|
&format!(
|
||||||
|
r#"
|
||||||
|
for _, id in ipairs(pmacs.buffer.list()) do
|
||||||
|
if pmacs.describe.buffer(id).name == {name:?} then
|
||||||
|
return id:slice(0, id:len())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drive frames until `pred` holds, pumping the process supervisor as
|
||||||
|
/// well as the async runtime — a compile run is a spawned child.
|
||||||
|
fn pump_processes_until(
|
||||||
|
s: &mut EditorState,
|
||||||
|
timeout_ms: u64,
|
||||||
|
mut pred: impl FnMut(&EditorState) -> bool,
|
||||||
|
) -> bool {
|
||||||
|
let stop = Instant::now() + Duration::from_millis(timeout_ms);
|
||||||
|
loop {
|
||||||
|
if pred(s) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if Instant::now() >= stop {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
s.tick_processes();
|
||||||
|
s.tick_async();
|
||||||
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk the journey to the point where a build would be run: launch on
|
||||||
|
/// the directory, then `RET` on `name` to open a file.
|
||||||
|
fn walk_to_open_file(dir: &Path, name: &str) -> EditorState {
|
||||||
|
let mut s = launch(dir);
|
||||||
|
bound_detection_to(&s, dir);
|
||||||
|
let line = line_of(&s, name);
|
||||||
|
exec(&s, &format!("pmacs.editor.move_to_line({line})"));
|
||||||
|
press(&mut s, KeyCode::Enter);
|
||||||
|
pump(&mut s);
|
||||||
|
assert_eq!(
|
||||||
|
active_name(&s),
|
||||||
|
dir.join(name).display().to_string(),
|
||||||
|
"precondition: the walk must actually open the file, or every pin \
|
||||||
|
below is really testing the dired buffer"
|
||||||
|
);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — `C-c c` reaches `compile.run`. Separate from N2 because a
|
||||||
|
/// prefill assertion alone stays green if the binding is removed and the
|
||||||
|
/// prompt is opened another way — and the binding is the thing
|
||||||
|
/// `COHERENCE.md` says is missing.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_compile_chord_opens_the_prompt() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert!(
|
||||||
|
minibuffer_active(&s),
|
||||||
|
"C-c c must open the compile prompt; active buffer is {}",
|
||||||
|
active_name(&s)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — the prompt is prefilled from the detected project kind.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_prompt_is_prefilled_for_a_cargo_project() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert_eq!(
|
||||||
|
minibuffer_contents(&s),
|
||||||
|
"cargo build",
|
||||||
|
"a Rust project's first compile prompt must offer its build command"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — the directory the prompt captured survives a window switch.
|
||||||
|
///
|
||||||
|
/// The prompt is opened against A, the active buffer then moves to an
|
||||||
|
/// unrelated directory B, and only then is the command accepted. Both
|
||||||
|
/// readings — the header pmacs writes and the shell's own `pwd` — must
|
||||||
|
/// say A. Falsified by dropping `{ cwd = ctx.cwd }` from `on_accept`,
|
||||||
|
/// which is what re-resolving at accept time looks like.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_prompt_runs_in_the_directory_it_captured() {
|
||||||
|
let a = cargo_project();
|
||||||
|
let b = project();
|
||||||
|
let mut s = walk_to_open_file(a.path(), "Cargo.toml");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert!(minibuffer_active(&s), "prompt must be open against A");
|
||||||
|
|
||||||
|
// Move the active buffer to B while the prompt waits for input.
|
||||||
|
exec(
|
||||||
|
&s,
|
||||||
|
&format!(
|
||||||
|
"pmacs.window.display_file({:?})",
|
||||||
|
b.path().join("alpha.txt").display().to_string()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
pump(&mut s);
|
||||||
|
|
||||||
|
// Accept a cheap command: the subject is the directory, so a fast
|
||||||
|
// one keeps the failure message about the directory. Only the
|
||||||
|
// editing of an already-open prompt is short-circuited; the prompt
|
||||||
|
// itself was opened through the real chord.
|
||||||
|
exec(&s, "pmacs.minibuffer.set_contents('pwd')");
|
||||||
|
press(&mut s, KeyCode::Enter);
|
||||||
|
let finished = pump_processes_until(&mut s, 10_000, |s| {
|
||||||
|
named_text(s, "*compilation*").contains("exited")
|
||||||
|
});
|
||||||
|
assert!(finished, "the accepted run must finish");
|
||||||
|
|
||||||
|
let text = named_text(&s, "*compilation*");
|
||||||
|
// Detection-canonical, not lexical: the compile cwd came from
|
||||||
|
// `pmacs.project.detect`, and `pwd` reports the physical directory.
|
||||||
|
let a_path = detected_root(a.path());
|
||||||
|
assert!(
|
||||||
|
text.contains(&format!("Directory: {a_path}")),
|
||||||
|
"the header must name the directory the prompt captured, not the newly active one;\n{text}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.contains(&a_path),
|
||||||
|
"and `pwd` must agree with the header;\n{text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — the offered command runs, in the offered directory.
|
||||||
|
///
|
||||||
|
/// Every other pin here compares values the prompt and the resolver
|
||||||
|
/// already agree on. A wrong directory inside `on_accept` passes all of
|
||||||
|
/// them. This one accepts what was offered, unedited, and observes a
|
||||||
|
/// real process.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_offered_command_builds_the_project() {
|
||||||
|
if !binary_available("cargo") {
|
||||||
|
assert!(
|
||||||
|
std::env::var_os("PMACS_REQUIRE_CARGO_BUILD").is_none(),
|
||||||
|
"PMACS_REQUIRE_CARGO_BUILD is set but `cargo` is not on PATH"
|
||||||
|
);
|
||||||
|
eprintln!("skipping: `cargo` is not on PATH");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let td = cargo_project();
|
||||||
|
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert_eq!(
|
||||||
|
minibuffer_contents(&s),
|
||||||
|
"cargo build",
|
||||||
|
"precondition: the prompt offers the build command"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Accept exactly what was offered — no editing.
|
||||||
|
press(&mut s, KeyCode::Enter);
|
||||||
|
let finished = pump_processes_until(&mut s, 120_000, |s| {
|
||||||
|
named_text(s, "*compilation*").contains("exited")
|
||||||
|
});
|
||||||
|
assert!(finished, "the offered build must finish");
|
||||||
|
|
||||||
|
let text = named_text(&s, "*compilation*");
|
||||||
|
assert!(
|
||||||
|
text.contains(&format!("Directory: {}", detected_root(td.path()))),
|
||||||
|
"the build runs in the detected project root;\n{text}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.contains("[compile exited with code 0]"),
|
||||||
|
"and the command that was offered is one that actually runs;\n{text}"
|
||||||
|
);
|
||||||
|
// Positive control on the run itself. A clean exit alone does not
|
||||||
|
// prove cargo compiled anything — this names the fixture crate, so
|
||||||
|
// the pin fails if the offered command ever stops being a build.
|
||||||
|
assert!(
|
||||||
|
text.contains("journey-fixture"),
|
||||||
|
"cargo must actually have built the fixture crate;\n{text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — the suggestion follows the directory the run will use.
|
||||||
|
///
|
||||||
|
/// A Node project nested inside a Cargo one, opened at the *inner* file.
|
||||||
|
/// Falsified by deriving the kind from the launch directory (the Cargo
|
||||||
|
/// root here) or from the process cwd — both yield `cargo build`.
|
||||||
|
///
|
||||||
|
/// It does **not** catch re-detecting from `project_root_of_active()`'s
|
||||||
|
/// answer: that helper already returns the innermost root, so detecting
|
||||||
|
/// from it yields `node` again and this pin stays green.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_a_nested_project_gets_its_own_kind_not_the_outer_one() {
|
||||||
|
let outer = cargo_project();
|
||||||
|
let sub = outer.path().join("sub");
|
||||||
|
std::fs::create_dir_all(&sub).expect("mkdir sub");
|
||||||
|
std::fs::write(sub.join("package.json"), b"{ \"name\": \"inner\" }\n").expect("write pkg");
|
||||||
|
std::fs::write(sub.join("index.js"), b"console.log(1)\n").expect("write index.js");
|
||||||
|
|
||||||
|
let mut s = walk_to_open_file(&sub, "index.js");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
// `minibuffer_contents` is "" both for an empty prefill and for no
|
||||||
|
// minibuffer at all, so the emptiness assertion below is vacuous
|
||||||
|
// without this. Bite A caught it: with the binding removed this pin
|
||||||
|
// stayed green while six others failed.
|
||||||
|
assert!(
|
||||||
|
minibuffer_active(&s),
|
||||||
|
"precondition: the prompt is actually open"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
eval::<String>(&s, "return pmacs.compile.context().kind"),
|
||||||
|
"node",
|
||||||
|
"the kind is detected from the resolved cwd, not the launch directory"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
minibuffer_contents(&s),
|
||||||
|
"",
|
||||||
|
"and `node` has no seeded default, so nothing is offered"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — `context()` is total in a launched session.
|
||||||
|
///
|
||||||
|
/// Asserted with the pathless dired buffer active, so the fallback
|
||||||
|
/// branch is the one under test. It pins a *property*, deliberately not
|
||||||
|
/// a value: which directory the fallback resolves to is the test
|
||||||
|
/// runner's cwd and pinning it would pin the environment.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_compile_context_is_total_even_with_no_file_open() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let s = launch(td.path());
|
||||||
|
assert!(
|
||||||
|
active_name(&s).starts_with("*dired:"),
|
||||||
|
"precondition: the pathless dired buffer is active"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
eval::<bool>(&s, "return pmacs.compile.context().cwd ~= nil"),
|
||||||
|
"a launched session always has somewhere to run a build"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
eval::<bool>(
|
||||||
|
&s,
|
||||||
|
"local c = pmacs.compile.context()
|
||||||
|
local ok, p = pcall(pmacs.project.detect, c.cwd)
|
||||||
|
local detected = (ok and p) and p.kind or nil
|
||||||
|
return c.kind == detected"
|
||||||
|
),
|
||||||
|
"and its kind is exactly what detection answers for that cwd"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **P** — `_last` still outranks the kind default.
|
||||||
|
///
|
||||||
|
/// Green on the pre-image (there was no default). The targeted mutation
|
||||||
|
/// is reordering the precedence chain to put `defaults[kind]` first.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_preservation_the_last_command_outranks_the_default() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let mut s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
exec(&s, "pmacs.compile.run('true')");
|
||||||
|
let finished = pump_processes_until(&mut s, 10_000, |s| {
|
||||||
|
named_text(s, "*compilation*").contains("exited")
|
||||||
|
});
|
||||||
|
assert!(finished, "precondition: a run completed and set _last");
|
||||||
|
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
assert_eq!(
|
||||||
|
minibuffer_contents(&s),
|
||||||
|
"true",
|
||||||
|
"a session that has compiled keeps its own command, not the project default"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **P** — the compile keys that already existed still dispatch.
|
||||||
|
///
|
||||||
|
/// Targeted mutation: writing the new binding as an unbind+bind pair
|
||||||
|
/// over one of these sequences.
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_preservation_the_existing_compile_bindings_survive() {
|
||||||
|
let td = cargo_project();
|
||||||
|
let s = walk_to_open_file(td.path(), "Cargo.toml");
|
||||||
|
for (sequence, command) in [
|
||||||
|
("M-g n", "error.next"),
|
||||||
|
("M-g p", "error.previous"),
|
||||||
|
("C-x `", "error.next"),
|
||||||
|
("M-!", "shell.command"),
|
||||||
|
("C-c c", "compile.run"),
|
||||||
|
] {
|
||||||
|
let bound: String = eval(
|
||||||
|
&s,
|
||||||
|
&format!("return pmacs.keymap.lookup({sequence:?}).command"),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bound, command,
|
||||||
|
"{sequence} must still dispatch to {command}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when `name` resolves on PATH — the fixture-dependency gate the
|
||||||
|
/// process suite uses, so a missing binary skips rather than fails.
|
||||||
|
fn binary_available(name: &str) -> bool {
|
||||||
|
std::process::Command::new("which")
|
||||||
|
.arg(name)
|
||||||
|
.output()
|
||||||
|
.is_ok_and(|o| o.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **N** — the compile directory is *detection*-canonical, not lexical.
|
||||||
|
///
|
||||||
|
/// This pin exists because its absence broke both macOS CI legs while
|
||||||
|
/// Ubuntu stayed green: `/var` is a symlink to `/private/var` there, so
|
||||||
|
/// a tempdir's lexical and canonical paths differ, and the original
|
||||||
|
/// assertions used the lexical one.
|
||||||
|
///
|
||||||
|
/// Reproducing it on Linux needs an explicit symlink — which is also a
|
||||||
|
/// real configuration `Workspace::detect`'s own doc comment names
|
||||||
|
/// ("`/tmp/sandbox/foo` symlinked to `/home/user/code/foo`"). Launching
|
||||||
|
/// through the link makes the two paths disagree on every platform, so
|
||||||
|
/// the regression can no longer hide behind a filesystem that happens
|
||||||
|
/// not to use symlinks.
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn journey_step9_the_compile_directory_is_detection_canonical() {
|
||||||
|
let parent = tempfile::tempdir().expect("tempdir");
|
||||||
|
let real = parent.path().join("real");
|
||||||
|
std::fs::create_dir_all(real.join("src")).expect("mkdir real");
|
||||||
|
std::fs::write(
|
||||||
|
real.join("Cargo.toml"),
|
||||||
|
b"[package]\nname = \"journey-fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
|
||||||
|
)
|
||||||
|
.expect("write Cargo.toml");
|
||||||
|
std::fs::write(real.join("src/main.rs"), b"fn main() {}\n").expect("write main.rs");
|
||||||
|
|
||||||
|
let link = parent.path().join("link");
|
||||||
|
std::os::unix::fs::symlink(&real, &link).expect("symlink");
|
||||||
|
|
||||||
|
// Precondition, or the pin is vacuous: the two spellings must
|
||||||
|
// actually differ, which is the whole condition macOS creates for
|
||||||
|
// free and Linux does not.
|
||||||
|
assert_ne!(
|
||||||
|
canon(&link),
|
||||||
|
detected_root(&link),
|
||||||
|
"the fixture must make lexical and canonical paths disagree"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut s = walk_to_open_file(&link, "Cargo.toml");
|
||||||
|
press_compile_chord(&mut s);
|
||||||
|
exec(&s, "pmacs.minibuffer.set_contents('pwd')");
|
||||||
|
press(&mut s, KeyCode::Enter);
|
||||||
|
let finished = pump_processes_until(&mut s, 10_000, |s| {
|
||||||
|
named_text(s, "*compilation*").contains("exited")
|
||||||
|
});
|
||||||
|
assert!(finished, "the accepted run must finish");
|
||||||
|
|
||||||
|
let text = named_text(&s, "*compilation*");
|
||||||
|
assert!(
|
||||||
|
text.contains(&format!("Directory: {}", detected_root(&link))),
|
||||||
|
"the header must name the directory detection resolved to;\n{text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue