Skip to main content

wm_tools/expansion/
geneseed.rs

1//! 🧬 Geneseed Codebase Vault Miner & Ingestion Engine
2//!
3//! High-performance Rust implementation for mining optimization patterns
4//! from git repository history. Analyzes commits, diffs, and architectural longevity.
5//! Ports proven patterns into the Geneseed Vault (`Galaxy::Codex` / `Galaxy::Research`).
6
7#![forbid(unsafe_code)]
8
9use async_trait::async_trait;
10use chrono::Utc;
11use serde::{Deserialize, Serialize};
12use serde_json::{Value, json};
13use std::path::Path;
14use std::process::Command;
15use std::sync::Arc;
16use wm_core::{Context, CoreError, EffectRow, Galaxy, Gana, Resource, Tool, ToolStats};
17use wm_memory::MemoryStore;
18use wm_memory::memory::{Memory, Tier};
19use wm_memory::typology::MemoryClass;
20
21use super::common::parse_galaxy;
22
23/// A mined optimization pattern from git history.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct OptimizationPattern {
26    pub pattern_id: String,
27    pub pattern_type: String, // 'performance', 'refactor', 'bugfix', 'feature'
28    pub commit_hash: String,
29    pub commit_message: String,
30    pub author: String,
31    pub timestamp: String,
32    pub files_changed: Vec<String>,
33    pub lines_added: i32,
34    pub lines_removed: i32,
35    pub confidence: f64,
36    pub longevity_days: i32, // Days since commit (older = more proven)
37}
38
39/// High-level git repository longevity and pattern statistics.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct GeneseedStats {
42    pub total_commits: i32,
43    pub optimization_commits: i32,
44    pub refactor_commits: i32,
45    pub bugfix_commits: i32,
46    pub feature_commits: i32,
47    pub total_files_tracked: i32,
48    pub avg_commit_age_days: f64,
49}
50
51fn unix_ts_to_date(ts: i64) -> String {
52    if ts <= 0 {
53        return "unknown".to_string();
54    }
55    let days = (ts as u64) / 86400;
56    let year = 1970u64 + days * 10000 / 3652425;
57    let day_of_year = days - (year - 1970) * 3652425 / 10000;
58    let months = [31u64, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
59    let mut rem = day_of_year;
60    let mut month = 1u64;
61    for &days_in_month in &months {
62        if rem < days_in_month {
63            break;
64        }
65        rem -= days_in_month;
66        month += 1;
67    }
68    let day = rem + 1;
69    format!("{year}-{month:02}-{day:02}")
70}
71
72#[allow(clippy::too_many_arguments)]
73fn classify_commit(
74    hash: String,
75    author: String,
76    timestamp: i64,
77    message: String,
78    files: Vec<String>,
79    added: i32,
80    removed: i32,
81    now: i64,
82    min_confidence: f64,
83) -> Option<OptimizationPattern> {
84    let message_lower = message.to_lowercase();
85    let longevity_days = ((now - timestamp).max(0) / 86400) as i32;
86
87    let (pattern_type, base_confidence): (&str, f64) = if message_lower.contains("perf")
88        || message_lower.contains("optim")
89        || message_lower.contains("speed")
90        || message_lower.contains("faster")
91        || message_lower.contains("cache")
92    {
93        ("performance", 0.8)
94    } else if message_lower.contains("refactor")
95        || message_lower.contains("cleanup")
96        || message_lower.contains("simplify")
97    {
98        ("refactor", 0.6)
99    } else if message_lower.contains("fix")
100        || message_lower.contains("bug")
101        || message_lower.contains("issue")
102    {
103        ("bugfix", 0.5)
104    } else if message_lower.contains("feat")
105        || message_lower.contains("add")
106        || message_lower.contains("implement")
107    {
108        ("feature", 0.4)
109    } else {
110        return None;
111    };
112
113    let longevity_boost = (f64::from(longevity_days) / 365.0).min(0.2);
114    let total_changes = added + removed;
115    let size_factor: f64 = if total_changes < 10 {
116        0.9
117    } else if total_changes < 100 {
118        1.1
119    } else if total_changes < 500 {
120        1.0
121    } else {
122        0.8
123    };
124
125    let confidence = (base_confidence + longevity_boost) * size_factor;
126    if confidence < min_confidence {
127        return None;
128    }
129
130    Some(OptimizationPattern {
131        pattern_id: format!("{pattern_type}_{}", &hash[..8.min(hash.len())]),
132        pattern_type: pattern_type.to_string(),
133        commit_hash: hash,
134        commit_message: message,
135        author,
136        timestamp: unix_ts_to_date(timestamp),
137        files_changed: files,
138        lines_added: added,
139        lines_removed: removed,
140        confidence,
141        longevity_days,
142    })
143}
144
145/// Mine optimization patterns from git history at `repo_path`.
146pub fn mine_geneseed_patterns(
147    repo_path: &Path,
148    min_confidence: f64,
149    max_commits: usize,
150) -> wm_core::Result<Vec<OptimizationPattern>> {
151    if !repo_path.exists() {
152        return Err(CoreError::NotFound(format!(
153            "Repository directory not found: {}",
154            repo_path.display()
155        )));
156    }
157
158    let output = Command::new("git")
159        .args([
160            "log",
161            &format!("--max-count={max_commits}"),
162            "--pretty=format:%H|%an|%at|%s",
163            "--numstat",
164        ])
165        .current_dir(repo_path)
166        .output()
167        .map_err(|e| CoreError::Tool(format!("Git command failed: {e}")))?;
168
169    if !output.status.success() {
170        return Err(CoreError::Tool("Git log command failed".into()));
171    }
172
173    let log_output = String::from_utf8_lossy(&output.stdout);
174    let mut patterns = Vec::new();
175    let mut current_commit: Option<(String, String, i64, String)> = None;
176    let mut files_changed: Vec<String> = Vec::new();
177    let mut lines_added = 0i32;
178    let mut lines_removed = 0i32;
179
180    let now_timestamp = i64::try_from(
181        std::time::SystemTime::now()
182            .duration_since(std::time::UNIX_EPOCH)
183            .unwrap_or_default()
184            .as_secs(),
185    )
186    .unwrap_or(0);
187
188    for line in log_output.lines() {
189        if line.contains('|') && !line.starts_with(|c: char| c.is_ascii_digit()) {
190            if let Some((hash, author, timestamp, message)) = current_commit.take() {
191                if let Some(pattern) = classify_commit(
192                    hash,
193                    author,
194                    timestamp,
195                    message,
196                    files_changed.clone(),
197                    lines_added,
198                    lines_removed,
199                    now_timestamp,
200                    min_confidence,
201                ) {
202                    patterns.push(pattern);
203                }
204            }
205            let parts: Vec<&str> = line.splitn(4, '|').collect();
206            if parts.len() >= 4 {
207                current_commit = Some((
208                    parts[0].to_string(),
209                    parts[1].to_string(),
210                    parts[2].parse().unwrap_or(0),
211                    parts[3].to_string(),
212                ));
213                files_changed.clear();
214                lines_added = 0;
215                lines_removed = 0;
216            }
217        } else if !line.is_empty() && line.chars().next().is_some_and(|c| c.is_ascii_digit()) {
218            let parts: Vec<&str> = line.splitn(3, '\t').collect();
219            if parts.len() >= 3 {
220                if let Ok(added) = parts[0].parse::<i32>() {
221                    lines_added += added;
222                }
223                if let Ok(removed) = parts[1].parse::<i32>() {
224                    lines_removed += removed;
225                }
226                files_changed.push(parts[2].to_string());
227            }
228        }
229    }
230
231    if let Some((hash, author, timestamp, message)) = current_commit {
232        if let Some(pattern) = classify_commit(
233            hash,
234            author,
235            timestamp,
236            message,
237            files_changed,
238            lines_added,
239            lines_removed,
240            now_timestamp,
241            min_confidence,
242        ) {
243            patterns.push(pattern);
244        }
245    }
246
247    Ok(patterns)
248}
249
250/// Compute high-level pattern statistics for a git repository.
251pub fn get_geneseed_stats(repo_path: &Path) -> wm_core::Result<GeneseedStats> {
252    if !repo_path.exists() {
253        return Err(CoreError::NotFound(format!(
254            "Repository directory not found: {}",
255            repo_path.display()
256        )));
257    }
258
259    let output = Command::new("git")
260        .args(["rev-list", "--count", "HEAD"])
261        .current_dir(repo_path)
262        .output()
263        .map_err(|e| CoreError::Tool(format!("Git rev-list failed: {e}")))?;
264
265    let total_commits = String::from_utf8_lossy(&output.stdout)
266        .trim()
267        .parse::<i32>()
268        .unwrap_or(0);
269
270    let output = Command::new("git")
271        .args(["log", "--pretty=format:%s", "--max-count=1000"])
272        .current_dir(repo_path)
273        .output()
274        .map_err(|e| CoreError::Tool(format!("Git log messages failed: {e}")))?;
275
276    let messages = String::from_utf8_lossy(&output.stdout);
277    let mut optimization_commits = 0i32;
278    let mut refactor_commits = 0i32;
279    let mut bugfix_commits = 0i32;
280    let mut feature_commits = 0i32;
281
282    for msg in messages.lines() {
283        let msg_lower = msg.to_lowercase();
284        if msg_lower.contains("perf") || msg_lower.contains("optim") {
285            optimization_commits += 1;
286        } else if msg_lower.contains("refactor") || msg_lower.contains("cleanup") {
287            refactor_commits += 1;
288        } else if msg_lower.contains("fix") || msg_lower.contains("bug") {
289            bugfix_commits += 1;
290        } else if msg_lower.contains("feat") || msg_lower.contains("add") {
291            feature_commits += 1;
292        }
293    }
294
295    let output = Command::new("git")
296        .args(["ls-files"])
297        .current_dir(repo_path)
298        .output()
299        .map_err(|e| CoreError::Tool(format!("Git ls-files failed: {e}")))?;
300
301    let total_files =
302        i32::try_from(String::from_utf8_lossy(&output.stdout).lines().count()).unwrap_or(0);
303
304    let output = Command::new("git")
305        .args(["log", "--pretty=format:%at", "--max-count=100"])
306        .current_dir(repo_path)
307        .output()
308        .map_err(|e| CoreError::Tool(format!("Git log timestamps failed: {e}")))?;
309
310    let timestamps = String::from_utf8_lossy(&output.stdout);
311    let now = Utc::now().timestamp();
312    let mut total_age = 0.0;
313    let mut count = 0;
314
315    for ts_str in timestamps.lines() {
316        if let Ok(ts) = ts_str.parse::<i64>() {
317            let age_days = (now - ts).max(0) as f64 / 86400.0;
318            total_age += age_days;
319            count += 1;
320        }
321    }
322
323    let avg_commit_age_days = if count > 0 {
324        total_age / f64::from(count)
325    } else {
326        0.0
327    };
328
329    Ok(GeneseedStats {
330        total_commits,
331        optimization_commits,
332        refactor_commits,
333        bugfix_commits,
334        feature_commits,
335        total_files_tracked: total_files,
336        avg_commit_age_days,
337    })
338}
339
340/// Store mined patterns into the Geneseed Vault in LMDB.
341pub fn store_patterns_in_vault(
342    store: &MemoryStore,
343    patterns: &[OptimizationPattern],
344    galaxy: Galaxy,
345) -> wm_core::Result<usize> {
346    let mut stored = 0usize;
347
348    for p in patterns {
349        let title = format!("Geneseed Pattern: {} ({})", p.pattern_id, p.pattern_type);
350        let files_list = p
351            .files_changed
352            .iter()
353            .take(5)
354            .cloned()
355            .collect::<Vec<_>>()
356            .join(", ");
357        let content = format!(
358            "### {}\n\n- **Pattern Type**: `{}`\n- **Commit**: `{}` ({})\n- **Author**: {}\n- **Longevity**: {} days (provenance score)\n- **Confidence**: {:.2}\n- **Impact**: +{} / -{} lines\n- **Primary Files**: {}\n\n> {}",
359            title,
360            p.pattern_type,
361            &p.commit_hash[..8.min(p.commit_hash.len())],
362            p.timestamp,
363            p.author,
364            p.longevity_days,
365            p.confidence,
366            p.lines_added,
367            p.lines_removed,
368            files_list,
369            p.commit_message
370        );
371
372        let mut mem = Memory::new(galaxy, content);
373        mem.metadata.importance = (p.confidence as f32).clamp(0.4, 0.95);
374        mem.metadata.class = Some(MemoryClass::Knowledge);
375        mem.metadata.tier = Tier::Semantic;
376        mem.metadata.tags = vec![
377            "geneseed:pattern".into(),
378            format!("pattern_type:{}", p.pattern_type),
379            format!("commit:{}", &p.commit_hash[..8.min(p.commit_hash.len())]),
380            format!("longevity:{}d", p.longevity_days),
381        ];
382
383        store.put(galaxy, &mem)?;
384        stored += 1;
385    }
386
387    Ok(stored)
388}
389
390// ── Tools ──────────────────────────────────────────────────────────────
391
392pub struct GeneseedMineTool {
393    store: Arc<MemoryStore>,
394    stats: ToolStats,
395    effects: EffectRow,
396}
397
398impl GeneseedMineTool {
399    pub fn new(store: Arc<MemoryStore>) -> Self {
400        Self {
401            store,
402            stats: ToolStats::default(),
403            effects: EffectRow {
404                reads: vec![Resource::Filesystem, Resource::Process],
405                writes: vec![Resource::Galaxy("codex".into())],
406                ..Default::default()
407            },
408        }
409    }
410}
411
412#[async_trait]
413impl Tool for GeneseedMineTool {
414    fn name(&self) -> &str {
415        "geneseed.mine"
416    }
417
418    fn gana(&self) -> Gana {
419        Gana::Ghost
420    }
421
422    fn effects(&self) -> &EffectRow {
423        &self.effects
424    }
425
426    fn description(&self) -> &str {
427        "Mine optimization patterns from git history and optionally store them in the Geneseed Vault."
428    }
429
430    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
431        let repo_path_str = args.get("repo_path").and_then(Value::as_str).unwrap_or(".");
432        let min_confidence = args
433            .get("min_confidence")
434            .and_then(Value::as_f64)
435            .unwrap_or(0.5);
436        let max_commits = args
437            .get("max_commits")
438            .and_then(Value::as_u64)
439            .unwrap_or(250) as usize;
440        let store_in_vault = args
441            .get("store_in_vault")
442            .and_then(Value::as_bool)
443            .unwrap_or(false);
444        let galaxy_str = args.get("vault_galaxy").and_then(Value::as_str);
445
446        let galaxy = match galaxy_str {
447            Some(g) => parse_galaxy(g)?,
448            None => Galaxy::Codex,
449        };
450
451        let repo_path = Path::new(repo_path_str);
452        let patterns = mine_geneseed_patterns(repo_path, min_confidence, max_commits)?;
453
454        let stored_count = if store_in_vault {
455            store_patterns_in_vault(&self.store, &patterns, galaxy)?
456        } else {
457            0
458        };
459
460        Ok(json!({
461            "status": "success",
462            "repo_path": repo_path.display().to_string(),
463            "patterns_mined": patterns.len(),
464            "stored_in_vault": store_in_vault,
465            "stored_count": stored_count,
466            "vault_galaxy": galaxy.db_name(),
467            "patterns": patterns.iter().take(20).collect::<Vec<_>>()
468        }))
469    }
470
471    fn stats(&self) -> &ToolStats {
472        &self.stats
473    }
474}
475
476pub struct GeneseedStatsTool {
477    _store: Arc<MemoryStore>,
478    stats: ToolStats,
479    effects: EffectRow,
480}
481
482impl GeneseedStatsTool {
483    pub fn new(store: Arc<MemoryStore>) -> Self {
484        Self {
485            _store: store,
486            stats: ToolStats::default(),
487            effects: EffectRow::read_only(vec![Resource::Filesystem, Resource::Process]),
488        }
489    }
490}
491
492#[async_trait]
493impl Tool for GeneseedStatsTool {
494    fn name(&self) -> &str {
495        "geneseed.stats"
496    }
497
498    fn gana(&self) -> Gana {
499        Gana::Ghost
500    }
501
502    fn effects(&self) -> &EffectRow {
503        &self.effects
504    }
505
506    fn description(&self) -> &str {
507        "Retrieve repository-level pattern and architectural longevity statistics."
508    }
509
510    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
511        let repo_path_str = args.get("repo_path").and_then(Value::as_str).unwrap_or(".");
512        let repo_path = Path::new(repo_path_str);
513        let stats = get_geneseed_stats(repo_path)?;
514
515        Ok(json!({
516            "status": "success",
517            "repo_path": repo_path.display().to_string(),
518            "total_commits": stats.total_commits,
519            "optimization_commits": stats.optimization_commits,
520            "refactor_commits": stats.refactor_commits,
521            "bugfix_commits": stats.bugfix_commits,
522            "feature_commits": stats.feature_commits,
523            "total_files_tracked": stats.total_files_tracked,
524            "avg_commit_age_days": stats.avg_commit_age_days
525        }))
526    }
527
528    fn stats(&self) -> &ToolStats {
529        &self.stats
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    #[test]
538    fn test_classify_commit_heuristics() {
539        let now = 1757100000;
540        let ts = now - (100 * 86400); // 100 days old
541
542        // Performance commit
543        let p = classify_commit(
544            "abc123456789".into(),
545            "maintainer".into(),
546            ts,
547            "perf: optimize LMDB batch reads and vector cache".into(),
548            vec!["store.rs".into()],
549            45,
550            12,
551            now,
552            0.5,
553        );
554        assert!(p.is_some());
555        let pattern = p.unwrap();
556        assert_eq!(pattern.pattern_type, "performance");
557        assert_eq!(pattern.longevity_days, 100);
558        assert!(pattern.confidence > 0.8);
559
560        // Feature commit
561        let f = classify_commit(
562            "def987654321".into(),
563            "maintainer".into(),
564            ts,
565            "feat: add new holographic coordinate mapper".into(),
566            vec!["coords.rs".into()],
567            150,
568            10,
569            now,
570            0.3,
571        );
572        assert!(f.is_some());
573        let feat = f.unwrap();
574        assert_eq!(feat.pattern_type, "feature");
575    }
576
577    #[test]
578    fn test_store_patterns_in_vault() {
579        let tmp = tempfile::tempdir().unwrap();
580        let store = MemoryStore::open_default(tmp.path()).unwrap();
581
582        let pattern = OptimizationPattern {
583            pattern_id: "performance_12345678".into(),
584            pattern_type: "performance".into(),
585            commit_hash: "1234567890abcdef".into(),
586            commit_message: "perf(cache): vector caching layer".into(),
587            author: "maintainer".into(),
588            timestamp: "2026-06-01".into(),
589            files_changed: vec!["cache.rs".into()],
590            lines_added: 30,
591            lines_removed: 5,
592            confidence: 0.92,
593            longevity_days: 90,
594        };
595
596        let stored = store_patterns_in_vault(&store, &[pattern], Galaxy::Codex).unwrap();
597        assert_eq!(stored, 1);
598
599        let mems = store.scan(Galaxy::Codex, 10).unwrap();
600        assert_eq!(mems.len(), 1);
601        assert!(mems[0].content.contains("Geneseed Pattern"));
602        assert_eq!(mems[0].metadata.class, Some(MemoryClass::Knowledge));
603        assert!(
604            mems[0]
605                .metadata
606                .tags
607                .contains(&"geneseed:pattern".to_string())
608        );
609    }
610}