docs(journey): frame Stage 1b-3 — say something when the editor opens

The last of the 1b split. Journey step 4 and `COHERENCE.md` §18: a
fresh `pmacs` greets the user with an empty buffer, an empty status
line, and no indication that `M-x` exists — the sole discovery
affordance in the product is knowing to press it.

Three findings shape the design:

- The existing step-2 ratchet pin collides with any welcome. It asserts
  `status.is_empty()` while its own message says "reports no error" —
  the same predicate only while nothing writes a non-error status at
  startup. The framing corrects the assertion to its message's claim
  and names that as the one existing assertion the stage changes,
  rather than burying it in a diff.

- `C-h` is not free. It is bound to word-delete because non-kitty
  terminals cannot disambiguate Ctrl+Backspace from Ctrl+H — both
  produce byte 0x08 — so rebinding it to a help prefix would break
  Ctrl+Backspace on every legacy terminal. §2's step-4 row calls it an
  oversight; it is a deliberate trade with a stated rationale. The
  help-prefix decision is deferred to the discovery arc with the
  constraint recorded so it is inherited rather than rediscovered.

- A `*help*` buffer mechanism already exists over `src/help.rs`, so the
  welcome can point at something real. Its two gaps are recorded rather
  than inherited silently: it writes with delete/insert instead of
  `set_generated_contents`, and it is found by name.

The welcome deliberately does NOT use `set_generated_contents`: that
lifts read-only, discards history and marks the buffer generated, all
wrong for the buffer step 5 requires the user to type into immediately.

Step 4 stays Partial — the row names a welcome, a cheat sheet and
`C-h`, and this closes the first plus a minimal second — so this is the
first 1b stage with no landed-evidence obligation on merge.

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 14:28:19 -04:00
parent 1f290d51c3
commit 02645a71b0
2 changed files with 346 additions and 0 deletions

View File

@ -347,6 +347,59 @@ If it does not, stop and repair the remote/fetch configuration.
githubsucks/journey-stage1b1-compile-defaults
```
## Journey Stage 1b-3 (P1) — FRAMING OPEN, revision 1
- **Branch `journey-stage1b3-welcome`**, worktree `../pmacs-journey-1b3`,
based on `githubsucks/main` @ `1f290d5`. **Framing only; no code, no
PR yet.** `docs/journey-stage1b3-welcome-framing.md` revision 1, no
review rounds. The last of the 1b split (1b-1 landed #203, 1b-2 is
PR #204).
- **What it is.** Journey step 4 / `COHERENCE.md` §18: a fresh `pmacs`
greets the user with an empty buffer, an empty status line, and no
indication that `M-x` exists. The stage renders a three-line welcome
into `*scratch*` and adds a minimal `M-x help`.
- **The existing step-2 pin collides with it.**
`journey_step2_launches_unconfigured_into_scratch` asserts
`status.is_empty()` while its own message says "reports no error" —
the same predicate only while nothing writes a *non-error* status at
startup. Acceptance 8 corrects the assertion to its message's claim
and calls it out rather than burying it, since the ratchet's rule is
that stages add rows.
- **`C-h` is NOT free, and the reason is load-bearing.** It is bound to
`buffer.delete-word-backward` because non-kitty terminals cannot
disambiguate Ctrl+Backspace from Ctrl+H — both produce byte 0x08
(`builtin/keymaps/default.lua:78-86`). Rebinding it to a help prefix
would break Ctrl+Backspace on every legacy terminal, so §2's step-4
row calling it an oversight is wrong: it is a deliberate trade. The
help-prefix decision is deferred to §20 Priority 4's discovery arc
with the constraint recorded.
- **`*help*` already exists** (`builtin/commands/default.lua:1226`,
`show_help_text`, reused buffer, buffer-local `q`), used by
`editor.describe-command` / `-setting`, over `src/help.rs`'s
renderers and link resolution. Two gaps recorded rather than
inherited silently: it writes with `buf:delete`/`buf:insert` instead
of `set_generated_contents`, and it is **found by name**, so a
foreign `*help*` would be cleared.
- **Deliberately NOT `set_generated_contents` for the welcome** — that
lifts read-only, discards history and marks the buffer generated, all
wrong for a buffer step 5 requires the user to type into immediately.
The one place not adopting that invariant is correct, stated so a
later audit does not "fix" it.
- **Step 4 stays Partial**, so this is the first 1b stage with no §25
landed-evidence obligation on merge: §2's row names a welcome, a
cheat sheet *and* `C-h`, and this closes the first plus a minimal
second.
- **Integrate late.** #204 is open and touches `COHERENCE.md`,
`docs/agent-handoff.md` and `docs/active-work.md`; this lane will
conflict there. Never open a standalone refresh PR.
```sh
git fetch githubsucks
git worktree add ../pmacs-journey-1b3 \
-b journey-stage1b3-welcome \
githubsucks/journey-stage1b3-welcome
```
## 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,293 @@
# Journey Stage 1b-3 — say something when the editor opens
**Status: framing, rev 1 — awaiting approval.**
**Serves `COHERENCE.md` §2 (the golden journey, step 4), §18
(onboarding), §19, §20 Priority 1.**
## 0. Revision history
- rev 1 (2026-07-30) — first framing. Scouted against `githubsucks/main`
@ `1f290d5` (Journey Stage 1b-1, #203).
## 1. What this stage is
The last of the 1b split. `COHERENCE.md` §20 named three things:
**1b-1** (compile binding + defaults, landed as #203), **1b-2** (LSP
spawn guidance, PR #204), and this — the welcome buffer.
§18 is unusually specific about the size of it:
> the cheap floor — a welcome buffer in `*scratch*` naming `M-x`, the
> keybinding cheat sheet as a help buffer, and a help prefix decision —
> has no prerequisites at all.
**This stage takes the first of those three.** §2's step-4 row lists all
three as what is missing, so §5 is explicit that step 4 does **not**
reach Works here, and says what remains.
## 2. Ground truth
Verified in the tree at `1f290d5`.
### 2.1 A fresh launch says nothing, and the audit's citation is right
`EditorCore::new` sets `status: String::new()` (`src/editor_core.rs:659`)
and creates `*scratch*` (`:629`). A user who runs `pmacs` with no
arguments and no config gets an empty buffer, an empty status line, and
no indication that `M-x` exists.
`builtin/keymaps/default.lua`'s own header comment calls `M-x` the
"command palette" — the sole discovery affordance in the product is
knowing to press it.
### 2.2 The step-2 ratchet pin will collide with any status-line welcome
This is the finding that most shapes the design.
`tests/journey_acceptance.rs`:
```rust
fn journey_step2_launches_unconfigured_into_scratch() {
let s = EditorState::new();
assert_eq!(active_name(&s), "*scratch*");
assert!(
status(&s).is_empty(),
"a clean launch reports no error; got {:?}", status(&s)
);
}
```
The assertion's *message* says "reports no error"; the assertion itself
says **the status is empty**. Those are the same predicate only while
nothing ever writes a non-error status at startup — which is exactly
what a welcome would do.
So a status-line welcome does not merely need a new row, it needs that
existing pin's predicate corrected to what its own message already
claims. §4 treats that as a deliberate, named amendment rather than a
silent edit, because the ratchet's rule is *stages add rows, none
removes them* and an assertion change deserves the same scrutiny.
### 2.3 `C-h` is not free, and the reason is load-bearing
§18 lists "a help prefix decision" as part of the cheap floor. It is
cheap to *decide* and not cheap to get wrong:
```lua
-- builtin/keymaps/default.lua:78-86
-- Why we also bind C-h: most terminals (anything not implementing the
-- kitty keyboard protocol) cannot disambiguate Ctrl+Backspace from
-- Ctrl+H — both legacy paths produce byte 0x08 …
bind("C-h", "buffer.delete-word-backward")
```
**Rebinding `C-h` to a help prefix would break Ctrl+Backspace on every
non-kitty terminal**, because those terminals cannot tell the two apart.
§2's step-4 row says "`C-h` deletes a word" as though it were an
oversight; it is a deliberate trade with a stated rationale.
This stage therefore **does not touch `C-h`** (§5), and the framing
records the constraint so the discovery arc inherits the reason rather
than rediscovering it.
### 2.4 There is already a `*help*` buffer mechanism
`builtin/commands/default.lua:1226-1260` has `show_help_text(text)`: a
single reused `*help*` buffer, found by name, with a buffer-local `q`
`buffer.kill-this`. `editor.describe-command` and
`editor.describe-setting` both render through it. `src/help.rs` supplies
`render_command` / `render_key` / `render_buffer` / `render_mode` /
`render_view` plus link resolution (`link_at`, `follow_link_at`).
**Two facts about it that matter to any consumer:**
- It writes with `buf:delete` + `buf:insert`, **not**
`pmacs.buffer.set_generated_contents`. It is one of the writer
mechanisms that has not adopted the generated-buffer write invariant,
so its buffer stays ordinarily editable and keeps its undo history.
Riding it inherits that; fixing it is not this stage's job, but a
stage that renders into it should not claim read-only guarantees it
does not have.
- It is **found by name**, which is the adoption hazard `listview` and
dired both refused. A foreign `*help*` buffer would be cleared.
### 2.5 A modified `*scratch*` does not block quitting
`editor.quit` runs the `editor.before-quit` hook and then `ed.quit()`
(`builtin/commands/default.lua:250-256`). There is no unsaved-buffer
confirmation, so welcome text left in `*scratch*` cannot strand a user
at exit. That removes the strongest objection to putting text in the
buffer, and it is stated because it is the kind of thing that would
otherwise be assumed in either direction.
## 3. Design
### 3.1 What the welcome says
Three lines, no more. The floor is "this editor is not inert and here is
the one key that opens everything":
```
Welcome to pmacs. M-x run a command C-x C-f open a file
C-c c build C-c t terminal
This buffer is *scratch* — type to edit it, or M-x help for more.
```
Every key named must be **bound in the default keymap and verified by
the acceptance suite**, or the welcome becomes documentation drift with
a user attached. §4 pins that as a property over the message, not a
hardcoded list.
### 3.2 Where it goes — `*scratch*`, not the status line
The status line is the wrong vehicle even though §2's audit cites it:
it is overwritten by the next status write, and a user who blinks loses
the only pointer to `M-x`. §18 says "a welcome buffer in `*scratch*`",
and that is right for the reason the audit itself gives — *the empty
`*scratch*` buffer is what greets the user*.
So: **welcome text is rendered into `*scratch*`**, under three
conditions, all necessary:
1. **No arguments.** `pmacs FILE` and `pmacs DIR` both put something
else on screen; a welcome would be noise, and for the directory case
it would fight Stage 1a's dired listing.
2. **`*scratch*` is empty.** Never overwrite content — including a
restored session's scratch, or anything an `init.lua` wrote.
3. **It leaves the buffer unmodified**, so nothing about the greeting
looks like unsaved work. §2.5 shows quitting is not blocked either
way; this is about not lying in the modeline.
### 3.3 What it must not do
- **Not read-only.** Step 5 is "edit immediately"; a read-only greeting
would break the journey one step after it starts. The user types over
it, as in Emacs.
- **Not a new buffer.** A `*welcome*` buffer displayed instead of
`*scratch*` would change what step 2 asserts and give the user a
buffer whose only purpose is to be closed.
- **Not `set_generated_contents`.** That lifts the rope's read-only,
discards history, and marks the buffer generated — all wrong for a
buffer the user is meant to edit immediately. This is the one place
where *not* adopting the generated-buffer invariant is correct, and it
is stated so a later audit does not "fix" it.
### 3.4 `M-x help`
The welcome names `M-x help`, so that command has to exist. It renders
the keybinding cheat sheet through the existing `show_help_text`
mechanism (§2.4).
**This is the smallest possible version of §18's second item**, and it
is included only because the welcome would otherwise point at nothing.
The full cheat sheet, `where-is`, `describe-key` and the help-prefix
question belong to §20 Priority 4's discovery arc (§5).
## 4. Acceptance
**N** = new behaviour, must fail on full revert. **P** = preservation,
falsified by a named mutation.
1. **N — journey step 4, through the real entry point.**
`EditorState::new()` — the same construction `pmacs` with no
arguments performs — leaves `*scratch*` active **and non-empty**, and
its text names `M-x`. This is the ratchet row.
2. **N — every key the welcome names is actually bound.** Parse the key
sequences out of the rendered text and assert each resolves through
`pmacs.keymap.lookup`. A property over the message, so the pin cannot
rot when the wording changes — and so the welcome cannot advertise a
binding that a later stage removes.
3. **N — `M-x help` renders the cheat sheet** into `*help*`, containing
at least the keys the welcome names. Asserts content produced.
4. **P — the buffer is editable and unmodified.** Typing a character
into the greeted `*scratch*` inserts it (step 5 still works from the
first frame), and the buffer reports unmodified *before* that
keystroke. Targeted mutation: rendering through
`set_generated_contents`, which would make the buffer read-only and
fail the insert.
5. **P — a file argument suppresses the welcome.** `EditorState::open`
on a file leaves that file active with no greeting anywhere.
Targeted mutation: greeting unconditionally in `EditorCore::new`.
6. **P — a directory argument suppresses it too**, so Stage 1a's dired
listing is what the user sees. Same mutation; separate pin because
the directory path reaches scratch differently (the bootstrap
replaces the buffer rather than never creating it).
7. **P — a non-empty `*scratch*` is never overwritten.** Write to
scratch, then trigger the greeting path: the content survives.
8. **P (amended pin) — step 2 still reports no error.**
`journey_step2_launches_unconfigured_into_scratch` currently asserts
`status.is_empty()` while its message says "reports no error"
(§2.2). The assertion is corrected to the message's claim — no error
text on the status line — rather than deleted or weakened. **This is
the one existing assertion this stage changes**, it is called out
here rather than buried in the diff, and the row itself is kept.
## 5. Deferred, and why
- **The help prefix / `C-h`.** §2.3: rebinding it breaks Ctrl+Backspace
on every non-kitty terminal. The decision belongs with §20 Priority
4's discovery arc, which can weigh a prefix (`F1`? `C-c ?`?) across
the whole command family instead of one key at a time.
- **The full cheat sheet and the discovery command family**
`where-is`, `describe-key`, richer M-x rows: §20 Priority 4, called
"the best payoff-per-effort in this document".
- **Making `show_help_text` adopt the generated-buffer write
invariant** (§2.4). A real gap, with its own lane.
- **Onboarding proper** — §18's ten-step teaching sequence. This stage
is the floor beneath it, not a down payment on it.
**Step 4 therefore stays Partial**, and the PR must say so: §2's row
names a welcome, a cheat sheet *and* `C-h`, and this closes the first
plus a minimal version of the second.
## 6. Coherence impact
- **Journey steps touched:** 4 (Partial → still Partial, with the
welcome half closed); 2 indirectly, whose pin is amended (§4.8). No
grade flips on merge, which makes this the first 1b stage with no
§25 landed-evidence obligation.
- **Interaction islands: none added.** `M-x help` renders through the
existing `*help*` mechanism rather than inventing a second help
surface — though §2.4 records that mechanism's own two gaps rather
than pretending it is exemplary.
- **Config registry:** not adopted. A `welcome.enabled` scalar would be
expressible, and is deliberately not added — a greeting the user can
delete by typing does not need a setting, and §11's adoption work
should not gain a token entry from a stage this small.
- **Background-work attribution:** unchanged; nothing here is
asynchronous.
- **Docs riding the PR:** `COHERENCE.md` §2's step-4 row and §18's
ground-truth grade (both stay Partial, with the closed half named);
`docs/keybindings.md` gains `M-x help`; `docs/agent-handoff.md` §1;
the ledger.
## 7. Questions
- **Q#W1 — three lines, or one?** One line ("`M-x` runs a command")
is the true floor and never wraps at 80 columns. Three teaches more
but risks looking like chrome the user must clear. Recommended:
three, because the whole complaint in §18 is that the editor teaches
nothing.
- **Q#W2 — should the welcome name `C-c c` and `C-c t`?** They are
real, bound, and journey steps 8 and 9 — but naming them means the
welcome must be updated whenever the default map changes.
Acceptance 2 turns that from a risk into a caught failure, which is
the argument for naming them.
- **Q#W3 — is `M-x help` the right name?** `help` is short and
guessable. The discovery arc may want `help.keys` /
`help.commands` as a family, and renaming later costs a deprecation.
## 8. Ledger
Branch `journey-stage1b3-welcome`, worktree `../pmacs-journey-1b3`,
based on `githubsucks/main` @ `1f290d5`. Framing only; no code, no PR.
**#204 is open and touches `COHERENCE.md`, `docs/agent-handoff.md` and
`docs/active-work.md`.** This lane will conflict there; integrate late
(at PR time), never by opening a refresh PR.
```sh
git fetch githubsucks
git worktree add ../pmacs-journey-1b3 \
-b journey-stage1b3-welcome \
githubsucks/journey-stage1b3-welcome
```