use crate::tasks::{TaskStatus, TasksFormat, TasksParseResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TraceStatus {
Ready,
Invalid {
missing_ids: Vec<String>,
},
Unavailable {
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoveredRequirement {
pub requirement_id: String,
pub covering_tasks: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnresolvedReference {
pub task_id: String,
pub requirement_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceabilityResult {
pub status: TraceStatus,
pub declared_requirements: Vec<String>,
pub covered_requirements: Vec<CoveredRequirement>,
pub uncovered_requirements: Vec<String>,
pub unresolved_references: Vec<UnresolvedReference>,
pub diagnostics: Vec<String>,
}
pub fn compute_traceability(
delta_requirements: &[(String, Option<String>)],
tasks: &TasksParseResult,
) -> TraceabilityResult {
let mut diagnostics: Vec<String> = Vec::new();
let total = delta_requirements.len();
let mut with_id: Vec<&(String, Option<String>)> = Vec::new();
let mut without_id: Vec<&(String, Option<String>)> = Vec::new();
for req in delta_requirements {
if req.1.is_some() {
with_id.push(req);
} else {
without_id.push(req);
}
}
if total == 0 || with_id.is_empty() {
return TraceabilityResult {
status: TraceStatus::Unavailable {
reason: "No requirement IDs declared in delta specs".to_string(),
},
declared_requirements: Vec::new(),
covered_requirements: Vec::new(),
uncovered_requirements: Vec::new(),
unresolved_references: Vec::new(),
diagnostics,
};
}
if tasks.format == TasksFormat::Checkbox {
return TraceabilityResult {
status: TraceStatus::Unavailable {
reason: "Tasks use checkbox format; enhanced format required for traceability"
.to_string(),
},
declared_requirements: Vec::new(),
covered_requirements: Vec::new(),
uncovered_requirements: Vec::new(),
unresolved_references: Vec::new(),
diagnostics,
};
}
if !without_id.is_empty() {
let mut missing_ids: Vec<String> = Vec::with_capacity(without_id.len());
for (title, _) in &without_id {
missing_ids.push(title.clone());
}
return TraceabilityResult {
status: TraceStatus::Invalid {
missing_ids: missing_ids.clone(),
},
declared_requirements: Vec::new(),
covered_requirements: Vec::new(),
uncovered_requirements: Vec::new(),
unresolved_references: Vec::new(),
diagnostics,
};
}
let mut declared: Vec<String> = Vec::with_capacity(with_id.len());
for (_, id) in &with_id {
declared.push(id.as_ref().unwrap().clone());
}
let mut seen_ids: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for id in &declared {
if !seen_ids.insert(id.clone()) {
diagnostics.push(format!("Duplicate requirement ID: {id}"));
}
}
declared.sort();
declared.dedup();
let declared_set: std::collections::BTreeSet<&str> =
declared.iter().map(String::as_str).collect();
let mut coverage: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
let mut unresolved: Vec<UnresolvedReference> = Vec::new();
for task in &tasks.tasks {
if task.status == TaskStatus::Shelved {
continue;
}
for req_id in &task.requirements {
if declared_set.contains(req_id.as_str()) {
coverage
.entry(req_id.clone())
.or_default()
.push(task.id.clone());
} else {
unresolved.push(UnresolvedReference {
task_id: task.id.clone(),
requirement_id: req_id.clone(),
});
}
}
}
let mut covered: Vec<CoveredRequirement> = Vec::new();
let mut uncovered: Vec<String> = Vec::new();
for req_id in &declared {
if let Some(covering_tasks) = coverage.get(req_id) {
covered.push(CoveredRequirement {
requirement_id: req_id.clone(),
covering_tasks: covering_tasks.clone(),
});
} else {
uncovered.push(req_id.clone());
}
}
TraceabilityResult {
status: TraceStatus::Ready,
declared_requirements: declared,
covered_requirements: covered,
uncovered_requirements: uncovered,
unresolved_references: unresolved,
diagnostics,
}
}