use ucm_graph_core::edge::UcmEdge;
use ucm_graph_core::entity::UcmEntity;
use ucm_graph_core::event::{EventPayload, UcmEvent};
use ucm_graph_core::graph::UcmGraph;
pub struct GraphProjection;
impl GraphProjection {
pub fn replay_all(events: &[UcmEvent]) -> UcmGraph {
let mut graph = UcmGraph::new();
for event in events {
Self::apply_event(&mut graph, event);
}
graph
}
pub fn apply_event(graph: &mut UcmGraph, event: &UcmEvent) {
match &event.payload {
EventPayload::EntityDiscovered {
entity_id,
kind,
name,
file_path,
language,
source,
line_range,
} => {
let mut entity = UcmEntity::new(
entity_id.clone(),
kind.clone(),
name.clone(),
file_path.clone(),
language.clone(),
source.clone(),
);
if let Some((start, end)) = line_range {
entity = entity.with_line_range(*start, *end);
}
graph.upsert_entity(entity);
}
EventPayload::EntityRemoved { entity_id, .. } => {
if let Some(file_path) = entity_id.file_path() {
graph.invalidate_file(file_path);
}
}
EventPayload::DependencyLinked {
source_entity,
target_entity,
relation_type,
confidence,
source,
description,
} => {
let edge = UcmEdge::new(
relation_type.clone(),
source.clone(),
*confidence,
description.clone(),
);
let _ = graph.add_relationship(source_entity, target_entity, edge);
}
EventPayload::ChangeDetected { file_path, .. } => {
graph.invalidate_file(file_path);
}
_ => {}
}
}
pub fn apply_batch(graph: &mut UcmGraph, events: &[UcmEvent]) {
for event in events {
Self::apply_event(graph, event);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ucm_graph_core::edge::RelationType;
use ucm_graph_core::entity::*;
#[test]
fn test_replay_builds_graph() {
let events = vec![
UcmEvent::new(EventPayload::EntityDiscovered {
entity_id: EntityId::local("src/auth/service.ts", "validateToken"),
kind: EntityKind::Function {
is_async: true,
parameter_count: 1,
return_type: Some("boolean".into()),
},
name: "validateToken".into(),
file_path: "src/auth/service.ts".into(),
language: "typescript".into(),
source: DiscoverySource::StaticAnalysis,
line_range: Some((10, 25)),
}),
UcmEvent::new(EventPayload::EntityDiscovered {
entity_id: EntityId::local("src/api/middleware.ts", "authMiddleware"),
kind: EntityKind::Function {
is_async: true,
parameter_count: 2,
return_type: None,
},
name: "authMiddleware".into(),
file_path: "src/api/middleware.ts".into(),
language: "typescript".into(),
source: DiscoverySource::StaticAnalysis,
line_range: None,
}),
UcmEvent::new(EventPayload::DependencyLinked {
source_entity: EntityId::local("src/api/middleware.ts", "authMiddleware"),
target_entity: EntityId::local("src/auth/service.ts", "validateToken"),
relation_type: RelationType::Imports,
confidence: 0.95,
source: DiscoverySource::StaticAnalysis,
description: "import statement".into(),
}),
];
let graph = GraphProjection::replay_all(&events);
let stats = graph.stats();
assert_eq!(stats.entity_count, 2);
assert_eq!(stats.edge_count, 1);
}
#[test]
fn test_replay_is_idempotent() {
let events = vec![UcmEvent::new(EventPayload::EntityDiscovered {
entity_id: EntityId::local("src/main.rs", "main"),
kind: EntityKind::Function {
is_async: false,
parameter_count: 0,
return_type: None,
},
name: "main".into(),
file_path: "src/main.rs".into(),
language: "rust".into(),
source: DiscoverySource::StaticAnalysis,
line_range: None,
})];
let graph1 = GraphProjection::replay_all(&events);
let graph2 = GraphProjection::replay_all(&events);
assert_eq!(graph1.stats().entity_count, graph2.stats().entity_count);
}
}