tracevault_core/
attribution_engine.rs1use crate::attribution::{AttributionSummary, FileAttribution, LineRange};
2
3pub fn compute_file_attribution(
9 path: &str,
10 old_content: Option<&str>,
11 new_content: &str,
12 is_ai_authored: bool,
13) -> FileAttribution {
14 let new_lines: Vec<&str> = new_content.lines().collect();
15 let line_count = new_lines.len() as u32;
16
17 if line_count == 0 {
18 return FileAttribution {
19 path: path.to_string(),
20 lines_added: 0,
21 lines_deleted: 0,
22 ai_lines: vec![],
23 human_lines: vec![],
24 mixed_lines: vec![],
25 };
26 }
27
28 match old_content {
29 None => {
30 let range = vec![LineRange {
32 start: 1,
33 end: line_count,
34 }];
35 FileAttribution {
36 path: path.to_string(),
37 lines_added: line_count,
38 lines_deleted: 0,
39 ai_lines: if is_ai_authored {
40 range.clone()
41 } else {
42 vec![]
43 },
44 human_lines: if is_ai_authored { vec![] } else { range },
45 mixed_lines: vec![],
46 }
47 }
48 Some(old) => {
49 let old_lines: Vec<&str> = old.lines().collect();
50 let old_count = old_lines.len() as u32;
51
52 let changed = find_changed_lines(&old_lines, &new_lines);
53 let added_count = line_count.saturating_sub(old_count);
54 let deleted_count = old_count.saturating_sub(line_count);
55
56 let changed_lines_total: u32 = changed.iter().map(|r| r.end - r.start + 1).sum();
57
58 let (ai_lines, human_lines) = if is_ai_authored {
59 (changed, vec![])
60 } else {
61 (vec![], changed)
62 };
63
64 FileAttribution {
65 path: path.to_string(),
66 lines_added: added_count + changed_lines_total,
67 lines_deleted: deleted_count,
68 ai_lines,
69 human_lines,
70 mixed_lines: vec![],
71 }
72 }
73 }
74}
75
76pub fn compute_attribution_summary(files: &[FileAttribution]) -> AttributionSummary {
78 let total_ai: u32 = files
79 .iter()
80 .flat_map(|f| &f.ai_lines)
81 .map(|r| r.end - r.start + 1)
82 .sum();
83
84 let total_human: u32 = files
85 .iter()
86 .flat_map(|f| &f.human_lines)
87 .map(|r| r.end - r.start + 1)
88 .sum();
89
90 let total_mixed: u32 = files
91 .iter()
92 .flat_map(|f| &f.mixed_lines)
93 .map(|r| r.end - r.start + 1)
94 .sum();
95
96 let total = total_ai + total_human + total_mixed;
97 let total_added: u32 = files.iter().map(|f| f.lines_added).sum();
98 let total_deleted: u32 = files.iter().map(|f| f.lines_deleted).sum();
99
100 let ai_pct = if total > 0 {
101 (total_ai as f32 / total as f32) * 100.0
102 } else {
103 0.0
104 };
105
106 AttributionSummary {
107 total_lines_added: total_added,
108 total_lines_deleted: total_deleted,
109 ai_percentage: ai_pct,
110 human_percentage: 100.0 - ai_pct,
111 }
112}
113
114fn find_changed_lines(old: &[&str], new: &[&str]) -> Vec<LineRange> {
116 let mut ranges = vec![];
117 let mut range_start: Option<u32> = None;
118
119 for (i, new_line) in new.iter().enumerate() {
120 let is_changed = old.get(i) != Some(new_line);
121
122 if is_changed {
123 if range_start.is_none() {
124 range_start = Some(i as u32 + 1); }
126 } else if let Some(start) = range_start.take() {
127 ranges.push(LineRange {
128 start,
129 end: i as u32, });
131 }
132 }
133
134 if let Some(start) = range_start {
136 ranges.push(LineRange {
137 start,
138 end: new.len() as u32,
139 });
140 }
141
142 ranges
143}