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 = if line_count > old_count {
54 line_count - old_count
55 } else {
56 0
57 };
58 let deleted_count = if old_count > line_count {
59 old_count - line_count
60 } else {
61 0
62 };
63
64 let changed_lines_total: u32 =
65 changed.iter().map(|r| r.end - r.start + 1).sum();
66
67 let (ai_lines, human_lines) = if is_ai_authored {
68 (changed, vec![])
69 } else {
70 (vec![], changed)
71 };
72
73 FileAttribution {
74 path: path.to_string(),
75 lines_added: added_count + changed_lines_total,
76 lines_deleted: deleted_count,
77 ai_lines,
78 human_lines,
79 mixed_lines: vec![],
80 }
81 }
82 }
83}
84
85pub fn compute_attribution_summary(files: &[FileAttribution]) -> AttributionSummary {
87 let total_ai: u32 = files
88 .iter()
89 .flat_map(|f| &f.ai_lines)
90 .map(|r| r.end - r.start + 1)
91 .sum();
92
93 let total_human: u32 = files
94 .iter()
95 .flat_map(|f| &f.human_lines)
96 .map(|r| r.end - r.start + 1)
97 .sum();
98
99 let total_mixed: u32 = files
100 .iter()
101 .flat_map(|f| &f.mixed_lines)
102 .map(|r| r.end - r.start + 1)
103 .sum();
104
105 let total = total_ai + total_human + total_mixed;
106 let total_added: u32 = files.iter().map(|f| f.lines_added).sum();
107 let total_deleted: u32 = files.iter().map(|f| f.lines_deleted).sum();
108
109 let ai_pct = if total > 0 {
110 (total_ai as f32 / total as f32) * 100.0
111 } else {
112 0.0
113 };
114
115 AttributionSummary {
116 total_lines_added: total_added,
117 total_lines_deleted: total_deleted,
118 ai_percentage: ai_pct,
119 human_percentage: 100.0 - ai_pct,
120 }
121}
122
123fn find_changed_lines(old: &[&str], new: &[&str]) -> Vec<LineRange> {
125 let mut ranges = vec![];
126 let mut range_start: Option<u32> = None;
127
128 for (i, new_line) in new.iter().enumerate() {
129 let is_changed = old.get(i).map_or(true, |old_line| old_line != new_line);
130
131 if is_changed {
132 if range_start.is_none() {
133 range_start = Some(i as u32 + 1); }
135 } else if let Some(start) = range_start.take() {
136 ranges.push(LineRange {
137 start,
138 end: i as u32, });
140 }
141 }
142
143 if let Some(start) = range_start {
145 ranges.push(LineRange {
146 start,
147 end: new.len() as u32,
148 });
149 }
150
151 ranges
152}