1use crate::error_bridge::IntoCoreResult;
8use crate::errors::{CoreError, CoreResult};
9use crate::show::{parse_change_show_json, read_change_delta_spec_files};
10use ito_domain::changes::ChangeRepository;
11use ito_domain::traceability::{TraceStatus, compute_traceability};
12use serde::Serialize;
13
14#[derive(Debug, Clone, Serialize)]
16pub struct CoveredEntry {
17 pub requirement_id: String,
19 pub covering_tasks: Vec<String>,
21}
22
23#[derive(Debug, Clone, Serialize)]
25pub struct UnresolvedEntry {
26 pub task_id: String,
28 pub requirement_id: String,
30}
31
32#[derive(Debug, Clone, Serialize)]
34pub struct TraceOutput {
35 pub change_id: String,
37 pub lifecycle: String,
39 pub status: String,
41 pub reason: Option<String>,
43 pub declared_requirements: Vec<String>,
45 pub covered: Vec<CoveredEntry>,
47 pub uncovered: Vec<String>,
49 pub unresolved: Vec<UnresolvedEntry>,
51 pub diagnostics: Vec<String>,
53}
54
55pub fn compute_trace_output(
72 change_repo: &(impl ChangeRepository + ?Sized),
73 change_id: &str,
74) -> CoreResult<TraceOutput> {
75 let change = change_repo.get(change_id).into_core()?;
76
77 let lifecycle = {
79 let path_str = change.path.to_string_lossy();
80 if path_str.contains("/archive/") {
81 "archived".to_string()
82 } else {
83 "active".to_string()
84 }
85 };
86
87 let delta_files = read_change_delta_spec_files(change_repo, change_id)?;
88 if delta_files.is_empty() {
89 return Err(CoreError::not_found(format!(
90 "No delta spec files found for change '{change_id}'"
91 )));
92 }
93
94 let show = parse_change_show_json(change_id, &delta_files);
95
96 let mut delta_requirements: Vec<(String, Option<String>)> = Vec::new();
98 for d in &show.deltas {
99 for req in &d.requirements {
100 delta_requirements.push((req.text.clone(), req.requirement_id.clone()));
101 }
102 }
103
104 let trace_result = compute_traceability(&delta_requirements, &change.tasks);
105
106 let (status, reason) = match &trace_result.status {
107 TraceStatus::Ready => ("ready".to_string(), None),
108 TraceStatus::Invalid { missing_ids } => {
109 let reason = format!("Requirements missing IDs: {}", missing_ids.join(", "));
110 ("invalid".to_string(), Some(reason))
111 }
112 TraceStatus::Unavailable { reason } => ("unavailable".to_string(), Some(reason.clone())),
113 };
114
115 let mut covered = Vec::new();
116 for c in &trace_result.covered_requirements {
117 covered.push(CoveredEntry {
118 requirement_id: c.requirement_id.clone(),
119 covering_tasks: c.covering_tasks.clone(),
120 });
121 }
122
123 let mut unresolved = Vec::new();
124 for u in &trace_result.unresolved_references {
125 unresolved.push(UnresolvedEntry {
126 task_id: u.task_id.clone(),
127 requirement_id: u.requirement_id.clone(),
128 });
129 }
130
131 Ok(TraceOutput {
132 change_id: change_id.to_string(),
133 lifecycle,
134 status,
135 reason,
136 declared_requirements: trace_result.declared_requirements,
137 covered,
138 uncovered: trace_result.uncovered_requirements,
139 unresolved,
140 diagnostics: trace_result.diagnostics,
141 })
142}