codemem_engine/enrichment/
blame.rs1use super::EnrichResult;
4use crate::CodememEngine;
5use codemem_core::{CodememError, GraphBackend, NodeKind};
6use serde_json::json;
7use std::collections::HashMap;
8
9impl CodememEngine {
10 pub fn enrich_blame(
12 &self,
13 path: &str,
14 namespace: Option<&str>,
15 ) -> Result<EnrichResult, CodememError> {
16 let file_nodes: Vec<String> = {
17 let graph = self.lock_graph()?;
18 graph
19 .get_all_nodes()
20 .into_iter()
21 .filter(|n| n.kind == NodeKind::File)
22 .map(|n| n.label.clone())
23 .collect()
24 };
25
26 let mut files_annotated = 0;
27 let mut insights_stored = 0;
28
29 for file_path in &file_nodes {
30 let output = std::process::Command::new("git")
31 .args(["-C", path, "log", "--format=%an", "--", file_path])
32 .output();
33
34 let output = match output {
35 Ok(o) if o.status.success() => o,
36 _ => continue,
37 };
38
39 let stdout = String::from_utf8_lossy(&output.stdout);
40 let mut author_counts: HashMap<String, usize> = HashMap::new();
41 for line in stdout.lines() {
42 let author = line.trim();
43 if !author.is_empty() {
44 *author_counts.entry(author.to_string()).or_default() += 1;
45 }
46 }
47
48 if author_counts.is_empty() {
49 continue;
50 }
51
52 let mut sorted_authors: Vec<_> = author_counts.into_iter().collect();
53 sorted_authors.sort_by(|a, b| b.1.cmp(&a.1));
54
55 let primary_owner = sorted_authors[0].0.clone();
56 let contributors: Vec<String> = sorted_authors.iter().map(|(a, _)| a.clone()).collect();
57
58 let node_id = format!("file:{file_path}");
60 {
61 let mut graph = self.lock_graph()?;
62 if let Ok(Some(mut node)) = graph.get_node(&node_id) {
63 node.payload
64 .insert("primary_owner".into(), json!(primary_owner));
65 node.payload
66 .insert("contributors".into(), json!(contributors));
67 let _ = graph.add_node(node);
68 files_annotated += 1;
69 }
70 }
71 }
72
73 let pending_ownership: Vec<(String, String)> = {
75 let graph = self.lock_graph()?;
76 graph
77 .get_all_nodes()
78 .iter()
79 .filter(|n| n.kind == NodeKind::File)
80 .filter_map(|node| {
81 let contribs = node.payload.get("contributors")?.as_array()?;
82 if contribs.len() > 5 {
83 let primary = node
84 .payload
85 .get("primary_owner")
86 .and_then(|v| v.as_str())
87 .unwrap_or("unknown");
88 let content = format!(
89 "Shared ownership: {} has {} contributors (primary: {}) — may need clear ownership",
90 node.label, contribs.len(), primary
91 );
92 Some((content, node.id.clone()))
93 } else {
94 None
95 }
96 })
97 .collect()
98 };
99
100 for (content, node_id) in &pending_ownership {
101 if self
102 .store_insight(
103 content,
104 "ownership",
105 &[],
106 0.5,
107 namespace,
108 std::slice::from_ref(node_id),
109 )
110 .is_some()
111 {
112 insights_stored += 1;
113 }
114 }
115
116 self.save_index();
117
118 Ok(EnrichResult {
119 insights_stored,
120 details: json!({
121 "files_annotated": files_annotated,
122 "insights_stored": insights_stored,
123 }),
124 })
125 }
126}