Files
Lumotia/docs/superpowers/plans/2026-05-12-engine-slop-residuals.md
Jake 792fb5ea08 agent: dev launcher — own Linux env-var contract, add dev:tauri, doc sweep
The 2026-05-12 engine-slop pass removed runtime std::env::set_var mutation from src-tauri/src/lib.rs and replaced it with warn_if_x11_env_unset_on_wayland. With no launcher owning the contract, Linux Wayland dogfood was about to regress.

run.sh:
- now owns Linux launcher env defaults via case "$(uname -s)"
  LIBCLANG_PATH=/usr/lib64/llvm21/lib64 (user-set wins)
  WEBKIT_DISABLE_DMABUF_RENDERER=1 (always on Linux; user-set wins)
  GDK_BACKEND=x11, WINIT_UNIX_BACKEND=x11 (Wayland only; user-set wins)
- 60s Vite readiness timeout
- detects early Vite exit (kill -0 + wait) instead of hanging forever
- trap installed before wait so Ctrl-C cleans up
- args forwarded: ./run.sh --release etc.
- non-exec final Tauri launch preserved so cleanup trap fires

package.json:
- "dev:tauri": "./run.sh" — canonical discoverable dev command

Docs:
- README, dev-setup, architecture-map runtime + launcher pages updated with the new contract; canonical command is npm run dev:tauri (./run.sh as direct equivalent)
- dev-launcher-and-scripts.md replaces the incorrect "kills process group" claim with honest "kills the spawned npm process"
- dev-setup.md path /CORBEL-Projects/magnotia → /CORBEL-Projects/transcription-app
- gpu-tuning/plan.md gets a superseded note rather than rewriting the original rationale
- engine-slop-residuals.md gains Area F (packaged-binary launcher contract) with honest wrapper-vs-.desktop trade-off documented

Verification: bash -n run.sh; shellcheck clean; cargo check -p magnotia green; npm pkg get 'scripts.dev:tauri' returns "./run.sh"; stale-ref sweep clean across living docs.
2026-05-12 22:05:33 +01:00

226 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<Mutex<Vec<f32>>>` 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<Mutex<T>>` 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<T, String>` instead of `Result<T, MagnotiaError>`.
**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<T, String>`, convert to `Result<T, MagnotiaError>` (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<T, String>` 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 AE 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 AB 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.