use super::types::ParseResult;
use crate::files::llm_output_extraction::xsd_validation::XsdValidationError;
use crate::files::llm_output_extraction::{
archive_xml_file_with_workspace, try_extract_from_file_with_workspace, validate_issues_xml,
xml_paths, IssuesElements,
};
use crate::phases::context::PhaseContext;
use crate::rendering::xml::render_skills_mcp_markdown;
use std::path::Path;
pub(super) fn extract_and_validate_review_output_xml(
ctx: &PhaseContext<'_>,
log_dir: &str,
issues_path: &Path,
) -> anyhow::Result<ParseResult> {
if let Some(xml_content) =
try_extract_from_file_with_workspace(ctx.workspace, Path::new(xml_paths::ISSUES_XML))
{
ctx.logger
.info("Found XML in .agent/tmp/issues.xml (file-based mode)");
return validate_and_process_issues_xml(ctx, &xml_content, issues_path);
}
if ctx.config.verbosity.is_debug() {
ctx.logger.info(&format!(
"Review output missing at .agent/tmp/issues.xml; expected log prefix: {log_dir}"
));
}
Ok(ParseResult::ParseFailed(
"No review output captured. Agent did not write to .agent/tmp/issues.xml. \
Ensure the agent produces valid XML output via the configured effects."
.to_string(),
))
}
pub(super) fn validate_and_process_issues_xml(
ctx: &PhaseContext<'_>,
xml_content: &str,
issues_path: &Path,
) -> anyhow::Result<ParseResult> {
let validated: Result<IssuesElements, XsdValidationError> = validate_issues_xml(xml_content);
match validated {
Ok(elements) => {
let markdown = render_issues_markdown(&elements);
ctx.workspace.write(issues_path, &markdown)?;
archive_xml_file_with_workspace(ctx.workspace, Path::new(xml_paths::ISSUES_XML));
if elements.no_issues_found.is_some() {
return Ok(ParseResult::NoIssuesExplicit {
xml_content: xml_content.to_string(),
});
}
if !elements.issues.is_empty() {
return Ok(ParseResult::IssuesFound {
issues: elements.issue_texts(),
xml_content: xml_content.to_string(),
});
}
Ok(ParseResult::ParseFailed(
"XML validated but contains no issues or no-issues-found element.".to_string(),
))
}
Err(xsd_error) => {
Ok(ParseResult::ParseFailed(xsd_error.format_for_ai_retry()))
}
}
}
fn render_issues_markdown(elements: &IssuesElements) -> String {
if let Some(message) = &elements.no_issues_found {
let trimmed = message.trim();
return if trimmed.is_empty() {
"# Issues\n\nNo issues found.\n".to_string()
} else {
format!("# Issues\n\n{}\n", trimmed)
};
}
if elements.issues.is_empty() {
return "# Issues\n\nNo issues found.\n".to_string();
}
let issues_markdown: String = elements
.issues
.iter()
.filter_map(|issue| {
let trimmed = issue.text.trim();
if trimmed.is_empty() {
None
} else {
let skills_markdown = render_skills_mcp_markdown(issue.skills_mcp.as_ref());
Some(format!("- [ ] {}\n{}", trimmed, skills_markdown))
}
})
.collect::<Vec<_>>()
.join("");
format!("# Issues\n\n{}", issues_markdown)
}