From 1e733a062bde278f7eddd509ec01b66e26a5ecca Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 18 May 2026 16:57:05 -0700 Subject: [PATCH] =?UTF-8?q?feat(m1):=20pattern=20system=20=E2=80=94=20Patt?= =?UTF-8?q?ernFile=20loader=20and=20classify()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add EventSource enum and update SystemEvent in watcher.rs (M0 stub updated to support Task 5 clean deletion). Create patterns.rs with PatternFile/Pattern/MatchedEvent types, TOML loader, and classify() matching against source + text. Five unit tests covering journald, applog, no-match, and source discrimination cases. --- src-tauri/src/lib.rs | 1 + src-tauri/src/patterns.rs | 168 ++++++++++++++++++++++++++++++++++++++ src-tauri/src/watcher.rs | 15 ++-- 3 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 src-tauri/src/patterns.rs diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 618356e..3fdaa91 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ mod commands; mod config; mod distro; +mod patterns; mod tray; mod watcher; diff --git a/src-tauri/src/patterns.rs b/src-tauri/src/patterns.rs new file mode 100644 index 0000000..31032d1 --- /dev/null +++ b/src-tauri/src/patterns.rs @@ -0,0 +1,168 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Deserialize)] +pub struct PatternMeta { + pub source_os: String, + pub target_distro_family: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Pattern { + pub id: String, + pub sources: Vec, + pub match_text: String, + pub severity: String, + pub title: String, + pub body: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PatternFile { + pub meta: PatternMeta, + #[serde(default)] + pub log_paths: HashMap, + #[serde(default)] + pub patterns: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct MatchedEvent { + pub pattern_id: String, + pub title: String, + pub body: String, + pub severity: String, + pub timestamp: u64, +} + +pub fn load(source_os: &str, distro_family: &str) -> Result { + let resource_path = format!("patterns/{}-to-{}.toml", source_os, distro_family); + let content = std::fs::read_to_string(&resource_path) + .with_context(|| format!("pattern file not found: {resource_path}"))?; + toml::from_str(&content).with_context(|| format!("failed to parse {resource_path}")) +} + +pub fn classify(event: &crate::watcher::SystemEvent, pf: &PatternFile) -> Option { + use crate::watcher::EventSource; + + for pattern in &pf.patterns { + if !event.raw_line.contains(&pattern.match_text) { + continue; + } + + let source_matches = pattern.sources.iter().any(|s| { + if s == "any" { + return true; + } + match &event.source { + EventSource::Journald => s == "journald", + EventSource::Kmsg => s == "kmsg", + EventSource::AppLog(app) => s == &format!("applog:{app}"), + } + }); + + if source_matches { + return Some(MatchedEvent { + pattern_id: pattern.id.clone(), + title: pattern.title.clone(), + body: pattern.body.clone(), + severity: pattern.severity.clone(), + timestamp: event.timestamp, + }); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + const FIXTURE: &str = r#" +[meta] +source_os = "macos" +target_distro_family = "arch" + +[log_paths] +steam = "~/.local/share/Steam/logs/content_log.txt" + +[[patterns]] +id = "aur-build-failure" +sources = ["journald"] +match_text = "error: failed to build" +severity = "warn" +title = "AUR package build failed" +body = "A package failed to compile from source." + +[[patterns]] +id = "proton-runtime" +sources = ["applog:steam"] +match_text = "wine: cannot find" +severity = "warn" +title = "Proton runtime issue" +body = "Steam Proton couldn't find a required file." +"#; + + #[test] + fn loads_pattern_file_from_toml() { + let pf: PatternFile = toml::from_str(FIXTURE).unwrap(); + assert_eq!(pf.meta.source_os, "macos"); + assert_eq!(pf.patterns.len(), 2); + assert_eq!( + pf.log_paths.get("steam").unwrap(), + "~/.local/share/Steam/logs/content_log.txt" + ); + } + + #[test] + fn classify_matches_journald_event() { + use crate::watcher::{EventSource, SystemEvent}; + let pf: PatternFile = toml::from_str(FIXTURE).unwrap(); + let event = SystemEvent { + source: EventSource::Journald, + raw_line: ":: error: failed to build (foo-git)".into(), + timestamp: 0, + }; + let matched = classify(&event, &pf).unwrap(); + assert_eq!(matched.pattern_id, "aur-build-failure"); + assert_eq!(matched.title, "AUR package build failed"); + } + + #[test] + fn classify_no_match_returns_none() { + use crate::watcher::{EventSource, SystemEvent}; + let pf: PatternFile = toml::from_str(FIXTURE).unwrap(); + let event = SystemEvent { + source: EventSource::Journald, + raw_line: "systemd: Started NetworkManager.service".into(), + timestamp: 0, + }; + assert!(classify(&event, &pf).is_none()); + } + + #[test] + fn classify_applog_matches_correct_source() { + use crate::watcher::{EventSource, SystemEvent}; + let pf: PatternFile = toml::from_str(FIXTURE).unwrap(); + let event = SystemEvent { + source: EventSource::AppLog("steam".into()), + raw_line: "wine: cannot find /run/media/user/game.exe".into(), + timestamp: 0, + }; + let matched = classify(&event, &pf).unwrap(); + assert_eq!(matched.pattern_id, "proton-runtime"); + } + + #[test] + fn classify_does_not_match_wrong_source() { + use crate::watcher::{EventSource, SystemEvent}; + let pf: PatternFile = toml::from_str(FIXTURE).unwrap(); + let event = SystemEvent { + source: EventSource::Journald, + raw_line: "wine: cannot find something".into(), + timestamp: 0, + }; + assert!(classify(&event, &pf).is_none()); + } +} diff --git a/src-tauri/src/watcher.rs b/src-tauri/src/watcher.rs index b0d1957..88d72e0 100644 --- a/src-tauri/src/watcher.rs +++ b/src-tauri/src/watcher.rs @@ -2,7 +2,6 @@ /// /// M0 stub: defines the types and the spawn interface so the rest of the app /// can wire up event handling now. Actual journald/dmesg reading lands in M1. - use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -13,11 +12,17 @@ pub enum EventSeverity { Crit, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] +pub enum EventSource { + Journald, + Kmsg, + AppLog(String), +} + +#[derive(Debug, Clone)] pub struct SystemEvent { - pub severity: EventSeverity, - pub source: String, - pub message: String, + pub source: EventSource, + pub raw_line: String, pub timestamp: u64, }