From cd888bed5ee1e67958b89665785afbe65009b78e Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 12 Aug 2026 14:29:04 +0200 Subject: [PATCH] docs: Stage 1a --- re-measure the framing's ground truth, and open the lane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs only, and the first commit on the 1a branch. **No ruling changes.** **1a needs no framing of its own** --- `docs/gui-stage1-input-framing.md` already governs every Stage 1 slice, and A1-A9, the eight Q#S1-9 precedence rules, §8's wire contract and §11's gates are ruled there. What 1a does need is §2 to be TRUE, and it was measured at `a994f37`, before 1-pre moved almost every GPU-side coordinate in it. A framing whose ground truth points at the wrong lines is how an implementation ends up arguing with the tree. Re-measured at `4f77491`, and split by kind rather than renumbered wholesale, because the two are not the same claim: STILL TRUE `FrontendEvent`'s sixteen variants, `PROTOCOL_VERSION = 23`, IME's zero occurrences, the TUI wheel arms (1-pre touched only `pmacs-gpu`), the handshake preceding the window, and `Outbox::enqueue`'s closed-return. MOVED `window_event` `:2734`/655 lines -> `:4450`/FOUR lines; `translate_key` `:10975` -> `:12053`; "eight arms, rest fall to `_`" -> three family decision functions over nine variants. **1a edits `apply_keyboard` and `translate_key`, not `window_event`.** **TWO CLAIMS WERE WRONG AT BOTH ANCHORS, so they are corrections rather than drift.** **"`KeyEvent.text` is never read" is false, and was false at `a994f37` too** (`:2800` there, `main.rs:3251` now): the AltGr rule reads it via `is_layout_text(key.text.as_deref(), pmods)`. The true claim is narrower and is the one that matters --- **`text` is never read as the text a keypress INSERTS**, only as a discriminator separating AltGr from a command chord. That is load-bearing for A5 rather than cosmetic. §5's rule 2 already exempts "printable Ctrl+Alt recognized by the existing AltGr rule", so the precedence table depends on code the section claimed did not exist. **1a widens `text` from discriminator to payload, and that is the actual change of kind** --- which "text is never read" hid, along with the one place the new payload must leave undisturbed. **A4's exit site moved without its behaviour changing.** 1-pre routed the idle-Escape quit through `EventOutcome`, so **A4 edits a branch and a return type in `apply_keyboard` (`main.rs:3219`) and does not touch `window_event`**, which holds the crate's only executable `event_loop.exit()` (`main.rs:4452`). And **`EventOutcome` survives A4**: a native close still returns `Exit`. The lane records the v24 serialization constraint and the bump's blast radius up front --- eight version-sensitive failures across six suites on the last bump, of which CI showed one, because cargo stops at the first failing target --- together with the rule for sorting them and the pin that must never fire, `ADVERTISED_PROTOCOL_VERSION == 20`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 50 ++++++++++++++ docs/gui-stage1-input-framing.md | 109 ++++++++++++++++++++++++------- 2 files changed, 135 insertions(+), 24 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index cbed93f..1394cea 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -270,6 +270,56 @@ hazard in a shape that looks committed. **A documented error message that never appears is worse than no documentation**, because the reader waits for a signal that is not coming. +## GUI arc Stage 1a — `TextInput` at v24 — branch OPEN, no PR yet + +**Written with the branch's first commit**, per the standing correction +from #171 and #215. + +- **Branch `gui-stage1a-textinput`**, base `githubsucks/main` @ + `4f77491` exactly. **`githubsucks/gui-stage1a-textinput` is the + authoritative tip** — the ref, not a SHA. Recover with + `git fetch githubsucks && git checkout gui-stage1a-textinput`. +- **No new framing.** `docs/gui-stage1-input-framing.md` already governs + every Stage 1 slice; A1–A9, the eight Q#S1-9 precedence rules, §8's + wire contract and §11's gates are ruled there. Writing a 1a framing + would duplicate an approved document. +- **First commit is a GROUND-TRUTH RE-MEASUREMENT of §2 (revision 12), + not code.** §2 was taken at `a994f37`, before 1-pre moved almost every + GPU-side coordinate in it. It is refreshed here rather than in its own + PR so the contract and the code that depends on it are reviewed + together. **No ruling changes.** + - Moved: `window_event` `:2734`/655 lines → **`:4450`/four lines**; + `translate_key` `:10975` → **`:12053`**; the eight-arms description + → three family decision functions over nine variants. + - **CORRECTION, wrong at BOTH anchors: "`KeyEvent.text` is never + read" is false** — the AltGr rule reads it (`a994f37:2800`, now + `main.rs:3251`). The true claim is narrower: `text` is never read as + the text a keypress **inserts**, only as a *discriminator*. This is + load-bearing for A5, because §5's rule 2 already exempts "printable + Ctrl+Alt recognized by the existing AltGr rule" — **1a widens `text` + from discriminator to payload, and that is the change of kind the + old wording hid.** + - **CORRECTION: A4's exit site.** 1-pre moved the mechanism without + changing behaviour. **A4 edits `apply_keyboard`'s branch and return + type, not `window_event`**, and **`EventOutcome` survives A4** — a + native close still returns `Exit`. +- **PROTOCOL-BEARING at v24 and therefore SERIALIZED.** `TextInput` is + an **appended** `FrontendEvent` variant; never widen a field in place, + because postcard is positional. `PROTOCOL_VERSION` is **23** at this + base and `ADVERTISED_PROTOCOL_VERSION` stays pinned at **20** and must + not be edited to chase it. A frozen-byte pin goes on `FrontendEvent`'s + **previous final variant**, since an appended variant's own round-trip + cannot detect a discriminant shift. +- **A `PROTOCOL_VERSION` bump's blast radius is every version-sensitive + test and NONE of them appear in the diff** — handoff §1a records eight + such failures across six suites on the last bump, of which CI showed + one because cargo stops at the first failing target. **Sort them:** a + tripwire `assert_eq!(PROTOCOL_VERSION, N)` is meant to fire; an + absolute contract expressed as arithmetic on a moving constant is a + defect. **`ADVERTISED_PROTOCOL_VERSION == 20` must NOT fire.** +- **Gates:** `./scripts/gate --protocol` plus the touched input suites. + **`--protocol` is required here** — 1a changes the wire. + ## GUI arc Stage 1 — 1-pre MERGED as #237 (`d038f71`); 1a is next, NOT STARTED **The lane is rewritten, not removed.** Rule 4 removes a lane when its diff --git a/docs/gui-stage1-input-framing.md b/docs/gui-stage1-input-framing.md index c5553ff..fd1a647 100644 --- a/docs/gui-stage1-input-framing.md +++ b/docs/gui-stage1-input-framing.md @@ -1,47 +1,108 @@ # GUI arc, Stage 1 — input foundation (framing) -**Status: revision 11 — APPROVED.** Revisions 1–8 rejected; revision 9 +**Status: revision 12 — APPROVED.** Revision 12 is §2's ground-truth +re-measurement for Stage 1a and changes no ruling; it carries two +corrections to claims that were wrong at the original anchor too. + +**Previously, revision 11 — APPROVED.** Revisions 1–8 rejected; revision 9 is the approved design. Revision 10 recorded a scope correction found against the 1-pre implementation and **also made a claim about P2 that review overturned; revision 11 retracts it and P2 is implemented as written** (§6). **Q#S1-8, Q#S1-9 and Q#S1-10 are RULED.** **1-pre is IMPLEMENTED**; 1a onward may begin from this document. -**Verification base:** checked in the `gui-arc-stage0` worktree at -`a994f37`, whose tree for these files is what `f8ad3e7` merged. +**Verification base:** §2 is **re-measured at `4f77491`** (2026-08-12), +the tip after 1-pre; it was originally taken at `a994f37`. Sections +other than §2 were written against `a994f37` and their *rulings* are +unaffected by 1-pre, which changed no behaviour — but **any line number +outside §2 predates 1-pre and should be re-checked before it is relied +on.** ## 1. What this stage closes Journey **step 5**. **Not step 12** (Stage 4b, P2-gated). Five of nine §3.1 blockers die here. -## 2. Ground truth at `a994f37` +## 2. Ground truth — RE-MEASURED at `4f77491` (2026-08-12) -`App::window_event` (`main.rs:2734`) is **655 lines**; **eight** -`WindowEvent` arms handled, the rest fall to `_`. +**Originally taken at `a994f37`, before 1-pre.** 1-pre (#237) moved +almost every GPU-side coordinate below, so the section is re-measured +rather than left to rot — **a framing whose ground truth points at the +wrong lines is how an implementation ends up arguing with the tree**. +Two claims were *wrong at both anchors* and are corrected, not merely +renumbered; they are marked **CORRECTION**. -**Two text producers.** `translate_key(logical: &Key, …)` -(`main.rs:10975`) reads the **logical key** and truncates via -`chars().next()`; **`KeyEvent.text` is never read.** -`WindowEvent::Ime::Commit(String)` is a **separate event, ignored -entirely** (`set_ime_allowed`/`WindowEvent::Ime`: zero occurrences). -**Today's two failures are therefore: multi-scalar keyboard input is -truncated to its first scalar, and an IME commit produces nothing.** +### Still true, re-checked -**`FrontendEvent`: sixteen variants, none carrying an open path or -command invocation.** `PROTOCOL_VERSION = 23`. +- **`FrontendEvent`: sixteen variants**, none carrying an open path or + command invocation. **`PROTOCOL_VERSION = 23`.** +- **`WindowEvent::Ime` is ignored entirely** — + `set_ime_allowed`/`WindowEvent::Ime` still **zero occurrences**, so + `Ime::Commit(String)` produces nothing. (1d's D1.) +- **TUI wheel arms unmoved**: `EditorState::dispatch_mouse` + (`src/editor.rs:3052`), `ScrollUp`/`ScrollDown` at **`:3203`** — 1-pre + touched only `pmacs-gpu`. +- **The handshake precedes any window**: the client is constructed and + the handshake done before `run_app` (connect `main.rs:702`, `run_app` + `main.rs:733`; the old citation `:696` was the enclosing block). +- **1c is producer-side only for Focus/Detach** — those variants exist + on the wire. **Title, Bell and `Goodbye` are GPU consumer work.** +- **`Outbox::enqueue` returns `false` once closed** and **coalesces by + kind** (`attach.rs:415`; the old `:414` was off by one at both + anchors). -**The handshake precedes any window** — `EventLoop` built, client -constructed, handshake done **before `run_app`** (`main.rs:696`). +### Moved by 1-pre -**TUI wheel arms**: `EditorState::dispatch_mouse` (`src/editor.rs:3052`), -`ScrollUp`/`ScrollDown` at **`:3203`**. +- **`App::window_event` is `main.rs:4450` and is FOUR lines**, not 655 + at `:2734`. It calls `dispatch_window_event` and performs the exit; + **routing lives in `route_event`, and the bodies in seven `apply_*` + methods.** +- **"Eight arms handled, the rest fall to `_`" is now three family + decision functions** — `route_lifecycle`, `route_keyboard` (plus + `route_key_action`), `route_pointer` — over **nine** named + `WindowEvent` variants, with `Route::Unrouted` as the wildcard. **1a + edits `apply_keyboard` and `translate_key`, not `window_event`.** +- **`translate_key(logical: &Key, …)` is `main.rs:12053`**, not + `:10975`. It still reads the **logical key** and still truncates via + `chars().next()`, and `_ => return None` is still there — so **A1's + witness holds**. -**1c is producer-side only for Focus/Detach** — those variants exist on -the wire. **Title, Bell and `Goodbye` are GPU consumer work.** +### CORRECTION 1 — `KeyEvent.text` IS read, and always was -**`Outbox::enqueue` returns `false` once closed** (`attach.rs:414`) and -**coalesces by kind**. +The original section said *"`KeyEvent.text` is never read."* **That is +false, and was false at `a994f37` too** (`:2800` there, `main.rs:3251` +now): the AltGr rule reads it, as `is_layout_text(key.text.as_deref(), +pmods)`. + +The claim the section meant, and which is true: **`KeyEvent.text` is +never read as the text a keypress INSERTS.** It is consulted only as a +*discriminator* — Ctrl+Alt plus printable text means AltGr rather than a +command chord — and the inserted character always comes from +`translate_key`'s logical key, truncated to one scalar. + +**This matters to A5, not just to accuracy.** §5's rule 2 exempts +"printable Ctrl+Alt recognized by the existing AltGr rule", so the +precedence table already depends on the code the section claimed did not +exist. **1a widens `text` from a discriminator to a payload**, and that +is the actual change of kind — stating it as "text is never read" hides +the one place the new payload must not disturb. + +### CORRECTION 2 — A4's exit site + +A4's witness cited *"exits (`main.rs:2771`)"*. 1-pre moved the mechanism +without changing the behaviour: an idle Escape still exits, but +`apply_keyboard` (`main.rs:3219`) now returns `EventOutcome::Exit` and +**`window_event` (`main.rs:4452`) performs the only executable +`event_loop.exit()` in the crate.** + +**A4 therefore deletes a branch in `apply_keyboard` and changes its +return type — it does not touch `window_event`.** And **`EventOutcome` +survives A4**: a native close still returns `Exit`, and +`dispatch_window_event` must still distinguish it from `Continue`. + +**Today's two failures are unchanged by any of this:** multi-scalar +keyboard input is truncated to its first scalar, and an IME commit +produces nothing. ## 3. PR topology @@ -229,7 +290,7 @@ The crate has **exactly one** executable `event_loop.exit()`, in | A1 | `F1`–`F35` → `F(1..=35)` | `_ => return None` | map `F13+` → `None` → F13–F35 rows | | A2 | Shift+Tab → `BackTab` with `Shift` set | produces `Tab` | drop `Shift` → A2 only | | A3 | `ContextMenu` → `Menu` | produces nothing | map to `Char('\0')` → A3 only | -| A4 | Idle Escape reaches the daemon, never exits | exits (`main.rs:2771`) | restore the quit branch → A4 only | +| A4 | Idle Escape reaches the daemon, never exits | exits — `apply_keyboard` (`main.rs:3219`) returns `EventOutcome::Exit`, performed at `main.rs:4452` | restore the quit branch → A4 only | | A5 | Precedence per §5 (1–8) | multi-scalar truncated; IME ignored | move rule 1 (control text → text) → the `Enter`-in-dired row | | A6 | One commit = one edit, undo unit, hook, eligible CRDT op | commit truncated to one scalar | one edit per scalar → undo-unit row (**and D3 surfaces here**) | | A7 | Prompts consume scalars **in order** | multi-scalar never arrives | reverse order → A7's prompt transcript |