write 0.4.0

A fullscreen, distraction-free, write-only Markdown editor that fades text away to silence the writer's inner editor.
# Completed Work

## Task 1a — Window, raw input capture, Markdown file append

Stood up the runnable, borderless, dark, keyboard-only egui/eframe (wgpu backend) app.

- `src/main.rs` — builds `NativeOptions` with a borderless, titlebar-less, fullscreen
  `ViewportBuilder` and calls `eframe::run_native`, returning `eframe::Result`.
- `src/app.rs``WriteApp` implementing `eframe::App` via the 0.34 `ui(&mut self, ui, frame)`
  signature:
  - On launch, creates `dirs::document_dir()/write/<epoch>.md` (falling back to `home_dir`),
    opened append-only and wrapped in a `BufWriter`.
  - Captures raw input each frame from `ctx.input(|i| i.events)`: `Event::Text` appended verbatim;
    Enter → idempotent `"\n\n"`; Backspace → idempotent single space (never deletes); Esc → quit;
    Tab focus navigation suppressed; arrows/Delete/Home/End/PageUp-Down and `command`-modified
    combos (paste/undo) ignored.
  - Durability: flush + `File::sync_data()` every ~30s (wall-clock `Instant`) and a final fsync on
    clean exit via `on_exit`. `request_repaint_after(30s)` keeps the idle fsync firing.
  - Renders a full-rect black background plus a single wrapped galley of the session buffer
    (`Painter::layout``Painter::galley`); `clear_color` overridden to opaque black.

Notes / deviations from the original design sketch:
- `WriteApp::new` returns `Box<dyn Error + Send + Sync>` (eframe's `AppCreator` requires the error
  to be `Send + Sync`).
- Text layout uses `Painter::layout(&self, ...)` instead of `ui.fonts(|f| f.layout(...))`: in
  egui 0.34 the `fonts` reader yields an immutable `&FontsView` whereas `FontsView::layout` needs
  `&mut self`; `Painter::layout` wraps the mutable access internally.
- Used `ctx.content_rect()` (the non-deprecated replacement for `screen_rect()`).

`cargo clippy` and `cargo fmt --check` are clean. No automated tests (GUI loop); the 1a acceptance
criteria are manual.

## Task 1b — The fading 30-second view

Replaced 1a's plain solid-gray render with the real focus view: only the last ~30s of typed text is
visible, fading linearly to fully transparent over ~1s once it crosses the threshold, never
reappearing. Purely visual — the Markdown file and append behavior from 1a are untouched.

- `src/app/render.rs` (new, pure/unit-tested per the `module.rs` + `module/submodule.rs` layout) —
  `Glyph { ch, birth }`; `fade_factor(age)` (opaque ≤30s, linear over the 1s fade, 0 past 31s);
  `quantize_alpha(factor)``u8`; `runs()` coalesces adjacent same-alpha glyphs into
  `(String, u8)` runs; `build_job()` builds a word-wrapped `LayoutJob` with one `TextFormat` section
  per run, fading via `Color32::gamma_multiply` (composite-over-black opacity, not fade-to-gray).
- `src/app.rs` — added `mod render;`, a `visible: VecDeque<Glyph>` render buffer fed by `append`
  (now `append(&mut self, s, now)`), with `now = ctx.input(|i| i.time)` stamped per frame. Each
  frame prunes fully-faded glyphs from the front, then draws a centered, bottom-anchored galley so
  newest text sits near the bottom and older lines scroll off the top. `document` is retained for
  1c's word count.
- CPU idle fix: repaint is now conditional — `request_repaint()` only while glyphs are visible;
  when blank, wake only at the next 30s fsync deadline
  (`request_repaint_after(SYNC_INTERVAL.saturating_sub(last_sync.elapsed()))`), replacing 1a's
  unconditional `request_repaint_after(SYNC_INTERVAL)`.

Notes / deviations:
- Layout goes through `Painter::layout_job(&self, ...)` rather than `ui.fonts(|f| f.layout_job(..))`:
  in egui 0.34 the `fonts` reader yields an immutable `&FontsView` but `FontsView::layout_job` needs
  `&mut self`; the painter wraps the mutable access internally (same pattern as 1a's `layout`).

`cargo clippy`, `cargo fmt`, and the 4 unit tests in `render.rs` (`cargo nextest run`) are clean.
The fade/scroll visuals remain manual acceptance.

## Task 1c — Explicit visible-buffer clears + Esc menu

Added the writer's manual "wipe the screen" controls and a key-driven Esc menu, without ever
removing anything already written to the file (clearing is visual only).

- `src/app/render.rs` — added `clear_factor(age_since_clear)` (immediate linear fade over
  `FADE_SECS`, no 30s hold) and `glyph_factor(birth, now, visibility_head)` which combines a glyph's
  own age-fade with the clear-fade (glyphs born at/after `visibility_head` are unaffected). `runs`
  and `build_job` now take `visibility_head` and use `glyph_factor`. New unit tests:
  `clear_factor_immediate`, `glyph_factor_unaffected_after_head`, `glyph_factor_clears_older_glyphs`.
- `src/app.rs` — added `visibility_head: f64`, replaced `last_was_enter: bool` with an `enter_run: u8`
  state machine (0 → none, 1 → one Enter written `\n\n`, 2+ → cleared/no-op), and added
  `menu_open: bool`. Triggers that advance `visibility_head` to "now" (simultaneous ~1s fade-out):
  Backspace (first of a run; also appends one idempotent space) and a consecutive second Enter (no
  extra newlines). The event loop gates on `menu_open` so the menu is key-driven (Q quits, Esc
  resumes) and typing into the void is ignored. Esc opens the menu and fsyncs. Prune now uses
  `glyph_factor` so clear-faded glyphs drain in birth order. A centered, title-bar-less
  `egui::Window` overlays a live `split_whitespace().count()` word count plus key hints.

All v1 tasks (1a/1b/1c) complete. `cargo clippy`, `cargo nextest run` (7 tests), and
`cargo fmt --check` all clean.

## Cursor-wiggle / visual feedback on backspace-clear

On the first backspace of a run, the visible text now performs a brief, decaying horizontal "shake"
(the classic macOS "no" wiggle) while it fades out, giving immediate kinetic acknowledgment that the
no-delete/wipe gesture was registered. Purely visual — the Markdown file, append behavior, and clear
logic are untouched. Double-Enter clears stay shake-free.

- `src/app/render.rs` — added `SHAKE_SECS`/`SHAKE_AMPLITUDE`/`SHAKE_FREQ` constants and a pure
  `shake_offset(since)`: a sine oscillation (12px peak, ~3 oscillations over 0.4s) under a
  linear-decay envelope, returning 0.0 at rest (outside the `(0, SHAKE_SECS)` window). New unit
  tests `shake_offset_at_rest_outside_window` and `shake_offset_bounded_and_decays`.
- `src/app.rs` — added `shake_start: f64` (init `f64::NEG_INFINITY` so early frames report at-rest),
  set to `now` in the same first-backspace-of-a-run guard that already sets `visibility_head`, and
  offset the galley's x-position by `shake_offset(now - shake_start)`. The offset (≤12px) stays well
  inside the 40px margin, so text never clips; the existing post-backspace fade repaint loop already
  covers the 0.4s shake.

`cargo clippy`, `cargo nextest run` (9 tests), and `cargo fmt --check` all clean.

## Keyboard shortcut to return to windowed (non-fullscreen) mode

Gave the writer a deliberate, non-colliding way to drop the borderless-fullscreen app back to a
windowed (maximized, chrome-hidden) shell so the window can be moved/resized/closed via the OS.
Driven from the Esc menu (the existing key-driven control surface where writing input is already
gated off), so it can never fire during normal prose entry.

- `src/app.rs` — added a `fullscreen: bool` live-state field (init `false`, set `true` in the
  launch-time deferred-entry block alongside `Fullscreen(true)`). `entered_fullscreen` stays a
  **distinct** one-shot deferral latch so a later manual exit to windowed never re-fires the
  deferral. New `egui::Key::W` arm in the menu key handler sends
  `ViewportCommand::Fullscreen(!self.fullscreen)`, flips the bool, and closes the menu to resume
  writing (eframe 0.34 has no toggle variant, so we track the state ourselves). The menu hint label
  is now state-aware: `"W — windowed"` while fullscreen, `"W — fullscreen"` while windowed.

Toggle is reachable only via the Esc menu; typing "w" mid-sentence still produces a literal "w".
GUI window-state, so manual acceptance per project convention. `cargo clippy`, `cargo nextest run`
(9 tests), and `cargo fmt --check` all clean.

## Anchor the text column to the vertical center

Moved the visible galley from a bottom-margin anchor to a center-relative one so the active writing
line holds a fixed focal point near the window's vertical center while older text scrolls upward.

- `src/app.rs` — hoisted the proportional font size into `const FONT_SIZE: f32 = 28.0;` and reused it
  for both the `egui::FontId` and the x-height calc (no duplicated literal). Replaced
  `y = rect.bottom() - margin - galley.size().y` with a center-relative anchor:
  `x_height = 0.5 * FONT_SIZE`, `center_y = rect.center().y`,
  `y = center_y + x_height - galley.size().y`, pinning the galley's bottom edge one x-height below
  the window center. The galley grows upward as lines accumulate; no upward clamp (the 30s fade
  bounds total text). Horizontal layout, `col_width`/`max_col`, and the backspace `shake_offset` dx
  are untouched; `margin` still drives the horizontal column-width calc only.

GUI positioning, so manual acceptance per project convention. `cargo fmt --check`, `cargo clippy`,
and `cargo nextest run` (9 render tests) all clean.

## Persistent word-count indicator (upper-right HUD)

Added an always-visible, small dim-gray **day-total** word count anchored to the upper-right corner,
seeded from the daily file's existing on-disk content so prior same-day sessions are included (the
in-memory `document` mirror still starts empty each launch). It's a write-time HUD, not a read mode.

- `src/app/render.rs` — added a pure `word_count(s: &str)` helper (`split_whitespace().count()`)
  shared by the HUD and the Esc menu so the two always agree and match the on-disk seed. New unit
  test `word_count_counts_whitespace_tokens` (the plan's expected value for the multi-whitespace
  case was off-by-one — that string has 4 tokens, not 3; the test asserts the correct 4).
- `src/app.rs``open_session_file` now returns `Option<(File, usize)>`, computing the seed via
  `word_count(&fs::read_to_string(&dir).unwrap_or_default())` right after the lock succeeds (a read
  error degrades to 0). Added a `seed_words: usize` field, threaded through `WriteApp::new`. After
  the main galley paints, the HUD draws `seed_words + word_count(&document)` as
  `format!("{words} words")` via `Painter::text` with `Align2::RIGHT_TOP` at `0.5 * FONT_SIZE` in
  `Color32::from_gray(110)`, only when `words >= 2`. The Esc-menu count uses the same expression.
- `src/main.rs` — destructures the new `(file, seed_words)` tuple and passes `seed_words` to
  `WriteApp::new`.

The count is monotonic (clears never shrink `document`); no repaint changes needed (it only moves on
input). GUI placement is manual acceptance. `cargo fmt --check`, `cargo clippy`, and
`cargo nextest run` (10 tests) all clean.

## Task 2 — Bounded backspace via file truncation

Softened the "no deletion ever" policy into a strictly bounded real deletion: `Backspace` now
truncates the day file by one whole char, but only within the last two whitespace-delimited words
and never below a monotonic floor that ratchets forward as the writer types. Once text is 2+ words
in the past it is permanently committed. The old append-a-space/clear/shake machinery is removed
from the handler (shake constants/field retained for sub-task 2.1).

- `src/app/render.rs` — added `start_byte_of_second_to_last_word(s)`, a single-pass
  `char_indices` scan returning the byte offset of `words[len-2]` (or `0` for <2 words), matching
  `split_whitespace` semantics. `Glyph` gained an `end_offset: usize` (byte end-of-char within
  `document` at append time) so the visible buffer truncates correctly. New unit tests:
  `start_byte_of_second_to_last_word_cases` (ASCII, leading/trailing whitespace, `\n\n`, multi-byte
  `"café déjà vu"` → byte 6) and `delete_floor_ratchet_is_monotonic` (folds the ratchet over a
  growing/shrinking doc sequence, asserting it never decreases and stays `>= boot_size`).
- `src/app.rs``open_session_file` now also returns `boot_size = file.metadata()?.len()` (return
  type aliased as `SessionFile = (File, usize, u64)` to keep clippy's `type_complexity` quiet).
  `WriteApp` gained `boot_size`/`delete_floor` (both init to `boot_size`) and dropped
  `last_was_backspace`. `append` computes per-glyph `end_offset` and ratchets
  `delete_floor = delete_floor.max(boot_size + start_byte_of_second_to_last_word(&document))`. New
  `fn backspace(&mut self)`: no-op when at/under the floor, else `document.pop()`, flush (mandatory
  before truncate — append-mode `BufWriter`), `set_len(boot_size + document.len())`, prune tail
  glyphs whose `end_offset > new doc len`, reset `enter_run`. The Backspace arm now just calls
  `self.backspace()`; the dead `last_was_backspace = false` assignments were removed from the Text
  and Enter arms.
- `src/main.rs` — destructures the `(file, seed_words, boot_size)` triple and threads `boot_size`
  into `WriteApp::new`.
- `README.md` — softened the intro/closing prose and rewrote the `Backspace` key-table row to
  describe the bounded delete.

`shake_start` field and `shake_offset` render call left in place (dx stays 0) for sub-task 2.1.
`cargo fmt`, `cargo clippy --all-targets`, and `cargo nextest run` (12 tests) all clean.

## Sub-task 2.1 — Wiggle when the backspace limit is hit

Reconnected the existing shake machinery (freed up by Task 2's bounded backspace) to give the writer
a visual "no" cue when a backspace is blocked, replacing the previous silent no-op.

- `src/app.rs``backspace` regained its `now: f64` param. Both blocked early-returns (document
  empty / at the `delete_floor`, and the would-cross-the-floor guard) now set `self.shake_start =
  now` before returning, driving the pre-existing `shake_offset(now - shake_start)` galley dx into a
  brief decaying horizontal wiggle. A *successful* deletion sets nothing, so it stays shake-free. The
  Backspace event arm now calls `self.backspace(now)`, and the stale "sub-task 2.1 will add the
  wiggle" comments were refreshed to describe the implemented behavior.

No new animation code and no new tests: the wiggle is a single field write feeding the already
unit-tested `shake_offset` math (`shake_offset_at_rest_outside_window`,
`shake_offset_bounded_and_decays`), and the render loop already requests continuous repaints while
text is visible so the 0.4s shake animates. `cargo clippy`, `cargo fmt`, and `cargo nextest run`
(12 tests) all clean.

## Tint the still-deletable tail ("fresh" text) toward red

Gave the writer a visual signal of how far back a backspace can reach: the deletable tail (document
bytes `>= delete_floor - boot_size`) now renders with a warmer/redder tint while locked, permanent
text keeps the normal gray body color. The boundary follows the monotonic `delete_floor` (not a
freshly recomputed second-to-last word), so the colored region matches exactly what a subsequent
backspace can remove.

- `src/app/render.rs` — added `FRESH_GB_SCALE` (0.7) and a `redden(Color32)` helper that keeps the
  red channel and attenuates green/blue. `runs` gained a `floor_byte` param and now splits on
  `(alpha, fresh)`, returning a `fresh` flag per run (`fresh = end_offset - ch.len_utf8() >=
  floor_byte`, saturating to avoid underflow). `build_job` gained a trailing `floor_byte` param and
  picks `redden(base)` vs `base` per run *before* `gamma_multiply`, so the tint composes with the
  age-fade (premultiplied-alpha path drains to black) and darker mode.
- `src/app.rs``ui` computes `floor_byte = (delete_floor - boot_size) as usize` (subtraction can't
  underflow: `delete_floor` is initialized to `boot_size` and only ratcheted up) and threads it into
  `build_job`.

Updated `runs_coalesce_same_age` / `runs_split_on_alpha_change` for the new tuple/signature and
added `runs_split_on_fresh_boundary` asserting the split at the floor boundary. `cargo fmt`,
`cargo clippy --all-targets`, and `cargo nextest run` (13 tests) all clean.

## Cursor — solid non-blinking rectangle

Added a passive insertion-point marker: a solid, filled, non-blinking rectangle that always sits at
the tail of the live text, exactly where `append` drops the next glyph.

**This is not a violation of the project's "no cursor movement" rule.** The marker never moves under
user command — there are no arrows, no click-to-position, and no selection. It is purely passive,
riding the end of the current text. The distinction matters: "no cursor movement" forbids the writer
from repositioning an insertion point, not the existence of a marker that follows the tail.

- `src/app.rs` — pure paint inside `WriteApp::ui`, in the gap around the existing
  `painter.galley(...)` call. The tail rect is computed *before* that call (because `painter.galley`
  takes `Arc<Galley>` by value and moves it): `galley.pos_from_cursor(galley.end())` gives a
  zero-width galley-local rect, offset by the galley paint origin `pos2(x, y)`. Width is
  `0.5 * FONT_SIZE`; height is `tail.height().max(FONT_SIZE)` (floored so it's never degenerate). The
  rect is only computed when `self.visible` is non-empty, so nothing draws on a blank/cleared/
  fully-faded screen. After the galley move, the rect is painted with `painter.rect_filled` using
  `body.gamma_multiply(factor)`, where `factor = glyph_factor(g.birth, now, self.visibility_head)`
  for the newest visible glyph (`self.visible.back()`) — so the cursor fades in lockstep with the
  text it sits against and inherits `darker` mode via `body`.

Because `x` already folds in the shake `dx`, the marker rides the backspace-limit wiggle for free.
No new struct fields, no changes to `render.rs`/`main.rs`/the event loop, and no repaint-loop change
(the render loop already requests continuous repaints while `visible` is non-empty). Consistent with
the GUI-only precedent, no new unit tests were added (this is pure paint with no testable logic);
positioning was confirmed by manual acceptance. `cargo fmt --check`, `cargo clippy --all-targets`,
and `cargo nextest run` (13 tests) all clean.

## `--edit` flag — open `$EDITOR` on today's file

Added the program's first CLI flag, `--edit`, an escape hatch to revisit and freely edit
today's session file outside the fading TUI.

- `src/app.rs` — extracted the inline date/dir path logic out of `open_session_file()` into a
  reusable `pub fn today_file_path()` (resolves `~/Documents/write/YYYY-MM-DD.md`, `create_dir_all`
  on `write/`, no open/lock); `open_session_file()` now calls it. Added `pub fn run_edit()`: touches
  the file so the editor opens an existing path, resolves the editor via `$VISUAL``$EDITOR``vi`,
  whitespace-splits the command (so `"code -w"` works), spawns with inherited stdio, waits, and
  propagates a non-zero editor exit code.
- `src/main.rs` — before acquiring the session file/lock, if `--edit` is in args, runs `run_edit()`
  and returns; never builds `NativeOptions`, never runs `run_native`, never takes the lock.

Dependency-free arg matching via `std::env::args()`. Manually verified: `EDITOR=true ... --edit`
exits 0, `EDITOR=false ... --edit` exits 1; plain `cargo run` launches the TUI unchanged.
`cargo fmt --check`, `cargo clippy --all-targets`, and `cargo nextest run` (13 tests) all clean.

## Session timestamp header — `## <datetime>` at session start

Each real writing session now stamps today's append-only file with a Markdown header
`## <datetime>` so sessions are timestamped and visually delimited within the day's file.

- `src/app.rs` — in the `Ok(())` lock arm of `open_session_file()`: bind `existing =
  fs::read_to_string(&path)` once, compute `seed = word_count(&existing)` **before** writing the
  header (so the header's tokens aren't counted as the writer's), then write the header to the raw
  append-mode fd **before** capturing `boot_size`. Capturing `boot_size` after the write folds the
  header into the hard delete floor, so the existing floor mechanism protects it from backspace for
  free, and the empty in-memory `document` mirror means it is never rendered or faded on screen.
- Idempotent blank-line separation: the newline prefix caps at exactly two trailing newlines
  (`""` for empty or already-`"\n\n"`-terminated, `"\n"` for one trailing newline, else `"\n\n"`),
  so repeated same-day launches don't grow blank-line runs.
- Format `%Y-%m-%d %-I:%M:%S%P %Z` → e.g. `## 2026-06-26 2:30:05pm PDT`. `--edit` does not call
  `open_session_file()`, so editing in `$EDITOR` injects no header.

Manual verification (GUI task), consistent with prior color/cursor work. `cargo clippy`,
`cargo fmt --check`, and `cargo nextest run` (13 tests) all clean. Known follow-up filed in PLAN.md:
prior-session headers inflate later sessions' day-total word count by ~4 tokens each until
`word_count` learns to skip ATX `## ` header lines.

## Header-aware word counting — skip `## ` ATX-header lines in `word_count`

`word_count` (`src/app/render.rs`) now drops ATX level-2 header lines (first non-space content
`## `) before tallying whitespace tokens. Session timestamp headers (`## <datetime>`, written by
`open_session_file`) previously inflated the writer's day-total by ~4 words per prior session,
because `seed = word_count(&existing)` ran over earlier sessions' header lines. The filter is
applied centrally in `word_count`, so the HUD, the Esc menu, and the on-disk seed stay consistent
and exclude both prior- and current-session headers. Level-3+ headers (`### …`) are unaffected.
Added `word_count_skips_atx_headers` unit test; `cargo fmt`, `cargo clippy`, and `cargo nextest run`
(14 tests) all clean.

## Invert the color dependency — primary green, older text a darker green

The text-color model now runs **primary → older** instead of primary → fresh. The light green is
the primary color: freshly-typed (still-backspaceable) text renders at `PRIMARY = rgb(154, 220,
154)` directly, and older/locked text is *derived* as a uniformly darkened shade via
`darken(primary)` (`OLDER_SCALE = 0.78` → `rgb(120, 171, 120)`), preserving the green hue. No pure
white anywhere; aging text now transitions green → darker-green, then still fades to black over
`FADE_SECS` on the premultiplied `gamma_multiply` path.

- `src/app/render.rs` — replaced the stale `redden`/`FRESH_SCALE` machinery (which mis-named green
  as "red") with `PRIMARY`, `PRIMARY_DARKER = rgb(21, 30, 21)`, `OLDER_SCALE`, and a `darken` fn
  that scales all three channels. In `build_job`, renamed the `base` param to `primary` and inverted
  the per-run choice to `if fresh { primary } else { darken(primary) }`.
- `src/app.rs` — replaced the near-white `body` computation with
  `let primary = if self.darker { PRIMARY_DARKER } else { PRIMARY };`, renamed `body``primary`
  at the `build_job` arg, the `painter.galley` fallback, and the cursor color; imported `PRIMARY`
  and `PRIMARY_DARKER` (`darken` stays internal to `render.rs`).

Darker mode now reads as a dim green (primary `rgb(21, 30, 21)`, older `≈ rgb(16, 23, 16)`) rather
than gray. Manual verification (GUI task); no test asserts RGB. `cargo fmt --check`, `cargo clippy`,
and `cargo nextest run` (14 tests) all clean.

## clap-based CLI — `edit` and `show` subcommands (replacing `--edit`)

Replaced the hand-rolled `std::env::args().skip(1).any(|a| a == "--edit")` check with a real
[`clap`](https://docs.rs/clap) derive CLI, and restructured the non-default modes as subcommands.
`write` (no args) launches the fading TUI exactly as before; subcommands run without taking the
advisory lock or flashing a window.

- `Cargo.toml` — added `clap = { version = "4", features = ["derive"] }` via
  `cargo add clap --features derive` (clap 4.6.1).
- `src/app.rs` — added `pub fn run_show()` next to `run_edit()`: resolves `today_file_path()` and
  `fs::read``stdout().write_all` (byte-verbatim, not `read_to_string`/`println!`, so headers and
  trailing-newline state are preserved). A missing file (`ErrorKind::NotFound`) is `Ok(())` with no
  output. Creates nothing and takes no lock.
- `src/main.rs` — added a clap `Cli`/`Command` derive (`#[command(version, about)]`) with an
  optional subcommand. `main()` parses and dispatches `Edit`/`Show` before any lock/window work
  (each returns `Ok(())` or `std::process::exit` to satisfy the `eframe::Result` return type);
  `None` falls through to the unchanged `open_session_file()``run_native` path. The `--edit`
  flag is **removed**`write --edit` now errors via clap (exit 2).
- `README.md` — documented `write edit` / `write show` and that a bare `write` launches the TUI.
- `CHANGELOG.md` — added an `[Unreleased]` section (clap CLI, `edit`/`show` subcommands, `--edit`
  removed).

Verified: `--help` lists `edit`/`show`, `--edit` errors with exit 2, `write show` is byte-identical
to `cat ~/Documents/write/$(date +%F).md` and prints nothing/exit 0 with no file. `cargo fmt`,
`cargo clippy --all-targets`, `cargo build`, and `cargo nextest run` (14 tests) all clean.