docs(discovery): frame P4 Stage 1 — the describe/list command family

`COHERENCE.md` §20 Priority 4, which §5 grades "substrate without
surface — the sharpest instance of §1.1" and calls the best
payoff-per-effort in the document. Nine describe/list commands over
introspection that already exists, picking up the family root `M-x
help` that #205 landed and documented for exactly this.

The stage adds no Rust. `pmacs.describe.*`, `pmacs.keymap.list()`,
`pmacs.command.list()` and `pmacs.config.list()` already return
everything the commands render, and `parse_completion_source` accepts a
Lua function as a `CompletionSource::Custom` — so `describe-setting`'s
free-text prompt, which today turns a typo into a status-line error
while `describe-command` has had completion all along, closes with no
new Rust either.

The consequential decision is a single rendering seam. `src/help.rs`'s
cross-referenced renderer is orphaned and the reachable Lua
`show_help_text` renders less, so nine new commands calling it directly
would turn a two-site migration into a ten-site one. Everything routes
through `pmacs.editor._show_help`, and an acceptance pin replaces that
seam with a counting stub — funnelling asserted, not merely intended.

Records that `Command.predicate` is stored and exposed but evaluated
nowhere, and deliberately does not start evaluating it: that makes
commands stop being invocable, which needs its own decision about what
"unavailable" means at each call site. A preservation pin registers a
raising predicate and asserts the command still runs, so a later stage
has to change that pin knowingly.

Adds no keybindings at all — the help-prefix question is taken once,
for the whole family, by the stage that can weigh it against the
constraint #205 recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lv428Fth9LRtffwJSsqH7T
This commit is contained in:
Levi Neuwirth 2026-07-31 17:42:03 -04:00
parent 54a092e0c9
commit 791bde1a0c
2 changed files with 303 additions and 0 deletions

View File

@ -380,6 +380,44 @@ which would have re-conflicted on every merge.
githubsucks/journey-stage1b3-welcome
```
## Discovery Stage 1 (P4) — FRAMING OPEN, revision 1
- **Branch `discovery-stage1-commands`**, worktree `../pmacs-p4-discovery`,
based on `githubsucks/main` @ `54a092e`. **Framing only; no code, no
PR yet.** `docs/discovery-stage1-command-family-framing.md` revision 1.
- **What it is.** `COHERENCE.md` §20 Priority 4 — "almost pure wiring,
the best payoff-per-effort in this document". Nine describe/list
commands (describe-key/mode/hook/buffer, where-is, list-commands,
list-keybindings, list-settings, apropos) over introspection that
already exists.
- **It adds NO Rust.** `pmacs.describe.*`, `pmacs.keymap.list()`,
`pmacs.command.list()` and `pmacs.config.list()` already return
everything needed, and `parse_completion_source` accepts a **Lua
`Function`** (`CompletionSource::Custom`), so even the prompts need no
new Rust — which also closes `describe-setting`'s free-text hole.
- **The decision that matters: ONE rendering seam.** `src/help.rs`'s
renderer is orphaned and the reachable Lua `show_help_text` renders
less, so nine new commands calling it directly would turn a two-site
migration into a ten-site one. Everything routes through
`pmacs.editor._show_help` (the seam #205 added) so the later help
unification stays a one-site change — pinned by a counting stub, not
left as a convention.
- **Deliberately deferred, each with a reason:** richer M-x rows
(`MinibufferPrompt.candidates` is `Vec<String>` — protocol change);
`Command` gaining title/category/flags (~147 definition sites);
predicate evaluation (**stored and exposed but never evaluated** — a
behaviour change); help-layer unification; and the **help prefix key**,
which this stage does not touch because #205 recorded why `C-h` is not
free.
- Recovery:
```sh
git fetch githubsucks
git worktree add ../pmacs-p4-discovery \
-b discovery-stage1-commands \
githubsucks/discovery-stage1-commands
```
## 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` @

View File

@ -0,0 +1,265 @@
# Discovery Stage 1 — the describe/list command family
**Status: framing, rev 1 — awaiting approval.**
**Serves `COHERENCE.md` §5 (unify discoverability), §1.1 (substrate
without surface), §20 Priority 4.**
## 0. Revision history
- rev 1 (2026-07-31) — first framing. Scouted against `githubsucks/main`
@ `54a092e` (Journey Stage 1b-3, #205).
## 1. Why this, and why now
`COHERENCE.md` §20 Priority 4 calls unified discovery **"almost pure
wiring — the best payoff-per-effort in this document"**, and §5 grades
it *"substrate without surface — the sharpest instance of §1.1"*.
Journey Stage 1b-3 (#205) just landed `M-x help` and documented it as
**the root of this family**: *"when the discovery arc adds `help.keys`
and friends, `help` stays the index they are reached from, so no rename
is owed."* This stage is that family. It also inherits 1b-3's deferred
question — the help prefix — with the constraint already recorded (§6).
Two journey steps are graded *works but undiscoverable* and move here
without any new machinery: step 7 (symbol search — `M-.`/`M-?`/`C-c o`
bound but "advertised nowhere") and step 11 (`*workers*` — "no
keybinding, no indicator").
## 2. Ground truth
Read in the tree at `54a092e`. **The substrate is already there**; this
stage adds no Rust.
### 2.1 What Lua can already ask
| Surface | Returns |
|---|---|
| `pmacs.command.list()` | every command name |
| `pmacs.describe.command(name)` | description, source, **`key_bindings`** (where-is, computed on demand) |
| `pmacs.describe.key(seq)` | resolved against the **active buffer + major mode** |
| `pmacs.describe.{buffer,view,mode,hook}` | structured tables |
| `pmacs.keymap.list()` | `{ sequence, command, scope }` for **every** binding, via `KeymapStack::iter_all` |
| `pmacs.keymap.lookup(seq)` | `{ sequence, command, scope, source, description }` |
| `pmacs.config.list()` / `pmacs.config.describe(name, buf)` | full typed descriptors |
Every one of the commands in §3 is a rendering of data already
reachable, and `CompletionSource::Custom(Function)` means even the
prompts need no new Rust (§3.2). **This stage adds no Rust at all.**
That is what "pure wiring" means here, and it is worth stating precisely
so the stage is not oversold: **the work is surface, and the risk is in
what the surface leaves out.**
### 2.2 What exists as a command today
`editor.describe-command`, `editor.describe-setting`,
`editor.describe-instance[-buffer]`, `editor.list-buffers`,
`editor.list-workers`, and `help` (#205).
**Missing entirely:** describe-key, describe-mode, describe-hook,
describe-buffer, where-is, list-commands, list-keybindings, apropos.
### 2.3 `describe-setting` prompts free-text, deliberately
```lua
-- builtin/commands/default.lua
pmacs.minibuffer.read {
prompt = "Describe setting: ",
history = "command", -- note: no `source`
on_accept = function(name) … end,
```
A typo yields a status-line error. `describe-command` **does** pass
`source = "commands"`. The asymmetry is real and this stage closes it
(§3.2) — but note *why* it was skipped: dired's `C-x d` records that a
completion source makes RET-on-empty accept whatever sorts first, and a
selected candidate shadows typed text. That is a genuine trade, not an
oversight, so §3.2 says what changes about it.
### 2.4 `Command.predicate` is stored, exposed, and never evaluated
`predicate: Option<Function>` (`src/command.rs:79`) is surfaced as
`has_predicate` (`mod.rs:6204`) and handed out whole (`:6257`). No call
site *evaluates* it — not `invoke`, not `invoke_interactive`, not
dispatch, not M-x filtering, not the menu. Its doc comment describes
palette gray-out that never shipped (§24 already logs this).
**This stage does not evaluate it either** (§5), because doing so makes
commands stop being invocable — a behaviour change needing its own
decision about what "unavailable" means at each call site.
### 2.5 The help layer is duplicated, and this stage would deepen it
`src/help.rs` has `render_command` / `render_key` / `render_buffer` /
`render_mode` / `render_hook` / `render_view` plus link resolution, and
is **orphaned** — the reachable renderer is the Lua `show_help_text`,
which renders *less* (no source, no scope).
**A family of eight new commands each calling `show_help_text` turns a
two-site migration into a ten-site one.** §3.4 is the answer to that,
and it is the most consequential decision in this framing.
## 3. Design
### 3.1 The family
Nine commands, all rendering existing data:
| Command | Reads |
|---|---|
| `describe-key` | `pmacs.describe.key` — prompts for a chord, resolved against the active buffer + mode |
| `describe-mode` | `pmacs.describe.mode` for the active buffer |
| `describe-buffer` | `pmacs.describe.buffer` |
| `describe-hook` | `pmacs.describe.hook` |
| `where-is` | `describe.command(name).key_bindings` |
| `list-commands` | `command.list()` + each description |
| `list-keybindings` | `keymap.list()`, grouped by scope |
| `apropos` | substring match over **names and descriptions** |
| `list-settings` | `config.list()` |
**Naming.** They are `help.*`-prefixed (`help.describe-key`,
`help.where-is`, …) with the existing `editor.describe-*` kept as
aliases-by-retention, not renamed. #205 established `help` as the index;
this makes the family's identity match. Existing names are not removed
`editor.describe-command` is bound in muscle memory and in
`docs/keybindings.md`.
### 3.2 `describe-setting` gains a completion source
It gains a completion source so a typo cannot reach `on_accept`. The
dired trade (§2.3) applies but resolves differently here: dired's prompt
takes an arbitrary *path*, where a shadowing candidate silently opens
the wrong directory; this prompt takes a name **from a closed set**, so
a candidate is what the user wants and free text is the failure mode.
**No Rust is needed for it.** `parse_completion_source`
(`src/lua_bindings/mod.rs:14145-14165`) accepts the strings `none` /
`commands` / `buffers` / `files` **and a Lua `Function`**, which becomes
`CompletionSource::Custom` and is called for candidates. So the source
is `function() return names_from(pmacs.config.list()) end` — the stage
stays entirely Lua, and `Custom` is the general escape hatch every other
command in §3.1 can use if it needs one.
### 3.3 `M-x help` becomes the index
#205 shipped `help` as a static cheat sheet. It now lists the family
above, so the arc's own promise — *`help` stays the index they are
reached from* — is kept rather than merely restated.
### 3.4 One rendering seam, so the later unification is one site
Every new command renders through **`pmacs.editor._show_help`** — the
seam #205 added — and **not** by calling `show_help_text` or building
its own buffer.
That is the whole mitigation for §2.5: when the help-layer unification
stage arrives, migrating to `src/help.rs`'s richer renderer is a change
at **one** Lua function, not at ten call sites. A stage that adds
consumers to a duplicated layer without funnelling them is how the
duplication becomes permanent.
**Corollary this stage must respect:** no new command may render
anything `src/help.rs` cannot eventually produce. Where the Lua renderer
is poorer (no source, no scope), the new commands render the poorer form
rather than inventing a third shape.
## 4. Acceptance
**N** = new behaviour, must fail on full revert. **P** = preservation,
falsified by a named mutation.
1. **N — each of the nine commands exists and renders content.** Driven
through `pmacs.command.invoke_interactive` (the M-x path), asserting
**content produced** in `*help*` — not that a buffer exists.
2. **N — `where-is` agrees with the keymap.** Bind a command to a known
chord, then assert `where-is` reports that chord. Falsified by
rendering a static string.
3. **N — `list-keybindings` covers every binding `keymap.list()`
reports.** A property over the data, not a fixed expected list, with
a non-empty precondition so the loop cannot be vacuous.
4. **N — `apropos` matches on descriptions, not only names.** Search for
a word that appears in exactly one command's *description* and in no
command *name*, and assert that command is listed. This is the pin
that distinguishes apropos from a name filter.
5. **N — `describe-setting` refuses a typo before `on_accept`.** With
the completion source attached, assert `pmacs.minibuffer.selected()`
resolves to a real setting — the pre-RET observable, since
`accept()` does `session.take()` and nothing survives it.
6. **N — `M-x help` lists the family.** Every command in §3.1 appears in
the index. A property over the family list, so adding a tenth command
without indexing it fails.
7. **P — every new command renders through `_show_help`.** Replace that
seam with a counting stub and assert the count equals the number of
commands exercised. This is §3.4's guarantee, and without it the
funnelling is a convention rather than a fact.
8. **P — the existing describe/list commands still work.**
`editor.describe-command`, `editor.describe-setting`,
`editor.list-buffers`, `editor.list-workers` unchanged. Targeted
mutation: renaming rather than retaining them.
9. **P — no command's predicate is evaluated.** Register a command whose
predicate raises, then invoke it through M-x and assert it **runs**.
Pins §2.4's deliberate non-change, so a later stage that starts
evaluating predicates has to change this pin knowingly.
## 5. Deferred, each with its reason
- **Richer M-x rows.** `MinibufferPrompt.candidates` is `Vec<String>` on
the wire; `CompletionPopupRow` already carries `kind`/`detail`, so the
pattern exists — but it is a **protocol change** and belongs to its
own stage with a version bump.
- **`Command` gaining title/category/aliases/flags/arg-schema.** A Rust
type change with ~147 definition sites; own stage.
- **Predicate evaluation** (§2.4) — a behaviour change.
- **Help-layer unification.** The reason §3.4 exists; own stage, made
cheaper by this one rather than harder.
- **A help prefix key.** #205 recorded the constraint in §18: `C-h`
deletes a word because non-kitty terminals cannot disambiguate
Ctrl+Backspace from Ctrl+H. This stage adds **no keybindings at all**,
so the prefix decision is taken once, for the whole family, by the
stage that can weigh `F1` / `C-c ?` / a rebind together.
- **Settings value provenance** (§11) — `describe-setting` will still
answer "who set this?" with the *definition* site.
## 6. Coherence impact
- **Journey steps touched:** 7 and 11, both graded *works but
undiscoverable*; this makes the existing bindings and the `*workers*`
view reachable by name. No step's grade flips on this stage alone —
both also want keybindings, which §5 defers.
- **§5's grade moves** from "substrate without surface" toward Partial:
the surface exists for commands, keys, modes, hooks and settings; it
does not yet exist for packages or workers (§13, §9). **That is a
§25 obligation on the landing PR.**
- **Interaction islands: none added.** Everything renders into the one
`*help*` buffer through the one seam (§3.4) — this stage's main job
is to *avoid* becoming the next island.
- **Config registry: not adopted.** Nothing here is a tunable.
- **Background-work attribution:** unchanged.
## 7. Questions
- **Q#D2 — `help.*` prefix, or keep `editor.*`?** §3.1 proposes `help.*`
with the old names retained. The counter-argument is that two names
for one thing is exactly the duplication this arc exists to remove.
- **Q#D3 — should `apropos` fuzzy-match?** `fuzzy_score` exists
(`src/minibuffer.rs:637-666`) and M-x already uses it. Substring is
more predictable for a search command; fuzzy is more consistent with
M-x. Recommended: substring, and say so in the command description.
## 8. Ledger
Branch `discovery-stage1-commands`, worktree `../pmacs-p4-discovery`,
based on `githubsucks/main` @ `54a092e`. Framing only; no code, no PR.
A sibling lane (`test-ambient-isolation-impl`) is in flight in another
worktree and touches `src/editor.rs` and `tests/`. This lane is
`builtin/` plus its own suite, so the surfaces are disjoint; integrate
`main` late regardless.
```sh
git fetch githubsucks
git worktree add ../pmacs-p4-discovery \
-b discovery-stage1-commands \
githubsucks/discovery-stage1-commands
```