1use crate::mem8::{
5 git_temporal::{GitCommit, GitTemporalAnalyzer},
6 integration::SmartTreeMem8,
7 wave::{FrequencyBand, MemoryWave},
8};
9use anyhow::Result;
10use chrono::{DateTime, Datelike, Timelike, Utc};
11use std::collections::HashMap;
12use std::path::Path;
13
14#[derive(Debug, Clone)]
16pub struct DeveloperPersona {
17 pub identity: String,
19
20 pub style_signature: CodingStyle,
22
23 pub temporal_pattern: TemporalPattern,
25
26 pub emotional_profile: EmotionalProfile,
28
29 pub collaboration: CollaborationPattern,
31
32 pub expertise_map: HashMap<String, f32>,
34
35 pub metrics: ContributionMetrics,
37}
38
39#[derive(Debug, Clone)]
40pub struct CodingStyle {
41 pub avg_commit_size: f32,
43
44 pub refactor_tendency: f32, pub bugfix_ratio: f32,
49
50 pub feature_ratio: f32,
52
53 pub documentation_ratio: f32,
55
56 pub test_ratio: f32,
58}
59
60#[derive(Debug, Clone)]
61pub struct TemporalPattern {
62 pub active_hours: [f32; 24],
64
65 pub active_days: [f32; 7],
67
68 pub chronotype: f32,
70
71 pub weekend_warrior: f32,
73
74 pub consistency: f32,
76}
77
78#[derive(Debug, Clone)]
79pub struct EmotionalProfile {
80 pub positivity: f32,
82
83 pub excitement: f32,
85
86 pub frustration: f32,
88
89 pub professionalism: f32,
91
92 pub humor: f32,
94}
95
96#[derive(Debug, Clone)]
97pub struct CollaborationPattern {
98 pub collaboration_score: f32,
100
101 pub frequent_collaborators: HashMap<String, f32>,
103
104 pub responsiveness: f32,
106
107 pub review_participation: f32,
109}
110
111#[derive(Debug, Clone)]
112pub struct ContributionMetrics {
113 pub total_commits: usize,
114 pub total_additions: usize,
115 pub total_deletions: usize,
116 pub files_touched: usize,
117 pub first_commit: DateTime<Utc>,
118 pub last_commit: DateTime<Utc>,
119 pub active_days: usize,
120}
121
122pub struct PersonaAnalyzer {
124 analyzer: GitTemporalAnalyzer,
125}
126
127impl PersonaAnalyzer {
128 pub fn new(repo_path: impl AsRef<Path>) -> Result<Self> {
129 Ok(Self {
130 analyzer: GitTemporalAnalyzer::new(repo_path)?,
131 })
132 }
133
134 pub fn analyze_all_developers(&self) -> Result<HashMap<String, DeveloperPersona>> {
136 let commits = self.analyzer.get_project_timeline()?;
137
138 let mut author_commits: HashMap<String, Vec<GitCommit>> = HashMap::new();
140 for commit in commits {
141 author_commits
142 .entry(commit.author.clone())
143 .or_default()
144 .push(commit);
145 }
146
147 let mut personas = HashMap::new();
149 for (author, commits) in author_commits {
150 if commits.len() >= 5 {
151 let persona = self.analyze_developer(&author, commits)?;
153 personas.insert(author, persona);
154 }
155 }
156
157 Ok(personas)
158 }
159
160 fn analyze_developer(
162 &self,
163 identity: &str,
164 commits: Vec<GitCommit>,
165 ) -> Result<DeveloperPersona> {
166 let style = self.analyze_coding_style(&commits);
167 let temporal = self.analyze_temporal_pattern(&commits);
168 let emotional = self.analyze_emotional_profile(&commits);
169 let collaboration = self.analyze_collaboration(&commits);
170 let expertise = self.analyze_expertise(&commits);
171 let metrics = self.calculate_metrics(&commits);
172
173 Ok(DeveloperPersona {
174 identity: identity.to_string(),
175 style_signature: style,
176 temporal_pattern: temporal,
177 emotional_profile: emotional,
178 collaboration,
179 expertise_map: expertise,
180 metrics,
181 })
182 }
183
184 fn analyze_coding_style(&self, commits: &[GitCommit]) -> CodingStyle {
185 let total = commits.len() as f32;
186
187 let avg_changes: f32 = commits
189 .iter()
190 .map(|c| (c.additions + c.deletions) as f32)
191 .sum::<f32>()
192 / total;
193
194 let mut bugfixes = 0;
196 let mut features = 0;
197 let mut docs = 0;
198 let mut tests = 0;
199 let mut large_commits = 0;
200
201 for commit in commits {
202 let msg = commit.message.to_lowercase();
203 if msg.contains("fix") || msg.contains("bug") {
204 bugfixes += 1;
205 }
206 if msg.contains("feat") || msg.contains("add") || msg.contains("implement") {
207 features += 1;
208 }
209 if msg.contains("doc") || msg.contains("readme") {
210 docs += 1;
211 }
212 if msg.contains("test") || msg.contains("spec") {
213 tests += 1;
214 }
215 if commit.additions + commit.deletions > 500 {
216 large_commits += 1;
217 }
218 }
219
220 CodingStyle {
221 avg_commit_size: avg_changes,
222 refactor_tendency: (large_commits as f32 / total).min(1.0),
223 bugfix_ratio: (bugfixes as f32 / total).min(1.0),
224 feature_ratio: (features as f32 / total).min(1.0),
225 documentation_ratio: (docs as f32 / total).min(1.0),
226 test_ratio: (tests as f32 / total).min(1.0),
227 }
228 }
229
230 fn analyze_temporal_pattern(&self, commits: &[GitCommit]) -> TemporalPattern {
231 let mut hour_counts = [0f32; 24];
232 let mut day_counts = [0f32; 7];
233 let mut morning_commits = 0;
234 let mut evening_commits = 0;
235 let mut weekend_commits = 0;
236
237 for commit in commits {
238 let hour = commit.timestamp.hour() as usize;
239 let day = commit.timestamp.weekday().num_days_from_monday() as usize;
240
241 hour_counts[hour] += 1.0;
242 day_counts[day] += 1.0;
243
244 if (5..12).contains(&hour) {
245 morning_commits += 1;
246 } else if !(5..20).contains(&hour) {
247 evening_commits += 1;
248 }
249
250 if day >= 5 {
251 weekend_commits += 1;
253 }
254 }
255
256 let max_hour = hour_counts.iter().fold(0.0f32, |a, &b| a.max(b)).max(1.0);
258 let max_day = day_counts.iter().fold(0.0f32, |a, &b| a.max(b)).max(1.0);
259
260 for h in &mut hour_counts {
261 *h /= max_hour;
262 }
263 for d in &mut day_counts {
264 *d /= max_day;
265 }
266
267 let chronotype = if evening_commits > morning_commits {
269 -((evening_commits as f32) / (evening_commits + morning_commits) as f32)
270 } else {
271 (morning_commits as f32) / (evening_commits + morning_commits).max(1) as f32
272 };
273
274 let commit_intervals = Self::calculate_commit_intervals(commits);
276 let consistency = 1.0 / (1.0 + commit_intervals);
277
278 TemporalPattern {
279 active_hours: hour_counts,
280 active_days: day_counts,
281 chronotype,
282 weekend_warrior: weekend_commits as f32 / commits.len() as f32,
283 consistency,
284 }
285 }
286
287 fn analyze_emotional_profile(&self, commits: &[GitCommit]) -> EmotionalProfile {
288 let mut positivity = 0.0;
289 let mut excitement = 0.0;
290 let mut frustration = 0.0;
291 let mut professionalism = 0.0;
292 let mut humor = 0.0;
293
294 for commit in commits {
295 let msg = &commit.message;
296
297 if msg.contains("awesome") || msg.contains("great") || msg.contains("excellent") {
299 positivity += 1.0;
300 }
301
302 excitement += msg.matches('!').count() as f32;
304 if msg.contains("finally") || msg.contains("yay") {
305 excitement += 1.0;
306 }
307
308 if msg.contains("fix") || msg.contains("bug") || msg.contains("broken") {
310 frustration += 1.0;
311 }
312 if msg.contains("damn") || msg.contains("crap") || msg.contains("wtf") {
313 frustration += 2.0;
314 }
315
316 if msg.len() > 50 && msg.contains(':') {
318 professionalism += 1.0;
319 }
320
321 if msg.contains("🤣") || msg.contains("😂") || msg.contains("lol") {
323 humor += 1.0;
324 }
325 }
326
327 let total = commits.len() as f32;
328 EmotionalProfile {
329 positivity: (positivity / total).min(1.0),
330 excitement: (excitement / (total * 2.0)).min(1.0),
331 frustration: (frustration / total).min(1.0),
332 professionalism: (professionalism / total).min(1.0),
333 humor: (humor / total).min(1.0),
334 }
335 }
336
337 fn analyze_collaboration(&self, _commits: &[GitCommit]) -> CollaborationPattern {
338 CollaborationPattern {
343 collaboration_score: 0.5, frequent_collaborators: HashMap::new(),
345 responsiveness: 0.5,
346 review_participation: 0.5,
347 }
348 }
349
350 fn analyze_expertise(&self, commits: &[GitCommit]) -> HashMap<String, f32> {
351 let mut file_counts: HashMap<String, usize> = HashMap::new();
352
353 for commit in commits {
354 for file in &commit.files_changed {
355 let expertise_key = if let Some(dir_end) = file.find('/') {
357 file[..dir_end].to_string()
358 } else if let Some(ext_start) = file.rfind('.') {
359 format!("*.{}", &file[ext_start + 1..])
360 } else {
361 file.clone()
362 };
363
364 *file_counts.entry(expertise_key).or_insert(0) += 1;
365 }
366 }
367
368 let max_count = file_counts.values().max().copied().unwrap_or(1) as f32;
370 file_counts
371 .into_iter()
372 .map(|(k, v)| (k, v as f32 / max_count))
373 .collect()
374 }
375
376 fn calculate_metrics(&self, commits: &[GitCommit]) -> ContributionMetrics {
377 let total_additions: usize = commits.iter().map(|c| c.additions).sum();
378 let total_deletions: usize = commits.iter().map(|c| c.deletions).sum();
379
380 let mut unique_files = std::collections::HashSet::new();
381 for commit in commits {
382 for file in &commit.files_changed {
383 unique_files.insert(file.clone());
384 }
385 }
386
387 ContributionMetrics {
388 total_commits: commits.len(),
389 total_additions,
390 total_deletions,
391 files_touched: unique_files.len(),
392 first_commit: commits.last().map(|c| c.timestamp).unwrap_or_else(Utc::now),
393 last_commit: commits
394 .first()
395 .map(|c| c.timestamp)
396 .unwrap_or_else(Utc::now),
397 active_days: Self::count_active_days(commits),
398 }
399 }
400
401 fn calculate_commit_intervals(commits: &[GitCommit]) -> f32 {
402 if commits.len() < 2 {
403 return 0.0;
404 }
405
406 let mut intervals = Vec::new();
407 for i in 1..commits.len() {
408 let interval = (commits[i - 1].timestamp - commits[i].timestamp).num_hours() as f32;
409 intervals.push(interval);
410 }
411
412 let mean = intervals.iter().sum::<f32>() / intervals.len() as f32;
414 let variance =
415 intervals.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / intervals.len() as f32;
416
417 variance.sqrt() / (mean + 1.0) }
419
420 fn count_active_days(commits: &[GitCommit]) -> usize {
421 let mut active_days = std::collections::HashSet::new();
422 for commit in commits {
423 active_days.insert(commit.timestamp.date_naive());
424 }
425 active_days.len()
426 }
427}
428
429impl SmartTreeMem8 {
431 pub fn import_developer_personas(&mut self, repo_path: impl AsRef<Path>) -> Result<()> {
433 let analyzer = PersonaAnalyzer::new(repo_path)?;
434 let personas = analyzer.analyze_all_developers()?;
435
436 println!("Found {} developer personas", personas.len());
437
438 for (developer, persona) in personas {
439 println!("\nCreating wave signature for: {}", developer);
440
441 let base_freq = if persona.style_signature.refactor_tendency > 0.5 {
443 FrequencyBand::DeepStructural.frequency(0.7) } else if persona.style_signature.bugfix_ratio > 0.5 {
445 FrequencyBand::Technical.frequency(0.8) } else if persona.style_signature.feature_ratio > 0.5 {
447 FrequencyBand::Implementation.frequency(0.6) } else {
449 FrequencyBand::Conversational.frequency(0.5) };
451
452 for (hour, &intensity) in persona.temporal_pattern.active_hours.iter().enumerate() {
454 if intensity > 0.2 {
455 let mut wave = MemoryWave::new(
456 base_freq + (hour as f32 * 10.0), intensity * 0.8,
458 );
459
460 wave.valence = persona.emotional_profile.positivity
462 - persona.emotional_profile.frustration;
463 wave.arousal = persona.emotional_profile.excitement;
464 wave.decay_tau = None; let x = (self.simple_hash(&developer) & 0xFF) as u8;
468 let y = (hour * 10) as u8;
469 let z = 64000
470 + (self.simple_hash(&format!("{}-{}", developer, hour)) & 0x3FF) as u16;
471
472 self.store_wave_at_coordinates(x, y, z, wave)?;
473 }
474 }
475
476 for (area, expertise) in persona.expertise_map {
478 if expertise > 0.3 {
479 let mut wave = MemoryWave::new(
480 base_freq + 100.0, expertise,
482 );
483
484 wave.valence = 0.7; wave.decay_tau = None; let (x, y) = self.string_to_coordinates(&format!("{}-{}", developer, area));
488 let z = 63000;
489
490 self.store_wave_at_coordinates(x, y, z, wave)?;
491 }
492 }
493
494 println!(
496 " Style: {:.0}% features, {:.0}% bugfixes, {:.0}% refactoring",
497 persona.style_signature.feature_ratio * 100.0,
498 persona.style_signature.bugfix_ratio * 100.0,
499 persona.style_signature.refactor_tendency * 100.0
500 );
501 println!(
502 " Chronotype: {} ({:.1})",
503 if persona.temporal_pattern.chronotype < -0.3 {
504 "Night Owl 🦉"
505 } else if persona.temporal_pattern.chronotype > 0.3 {
506 "Early Bird 🐦"
507 } else {
508 "Flexible ⏰"
509 },
510 persona.temporal_pattern.chronotype
511 );
512 println!(
513 " Emotional: {:.0}% positive, {:.0}% excited",
514 persona.emotional_profile.positivity * 100.0,
515 persona.emotional_profile.excitement * 100.0
516 );
517 println!(
518 " Contributions: {} commits, {} files touched",
519 persona.metrics.total_commits, persona.metrics.files_touched
520 );
521 }
522
523 Ok(())
524 }
525
526 pub fn query_developer_memories(&self, _developer_name: &str) -> Vec<(MemoryWave, String)> {
528 Vec::new()
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 #[test]
539 fn test_persona_analysis() {
540 if let Ok(analyzer) = PersonaAnalyzer::new(".") {
542 if let Ok(personas) = analyzer.analyze_all_developers() {
543 for (dev, persona) in personas {
544 println!(
545 "Developer: {} - {} commits",
546 dev, persona.metrics.total_commits
547 );
548 }
549 }
550 }
551 }
552}