use ucm_graph_core::entity::*;
use ucm_graph_core::event::*;
pub fn parse_diff(file_path: &str, before: &str, after: &str) -> Vec<UcmEvent> {
let mut events = Vec::new();
let before_fns = extract_function_signatures(before);
let after_fns = extract_function_signatures(after);
for (name, sig) in &after_fns {
if let Some(old_sig) = before_fns.get(name.as_str()) {
if sig != old_sig {
events.push(UcmEvent::new(EventPayload::ChangeDetected {
file_path: file_path.to_string(),
change_type: ChangeType::SignatureChange,
affected_entities: vec![EntityId::local(file_path, name)],
before_snapshot: Some(old_sig.clone()),
after_snapshot: Some(sig.clone()),
}));
}
} else {
events.push(UcmEvent::new(EventPayload::ChangeDetected {
file_path: file_path.to_string(),
change_type: ChangeType::EntityAdded,
affected_entities: vec![EntityId::local(file_path, name)],
before_snapshot: None,
after_snapshot: Some(sig.clone()),
}));
}
}
for (name, sig) in &before_fns {
if !after_fns.contains_key(name.as_str()) {
events.push(UcmEvent::new(EventPayload::ChangeDetected {
file_path: file_path.to_string(),
change_type: ChangeType::EntityDeleted,
affected_entities: vec![EntityId::local(file_path, name)],
before_snapshot: Some(sig.clone()),
after_snapshot: None,
}));
}
}
let before_imports = extract_import_lines(before);
let after_imports = extract_import_lines(after);
if before_imports != after_imports {
events.push(UcmEvent::new(EventPayload::ChangeDetected {
file_path: file_path.to_string(),
change_type: ChangeType::ImportChange,
affected_entities: vec![],
before_snapshot: Some(before_imports.join("\n")),
after_snapshot: Some(after_imports.join("\n")),
}));
}
if events.is_empty() && before != after {
events.push(UcmEvent::new(EventPayload::ChangeDetected {
file_path: file_path.to_string(),
change_type: ChangeType::BodyChange,
affected_entities: vec![],
before_snapshot: None,
after_snapshot: None,
}));
}
events
}
fn extract_function_signatures(source: &str) -> std::collections::HashMap<String, String> {
let mut sigs = std::collections::HashMap::new();
for line in source.lines() {
let trimmed = line.trim();
if trimmed.contains("function ") && trimmed.contains('(') {
let parts: Vec<&str> = trimmed.split("function ").collect();
if parts.len() >= 2 {
let after = parts.last().unwrap();
let name: String = after
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if !name.is_empty() {
let sig = if let Some(end) = trimmed.find('{') {
trimmed[..end].trim().to_string()
} else {
trimmed.to_string()
};
sigs.insert(name, sig);
}
}
}
}
sigs
}
fn extract_import_lines(source: &str) -> Vec<String> {
source
.lines()
.filter(|line| line.trim().starts_with("import "))
.map(|line| line.trim().to_string())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_signature_change() {
let before = r#"
function validateToken(token: string): boolean {
return jwt.verify(token);
}
"#;
let after = r#"
function validateToken(token: string): Result<Claims, AuthError> {
return jwt.verify(token);
}
"#;
let events = parse_diff("src/auth/service.ts", before, after);
assert!(!events.is_empty());
let change = &events[0];
match &change.payload {
EventPayload::ChangeDetected { change_type, .. } => {
assert!(matches!(change_type, ChangeType::SignatureChange));
}
_ => panic!("Expected ChangeDetected event"),
}
}
#[test]
fn test_detect_new_function() {
let before = "function existing() {}";
let after = "function existing() {}\nfunction newFunction() {}";
let events = parse_diff("src/main.ts", before, after);
let added: Vec<_> = events
.iter()
.filter(|e| {
matches!(
&e.payload,
EventPayload::ChangeDetected {
change_type: ChangeType::EntityAdded,
..
}
)
})
.collect();
assert!(!added.is_empty());
}
#[test]
fn test_detect_removed_function() {
let before = "function toRemove() {}\nfunction toKeep() {}";
let after = "function toKeep() {}";
let events = parse_diff("src/main.ts", before, after);
let removed: Vec<_> = events
.iter()
.filter(|e| {
matches!(
&e.payload,
EventPayload::ChangeDetected {
change_type: ChangeType::EntityDeleted,
..
}
)
})
.collect();
assert!(!removed.is_empty());
}
}