Files

200 lines
10 KiB
Markdown

# Session handoff — 2026-06-02 (55)
Fifty-fifth handover. **One issue resolved: #11 — copy the output
panel to the system clipboard.** New ADR-0041; new `copy` app-command;
first native-clipboard dependency (`arboard`). The session is a clean
run of the full phased method: all four design axes escalated to and
decided by the user (twice — a first round of four, then a second
round of two once a hard technical constraint surfaced), `/runda` on
the **design** before any code, test-first build, full-stack PTY
verification at the end.
## §1. State at handoff
**Branch:** `main`. **HEAD `1ea376b`** (handoff-54). **3 commits ahead
of `origin/main`** at session start — the three session-54 commits
(`4cd574b`, `516848f`, `1ea376b`); handoff-53's `#15/#16` and earlier
are now pushed (origin advanced since handoff-54 was written).
**This session's #11 work is committed on top** (see the commit that
accompanies this handoff). **Tests: 2151 passing / 0 failing / 1
ignored** (full `cargo test`; lib-only **1578**). Baseline at session
start was **2134 / 0 / 1****+17** new tests, no regressions, no new
skips. The 1 ignored is the same long-standing `` ```ignore `` doctest
in `src/friendly/mod.rs`. **Clippy: clean** (nursery, all targets,
`-D warnings`). Push is the user's step.
## §2. #11 — copy the output panel to the clipboard (ADR-0041)
**Friction removed:** filing a bug report meant terminal-selecting the
output panel and fighting wrapping/borders. Now: a typed command.
**Command (app-level, sigil-free, both modes — ADR-0003 registry):**
| Form | Copies |
|------|--------|
| `copy`, `copy all` | the whole output panel |
| `copy last` | from the most recent echo line to the end |
`copy last` boundary is fuzzy by design: app commands push echo-less
`[system]` lines, so "from the last echo" reaches past them to the
previous DSL command — documented + accepted in ADR-0041.
**Mechanism — OSC 52 *and* native `arboard`, ALWAYS BOTH.** The
deciding constraint (surfaced in escalation round 2): OSC 52 acceptance
is **undetectable** — the terminal sends no ack — so a literal "fall
back when OSC 52 is unsupported" *cannot be built*. So both fire every
time: emit the OSC 52 escape (`ESC ]52;c;<base64>BEL`; tmux-passthrough
wrapped when `$TMUX` set), then a best-effort native write whose
failure is ignored (headless host → OSC 52 carried it). Identical
content, so the local double-write is harmless. OSC 52 needs **no new
dep** (`base64` + `crossterm` already present); it works over SSH.
**Format — plain text verbatim *as rendered*:** each line's
`[tag] body ✓/✗` joined by `\n`, **without** the viewport's right-edge
padding or soft-wrapping (full logical lines, reflow-friendly, no
trailing whitespace). A **drift-lock test** (`ui.rs`
`plain_text_matches_rendered_line_content`) pins `OutputLine::
plain_text` to the concatenated span content of `render_output_line`,
so the copy can never silently diverge from the screen.
**Files (6 new, 18 modified):**
- `src/clipboard.rs` *(new)* — pure `osc52_sequence` / `emit_osc52` /
`in_tmux`, the `NativeClipboard` trait + `deliver`, and the
`SystemClipboard` arboard impl (lazy-init, kept alive — X11 serves
the selection from a thread owned by the handle).
- `src/action.rs` — `Action::CopyToClipboard(String)`.
- `src/app.rs` — `OutputLine::plain_text`, `App::copy_text`,
`handle_copy_command`, dispatch arm + 6 Tier-1 tests.
- `src/dsl/command.rs` / `dsl/mod.rs` — `AppCommand::Copy { scope }` +
`CopyScope { All, Last }` + re-export + source mapping.
- `src/dsl/grammar/app.rs` + `grammar/mod.rs` — `COPY` CommandNode
(`copy [all|last]`, unknown→`copy.unknown`), REGISTRY entry.
- `src/runtime.rs` — long-lived `SystemClipboard` + the
`Action::CopyToClipboard` arm (thin call to `clipboard::deliver`).
- `src/ui.rs` — the drift-lock test.
- `src/friendly/strings/en-US.yaml` + `friendly/keys.rs` —
`copy.done`/`copy.nothing`/`copy.unknown`, `help.app.copy`,
`parse.usage.copy`, CLI `--help` banner line.
- `src/completion.rs` — `copy` added to the app-keyword test list
(the candidate itself is REGISTRY-driven).
- `tests/typing_surface/{mod,app_commands}.rs` + 4 new snapshots.
- `Cargo.toml` / `Cargo.lock` — `arboard` `--no-default-features`.
- `docs/adr/0041-*` *(new)*, `docs/adr/0003-*` (registry note),
`docs/adr/README.md` (index + 0003 note), `docs/requirements.md`
(new **V6**, done).
**Coverage (17):** clipboard module 6 (exact OSC 52 bytes, base64
round-trip incl. non-ASCII, tmux wrap, `deliver` both-paths +
writer-error-still-fires-native); app 6 (all / bare-=-all / last-slice
/ empty-nothing / no-echo-nothing / unknown-target-friendly-error);
drift-lock 1; typing-surface 4 (copy / copy all / copy last parse,
`copy ` offers all+last).
**Full-stack PTY verification (the real binary, not a mock):** drove
`target/debug/rdbms-playground` through a pseudo-terminal, typed
`create table …` then `copy`, and confirmed the OSC 52 escape reached
the tty with a base64 payload decoding to the exact on-screen content
(echo + `` + `[system]` structure + box-drawing table). This is the
end-to-end proof the unit seams don't give on their own.
## §3. The arboard decision — current state (READ if revisiting)
`arboard` `3.6.1` (1Password, `MIT OR Apache-2.0`), added
**`--no-default-features`**. Two deliberate choices are **flagged to
the user as open-for-correction** — they were made on low-stakes,
principle-aligned grounds and explicitly surfaced, not silently
decided:
1. **No `wayland-data-control` feature → X11-only native on Linux.**
That feature ~doubles the dep tree (~30 crates: `wl-clipboard-rs`,
`wayland-*`, `quick-xml`, `nom`, `petgraph`). Since "always do both"
makes **OSC 52 the universal path** (covers native-Wayland
regardless) and most Wayland desktops run XWayland (so plain
`x11rb` works), the feature was omitted to keep the dependency
surface minimal (secure-by-default). **If a concrete
native-Wayland-without-OSC-52 need appears, add the feature** — one
line in `Cargo.toml`.
2. **Copy is verbatim on-screen, including the per-line `[system]`
tags** (the PTY payload shows `[system] ┌──…`). This is the literal
reading of the user's "exactly what's rendered" choice. **If tag
stripping on table rows is wanted, that's a `plain_text` change**
(+ re-baseline the drift-lock test).
**Security posture (re-run before any dep bump):** arboard introduces
**zero new vulnerabilities** — `cargo audit`, `osv-scanner`, and
`grype` are all clean for arboard's tree (`log` / `parking_lot` /
`percent-encoding` / `x11rb`, with `gethostname`/`rustix` already ours).
Write-only (no OSC 52 *read* — the exfiltration vector is out of play).
`gitleaks` + `trivy` clean on the new code.
**Pre-existing, orthogonal, FLAGGED not fixed:** `osv-scanner` /
`cargo audit` report **RUSTSEC-2025-0067 (`libyml`)** and
**RUSTSEC-2025-0068 (`serde_yml`)**. These are a **pre-existing direct
dependency** (`Cargo.toml` `serde_yml = "0.0.12"` → `libyml`), **not
pulled by arboard** (`cargo tree -i serde_yml` shows only
rdbms-playground as parent), known since **handoff-14**. cargo audit
treats them as allowed warnings (exit 0). **Out of scope for #11**;
fixing means migrating off `serde_yml` (the YAML lib behind
`project.yaml`) — its own task, needs user sign-off.
## §4. Process notes (what worked)
- **Escalate genuine forks — don't decide.** Six questions across two
rounds. Round 1: mechanism / scope / trigger / format. Round 2 was
*forced by honesty* — the chosen "OSC 52 + native **fallback**" is
not literally buildable (no ack), so the coordination had to be
re-decided (→ "always do both") and the trigger confirmed. Surfacing
the constraint beat shipping a thing that didn't match the label.
- **`/runda` on the design.** The DA pass on ADR-0041 found: the
`copy last` boundary fuzziness (→ documented), the "exactly on
screen" overclaim (→ reworded to "as rendered, not padded"), and the
full-stack-coverage obligation (→ `deliver` both-paths test + PTY
run). Folded in before any code.
- **Test-first + drift-lock.** The renderer composes the visible line
at render time (tag, `running:`→`✓/✗`, teaching prefix), so copying
raw `OutputLine.text` would be wrong; `plain_text` mirrors the
renderer and the drift-lock test guarantees they stay in step.
- **Verify by running the app.** The PTY harness proved the seam the
unit tests can't (OSC 52 bytes actually reaching a real tty).
## §5. What's open
### Bug reports / enhancements
**#11 is resolved this session.** The handoff-50 bug-list trio
(#10/#11/#14) is now fully closed. No open issues remain on the
recent list — **confirm on GitHub after push.**
### Other tracks (from `requirements.md`)
- Track 2 Iter 6 leftovers: `history.log`-based persistent
input-history hydration polish (§12), migration-framework exercise.
- C3a (modify relationship), C4 (m:n convenience).
- H1 friendly DB-error layer (partial); tutorial/lesson system (needs
its own ADR); **V4** session-log + Markdown export (the broader
output-panel UX — V6/clipboard is a complementary "get the session
out" path now shipped); I1/I1b multi-line + readline; I3
tab-completion polish; I4 highlighting beyond input echo.
- **Pre-existing security debt:** migrate off `serde_yml`/`libyml`
(RUSTSEC-2025-0067/0068) — needs user sign-off (§3).
## §6. How to take over
1. Read this note, then `CLAUDE.md`, then `docs/requirements.md`
(V6 now done), then `docs/adr/README.md` and any ADR you'll touch
(ADR-0041 is new; ADR-0003 gained a registry note).
2. The codebase is on `main`; this session's #11 commit sits on top of
the 3 unpushed session-54 commits. Closing **#11** on GitHub is
pending the push (the user's step).
3. If revisiting the clipboard: §3 has the two open-for-correction
arboard decisions (no-Wayland-feature; verbatim-with-tags) and the
security posture. Re-run the scanners before any dep bump.
4. Honour the process pins: escalate genuine forks (and surface
technical constraints that contradict a chosen option's label),
`/runda` the design *and* implementation, test-first with a
drift-lock where a copy mirrors a renderer, and verify by running
the real app (the PTY harness pattern is reusable for any
terminal-side behaviour).