1#![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#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct OptimizationPattern {
26 pub pattern_id: String,
27 pub pattern_type: String, 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, }
38
39#[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
145pub 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
250pub 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
340pub 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
390pub 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); let p = classify_commit(
544 "abc123456789".into(),
545 "Lucas".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 let f = classify_commit(
562 "def987654321".into(),
563 "Lucas".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: "Lucas".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}