use serde::{Deserialize, Serialize};
use ucm_graph_core::edge::*;
use ucm_graph_core::entity::*;
use ucm_graph_core::event::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JiraTicket {
pub key: String,
pub summary: String,
pub description: String,
pub acceptance_criteria: Vec<String>,
pub linked_files: Vec<String>,
pub status: String,
}
pub fn ingest_ticket(ticket: &JiraTicket) -> Vec<UcmEvent> {
let mut events = Vec::new();
let entity_id = EntityId::local(&format!("jira/{}", ticket.key), &ticket.key);
events.push(UcmEvent::new(EventPayload::EntityDiscovered {
entity_id: entity_id.clone(),
kind: EntityKind::Requirement {
ticket_id: Some(ticket.key.clone()),
acceptance_criteria: ticket.acceptance_criteria.clone(),
},
name: format!("{}: {}", ticket.key, ticket.summary),
file_path: format!("jira/{}", ticket.key),
language: "requirement".to_string(),
source: DiscoverySource::TicketSystem,
line_range: None,
}));
events.push(UcmEvent::new(EventPayload::EntityDiscovered {
entity_id: EntityId::local(
&format!("jira/{}", ticket.key),
&format!("feature:{}", ticket.key),
),
kind: EntityKind::Feature {
description: ticket.description.clone(),
source: "jira".to_string(),
},
name: ticket.summary.clone(),
file_path: format!("jira/{}", ticket.key),
language: "requirement".to_string(),
source: DiscoverySource::TicketSystem,
line_range: None,
}));
for file_path in &ticket.linked_files {
events.push(UcmEvent::new(EventPayload::DependencyLinked {
source_entity: EntityId::local(file_path, &format!("module:{file_path}")),
target_entity: entity_id.clone(),
relation_type: RelationType::Implements,
confidence: 0.70,
source: DiscoverySource::TicketSystem,
description: format!("File {file_path} linked to ticket {}", ticket.key),
}));
}
events
}
pub fn ingest_tickets_json(json: &str) -> Result<Vec<UcmEvent>, serde_json::Error> {
let tickets: Vec<JiraTicket> = serde_json::from_str(json)?;
let mut events = Vec::new();
for ticket in &tickets {
events.extend(ingest_ticket(ticket));
}
Ok(events)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ingest_ticket() {
let ticket = JiraTicket {
key: "AUTH-42".into(),
summary: "Implement OAuth2 authentication".into(),
description: "Users should be able to log in via OAuth2 providers".into(),
acceptance_criteria: vec![
"Users can log in with Google".into(),
"Users can log in with GitHub".into(),
"Session persists for 24 hours".into(),
],
linked_files: vec!["src/auth/service.ts".into()],
status: "In Progress".into(),
};
let events = ingest_ticket(&ticket);
assert!(
events.len() >= 2,
"Should create requirement + feature entities"
);
let deps: Vec<_> = events
.iter()
.filter(|e| matches!(&e.payload, EventPayload::DependencyLinked { .. }))
.collect();
assert_eq!(deps.len(), 1, "Should link file to requirement");
}
}