agent: engine slop pass — DSP, typed errors, regex parsing, tracing, audit fixes

External code review on 2026-05-12 rated the codebase 4/10 across audio DSP, error typing, JS injection, env-var safety, ALSA parsing, and async logging. This commit lands the prognosis-level fixes plus three audit follow-ups.

Audio/DSP:
- StreamingResampler/rubato confirmed in the live capture path
- regression test at 12 kHz (rms < 0.01, ~40 dB) catches naive decimation
- near-Nyquist test at 9 kHz (rms < 0.05, ~26 dB) exercises transition band

Core errors:
- Other(String) removed; ProviderNotRegistered introduced
- Io variant restructured as struct with kind/message/raw_os_error
- FileNotFound display quotes paths
- Configuration variant removed (unused)

Core types:
- ModelId, EngineName backed by Cow<'static, str>; const borrowed ctor
- Megabytes::from_gb takes u64 (was f64)
- AudioSamples::sample_rate is NonZeroU32; zero-rate defensive branch removed

Capture:
- /proc/asound/cards parsing rewritten as anchored regex (OnceLock)
- regression test covers product names with embedded colons
- monitor_pattern_detection test restored alongside the regex test
- DEAD_SILENCE_FLOOR promoted to module-level with rationale
- DEVICE_VALIDATION_MS, SILENCE_RMS_FLOOR documented with field-observation rationale
- RMS validation loop made idiomatic
- eprintln! migrated to tracing with structured fields and targets

Tauri startup:
- unsafe std::env::set_var removed; ensure_x11_on_wayland renamed to warn_if_x11_env_unset_on_wayland (launcher/wrapper owns env-var contract)
- DB init + log prune + preferences load collapsed to one block_on
- build_preferences_script rewrites JS injection from JSON.parse string to direct object literal plus malformed-JSON guard and unit tests
- WebKitGTK microphone auto-grant logs warning at startup
- tracing subscriber initialised at top of run() (warn,magnotia=info,... on stderr; honors RUST_LOG); previously eprintln→tracing migration was silent because no subscriber existed

Filename counter:
- RECORDING_COUNTER uses SeqCst

Tests: cargo test --workspace --lib green (322 passed, 0 failed across 10 crates).

Three independent audits (original cleanup → Wren → fresh Codex subagent) concur on no critical findings.

Deferred to docs/superpowers/plans/2026-05-12-engine-slop-residuals.md: storage-layer typed errors, remaining eprintln→tracing sweep, capture actor-model refactor, property-based DSP testing, frontend/backend error boundary cleanup.
This commit is contained in:
2026-05-12 22:03:58 +01:00
parent b463c32f17
commit db654deecc
14 changed files with 557 additions and 208 deletions

View File

@@ -31,30 +31,31 @@ pub enum MagnotiaError {
#[error("model download failed: {0}")]
DownloadFailed(String),
#[error("file not found: {}", .0.display())]
#[error("file not found: '{}'", .0.display())]
FileNotFound(PathBuf),
#[error("storage error: {0}")]
StorageError(String),
#[error("io error: {0}")]
Io(
#[from]
#[serde(serialize_with = "serialize_io_error")]
std::io::Error,
),
#[error("provider not registered: {0}")]
ProviderNotRegistered(String),
#[error("{0}")]
Other(String),
#[error("io error ({kind}): {message}")]
Io {
kind: String,
message: String,
raw_os_error: Option<i32>,
},
}
/// Serialises `std::io::Error` as its display string, since it does
/// not implement `Serialize` natively.
fn serialize_io_error<S: serde::Serializer>(
err: &std::io::Error,
s: S,
) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(&err.to_string())
impl From<std::io::Error> for MagnotiaError {
fn from(err: std::io::Error) -> Self {
Self::Io {
kind: format!("{:?}", err.kind()),
message: err.to_string(),
raw_os_error: err.raw_os_error(),
}
}
}
pub type Result<T> = std::result::Result<T, MagnotiaError>;

View File

@@ -49,7 +49,7 @@ pub fn score_model(model: &'static ModelEntry, profile: &SystemProfile) -> Optio
}
let headroom = Megabytes(profile.ram.0.saturating_sub(model.ram_required.0));
if headroom > Megabytes::from_gb(4.0) {
if headroom > Megabytes::from_gb(4) {
score += 10.0;
}

View File

@@ -1,11 +1,18 @@
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::num::NonZeroU32;
use serde::{Deserialize, Deserializer, Serialize};
/// Prevents passing raw strings where model IDs are expected.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ModelId(String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct ModelId(Cow<'static, str>);
impl ModelId {
pub fn new(id: impl Into<String>) -> Self {
pub const fn borrowed(id: &'static str) -> Self {
Self(Cow::Borrowed(id))
}
pub fn new(id: impl Into<Cow<'static, str>>) -> Self {
Self(id.into())
}
@@ -14,6 +21,15 @@ impl ModelId {
}
}
impl<'de> Deserialize<'de> for ModelId {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(|s| Self(Cow::Owned(s)))
}
}
impl std::fmt::Display for ModelId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
@@ -21,11 +37,15 @@ impl std::fmt::Display for ModelId {
}
/// Prevents passing raw strings where engine names are expected.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineName(String);
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct EngineName(Cow<'static, str>);
impl EngineName {
pub fn new(name: impl Into<String>) -> Self {
pub const fn borrowed(name: &'static str) -> Self {
Self(Cow::Borrowed(name))
}
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self(name.into())
}
@@ -34,6 +54,15 @@ impl EngineName {
}
}
impl<'de> Deserialize<'de> for EngineName {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).map(|s| Self(Cow::Owned(s)))
}
}
impl std::fmt::Display for EngineName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
@@ -45,8 +74,12 @@ impl std::fmt::Display for EngineName {
pub struct Megabytes(pub u64);
impl Megabytes {
pub fn from_gb(gb: f64) -> Self {
Self((gb * 1024.0) as u64)
pub const fn from_gb(gb: u64) -> Self {
Self(gb.saturating_mul(1024))
}
pub const fn from_mb(mb: u64) -> Self {
Self(mb)
}
pub fn as_gb(&self) -> f64 {
@@ -68,23 +101,36 @@ impl std::fmt::Display for Megabytes {
#[derive(Debug, Clone)]
pub struct AudioSamples {
samples: Vec<f32>,
sample_rate: u32,
sample_rate: NonZeroU32,
channels: u16,
}
impl AudioSamples {
pub fn new(samples: Vec<f32>, sample_rate: u32, channels: u16) -> Self {
Self {
Self::try_new(samples, sample_rate, channels)
.expect("AudioSamples sample_rate must be non-zero")
}
pub fn try_new(
samples: Vec<f32>,
sample_rate: u32,
channels: u16,
) -> std::result::Result<Self, &'static str> {
let Some(sample_rate) = NonZeroU32::new(sample_rate) else {
return Err("sample_rate must be non-zero");
};
Ok(Self {
samples,
sample_rate,
channels,
}
})
}
pub fn mono_16khz(samples: Vec<f32>) -> Self {
Self {
samples,
sample_rate: crate::constants::WHISPER_SAMPLE_RATE,
sample_rate: NonZeroU32::new(crate::constants::WHISPER_SAMPLE_RATE)
.expect("WHISPER_SAMPLE_RATE must be non-zero"),
channels: crate::constants::WHISPER_CHANNELS,
}
}
@@ -98,7 +144,7 @@ impl AudioSamples {
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
self.sample_rate.get()
}
pub fn channels(&self) -> u16 {
@@ -106,10 +152,7 @@ impl AudioSamples {
}
pub fn duration_secs(&self) -> f64 {
if self.sample_rate == 0 {
return 0.0;
}
self.samples.len() as f64 / self.sample_rate as f64
self.samples.len() as f64 / self.sample_rate.get() as f64
}
}