Skip to main content

st/mem8/
developer_personas.rs

1//! Developer Persona Analysis for MEM8
2//! Creates unique wave signatures for each developer based on their git history
3
4use 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/// Developer persona with unique characteristics
15#[derive(Debug, Clone)]
16pub struct DeveloperPersona {
17    /// Developer name/email
18    pub identity: String,
19
20    /// Coding style signature
21    pub style_signature: CodingStyle,
22
23    /// Temporal patterns (when they work)
24    pub temporal_pattern: TemporalPattern,
25
26    /// Emotional profile from commit messages
27    pub emotional_profile: EmotionalProfile,
28
29    /// Collaboration patterns
30    pub collaboration: CollaborationPattern,
31
32    /// Expertise areas (files/directories they work on)
33    pub expertise_map: HashMap<String, f32>,
34
35    /// Overall contribution metrics
36    pub metrics: ContributionMetrics,
37}
38
39#[derive(Debug, Clone)]
40pub struct CodingStyle {
41    /// Average commit size (lines changed)
42    pub avg_commit_size: f32,
43
44    /// Preference for large refactors vs small changes
45    pub refactor_tendency: f32, // 0.0 = small changes, 1.0 = large refactors
46
47    /// Bug fix ratio
48    pub bugfix_ratio: f32,
49
50    /// Feature development ratio
51    pub feature_ratio: f32,
52
53    /// Documentation contribution ratio
54    pub documentation_ratio: f32,
55
56    /// Test writing ratio
57    pub test_ratio: f32,
58}
59
60#[derive(Debug, Clone)]
61pub struct TemporalPattern {
62    /// Preferred hours of day (0-23)
63    pub active_hours: [f32; 24],
64
65    /// Preferred days of week (0=Monday, 6=Sunday)
66    pub active_days: [f32; 7],
67
68    /// Night owl vs early bird (-1.0 = night owl, 1.0 = early bird)
69    pub chronotype: f32,
70
71    /// Weekend warrior score (0.0 = weekday only, 1.0 = weekend heavy)
72    pub weekend_warrior: f32,
73
74    /// Consistency score (0.0 = sporadic, 1.0 = very regular)
75    pub consistency: f32,
76}
77
78#[derive(Debug, Clone)]
79pub struct EmotionalProfile {
80    /// Overall positivity in commit messages
81    pub positivity: f32,
82
83    /// Excitement level (exclamation marks, enthusiastic words)
84    pub excitement: f32,
85
86    /// Frustration level (curse words, "fix", "bug", "broken")
87    pub frustration: f32,
88
89    /// Professionalism (formal vs casual language)
90    pub professionalism: f32,
91
92    /// Humor level (jokes, puns, emojis)
93    pub humor: f32,
94}
95
96#[derive(Debug, Clone)]
97pub struct CollaborationPattern {
98    /// Solo vs team player (0.0 = solo, 1.0 = highly collaborative)
99    pub collaboration_score: f32,
100
101    /// Developers they frequently work with
102    pub frequent_collaborators: HashMap<String, f32>,
103
104    /// Response time to others' changes
105    pub responsiveness: f32,
106
107    /// Code review participation
108    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
122/// Persona analyzer for git repositories
123pub 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    /// Analyze all developers in the repository
135    pub fn analyze_all_developers(&self) -> Result<HashMap<String, DeveloperPersona>> {
136        let commits = self.analyzer.get_project_timeline()?;
137
138        // Group commits by author
139        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        // Analyze each developer
148        let mut personas = HashMap::new();
149        for (author, commits) in author_commits {
150            if commits.len() >= 5 {
151                // Need at least 5 commits for meaningful analysis
152                let persona = self.analyze_developer(&author, commits)?;
153                personas.insert(author, persona);
154            }
155        }
156
157        Ok(personas)
158    }
159
160    /// Analyze a specific developer
161    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        // Calculate average commit size
188        let avg_changes: f32 = commits
189            .iter()
190            .map(|c| (c.additions + c.deletions) as f32)
191            .sum::<f32>()
192            / total;
193
194        // Categorize commits
195        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                // Saturday or Sunday
252                weekend_commits += 1;
253            }
254        }
255
256        // Normalize
257        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        // Calculate chronotype
268        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        // Calculate consistency (standard deviation of commit times)
275        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            // Positivity indicators
298            if msg.contains("awesome") || msg.contains("great") || msg.contains("excellent") {
299                positivity += 1.0;
300            }
301
302            // Excitement indicators
303            excitement += msg.matches('!').count() as f32;
304            if msg.contains("finally") || msg.contains("yay") {
305                excitement += 1.0;
306            }
307
308            // Frustration indicators
309            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            // Professionalism (longer, structured messages)
317            if msg.len() > 50 && msg.contains(':') {
318                professionalism += 1.0;
319            }
320
321            // Humor indicators
322            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        // Simplified collaboration analysis
339        // In a real implementation, we'd analyze co-authored commits,
340        // PR reviews, and response times
341
342        CollaborationPattern {
343            collaboration_score: 0.5, // Default middle ground
344            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                // Extract directory or file type as expertise area
356                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        // Normalize to 0-1 range
369        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        // Calculate standard deviation
413        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) // Normalized by mean
418    }
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
429/// Extension for MEM8 to create developer-specific wave patterns
430impl SmartTreeMem8 {
431    /// Import developer personas as unique wave signatures
432    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            // Create base frequency from coding style
442            let base_freq = if persona.style_signature.refactor_tendency > 0.5 {
443                FrequencyBand::DeepStructural.frequency(0.7) // Architects
444            } else if persona.style_signature.bugfix_ratio > 0.5 {
445                FrequencyBand::Technical.frequency(0.8) // Fixers
446            } else if persona.style_signature.feature_ratio > 0.5 {
447                FrequencyBand::Implementation.frequency(0.6) // Builders
448            } else {
449                FrequencyBand::Conversational.frequency(0.5) // Generalists
450            };
451
452            // Create temporal rhythm from work patterns
453            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), // Slight frequency shift per hour
457                        intensity * 0.8,
458                    );
459
460                    // Emotional modulation
461                    wave.valence = persona.emotional_profile.positivity
462                        - persona.emotional_profile.frustration;
463                    wave.arousal = persona.emotional_profile.excitement;
464                    wave.decay_tau = None; // Persistent persona pattern
465
466                    // Store in persona layer (high Z values)
467                    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            // Create expertise signatures
477            for (area, expertise) in persona.expertise_map {
478                if expertise > 0.3 {
479                    let mut wave = MemoryWave::new(
480                        base_freq + 100.0, // Expertise frequency band
481                        expertise,
482                    );
483
484                    wave.valence = 0.7; // Positive association with expertise
485                    wave.decay_tau = None; // Persistent
486
487                    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            // Print persona summary
495            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    /// Query memories specific to a developer
527    pub fn query_developer_memories(&self, _developer_name: &str) -> Vec<(MemoryWave, String)> {
528        // Implementation would search for waves in the developer's frequency/spatial range
529        // For now, return empty vec as placeholder
530        Vec::new()
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn test_persona_analysis() {
540        // Test would run on a real git repo
541        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}