use super::*;
pub(crate) struct RootCause {
pub(crate) cause: String,
pub(crate) evidence: String,
pub(crate) hint: String,
}
pub(crate) fn extract_root_cause(check: &CheckResult) -> Option<RootCause> {
let output = &check.output;
let name_lower = check.name.to_lowercase();
if name_lower.contains("cargo test")
&& let Some(root_cause) = extract_cargo_test_root_cause(output)
{
return Some(root_cause);
}
if let Some(ref prov) = check.provenance
&& (prov.exit_code == Some(-1)
|| output.contains("killed (>")
|| output.contains("timed out"))
{
return Some(RootCause {
cause: "Process timed out".into(),
evidence: format!("Exit code: {:?}", prov.exit_code),
hint: "Consider increasing timeout or investigating infinite loops".into(),
});
}
if let Some(ref prov) = check.provenance
&& !prov.hard_fail_signatures.is_empty()
{
return Some(RootCause {
cause: format!("Hard failure detected: {}", prov.hard_fail_signatures[0]),
evidence: prov.hard_fail_signatures.join(", "),
hint: "This indicates a crash or unhandled exception, not a normal check failure"
.into(),
});
}
if name_lower.contains("cargo")
&& !name_lower.contains("test")
&& !name_lower.contains("audit")
&& !name_lower.contains("geiger")
|| name_lower.contains("clippy")
{
return extract_rust_compiler_root_cause(output);
}
if name_lower.contains("audit") {
return extract_cargo_audit_root_cause(output);
}
if name_lower.contains("geiger") {
return Some(RootCause {
cause: "Cargo geiger analysis issue".into(),
evidence: output
.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or("(empty output)")
.to_string(),
hint: "Geiger scans can be slow on large dependency trees".into(),
});
}
if name_lower.contains("typescript") || name_lower == "tsc" {
return extract_typescript_root_cause(output);
}
if name_lower.contains("eslint") {
return extract_eslint_root_cause(output);
}
if name_lower.contains("stylelint") {
let violation_count = output
.lines()
.filter(|l| l.contains("✖") || l.contains("error") || l.contains("warning"))
.count();
return Some(RootCause {
cause: format!("{} style violation(s)", violation_count),
evidence: output
.lines()
.find(|l| l.contains("✖") || l.contains("error"))
.unwrap_or("")
.to_string(),
hint: "Run stylelint --fix to auto-fix what's possible".into(),
});
}
if name_lower.contains("vitest")
|| (name_lower.contains("test") && !name_lower.contains("cargo"))
{
return extract_vitest_root_cause(output);
}
if name_lower.contains("rustfmt") || name_lower.contains("fmt") {
let diff_files: Vec<&str> = output
.lines()
.filter(|l| l.starts_with("Diff in ") || l.starts_with("--- ") || l.contains(".rs"))
.take(3)
.collect();
return Some(RootCause {
cause: "Formatting differences detected".into(),
evidence: if diff_files.is_empty() {
output
.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or("")
.to_string()
} else {
diff_files.join("; ")
},
hint: "Run cargo fmt to auto-fix".into(),
});
}
if name_lower.contains("ruff") {
let error_count = output
.lines()
.filter(|l| l.contains(" error") || l.contains(" E"))
.count();
let first_error = output
.lines()
.find(|l| l.contains(":") && (l.contains(" E") || l.contains(" error")));
return Some(RootCause {
cause: format!("{} linting issue(s)", error_count.max(1)),
evidence: first_error.unwrap_or("").to_string(),
hint: "Run ruff check --fix to auto-fix what's possible".into(),
});
}
if name_lower.contains("mypy") {
if crate::checks::tool_spawn_failure_in_output(output) {
let evidence = output
.lines()
.find(|l| {
let lower = l.to_ascii_lowercase();
lower.contains("failed to spawn") || lower.contains("no such file or directory")
})
.or_else(|| output.lines().find(|l| !l.trim().is_empty()))
.unwrap_or("")
.to_string();
return Some(RootCause {
cause: "mypy not installed / could not be launched".into(),
evidence,
hint: "Install mypy (e.g. `uv add --dev mypy` or `pip install mypy`) or scope it out of this run".into(),
});
}
let error_line = output.lines().find(|l| l.contains(": error:"));
let summary = output.lines().find(|l| l.starts_with("Found "));
return Some(RootCause {
cause: summary.unwrap_or("Type checking errors").to_string(),
evidence: error_line.unwrap_or("").to_string(),
hint: "Fix type annotations or add type: ignore comments".into(),
});
}
if name_lower.contains("pytest") {
return extract_pytest_root_cause(output);
}
let first_line = output
.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or("No output");
Some(RootCause {
cause: "Check failed".into(),
evidence: first_line.to_string(),
hint: format!(
"See full log: 20_quality/{}.log",
check_id_from_name(&check.name)
),
})
}
pub(crate) fn extract_rust_compiler_root_cause(output: &str) -> Option<RootCause> {
let error_line = output
.lines()
.find(|l| l.contains("error[E") || (l.starts_with("error") && l.contains("aborting")));
let location = output.lines().find(|l| l.trim_start().starts_with("-->"));
let cause = error_line.unwrap_or("Compilation error");
let evidence = location.map(|l| l.trim().to_string()).unwrap_or_default();
Some(RootCause {
cause: cause.to_string(),
evidence,
hint: "Fix the compilation error(s) listed above".into(),
})
}
pub(crate) fn extract_cargo_test_root_cause(output: &str) -> Option<RootCause> {
let failed_tests = parsers::cargo_test::extract_failed_test_names(output);
let failed_summary = output
.lines()
.find(|l| l.starts_with("test result:") && l.contains("FAILED"));
let last_summary = output.lines().fold(None, |acc, l| {
if l.starts_with("test result:") {
Some(l)
} else {
acc
}
});
if failed_tests.is_empty() {
let error_line = output.lines().find(|line| {
let trimmed = line.trim();
trimmed.starts_with("error:")
|| trimmed.starts_with("error[")
|| trimmed.contains(": error:")
});
return error_line.map(|line| RootCause {
cause: "cargo test failed before named test failures were reported".to_string(),
evidence: line.trim().to_string(),
hint: failed_summary
.or(last_summary)
.unwrap_or("Inspect cargo_test.log for compiler or doctest output")
.to_string(),
});
}
let cause = if failed_tests.len() == 1 {
format!("1 test failed: {}", failed_tests[0])
} else {
format!("{} tests failed", failed_tests.len().max(1))
};
let hint = failed_summary
.or(last_summary)
.unwrap_or("Run cargo test to reproduce")
.to_string();
Some(RootCause {
cause,
evidence: if failed_tests.len() <= 3 {
failed_tests.join(", ")
} else {
format!(
"{}, ... and {} more",
failed_tests[..3].join(", "),
failed_tests.len() - 3
)
},
hint,
})
}
pub(crate) fn extract_cargo_audit_root_cause(output: &str) -> Option<RootCause> {
let findings = parse_cargo_audit_findings(output);
if !findings.is_empty() {
return Some(RootCause {
cause: cargo_audit_summary_cause(&findings),
evidence: cargo_audit_advisory_ids(&findings, 3),
hint: "Update the affected crates and refresh `Cargo.lock` (for example with `cargo update -p <crate>` or a dependency bump)".into(),
});
}
let vuln_line = output
.lines()
.find(|l| l.contains("vulnerabilit") || l.contains("RUSTSEC"));
let advisory = output.lines().find(|l| l.contains("RUSTSEC-"));
Some(RootCause {
cause: vuln_line
.unwrap_or("Security vulnerabilities found")
.to_string(),
evidence: advisory.unwrap_or("").to_string(),
hint: "Run cargo audit fix or update affected dependencies".into(),
})
}
pub(crate) fn extract_typescript_root_cause(output: &str) -> Option<RootCause> {
let ts_error = output
.lines()
.find(|l| l.contains("error TS") || l.contains(": error TS"));
let error_count = output.lines().filter(|l| l.contains("error TS")).count();
Some(RootCause {
cause: format!("{} TypeScript error(s)", error_count.max(1)),
evidence: ts_error.unwrap_or("").to_string(),
hint: "Fix type errors or update tsconfig.json".into(),
})
}
pub(crate) fn extract_eslint_root_cause(output: &str) -> Option<RootCause> {
let problems_line = output.lines().find(|l| l.contains(" problem"));
let first_error = output
.lines()
.find(|l| l.contains("error") && l.contains(":") && !l.contains("problem"));
Some(RootCause {
cause: problems_line.unwrap_or("ESLint errors").to_string(),
evidence: first_error.unwrap_or("").to_string(),
hint: "Run eslint --fix to auto-fix what's possible".into(),
})
}
pub(crate) fn extract_vitest_root_cause(output: &str) -> Option<RootCause> {
let mut failed_tests = Vec::new();
for line in output.lines() {
let trimmed = line.trim();
if trimmed.starts_with("FAIL") || trimmed.starts_with("×") || trimmed.contains("✗") {
failed_tests.push(trimmed.to_string());
}
}
let summary = output
.lines()
.find(|l| l.contains("Tests") && (l.contains("failed") || l.contains("passed")));
let cause = if !failed_tests.is_empty() {
format!("{} test(s) failed", failed_tests.len())
} else {
"Test suite failed".into()
};
Some(RootCause {
cause,
evidence: if failed_tests.len() <= 3 {
failed_tests.join("; ")
} else {
format!(
"{}, ... and {} more",
failed_tests[..3].join("; "),
failed_tests.len() - 3
)
},
hint: summary.unwrap_or("Run test suite to reproduce").to_string(),
})
}
pub(crate) fn extract_pytest_root_cause(output: &str) -> Option<RootCause> {
let summary = output.lines().find(|l| {
l.contains("failed") && l.contains("passed")
|| l.starts_with("FAILED")
|| l.contains("error")
});
let first_failure = output
.lines()
.find(|l| l.starts_with("FAILED") || l.contains("ERRORS"));
Some(RootCause {
cause: summary.unwrap_or("Pytest failures").to_string(),
evidence: first_failure.unwrap_or("").to_string(),
hint: "Run pytest -x to reproduce first failure".into(),
})
}