--- name: Engine slop residuals (post-prognosis pass) type: plan tags: [engine, slop-cleanup, plan, deferred] description: Roadmap for the deferred code-quality items that did not fit in the 2026-05-12 prognosis-fix pass. Each area is scoped for its own focused plan when ready to execute. --- # Engine slop residuals — post-prognosis pass > **For agentic workers:** This is a meta-plan. It catalogues independent residual work areas, each of which should get its own detailed TDD plan when scheduled. Do **not** treat the sections below as a single execution checklist. ## Context On 2026-05-12 an external code review rated the codebase 4/10 and laid out a four-bucket de-sloppifying plan. That pass shipped (uncommitted in working tree at time of writing). It hit the *prognosis-level* items only: - DSP: naive decimation removed, `StreamingResampler`/rubato in place, regression tests at 12 kHz (40 dB) + 9 kHz (26 dB). - Core errors: `Other(String)` removed; `Io` is structured; `FileNotFound` quotes paths; `ProviderNotRegistered` introduced. - Core types: `Cow<'static, str>` for `ModelId`/`EngineName`, `NonZeroU32` for `AudioSamples::sample_rate`, `Megabytes::from_gb` takes `u64`. - Capture cleanup: regex-based `/proc/asound/cards` parser (test covers product names with embedded colons), constants documented, RMS sum idiomatic, Codex-review comments removed. - Tauri startup: `unsafe std::env::set_var` removed (logs warning instead); function renamed `warn_if_x11_env_unset_on_wayland`; setup collapsed to single `block_on`; JS injection rewritten (`var p = {...}` direct, no `JSON.parse`); WebKitGTK mic auto-grant logs warning at startup. - Counter: `RECORDING_COUNTER` uses `SeqCst`. - Tracing: subscriber initialised at top of `run()` with sensible default filter, honors `RUST_LOG`, writes to stderr. The code-proposal and macro-architecture captures from the same review session went further (10/10 demands and workspace-level critique). Those items are the residuals below. ## Residual areas Each area is independent. They can be tackled in any order, but the suggested order minimises rework. ### A. Storage-layer typed errors **Status:** identified, not started. **Scope:** ~25 `StorageError(String)` sites in `crates/storage/src/{database.rs, migrations.rs}`. **Why it matters:** The frontend cannot distinguish "schema migration failed" from "FTS search failed" from "transcript not found" — all surface as one stringly-typed variant. This is the same class of bug that motivated removing `Other(String)`. **Approach:** Split `StorageError` into a `StorageError` enum (sub-error in its own right) with variants like `Connect { path, source }`, `MigrationFailed { version, source }`, `TransactionFailed { phase, source }`, `QueryFailed { kind, source }`, `NotFound { table, id }`. Wrap `sqlx::Error` (or whatever `rusqlite::Error` is in use) as `#[source]`. **Decisions needed:** - Is `sqlx::Error`/`rusqlite::Error` serializable across the Tauri boundary? If not, what is preserved (kind + display)? - Do we keep one flat error type for the crate or nest by domain (`MigrationError`, `TranscriptError`, `TaskError`)? - Backwards-compat for any existing error-handling code in frontend. **Files in scope:** - `crates/core/src/error.rs` — add `StorageError` variant if changing top-level shape - `crates/storage/src/error.rs` — new file (likely) - `crates/storage/src/database.rs` — 19 call sites - `crates/storage/src/migrations.rs` — 6 call sites **Acceptance:** All `StorageError(format!(...))` calls replaced with typed variants. Frontend can `match` on storage-error kind. `cargo test --workspace` green. ### B. eprintln → tracing sweep (rest of repo) **Status:** identified, not started. **Scope:** ~30 `eprintln!` sites in production code paths (excluding tests and tooling binaries — see below). **Why it matters:** Now that the subscriber is wired, every `eprintln!` is a missed observability signal. The startup/capture paths are clean; the runtime hot paths are not. **Approach:** Mechanical migration, one file at a time, with appropriate target and structured fields. **Files in scope (production):** - `src-tauri/src/commands/live.rs` — 8 sites (highest priority — session lifecycle + inference errors) - `src-tauri/src/commands/models.rs` — 5 sites (Whisper warmup) - `src-tauri/src/commands/power.rs` — 4 sites (macOS App Nap) - `src-tauri/src/commands/transcription.rs` — 1 site - `src-tauri/src/commands/diagnostics.rs` — 1 site - `src-tauri/src/commands/tasks.rs` — 1 site - `crates/hotkey/src/linux.rs` — 2 sites **Files explicitly out of scope:** - `crates/mcp/src/main.rs` — separate binary, its own stdio protocol; keep eprintln as-is unless that binary gets its own subscriber. - `crates/*/tests/*.rs` — test diagnostic output, `--nocapture` semantics depend on stdio. **Decisions needed:** - New tracing target names for `commands::live`, `commands::models`, etc. — extend the default filter in `src-tauri/src/lib.rs::init_tracing`. **Acceptance:** No `eprintln!` in `src-tauri/src/commands/` or `crates/hotkey/src/linux.rs`. Default filter shows live-session events at INFO. `cargo test --workspace` green. ### C. Actor-model refactor for capture + inference **Status:** identified, not started. **Largest scope.** **Scope:** Replace `Arc>>` shared-state pattern in `CaptureWorker` (and analogous patterns in live inference) with isolated actor loops + `mpsc` channels. **Why it matters:** Reviewer's core architectural critique. Shared mutable state across async boundaries with `std::sync::Mutex` risks executor stalls under contention. Architecturally also makes it impossible to add features like multi-source capture, parallel inference, or recording-while-streaming without lock contention. **Approach:** Spawn dedicated tokio tasks for the capture worker and the inference worker. Communicate exclusively via bounded `mpsc` channels. Tauri command handlers hold only channel senders. **Decisions needed (before plan):** - Bounded vs unbounded mpsc? Recommend bounded for backpressure visibility. - Channel capacity? (current ad-hoc cap is `AUDIO_CHANNEL_CAPACITY = 32`) - Backpressure policy when capture outpaces inference: drop oldest, drop newest, await consumer? - Worker shutdown protocol: drop-channel-and-await, explicit stop message, or cancellation token? - How are runtime errors (already typed via `CaptureRuntimeError`) propagated through the actor boundary? - Migration plan: parallel implementation behind a feature flag, or in-place replacement? **Risk:** This is a substantial refactor. Likely worth a brainstorming session before plan-writing. **Files in scope:** - `crates/audio/src/capture.rs` — worker lifecycle rewrite - `src-tauri/src/commands/audio.rs` — command handler refactor - `src-tauri/src/commands/live.rs` — inference worker integration - Likely new files: `crates/audio/src/capture_actor.rs`, `crates/transcription/src/inference_actor.rs` **Acceptance:** No `Arc>` across `.await` points in capture or inference code paths. Capture and inference are independent tokio tasks. End-to-end recording flow works in dev mode. `cargo test --workspace` green. Manual dogfood pass. ### D. Property-based DSP testing **Status:** identified, not started. **Scope:** Add `proptest` (or similar) generators for audio inputs and assert invariants of the resampling and VAD pipelines. **Why it matters:** The current DSP tests are point checks at 12 kHz and 9 kHz. The reviewer's 10/10 demand is property-based testing that covers the input space (any frequency, any chunk size, any noise floor) and asserts invariants (output is bounded, energy is bounded, duration is approximately preserved). **Approach:** Add `proptest` dev-dependency. Write generators for `(sample_rate, signal_frequency, amplitude, chunk_size, total_secs)` tuples. Assert: - Output samples are finite (no NaN/Inf). - Output peak ≤ input peak + small tolerance (no amplification). - Output duration within ±5% of expected. - Output energy at frequencies above Nyquist of target rate is at least 30 dB below input energy at those frequencies. **Decisions needed:** - `proptest` vs `quickcheck`? Recommend `proptest` (more flexible shrinking). - Run as part of standard `cargo test` or as a separate `cargo test --features slow-tests`? **Files in scope:** - `crates/audio/Cargo.toml` — add `proptest` to `[dev-dependencies]` - `crates/audio/src/streaming_resample.rs` — extend `#[cfg(test)]` module - Likely also `crates/transcription/src/streaming/rms_vad.rs` — VAD has equally testable invariants **Acceptance:** `cargo test -p magnotia-audio` includes property tests. Property tests have run with at least 1000 cases per invariant. ### E. Frontend/backend error boundary cleanup **Status:** partially complete (orchestrator's `ProviderNotRegistered`). **Scope:** Audit every Tauri command's return type and ensure errors propagate with full type information across the IPC boundary. Currently many commands return `Result` instead of `Result`. **Why it matters:** Reviewer's frontend-backend boundary critique. The frontend can't `switch` on error kind if the backend serialises errors as strings. **Approach:** Inventory all `#[tauri::command]` functions, identify those returning `Result`, convert to `Result` (or a command-specific error enum that serializes cleanly). Update frontend handlers to use the typed shape. **Decisions needed:** - One `MagnotiaError` for all commands, or per-command enums? - How do we surface the new shape to the Svelte side without manually maintaining TypeScript types? (specta? hand-rolled?) **Files in scope:** - All files in `src-tauri/src/commands/` - All call sites in `src/lib/` (Svelte) **Acceptance:** No `Result` in `src-tauri/src/commands/`. Frontend has typed error access. `cargo test --workspace` green; frontend builds. ### F. Packaged-binary launcher contract (Linux) **Status:** identified, not started. Direct follow-on from the 2026-05-12 dev-launcher fix; same contract, distribution-time scope. **Scope:** Set the rendering env-vars at app launch time for distributed Linux builds. **Env-var contract per distribution path:** - `WEBKIT_DISABLE_DMABUF_RENDERER=1` — always set on Linux (iGPU idle-cost workaround, applies on X11 and Wayland alike). - `GDK_BACKEND=x11`, `WINIT_UNIX_BACKEND=x11` — set only when `XDG_SESSION_TYPE=wayland`. Static `.desktop` `Exec=env` cannot do this conditional; either ship a wrapper or accept always-on X11 backend. **Preferred — wrapper script:** A wrapper preserves user-set values via shell defaults: ```sh #!/usr/bin/env bash export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-1}" if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then export GDK_BACKEND="${GDK_BACKEND:-x11}" export WINIT_UNIX_BACKEND="${WINIT_UNIX_BACKEND:-x11}" fi exec /usr/lib/magnotia/magnotia-bin "$@" ``` Per-format wrapper locations: - `.deb` / `.rpm`: ship the wrapper as `/usr/bin/magnotia`; binary lives elsewhere. - AppImage: `AppRun` is the wrapper. - Flatpak: wrapper under `/app/bin/`; the manifest's `command` points at it. **Fallback — static `.desktop` `Exec`:** ```desktop Exec=env WEBKIT_DISABLE_DMABUF_RENDERER=1 GDK_BACKEND=x11 WINIT_UNIX_BACKEND=x11 magnotia %U ``` User-set values do not win through this path (the `env` invocation hardcodes them in the child env). Document the terminal-launch override path in user-facing docs. **Decisions needed:** - Wrapper vs hardcoded-`Exec` per format. Default to wrapper. - For Flatpak: finish-args env defaults vs wrapper — both work; pick consistency. **Acceptance:** - Each Linux distribution path sets the contract on launch. - Where the packaging format uses a wrapper, user-set values still win. - Where the `.desktop` `Exec` hardcodes env directly, the terminal-launch override path is documented in user docs. - `lib.rs::warn_if_x11_env_unset_on_wayland` continues to surface missed contracts during dev/diagnostics. ## Suggested sequencing 1. **B (eprintln sweep)** first — mechanical, low-risk, immediate observability win. 2. **A (storage typed errors)** next — well-scoped, no architectural change, unblocks E. 3. **D (property-based DSP)** in parallel with A — independent surface, can be done by anyone any time. 4. **E (FE/BE error boundary)** after A — depends on storage error shape. 5. **C (actor model)** last — biggest surface, deserves its own brainstorm + plan + likely its own branch. 6. **F (packaged launcher contract)** when packaging resumes — independent of A–E but required before distributed Linux builds are treated as covered. ## Cross-cutting deferrals (Phase 10a-blocking) Two things are explicitly **not** in these residuals because they belong to higher-level workstreams: - **Lumotia rebrand cascade** (Wyrdnote → Lumotia in code, paths, GitHub repo, bundle ID). Hold until A–B done so rename diff doesn't hide regressions. - **Phase 10a QC dogfood** — `docs/superpowers/audits/2026-05-10-phase10a-dogfood-notes.md` is queued. Best done after B at minimum so live-session logs are visible. ## What this plan does not do - No fix recipes for bugs not yet found. Each area's plan should start by reading the current state and writing failing tests against the current behaviour. - No timeline commitments. Sequencing is suggested, not mandatory. - No architectural decisions deferred to "Phase B" earlier are pre-committed here. Each area writes its own brainstorm + plan when scheduled.