Merge pull request #237 from levineuwirth/gui-stage1-pre

GUI Stage 1-pre — the input seam: window_event from 655 lines to four
This commit is contained in:
Levi Neuwirth 2026-08-12 09:56:04 +00:00 committed by GitHub
commit d038f7144e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 2543 additions and 659 deletions

View File

@ -250,7 +250,188 @@ hazard in a shape that looks committed. **A documented error message
that never appears is worse than no documentation**, because the reader that never appears is worse than no documentation**, because the reader
waits for a signal that is not coming. waits for a signal that is not coming.
## The GUI arc — Stage 0 branch OPEN, absorption COMPLETE, ready for PR ## GUI arc Stage 1 — 1-pre OPEN as PR #237
**Written at the branch's first commit**, with the framing, as the arc's
§5 requires of every PR in it.
- **PR #237** — https://github.com/levineuwirth/pmacs/pull/237.
- **Branch `gui-stage1-pre`**, base `githubsucks/main` @ `f8ad3e7` (the
Stage 0 merge, #236). **`githubsucks/gui-stage1-pre` is the
authoritative tip** — the ref, not a SHA. Recover with
`git fetch githubsucks && git checkout gui-stage1-pre`.
- **Framing `docs/gui-stage1-input-framing.md`, revision 11, APPROVED**
after **eight rejected revisions**. Revision 9 is the approved design;
revision 10 recorded a scope correction found against this
implementation and **also argued P2 was satisfied by classification
alone, which review overturned — revision 11 retracts it**. It is
Stage 1's framing for **all** slices and governs the later branches;
only Stage 0 was framed by the arc document itself.
- **Scope of THIS branch: 1-pre only — the input seam. No behaviour
change.** `App::window_event` was **655 lines** and is now **four** —
call `dispatch_window_event`, exit if it asks. The dispatch it calls
is one `route_event` call and one arm per route. Nothing below it
could be witnessed without a display, which is why the seam precedes
every other slice.
- **IMPLEMENTED in four commits, one per event family**, so each lands
with its own witnesses and mutations rather than as one 600-line
diff: `014110f` lifecycle (close / modifiers / resize), `7f0f9db`
redraw, `0705564` keyboard, `e955645` the four pointer arms. Review
round 1 added `f976dc1`, the P2 effect harness.
- **P2 HAS TWO HARNESSES, and the second was review round 1's blocker.**
The routing harness answers *where did this event go*; `EffectHarness`
answers *what did it do*. Classification alone could not satisfy P2 —
**a wheel route carries a delta, and whether that becomes a viewport
update, a panel event, a terminal event or nothing depends on
`State`.** The effect harness drives a real `AttachClient` over a
`socketpair` (real handshake, outbox, writer thread and encoder, so
the transcript is the wire), a real windowless `State`, and
`App::dispatch_window_event`.
- **`App::dispatch_window_event` is what made P2 reachable.** Left inside
`window_event`, the dispatch would force the harness to re-implement
it, and a harness that re-implements what it tests witnesses its own
copy. `window_event` is now **four lines**, so **P3 shrinks from a
33-line match to a single `if`**.
- **Steps are delimited by a non-coalesceable sentinel key, not a
sleep** — otherwise "this step sent nothing" is undecidable without
waiting, and a fixed-duration wait against a writer thread is exactly
the core-count assumption behind PR #235's CI red.
- **The effect rows never skip**: a missing wgpu adapter is an assertion
failure. M21 confirms all **nine** effect rows fail loudly while the
**thirteen** GPU-free routing rows stay green. The assert is
**unconditional**, not `PMACS_REQUIRE_GPU`-gated, so no invocation
anywhere can turn a missing adapter into a quiet `ok`.
- **They execute in exactly ONE CI job, and that is checked rather than
assumed.** `cargo metadata` reports `workspace_default_members` as the
root `pmacs` package alone, so the `test` matrix and `crdt-test` —
both bare `cargo test --all-targets` — never compile `pmacs-gpu`'s
unit tests at all. Only **`gpu-render`** runs them, and it installs
lavapipe, proves the adapter with `vulkaninfo`, and sets
`PMACS_REQUIRE_GPU=1`. This is the handoff's "`gpu-render` runs a
DIFFERENT PACKAGE" fact showing up as a dependency: these rows live or
die with that one job.
- **Three manufactured absences, all found by running the rows**, and
each the same shape: the harness withheld something production
supplies, then witnessed its own omission. `resumed` sets the frontend
id and session version before any geometry flush (the resize row);
the fixture document was two lines and could not scroll; a headless
`State` has no attached buffer, so `scroll_by_lines` returned `None`.
A fourth was a vacuous assertion — `.all(|e| matches!(..))` over an
empty transcript is true — caught by the outbound-blind mutation.
- **The shape.** Deciding is `route_event(&WindowEvent) -> Route`, a
free function composing one decision function per family
(`route_lifecycle`, `route_keyboard` + `route_key_action`,
`route_pointer`). Performing stays on `App` in seven `apply_*`
methods. A route carries the **decision**, not the effect: a wheel
route holds a delta, and whether that becomes a viewport update, a
panel event, a terminal event or nothing depends on `State`. Effects
are witnessed separately — see the P2 bullet below.
- **Every moved body verified as the original, mechanically.** The
keyboard body is byte-identical modulo two named conversions; the four
pointer bodies were checked by re-running rustfmt on the pre-move text
at the new indent level and diffing, since de-indenting by 8 columns
lets rustfmt rejoin lines. **22 witnesses — 13 routing, 9 effect — and
24 mutations, M1–M24.** Twenty-three fail their own rows; **M6 is the
P3 exception check and must stay green**.
- **P3 IS NOW MEASURED, NOT ASSUMED — and re-measured after the effect
harness landed.** Replacing `window_event`'s whole body with `let _ =
(event_loop, event);` — a GUI that responds to no input at all —
leaves **all 265 `pmacs-gpu` tests green** under `PMACS_REQUIRE_GPU=1`
at the current shape (it was 256 before the effect rows; the number is
re-run, not carried forward). That is the exception's true extent:
`ActiveEventLoop` cannot exist outside a live event loop, so **no**
headless test in the crate observes the delegation. What it covers is
now **one `if`**, not a 33-line match.
- **A SECOND ACCEPTED STRUCTURAL EXCEPTION, found here and winit's
rather than ours.** `KeyEvent` carries a `pub(crate)
platform_specific` field, so **no `WindowEvent::KeyboardInput` can be
constructed outside winit** and the keyboard family's routing arm
cannot be fed by a test. Bounded three ways: it does **not** extend to
the pointer families (`DeviceId::dummy()` exists for exactly this, and
all three pointer events are constructible — checked before writing
the exception down); the family's only real decision is factored into
`route_key_action(ElementState)` and witnessed directly; and what
stays unwitnessed is one pattern arm containing a match and a call.
- **The crate now has exactly ONE executable `event_loop.exit()`**, in
`window_event`. Bodies return an `EventOutcome` rather than taking an
`&ActiveEventLoop`, which is what keeps them reachable in principle.
**`EventOutcome` has TWO producers and they differ in kind**: a native
close (`LifecycleRoute::Exit`), which must always exit, and
`apply_keyboard`'s idle Escape, which is a local quit. **Stage 1a's A4
removes the keyboard producer only**, leaving **one** `Exit`
producer — the native close. **One producer is not one variant**:
`EventOutcome` survives because `dispatch_window_event` must still
distinguish `Continue` from `Exit` on every event, and only the close
exits. A4 changes `apply_keyboard`'s signature, not the type. An
earlier revision of this bullet claimed the type would collapse and
should go with the Escape branch; that was wrong.
- **Slice order (each its own branch and PR):** `1-pre` → `1a`\* → `1b`
→ `1c` → `1d` → `1e`\*. **`1a` and `1e` are protocol-bearing (v24
`TextInput`, v25 `OpenTarget`/`OpenTargetResult`) and are
SERIALIZED.** 1c is **not** protocol-bearing under Q#S1-8.
- **Q#S1-7 obligation carried by THIS PR:** Meta/Super policy moves to
Stage 2, so the arc framing's §2.5 and the standing backlog are
amended here. Stage 1 keeps the deliberate OS reservation and adds no
island.
- **Gates:** `./scripts/gate --acceptance gpu_invocation_acceptance`.
That one invocation already runs `PMACS_REQUIRE_GPU=1 cargo test -p
pmacs-gpu` (step `gpu`) and the full `--workspace --no-fail-fast`
sweep in both feature configurations, so the framing §11 phrase "plus
touched input suites" is satisfied by the sweep rather than by a
hand-picked list. **No `--protocol`** — 1-pre changes no wire.
- **THE FIRST GATE RUN WENT RED ON A STRAY `/tmp/.git`, NOT ON THIS
BRANCH.** `m4` and the sweep failed
`m4_24_bare_string_glob_stays_relative` and
`m4_24_d3_fallback_base_is_the_smallest_attachment_dir` — two LSP
file-watcher tests — with every other target green. Established as
environmental three ways, in increasing strength:
- **Structurally impossible for this branch to cause.** The branch
changes seven files, five of them under `docs/`; **the whole
executable diff is inside the `pmacs-gpu` crate**
(`src/main.rs` and `src/attach.rs`), and `pmacs-gpu` is a workspace
**member but not a dependency** of the root package, so the
`m4_acceptance` binary never links it.
- **The marker is older than the session.** `/tmp/.git` is empty and
was created at 20:14 CEST, **3.5 h before** the gate run at 23:45;
`/tmp` held 8,920 entries. That is handoff §1's recorded hazard
exactly.
- **A discriminating pair compared on SIGNATURE, not test name**, same
binary and commit, one variable — run as **four literal `--exact`
invocations, one test each**, since `m4_24_` is a prefix matching
**18** tests. Contaminated: `0 passed; 1 failed` each, panicking at
`m4_acceptance.rs:5668:5` and `:6615:5` with `.received = ""`,
**matching the gate red's own signature**. Clean: `1 passed; 0
failed` each, **zero panics**. The exact commands are in handoff
§1's hazard bullet. A rerun would have established only
intermittence; the pair establishes the cause.
**`scripts/gate` isolates the target dir and five ambient roots but
NOT `TMPDIR`** — recorded in handoff §1, where the standing fix is
assigned to the gate lane rather than to this PR.
**The marker was left in place**: it is foreign, deleting it is
unnecessary, and the isolated `TMPDIR` is the correct remedy. It must
be **outside `/tmp` and outside every git worktree** — a child of
`/tmp` is not isolated, because `/tmp/.git` remains its ancestor.
- **GREEN under an isolated `TMPDIR`: all nine gates pass on the final
EXECUTABLE tree** (log `20260812T090034Z-2989598`, review round 2) —
executable, not final, because prose and doc comments changed after
it, as the sentence below records. fmt, clippy,
lib, lib-crdt, `gpu_invocation_acceptance`, **m4 168/0/3**,
`PMACS_REQUIRE_GPU=1 -p pmacs-gpu` (**265 tests, none filtered and
none skipped**, all nine effect rows included), the **117-target
`--workspace --no-fail-fast` sweep with zero failures anywhere**
(`m4_acceptance` running all 171 in it), and `diff-check`. Earlier
full-green runs at `20260811T215605Z-2664352` (round 1) and
`20260812T083735Z-2869707` (the P2 harness) are superseded by this
one. **What changed after it is doc comments and prose only** — the
`EventOutcome` correction below and this paragraph; `cargo fmt
--check`, `clippy -D warnings` and the 22 routing/effect rows were
re-run on the result. Stated rather than glossed, because "the gate
was green" and "the gate was green on exactly this tree" are
different claims.
## The GUI arc — Stage 0 MERGED as #236 (`f8ad3e7`)
**Written at the branch's first commit**, with the framing, which is **Written at the branch's first commit**, with the framing, which is
what this arc's own §5 requires of every PR in it. The standing what this arc's own §5 requires of every PR in it. The standing

View File

@ -210,6 +210,68 @@ commands, read `docs/active-work.md` immediately after this file.
server-rooted watcher then faithfully watched. A server-rooted watcher then faithfully watched. A
markerless-fixture red that looks like a watcher bug may be an markerless-fixture red that looks like a watcher bug may be an
ancestor marker, on any machine. ancestor marker, on any machine.
**SEEN AGAIN 2026-08-11, and `scripts/gate` DOES NOT PROTECT YOU
FROM IT.** The gate isolates the target directory and five ambient
roots but **not `TMPDIR`**, so `tempfile::tempdir()` still lands
under a `/tmp` that may carry a marker. It surfaced inside a gate
run on an unrelated lane (GUI 1-pre, whose whole **executable** diff
is inside the `pmacs-gpu` crate) as **`m4_24_bare_string_glob_stays_relative`
and `m4_24_d3_fallback_base_is_the_smallest_attachment_dir`**, in
both the `m4` step and the `--workspace` sweep, with every other
target in the corpus green.
**Diagnose it with the discriminating pair, not a rerun** — same
binary, same commit, one variable. **Four literal `--exact`
invocations, one test each**, because `m4_24_` is a PREFIX matching
**18** tests and a prefix run reports ~16/2 and 18/0 rather than the
0/1 and 1/0 that make the pair readable:
```sh
C=<marker-free dir> # outside /tmp AND outside every git worktree
TMPDIR=/tmp cargo test --test m4_acceptance -- \
--exact m4_24_bare_string_glob_stays_relative # 0 passed; 1 failed
TMPDIR=$C cargo test --test m4_acceptance -- \
--exact m4_24_bare_string_glob_stays_relative # 1 passed; 0 failed
TMPDIR=/tmp cargo test --test m4_acceptance -- \
--exact m4_24_d3_fallback_base_is_the_smallest_attachment_dir # 0 passed; 1 failed
TMPDIR=$C cargo test --test m4_acceptance -- \
--exact m4_24_d3_fallback_base_is_the_smallest_attachment_dir # 1 passed; 0 failed
```
Each prints `running 1 test` and `171 filtered out`. **Read that
line** — see the libtest-filter bullet below for why a filter that
reaches nothing still prints green.
Check the ancestors of the temp root for `.git`, `Cargo.toml` and
friends before believing any markerless-fixture red, and **compare
SIGNATURES, not test names** — the contaminated legs panic at
`m4_acceptance.rs:5668:5` and `:6615:5` with `.received = ""`,
matching the gate red's own signature, and the clean legs panic
nowhere. That is what makes the pair evidence rather than
coincidence.
**The isolated `TMPDIR` must be outside `/tmp` AND outside every git
worktree.** A child of `/tmp` is not isolated: `/tmp/.git` is still
its ancestor. Verify with `git -C <dir> rev-parse --show-toplevel`
failing, not by eye. **Do not delete a foreign marker** — isolating
is sufficient and deletion is someone else's call. **Isolating
`TMPDIR` inside `scripts/gate` is the standing fix and belongs to
the gate lane**, not to whichever feature PR happens to trip over
it.
- **A libtest filter that matches nothing reports `test result: ok`.**
`0 passed; 0 failed; N filtered out` and a zero exit code are what a
*typo'd or mis-quoted filter* looks like, and it is indistinguishable
from success at a glance. **This shell is zsh, which does NOT
word-split unquoted parameter expansions**, so `NAMES="a b"; cargo
test -- $NAMES` passes ONE argument `"a b"`, matches no test, and
prints a green line. Seen 2026-08-11 while running a
contaminated/clean pair: the contaminated leg "passed" and briefly
looked like the environmental hypothesis collapsing. **Read the
`running N tests` line, not just `test result`** — it is the only
place the filter's actual reach is stated.
- **Deliberately unbuilt**: kernel notification (framing option E) — - **Deliberately unbuilt**: kernel notification (framing option E) —
a framed option, not residue; its trigger is the 4 s worst-case a framed option, not residue; its trigger is the 4 s worst-case
external-change latency mattering in practice. external-change latency mattering in practice.

View File

@ -275,7 +275,8 @@ reference** — each keeps its own framing.
| Backlog item | Disposition | | Backlog item | Disposition |
|---|---| |---|---|
| Command/minibuffer chord forwarding; Meta/Super chords | Stage 1a | | Command/minibuffer chord forwarding | **SHIPPED** (`bc32332`) — `Char`/`Enter`/`Tab` with Ctrl or Alt reach the daemon keymap, subsuming the old per-feature allowlists |
| **Meta/Super chords** | **Stage 2 (Q#S1-7 ruling, 2026-08-11)** — *moved from Stage 1a.* Blind forwarding would turn platform Command/Super shortcuts into daemon Meta chords **before** capability-aware and local-binding policy exists. Stage 1 keeps the deliberate, tested OS reservation (`pmacs-gpu/src/main.rs:11050`) and adds no island |
| Rebindable local `Ctrl-V`/`Escape` | Escape half → Stage 1a; `Ctrl-V` half → Stage 2 (it is a keymap-vocabulary question, not an input-plumbing one) | | Rebindable local `Ctrl-V`/`Escape` | Escape half → Stage 1a; `Ctrl-V` half → Stage 2 (it is a keymap-vocabulary question, not an input-plumbing one) |
| Middle-click paste | Stage 1b | | Middle-click paste | Stage 1b |
| Right-click context menu | **Already shipped** (§2.1) — the backlog item is stale and Stage 0 retires the line | | Right-click context menu | **Already shipped** (§2.1) — the backlog item is stale and Stage 0 retires the line |

View File

@ -0,0 +1,541 @@
# GUI arc, Stage 1 — input foundation (framing)
**Status: 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.
## 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`
`App::window_event` (`main.rs:2734`) is **655 lines**; **eight**
`WindowEvent` arms handled, the rest fall to `_`.
**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.**
**`FrontendEvent`: sixteen variants, none carrying an open path or
command invocation.** `PROTOCOL_VERSION = 23`.
**The handshake precedes any window** — `EventLoop` built, client
constructed, handshake done **before `run_app`** (`main.rs:696`).
**TUI wheel arms**: `EditorState::dispatch_mouse` (`src/editor.rs:3052`),
`ScrollUp`/`ScrollDown` at **`:3203`**.
**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** (`attach.rs:414`) and
**coalesces by kind**.
## 3. PR topology
`1-pre` → `1a`\* → `1b` → `1c` → `1d` → `1e`\* (\* `--protocol`)
**1c is NOT protocol-bearing** under Q#S1-8's ruling. Protocol slices
are serialized.
## 4. Q#S1-8 — RULED: (A), preserve pre-window readiness
`AttachRequest.initial_size` for a semantic session is a **named,
provisional `SEMANTIC_BOOTSTRAP_GRID` of 24×80**. It is **not measured
geometry** and **must never become semantic frame or panel authority**.
**`FrontendCellGeometry`, sent after window creation, is the sole real
frame declaration.**
This **codifies current daemon behaviour**, so **1c stays
non-protocol-bearing** — unless implementation changes that behaviour,
which would be a wire-contract change even with no bytes moved.
## 5. Q#S1-9 — RULED: `TextInput` precedence
**A `KeyboardInput` stays `Key` unless a rule below moves it.**
1. **Named keys and control text remain `Key`**, regardless of
`KeyEvent.text` — so `Enter`'s `"\r"` never becomes text.
2. **Ctrl/Alt chords remain `Key`**, except **printable Ctrl+Alt
recognized by the existing AltGr rule**.
3. **Meta/Super-only text stays reserved to the OS.**
4. **Plain printable SINGLE-scalar remains `Key`** — preserving mode
keymaps and today's typed provenance.
5. **Printable MULTI-scalar becomes one `TextInput`.**
6. **Every non-empty `Ime::Commit` becomes one `TextInput`**, even
single-scalar.
7. **`Key::Dead` is 1d-owned; 1a buffers nothing.**
8. **`Shift` is already reflected in resolved text** and is **not**
carried on `TextInput`.
**Provenance and chain.** A **single-scalar** `TextInput` rotates to
`buffer.self-insert` and creates **today's one-codepoint typed
provenance**. A **multi-scalar** `TextInput` **breaks the command chain
and creates no typed provenance**. Both are **one edit, one undo unit,
one hook, one eligible CRDT op**.
**Modal precedence is preserved:** terminals take **raw UTF-8**;
search and minibuffer **consume** text; menu and query-replace **retain
their shadow behaviour**; **only the ordinary document path performs the
atomic edit**.
**Payload cap: 64 KiB UTF-8, oversize REJECTED, never truncated.**
## 6. Evidence
**The promise, corrected.** Revision 4 claimed every mutation fails only
its own clause. That is **false and cannot be made true**: clauses have
real dependencies — D1 gates every 1d row, D3's failure surfaces at A6,
and **E0 and E2–E6 all presuppose E1** (no transport, no receiver). The promise is therefore
**dependency-aware, and split by clause kind** — revision 5 still said
"every clause fails today", which C6 falsifies by design:
- **CHANGE clauses** have a witness that **fails today** and a mutation
that fails **at least** its own clause.
- **PRESERVATION clauses** (marked **[P]**) **pass today**; their
witness pins behaviour that must not regress, and their mutation is
the change that would break it.
- Where a mutation necessarily breaks dependents, **the dependency is
named**.
P3 remains an accepted structural exception: not headlessly testable.
### 1-pre
| # | Contract | Witness (fails today because) | Mutation |
|---|---|---|---|
| P1 | Every handled family routes through an extracted function | no extracted functions exist | misroute one family → that family's row |
| P2 | Harness records outbound events **and local effects** (exit, redraw, resize, state mutation) | no harness | record outbound only → exit/redraw rows |
| P3 | `window_event` is a thin call-through | — | **structural/code-review invariant; not testable headlessly** (no `ActiveEventLoop`) |
**Revision 10 — one finding against the implementation, not the
design.** The 1-pre seam is built and the table above holds, with one
scope correction that could not be seen from the design. *(Revision 10
also argued P2 was satisfied by classification alone. It is not — see
revision 11 at the end of this section.)*
**P1 has a SECOND structural exception, and it is winit's rather than
this seam's.** `KeyEvent` carries a `pub(crate) platform_specific`
field (`winit-0.30.13/src/event.rs:655`), so **no
`WindowEvent::KeyboardInput` can be constructed outside winit** and no
headless test can feed one to the router. P1's mutation — misroute a
family, fail that family's row — is therefore unavailable for the
**keyboard** family alone.
Three things bound it, so it is a measured exception rather than a
blanket one:
- **The exception does not extend to the pointer families.** Winit
provides `DeviceId::dummy()` for exactly this purpose, and
`CursorMoved` / `MouseInput` / `MouseWheel` are constructible. Checked
before the exception was written down; all three are witnessed.
- **What stays unwitnessed is one pattern arm with no logic in it.** The
family's only decision — a press is acted on, a release is claimed and
discarded — is factored into `route_key_action(ElementState) ->
KeyAction`, which takes a constructible argument and is witnessed
directly, with both misroute mutations failing that row alone.
- **P3 is now measured, not assumed.** Replacing `window_event`'s entire
body with `let _ = (event_loop, event);` — a GUI that responds to no
input at all — leaves **every `pmacs-gpu` test green**. That is the
exception's true extent: no headless test anywhere in the crate
observes the delegation. **Re-measured at the current shape after
revision 11: 265/265 under `PMACS_REQUIRE_GPU=1`** (it was 256 before
the effect rows existed, and the number is re-run rather than carried
forward). Revision 11 also shrinks what the exception *covers*:
`window_event` is four lines, so the unwitnessed residue is one `if`
rather than a 33-line match.
**Revision 11 — P2 IS IMPLEMENTED AS WRITTEN. Revision 10's argument
here was wrong and is retracted.**
Revision 10 claimed a route-classification transcript covered both
halves of P2 because a route "names its local effect". **The wheel
falsifies that.** A wheel route carries a delta; whether that delta
becomes a viewport update, a panel event, a terminal event or nothing at
all depends on `State`. The route names the *family*, and only running
the body names the *effect* — so classification could not have
satisfied P2, and arguing that it did was a narrowing wearing the
costume of a mechanism.
P2 now has a second harness beside the routing one:
- **`EffectHarness` drives production end to end** — a real
`AttachClient` over a `socketpair` (real handshake, outbox, writer
thread, encoder, so the transcript is the wire), a real windowless
`State`, and `App::dispatch_window_event`.
- **`App::dispatch_window_event` is what made this reachable.** Left
inside `window_event`, the dispatch would force a harness to
re-implement it, and a harness that re-implements what it tests
witnesses its own copy. **P3 therefore narrows from a 33-line match to
a single `if`**: `window_event` is now `call dispatch, exit if it
asks`.
- **Local effects are read where they land**: exit from the returned
`EventOutcome`, redraw from a test-only `render_calls`, resize from
the surface config, modifiers from `App`, scroll from `scroll_top`.
- **Steps are delimited by a non-coalesceable sentinel key, not a
sleep** — "this step sent nothing" is otherwise undecidable without
waiting, and a fixed-duration wait against a writer thread is the
core-count assumption of PR #235's CI red.
- **The rows never skip.** A missing wgpu adapter is an assertion
failure; mutation M21 confirms all nine effect rows fail loudly while
the thirteen GPU-free routing rows stay green.
`M22` (blind to outbound) and `M23` (blind to local) fail rows in both
directions, which is P2's contract executable rather than asserted.
The routing rows stay, and the division of labour is deliberate: the
routing harness answers *where did this event go*, the effect harness
answers *what did it do*. **P2 is owned by the effect rows.** The
routing transcript row is only the sole owner of the *routing*
harness's own recording, which is what keeps that one mutation
surgical — revision 10 claimed it owned P2, and it never did.
**One further correction revision 11 carries, and it is the design
consequence 1a inherits.** Revision 10 stated that Stage 1a's A4 would
leave `EventOutcome` with a single variant and that the type should go
with the Escape branch. **Both are wrong.**
`EventOutcome` has **two producers today**: `LifecycleRoute::Exit`, a
native window close that must always exit, and `apply_keyboard`'s idle
Escape, a local quit. **A4 removes the keyboard one**, leaving **exactly
one `Exit` producer** — the native close.
**One producer is not one variant.** The type survives because
`dispatch_window_event` still has to distinguish `Continue` from
`Exit` on every event it handles: the overwhelming majority of
dispatches must *not* exit, and the native close must. What A4 actually
removes is `apply_keyboard`'s need to return an outcome at all, which is
a change to that one signature rather than to this type.
The crate has **exactly one** executable `event_loop.exit()`, in
`window_event`.
### 1a — `TextInput` (v24)
| # | Contract | Witness (fails today because) | Mutation |
|---|---|---|---|
| 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 |
| 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 |
| A8 | Terminals get **raw UTF-8, never bracketed paste** | multi-scalar never arrives | route via `Paste` → terminal row shows bracket markers |
| A9 | **64 KiB cap; oversize rejected, not truncated** | no cap exists | truncate instead → the oversize row observes silent loss |
### 1b — pointer and scroll
| # | Contract | Witness | Mutation |
|---|---|---|---|
| B1 | Residual per **axis and surface** | deltas discarded | share one accumulator → surface-switch jump |
| B2 | Wheel-right raises leftmost column; wheel-down raises top line | `x` discarded | invert a sign → that axis's row |
| B3 | Clamps at content bounds; never a negative origin | no horizontal scroll to clamp | remove clamp → **at-bounds row: origin goes negative and the view blanks** |
| B4 | Middle-click paste uses **PRIMARY on Linux** | no middle-click path | use `CLIPBOARD` → B4 only |
| B5 | I-beam over text content only | no I-beam | extend over the gutter → B5 only |
| B6 | Wheel over the minimap scrolls the **document viewport** with **its own residual accumulator**; click/drag remains scrub | **a FULL tick already scrolls today** — minimap pixels are `Elsewhere` (`main.rs:2061`) and the wheel falls through to `scroll_by_lines` (`main.rs:3373`). What fails is **fractional accumulation**, and **residual ownership distinct from the document's**: sub-tick minimap deltas are discarded, and a **surface-switch fractional witness** (part-tick over the minimap, then over the document) must not carry residue across | share the document's accumulator → the surface-switch fractional row jumps |
| B7 | **TUI horizontal**: **three columns per wheel tick**, sign per B2; **left origin clamps at 0 and at (widest display-line width − text viewport width), SATURATING AT ZERO**; **wrap pins the origin to 0** | events arrive at `:3203` and are dropped | step of one → the three-column row; clamp at the widest line's **full width** → **the right-bound row blanks the viewport**; drop the wrap guard → the wrap row scrolls a wrapped buffer sideways |
**B7's right bound corrected.** Clamping at the widest line's full width
lets the origin pass every glyph and leave the viewport **entirely
blank**. The bound is *width − viewport*, saturating at zero for buffers
narrower than the viewport, and **the right-bound witness asserts the
final display column is still visible**.
**Why B6 changed — and revision 5's reason was wrong.** Scrubbing on
wheel is not *impossible*: the wheel handler already reads the cached
`state.pointer_pos` for surface routing (`main.rs:3337`), so an absolute
target is available. It is the **wrong semantics**: a wheel is a
**relative** gesture, and mapping relative ticks onto an absolute
position would make one notch jump to wherever the pointer happens to
rest. Click and drag remain scrub because those *are* absolute.
### 1c — session and window signals
| # | Contract | Witness | Mutation |
|---|---|---|---|
| C1 | Title `"<buffer> — pmacs"`, `"pmacs"` when unnamed | title is static | drop the name → C1 only |
| C2 | **Visible bell: 120 ms, WHOLE CLIENT AREA visibly changes**; repeats **neither queue nor extend** the first deadline | `Bell` unconsumed | let repeats extend → the repeat row's flash outlasts 120 ms; flash a sub-region → the headless render witness cannot see it |
| C3 | `Goodbye` names the daemon's reason, else an explicitly **locally-classified** transport/EOF reason; never blank | live-loop reason discarded | blank the fallback → EOF row |
| C4 | `FocusLost` precedes `Detach`; `FocusGained` only after attach completes | `Focused` unhandled | swap → C4 only |
| C5a | **Local DPI correctness**: at scale 1→2 with **unchanged logical size**, logical wrapping, row count and hit testing are **stable** while **physical pixels double** — glyphs, clips, caret, hit tests and overlays all rescale | `scale: 1.0` hardcoded (`main.rs:8950`) | rescale glyphs only → **caret, clip and hit-test rows fail while text still looks right**, which is the bug this splits out |
| C5b | **Geometry declaration**: the epoch advances and `FrontendCellGeometry` is emitted | no `ScaleFactorChanged` arm | suppress the emit → C5b only, **C5a still passing** |
| C6 **[P]** | `initial_size` = `SEMANTIC_BOOTSTRAP_GRID` (24×80), **never** frame or panel authority | **passes today** — this codifies current daemon behaviour; the witness pins that a semantic frame/panel is sized from `FrontendCellGeometry` alone | derive a frame or panel extent from `initial_size` → the panel-authority row |
| C7 | Close contract — §7 | see §7 | see §7 |
**C5 was one row and hid half the defect** — suppressing the wire emit
says nothing about glyphs or hit tests staying at scale 1.
### 1d — IME
| # | Contract | Witness | Mutation |
|---|---|---|---|
| D1 | `set_ime_allowed(true)` | zero occurrences — **no composition arrives at all** | omit → **every 1d row (stated dependency, not a matrix defect)** |
| D2 | Preedit overlay with caret and selection; **indices are BYTE OFFSETS** into the preedit string | no overlay | treat as char indices → multibyte row |
| D3 | `Ime::Commit` emits A5's `TextInput` | commit ignored | emit per-scalar `Key`s → **A6's undo row (named dependency)** |
| D4 | **`set_ime_cursor_area` updated** after caret motion, scroll, resize, font/DPI change, and every preedit change | never called → candidates misplaced | update on caret only → scroll and resize rows |
| D5 | Overlay clears on **empty `Preedit`, `Ime::Disabled`, and focus loss** — all three | no overlay | clear on focus loss only → `Disabled` row leaves stale text |
| D6 | Dead-key state owned here; 1a buffers nothing | dead keys dropped | buffer in 1a → D6 by construction |
### 1e — `OpenTarget` (v25)
| # | Contract | Witness (fails today because) | Mutation |
|---|---|---|---|
| E0 | **SUCCESS**: a **successfully handled valid target** is **resolved, installed, hooked and dispatched through the existing file/directory pipeline**, and the originating frontend receives **exactly one terminal result** — `Opened { request_id, buffer_id }` when a commit lands, **or `Handled` when an extension legitimately claims it** | no receiver — a drop does nothing | dispatch without firing the open hooks → the hook row; settle before the deferred commit lands → the async-directory row (Q#S1-10) |
| E1 | Versioned `OpenTarget { request_id: u64, cwd, path }` carrying `InitialTarget`'s raw shape. **`request_id` is unique among a frontend's OUTSTANDING requests; a duplicate is REJECTED at the protocol boundary** and must never replace or settle the original completion | no variant exists | **omit `request_id`** → the two-concurrent-drops row misattributes; **accept a duplicate id** → the reuse row settles the first drop's completion with the second drop's outcome |
| E2 | Source is **authenticated** | no receiver | accept an unauthenticated sender → E2's forged-source row |
| E3 | Primary document `ViewDestination` captured **immediately on receipt**, before any await | no receiver | capture after the open resolves → **frontend-switch row: the file lands in whichever frontend is ambient** (the #231 defect) |
| E4 | **No window identity trusted from the wire** | no receiver | accept a window id → E4's forged-window row |
| E5 | Failures **before terminal disposition** — including completion-aware `Deferred` work — are **visible to the ORIGINATING frontend** as bounded `Failed { request_id, message }`. After `Handled`, responsibility and any later failure belong to the claiming extension, and no second result is sent — see §8 | no receiver | swallow the error → the permission and embedded-NUL rows |
| E6 | `InitialTarget` limits enforced: **32 KiB per raw path**, **non-empty path**, **absolute non-empty cwd**, **embedded NUL rejected** | no receiver | drop the NUL check → the embedded-NUL row |
**The failure taxonomy was wrong in revision 5.** **A missing path is
NOT a failure**: the resolver deliberately creates an **empty
path-backed buffer** on `NotFound` (`src/editor_core.rs:1177`), which is
how "open a file that does not exist yet" works. **Directories are valid
targets too.** Genuine failures are permission denial, validation
rejection (E6), and open errors that are not `NotFound`. Revision 5
listed "missing-path" and "not-a-directory" rows that would have pinned
the opposite of the intended behaviour.
### Q#S1-10 — RULED: the result is TERMINAL
Directory opening completes **asynchronously**, so `OpenTargetResult`
either waits for the captured-destination commit or merely acknowledges
dispatch. **Ruling: terminal.** The result is sent **when the request
reaches a terminal disposition** — for `Opened`, after the commit
resolves; for `Handled`, at the moment responsibility transfers; for
`Failed`, at the failure, **which for several paths has no commit at
all**. Asynchronous failure is reported through the same result — an acknowledgement-plus-later-channel design would need a
second source-scoped mechanism to carry exactly the failures that matter
most.
```
OpenTargetResult::Opened { request_id: u64, buffer_id: BufferId }
OpenTargetResult::Handled { request_id: u64 } // claimed; no buffer attributable
OpenTargetResult::Failed { request_id: u64, message: String } // 4 KiB cap
```
**Terminal completion must be TOTAL over the existing pipeline, and
"after the commit resolves" is not** — three legitimate paths reach
neither a commit nor a result:
- **Claimed**: a `path.open-directory` listener returns `proceed =
false` and the dispatch **returns without committing**
(`src/editor.rs:1375`).
- **Disabled**: the `directory_handler` slot is clear, which is a
supported configuration — the dispatch emits a **status message and no
commit** (`src/editor.rs:1389`).
- **Asynchronous fallback**: the default handler calls `open_async` and
**returns immediately** (`builtin/runtime/dired.lua:750`), so the
commit lands long after the dispatch unwinds.
**Mechanism: a request-scoped ONE-SHOT COMPLETION with an EXPLICIT
STATE MACHINE**, carried through the directory pipeline, settled
**exactly once**, and **discarded on source detach** — a frontend that
left cannot be told anything.
```
Pending ──(defer before scheduling)──▶ Deferred ──▶ Settled
│ │
└──────────(commit / failure)────────┴──▶ Settled
│
└──(source detach, from Pending or Deferred)──▶ Cancelled
```
**`open_async` transitions to `Deferred` BEFORE handing the completion
to scheduled work.** The ownership transfer is explicit and does not
depend on whether today's scheduler starts a coroutine synchronously or
a future scheduler defers its first step. Without the transition the
dispatch unwinds while the completion is still `Pending`, the
end-of-turn fallback fires, and the request is settled *before* the
commit it was waiting for.
**The end-of-turn fallback acts ONLY on `Pending`.** A `Deferred`
completion is owned by the scheduled work. **Any later settlement is
exactly-once**: a second attempt against `Settled` or `Cancelled` is a
no-op, never a second message.
*Mutation:* omit the `Deferred` transition, or delay it until after the
end-of-turn fallback → the async-directory row receives a premature
`Handled`/`Failed`; exactly-once settlement then suppresses the later
`Opened` attempt when the commit lands.
`open_async` carrying the completion to the commit is what makes the
asynchronous path terminal rather than silent.
**Total disposition, so no path can fall through:**
| path | settles as |
|---|---|
| commit lands | `Opened { buffer_id }` |
| listener **claimed** and did not settle | `Handled` |
| **replacement handler** ran and did not settle | `Handled` |
| handler slot **disabled** | `Failed` naming that directory opening is disabled |
| **synchronous error** (listener raised, validation, permission) | `Failed` with the reason |
| pipeline unwinds **still `Pending`** at end of dispatch turn | `Handled` if a listener claimed or a replacement handler ran; **otherwise `Failed`** |
| **source detached** before settlement | discarded — nothing is sent, and the completion is dropped rather than leaked |
**`Handled` exists because `Opened` cannot be honest there.** A claim
means a user listener took responsibility and **no buffer is
attributable**; reporting `Opened` would require inventing a
`buffer_id`, and reporting `Failed` would mislabel a supported
extension point as an error. `Handled` is the terminal responsibility
transfer: any later extension-owned failure uses the extension's own
reporting surface and cannot emit a second `OpenTargetResult`.
*Witness:* one case per **live-source** row, each asserting **exactly
one** result. **The detach row asserts the opposite and must not be
read as "one result":** zero messages sent, **no completion retained**,
and a later settlement attempt **ignored** rather than delivered.
*Mutation:* drop the end-of-turn fallback → the claimed and disabled
rows hang with no result at all, which is the defect this ruling
closes.
`message` uses the **existing 4 KiB error cap**. **Both affected enums
get an independent frozen-byte pin on their own preceding final
variant** — `FrontendEvent` for `OpenTarget`, `InstanceMessage` for
`OpenTargetResult` — because an appended variant's own round-trip cannot
detect a discriminant shift in either.
## 7. The close contract
`send_event` only enqueues (`attach.rs:1145`); the writer takes a batch
and releases the lock before blocking writes (`:671`); **`enqueue`
returns `false` once closed** (`:414`).
**So revision 4's order was literally unexecutable** — closing first
would have rejected the `Detach` it then tried to enqueue.
**Contract — state inspection plus any append/transition is ONE atomic
critical section under a single lock hold, dispatched on the outbox
STATE. The lock is released before notifying the writer, waiting on an
acknowledgement, returning, or shutting down.** Holding it while waiting
would prevent the writer from taking the suffix or transitioning to
`Drained`. The four states are tabulated below; this paragraph is their
normative statement, and revision 6's "append `Detach` /
already-closed → fallback" wording is superseded:
- **Open** → under the lock, append the **complete suffix** (`FocusLost`
when currently focused, then `Detach`), transition to `Sealed`, and
capture its acknowledgement handle; release the lock, wake the writer,
then wait.
- **Sealed** → under the lock, capture the **existing acknowledgement**;
release the lock, then join it. Do not re-append, do not seal again,
do not shut down.
- **Drained** → release the lock, then return immediately.
- **Failed** → release the lock, then take the socket-shutdown fallback.
**An ordinary post-seal `send_event` REJECTS and does nothing else** —
it must not invoke shutdown, because a healthy drain is in flight.
**`Detach` is exempt from coalescing.** **Wake the writer after
append-and-seal**, or
a sealed outbox with a pending `Detach` waits on a condvar nobody
signals and the 250 ms bound expires on a daemon that was reading fine.
**The exactly-full case, which revision 5 left undefined.**
`OUTBOX_MAX` is **8192**, and a queue at exactly that length is a
**valid OPEN state**: the *next* ordinary `enqueue` both **sets
`closed`** and **rejects the event** (`attach.rs:427`). So terminal
close must not go through the ordinary path. **Ruling: the terminal operation appends the whole required SUFFIX
atomically** — **`FocusLost` when currently focused, then `Detach`** —
reserving **at most `OUTBOX_MAX + 2`**.
**One slot was not enough, and C4 is why.** C4 requires `FocusLost`
before `Detach`; at exactly `OUTBOX_MAX` an *ordinary* `FocusLost`
enqueue is **rejected and sets `closed`**, so the terminal append would
then find a closed outbox and fall back — losing both events. Revision 6
reserved one slot and so contradicted a contract two sections above it.
**Exact-cap witness:** fill to exactly `OUTBOX_MAX` **while focused**,
then close. The transcript ends **`… FocusLost, Detach`**, the writer is
woken, and acknowledgement precedes exit. *Mutation:* reserve one slot →
`FocusLost` is dropped and C4's ordering row fails at exact cap only.
**Outbox states — clean SEALED is not failed CLOSED.** Revision 6 had
one `closed` flag doing both jobs, so a duplicate close or a post-seal
send would see "closed" and **invoke the socket-shutdown fallback,
aborting the drain it should have joined**. Four states, with distinct
behaviour:
| state | ordinary enqueue | close called again |
|---|---|---|
| **Open** | ordinary policy: accepted below the cap; a cap-crossing lossless append is rejected and transitions to `Failed` | performs the terminal append-and-seal |
| **Sealed** (suffix appended, drain in flight) | rejected | **waits on the existing acknowledgement** — never re-seals, never falls back |
| **Drained** (acknowledged) | rejected | returns immediately |
| **Failed** (overflow, or transport error) | rejected | **takes the socket-shutdown fallback** |
*Mutation:* collapse `Sealed` into `Failed` → the duplicate-close witness
aborts a healthy drain and exits before `Detach` is written.
**Acknowledgement point:** the batch containing `Detach` **fully written
and flushed to the socket**. **Bound: 250 ms.**
**Two witnesses, mutually exclusive:**
- **Responsive reader** — all preceding lossless events **and** `Detach`
are written, acknowledgement occurs, **then** exit. *Mutation:* drop
the drain → exit-before-`Detach` is observable.
- **Stalled reader** — the deadline fires, socket shutdown yields EOF
cleanup, the frontend **exits anyway**. *Mutation:* remove the bound
→ this test hangs.
*Seal mutation:* permit a late enqueue → an event appears after
`Detach` in the responsive transcript.
## 8. Wire contracts
| | **1a — `TextInput`** | **1e — `OpenTarget` + `OpenTargetResult`** |
|---|---|---|
| **floor** | **v24** | **v25**, after v24, serialized |
| **encoding** | **appended variant**; never widen a field in place — postcard is positional | appended variants |
| **byte pin** | frozen-byte fixture on the **previous final variant** | same |
| **gate** | daemon accepts from `>= 24`; producer withholds below | `>= 25`; producer withholds below |
| **old peer** | a `< 24` frontend **retains its existing `Key` behaviour and its existing limitations** — it truncates multi-scalar input today and ignores IME, and continues to. **The guarantee is NO REGRESSION, not retroactive correctness** | a `< 25` frontend cannot drop-open; nothing it already had degrades |
| **bounds** | **64 KiB** UTF-8; oversize **rejected** | **32 KiB** per raw path; non-empty path; absolute non-empty cwd; **embedded NUL rejected**; `Failed.message` capped at the **existing 4 KiB** error cap |
| **pins** | frozen bytes on `FrontendEvent`'s previous final variant | **two independent pins** — `FrontendEvent` for `OpenTarget`, `InstanceMessage` for `OpenTargetResult` |
**E5's delivery mechanism.** `StatusFacts.message` is **global and can
be cleared before the originating frontend observes it**, so it cannot
carry this. **1e adds a source-scoped `OpenTargetResult`, correlated by
a frontend request ID**, so a failure reaches the frontend that dropped
the file and no other.
## 9. Coherence impact (§20)
- **Journey steps**: **5**; **3** (1e); **6(e)** on the GPU column.
- **Islands**: Escape ceasing to be a local quit **removes** one;
Q#S1-7 adds none → the census **falls by one**.
- **Config registry**: none. The bell's 120 ms is a constant.
- **Background work**: **1e adds no new worker**, but it **attributes
the existing asynchronous directory operation to an originating
frontend and request** until terminal settlement — the one-shot
completion is that attribution — **and drops it on source detach**.
That is §9's ownership question appearing in miniature, and it is
answered here for this one operation rather than in general.
## 10. Rulings
**Q#S1-1** native close detaches, `editor.quit` shuts down the daemon
and its attachments, Escape only cancels/round-trips · **Q#S1-5** A/`1e`
· **Q#S1-6** B/`TextInput` · **Q#S1-7** Meta/Super → Stage 2, arc §2.5
and the backlog amended by 1-pre's first PR · **Q#S1-8** (A),
`SEMANTIC_BOOTSTRAP_GRID` · **Q#S1-9** precedence per §5 · **Q#S1-10** terminal `OpenTargetResult`.
## 11. Gates
`./scripts/gate --acceptance gpu_invocation_acceptance` plus touched
input suites, and `PMACS_REQUIRE_GPU=1 cargo test -p pmacs-gpu`.
**`--protocol` for 1a and 1e only.**

View File

@ -193,9 +193,12 @@ external-change detection (verify-modtime-while-open, revert?).
## GPU frontend mechanics (non-theme) ## GPU frontend mechanics (non-theme)
- **Input:** full command/minibuffer chord forwarding to the GUI, - **Input:** ~~full command/minibuffer chord forwarding to the GUI~~
Meta/Super chords, rebindable local `Ctrl-V`/`Escape`, middle-click (**shipped**, `bc32332`), **Meta/Super chords — now GUI arc Stage 2**,
paste, right-click context menu, frontend-local provisional selection. not Stage 1a (Q#S1-7, 2026-08-11: forwarding them before
capability-aware policy exists would capture platform Command/Super
shortcuts), rebindable local `Ctrl-V`/`Escape`, middle-click paste,
right-click context menu, frontend-local provisional selection.
- **Minibuffer:** `i/total` hint (already on the wire), Telescope-style - **Minibuffer:** `i/total` hint (already on the wire), Telescope-style
preview pane, candidate kind/doc annotations, unify TUI inline vs GPU preview pane, candidate kind/doc annotations, unify TUI inline vs GPU
dropdown, multibyte-exact band caret, the nav highlight-wrap bug. dropdown, multibyte-exact band caret, the nav highlight-wrap bug.

View File

@ -1169,6 +1169,24 @@ impl AttachClient {
} }
} }
/// Build a real [`AttachClient`] over an already-connected stream, for
/// the GUI 1-pre effect harness in `main.rs`.
///
/// The harness needs to observe the protocol messages a dispatch
/// actually produces. Faking the client would only witness the fake, so
/// it drives the **real** handshake, outbox, writer thread and encoder
/// over a `socketpair`, and reads the encoded `FrontendEvent`s off the
/// other end. This wrapper exists solely because
/// [`connect_stream_with_sink`] is private to this module and the
/// harness is a sibling; it adds no behaviour of its own.
#[cfg(test)]
pub(crate) fn connect_stream_for_test(
stream: UnixStream,
sink: impl Fn(AttachEvent) -> bool + Send + 'static,
) -> Result<AttachClient, AttachClientError> {
connect_stream_with_sink(stream, None, sink)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

File diff suppressed because it is too large Load Diff