ucm_ingest/
diff_parser.rs1use ucm_graph_core::entity::*;
8use ucm_graph_core::event::*;
9
10pub fn parse_diff(file_path: &str, before: &str, after: &str) -> Vec<UcmEvent> {
18 let mut events = Vec::new();
19
20 let before_fns = extract_function_signatures(before);
21 let after_fns = extract_function_signatures(after);
22
23 for (name, sig) in &after_fns {
25 if let Some(old_sig) = before_fns.get(name.as_str()) {
26 if sig != old_sig {
27 events.push(UcmEvent::new(EventPayload::ChangeDetected {
28 file_path: file_path.to_string(),
29 change_type: ChangeType::SignatureChange,
30 affected_entities: vec![EntityId::local(file_path, name)],
31 before_snapshot: Some(old_sig.clone()),
32 after_snapshot: Some(sig.clone()),
33 }));
34 }
35 } else {
36 events.push(UcmEvent::new(EventPayload::ChangeDetected {
38 file_path: file_path.to_string(),
39 change_type: ChangeType::EntityAdded,
40 affected_entities: vec![EntityId::local(file_path, name)],
41 before_snapshot: None,
42 after_snapshot: Some(sig.clone()),
43 }));
44 }
45 }
46
47 for (name, sig) in &before_fns {
49 if !after_fns.contains_key(name.as_str()) {
50 events.push(UcmEvent::new(EventPayload::ChangeDetected {
51 file_path: file_path.to_string(),
52 change_type: ChangeType::EntityDeleted,
53 affected_entities: vec![EntityId::local(file_path, name)],
54 before_snapshot: Some(sig.clone()),
55 after_snapshot: None,
56 }));
57 }
58 }
59
60 let before_imports = extract_import_lines(before);
62 let after_imports = extract_import_lines(after);
63 if before_imports != after_imports {
64 events.push(UcmEvent::new(EventPayload::ChangeDetected {
65 file_path: file_path.to_string(),
66 change_type: ChangeType::ImportChange,
67 affected_entities: vec![],
68 before_snapshot: Some(before_imports.join("\n")),
69 after_snapshot: Some(after_imports.join("\n")),
70 }));
71 }
72
73 if events.is_empty() && before != after {
75 events.push(UcmEvent::new(EventPayload::ChangeDetected {
76 file_path: file_path.to_string(),
77 change_type: ChangeType::BodyChange,
78 affected_entities: vec![],
79 before_snapshot: None,
80 after_snapshot: None,
81 }));
82 }
83
84 events
85}
86
87fn extract_function_signatures(source: &str) -> std::collections::HashMap<String, String> {
89 let mut sigs = std::collections::HashMap::new();
90
91 for line in source.lines() {
92 let trimmed = line.trim();
93 if trimmed.contains("function ") && trimmed.contains('(') {
94 let parts: Vec<&str> = trimmed.split("function ").collect();
95 if parts.len() >= 2 {
96 let after = parts.last().unwrap();
97 let name: String = after
98 .chars()
99 .take_while(|c| c.is_alphanumeric() || *c == '_')
100 .collect();
101 if !name.is_empty() {
102 let sig = if let Some(end) = trimmed.find('{') {
104 trimmed[..end].trim().to_string()
105 } else {
106 trimmed.to_string()
107 };
108 sigs.insert(name, sig);
109 }
110 }
111 }
112 }
113
114 sigs
115}
116
117fn extract_import_lines(source: &str) -> Vec<String> {
119 source
120 .lines()
121 .filter(|line| line.trim().starts_with("import "))
122 .map(|line| line.trim().to_string())
123 .collect()
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_detect_signature_change() {
132 let before = r#"
133function validateToken(token: string): boolean {
134 return jwt.verify(token);
135}
136"#;
137 let after = r#"
138function validateToken(token: string): Result<Claims, AuthError> {
139 return jwt.verify(token);
140}
141"#;
142
143 let events = parse_diff("src/auth/service.ts", before, after);
144 assert!(!events.is_empty());
145
146 let change = &events[0];
147 match &change.payload {
148 EventPayload::ChangeDetected { change_type, .. } => {
149 assert!(matches!(change_type, ChangeType::SignatureChange));
150 }
151 _ => panic!("Expected ChangeDetected event"),
152 }
153 }
154
155 #[test]
156 fn test_detect_new_function() {
157 let before = "function existing() {}";
158 let after = "function existing() {}\nfunction newFunction() {}";
159
160 let events = parse_diff("src/main.ts", before, after);
161 let added: Vec<_> = events
162 .iter()
163 .filter(|e| {
164 matches!(
165 &e.payload,
166 EventPayload::ChangeDetected {
167 change_type: ChangeType::EntityAdded,
168 ..
169 }
170 )
171 })
172 .collect();
173
174 assert!(!added.is_empty());
175 }
176
177 #[test]
178 fn test_detect_removed_function() {
179 let before = "function toRemove() {}\nfunction toKeep() {}";
180 let after = "function toKeep() {}";
181
182 let events = parse_diff("src/main.ts", before, after);
183 let removed: Vec<_> = events
184 .iter()
185 .filter(|e| {
186 matches!(
187 &e.payload,
188 EventPayload::ChangeDetected {
189 change_type: ChangeType::EntityDeleted,
190 ..
191 }
192 )
193 })
194 .collect();
195
196 assert!(!removed.is_empty());
197 }
198}