docs(journey): frame Stage 1b-1 — make building discoverable
`COHERENCE.md` §20 Priority 1 names Journey Stage 1b as the golden journey's remainder and it had no branch, no framing, and no lane — the only §20 priority with nothing in flight. This frames the first third of it: journey step 9, build or test the project. Scope is a global `C-c c` for `compile.run` plus a first prompt prefilled from the detected project kind instead of empty. Lua, tests and docs; no Rust change, no protocol change. LSP spawn guidance and the welcome buffer are separate stages (§7). Three ground-truth findings the framing rests on: - `ProjectKind::Cargo` does not exist. COHERENCE names it twice; the variant is `ProjectKind::Rust` and line 77 is its doc comment. Lua matches the tag string `pmacs.project.detect` already returns, so the stage needs no new Rust primitive. - The compile cwd is resolved inside `run`, after the prompt closes, so a suggestion built in the command's `fn` would obey a different rule than the run. The stage extracts one resolution and has both consume it. - The last-resort cwd is `std::env::current_dir()` at call time, which in tests is the pmacs repo root — itself a Cargo project. Negative pins are built so that fallback is never reached. Records one limitation rather than fixing it: after `pmacs <dir>` the active buffer is dired's and pathless, so the cwd falls through to the process cwd. That needs COHERENCE §8's execution-location model, not a reach into dired's private handle table. Adds the journey lane to the active-work ledger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
This commit is contained in:
parent
22df6ab11a
commit
e26ddd5ec3
|
|
@ -253,6 +253,66 @@ If it does not, stop and repair the remote/fetch configuration.
|
|||
never been enforced. Any CI job that compiles the `crdt` targets has to
|
||||
fix them first or it will be red on arrival.
|
||||
|
||||
## Journey lane (P1) — STAGE 1a MERGED; STAGE 1b-1 FRAMING OPEN, revision 1
|
||||
|
||||
- **Branch `journey-stage1b1-compile-defaults`**, worktree
|
||||
`../pmacs-journey-1b1`, based on `githubsucks/main` @ `22df6ab`.
|
||||
**Framing only; no code, no PR yet.**
|
||||
`docs/journey-stage1b1-compile-defaults-framing.md` revision 1, no
|
||||
review rounds yet.
|
||||
- **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
|
||||
|
||||
**Framing #188 (revision 7) and Stage 1 #191 are both on `main` @
|
||||
|
|
|
|||
|
|
@ -0,0 +1,563 @@
|
|||
# Journey Stage 1b-1 — make building discoverable
|
||||
|
||||
**Status: framing, rev 1 — awaiting approval.**
|
||||
**Serves `COHERENCE.md` §2 (the golden product journey, step 9), §19
|
||||
(coherence acceptance tests), §20 Priority 1.**
|
||||
|
||||
## 0. Revision history
|
||||
|
||||
- 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()`. **The suggestion is therefore a function of
|
||||
the directory the command will execute in**, and the two cannot drift.
|
||||
|
||||
`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 N4 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 prefill is executable as offered.**
|
||||
Same walk; assert the prompt contents are exactly
|
||||
`pmacs.compile.defaults[pmacs.compile.context().kind]`, and that
|
||||
`context().cwd` is the fixture root.
|
||||
*Falsifier:* compute the prefill from `project_root_of_active()` instead
|
||||
of from `context()`. In the plain fixture the two agree, so this pin
|
||||
alone is **not** sufficient — which is exactly why N4 exists.
|
||||
|
||||
**N4 — 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:* any rule that is not "detect from the resolved cwd" — take
|
||||
the outermost marker, or the launch directory, or the process cwd — all
|
||||
yield `cargo build`.
|
||||
This is the pin that carries §3.1's whole claim.
|
||||
|
||||
**N5 — `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 `Cargo.toml`.
|
||||
|
||||
N5 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
|
||||
```
|
||||
Loading…
Reference in New Issue