Files
Lumotia/docs/superpowers/plans/2026-05-12-engine-slop-residuals.md
2026-05-12 23:27:15 +01:00

14 KiB
Raw Blame History

name, type, tags, description
name type tags description
Engine slop residuals (post-prognosis pass) plan
engine
slop-cleanup
plan
deferred
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.

Status update — 2026-05-13

Completed:

  • B. eprintln → tracing sweep — landed in 184214b.
  • Tracing finishing pass — hotkey log:: migrated to tracing:: in 48c4838.
  • A. Storage-layer typed errors — survey in fdab777, migration in 52565ea, legacy StorageError(String) removal in a36ae7e.

Next recommended:

  • Phase 10a dogfood before Area E, to validate the new observability and storage-error semantics in real app use.

Still open:

  • D. Property-based DSP testing.
  • E. FE/BE error boundary cleanup.
  • C. Actor-model refactor.
  • F. Packaged-binary Linux launcher contract.

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:

#!/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:

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 dogfooddocs/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.