difflore-core 0.2.0

Core library for the difflore CLI — rule store, retrieval, MCP server, hooks, cloud sync. Not intended for direct use; depend on `difflore-cli` instead.
use super::super::parse::{parse_summary_response, parse_verify_response};
use super::super::prompts::{
    SUMMARY_SYSTEM_PROMPT, VERIFY_SYSTEM_PROMPT, build_summary_user_prompt,
    build_verify_user_prompt,
};
use super::super::{ReviewIssueRecord, ReviewLlm};
use super::{collect_diff_files, count_blocking};
use crate::domain::models::ReviewSummary;

/// Self-check: re-run a cheap LLM pass over merged issues to score
/// confidence and drop obvious false positives.
///
/// Graceful degradation is the #1 invariant: on any failure (disabled,
/// empty, LLM error, parser error) the candidate issues come back
/// untouched. We never drop issues because the verify pass broke.
pub(in super::super) async fn verify_pass(
    llm: &dyn ReviewLlm,
    self_check_enabled: bool,
    diff: &str,
    issues: Vec<ReviewIssueRecord>,
) -> Vec<ReviewIssueRecord> {
    if !self_check_enabled {
        return issues;
    }
    if issues.is_empty() {
        return issues;
    }
    let original_issues = issues.clone();

    let user_prompt = build_verify_user_prompt(diff, &issues);
    let response = match llm.chat(VERIFY_SYSTEM_PROMPT, &user_prompt).await {
        Ok(r) => r,
        Err(e) => {
            if crate::infra::env::fix_debug() {
                eprintln!("[verify_pass] cheap-model call failed: {e:?}");
            }
            return issues;
        }
    };

    let map = match parse_verify_response(&response) {
        Some(m) if !m.is_empty() => m,
        _ => {
            if crate::infra::env::fix_debug() {
                eprintln!(
                    "[verify_pass] could not parse verify response; keeping issues unchanged"
                );
            }
            return issues;
        }
    };

    let mut out: Vec<ReviewIssueRecord> = Vec::with_capacity(issues.len());
    for (idx, mut issue) in issues.into_iter().enumerate() {
        match map.get(&idx) {
            Some((confidence, keep)) => {
                if !*keep {
                    continue;
                }
                issue.confidence = *confidence;
                out.push(issue);
            }
            None => {
                // Model didn't score this one — keep it rather than drop.
                out.push(issue);
            }
        }
    }

    if out.is_empty() && !original_issues.is_empty() {
        if crate::infra::env::fix_debug() {
            eprintln!(
                "[verify_pass] verifier dropped every candidate issue; keeping original issues to avoid a false-negative review"
            );
        }
        return original_issues;
    }

    out
}

/// Review summary: emit a one-line PR description plus per-file walkthrough and
/// blocking / non-blocking counts. Graceful: returns `None` on any error.
pub(in super::super) async fn run_review_summary(
    llm: &dyn ReviewLlm,
    review_summary_enabled: bool,
    diff: &str,
    issues: &[ReviewIssueRecord],
) -> Option<ReviewSummary> {
    if !review_summary_enabled {
        return None;
    }
    if diff.is_empty() {
        return None;
    }

    let files = collect_diff_files(diff);
    let user_prompt = build_summary_user_prompt(diff, &files);
    let response = match llm.chat(SUMMARY_SYSTEM_PROMPT, &user_prompt).await {
        Ok(r) => r,
        Err(e) => {
            if crate::infra::env::fix_debug() {
                eprintln!("[run_review_summary] cheap-model call failed: {e:?}");
            }
            return None;
        }
    };

    let (one_line, walkthrough) = parse_summary_response(&response)?;
    let (blocking_count, non_blocking_count) = count_blocking(issues);

    Some(ReviewSummary {
        one_line_summary: one_line,
        walkthrough_by_file: walkthrough,
        blocking_count,
        non_blocking_count,
    })
}