use std::sync::Arc; use compliance_core::models::Finding; use crate::error::AgentError; use crate::llm::LlmClient; const DESCRIPTION_SYSTEM_PROMPT: &str = r#"You are a security engineer writing a bug tracker issue for a developer to fix. Be direct and actionable — developers skim issue descriptions, so lead with what matters. Format in Markdown: 1. **What**: 1 sentence — what's wrong and where (file:line) 2. **Why it matters**: 1-2 sentences — concrete impact if not fixed. Avoid generic "could lead to" phrasing; describe the specific attack or failure scenario. 3. **Fix**: The specific code change needed. Use a code block with the corrected code if possible. If the fix is configuration-based, show the exact config change. 4. **References**: CWE/CVE link if applicable (one line, not a section) Rules: - No filler paragraphs or background explanations - No restating the finding title in the body - Code blocks should show the FIX, not the vulnerable code (the developer can see that in the diff) - If the remediation is a one-liner, just say it — don't wrap it in a section header"#; pub async fn generate_issue_description( llm: &Arc, finding: &Finding, ) -> Result<(String, String), AgentError> { let user_prompt = format!( "Generate an issue title and body for this finding:\n\ Scanner: {}\n\ Type: {}\n\ Severity: {}\n\ Rule: {}\n\ Title: {}\n\ Description: {}\n\ File: {}\n\ Line: {}\n\ Code:\n```\n{}\n```\n\ CWE: {}\n\ CVE: {}\n\ Remediation hint: {}", finding.scanner, finding.scan_type, finding.severity, finding.rule_id.as_deref().unwrap_or("N/A"), finding.title, finding.description, finding.file_path.as_deref().unwrap_or("N/A"), finding .line_number .map(|n| n.to_string()) .unwrap_or_else(|| "N/A".to_string()), finding.code_snippet.as_deref().unwrap_or("N/A"), finding.cwe.as_deref().unwrap_or("N/A"), finding.cve.as_deref().unwrap_or("N/A"), finding.remediation.as_deref().unwrap_or("N/A"), ); let response = llm .chat(DESCRIPTION_SYSTEM_PROMPT, &user_prompt, Some(0.3)) .await?; // Extract title from first line, rest is body let mut lines = response.lines(); let title = lines .next() .unwrap_or(&finding.title) .trim_start_matches('#') .trim() .to_string(); let body = lines.collect::>().join("\n").trim().to_string(); let body = if body.is_empty() { response } else { body }; Ok((title, body)) }