repopilot 0.14.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use repopilot::compare::diff::diff_summaries;
use repopilot::compare::render::render_json;
use repopilot::findings::types::{Evidence, Finding, FindingCategory, Severity};
use repopilot::scan::types::ScanSummary;
use std::path::PathBuf;

#[test]
fn compare_uses_rule_and_evidence_as_stable_key() {
    let before = summary(vec![finding(
        "old-generated-id",
        "architecture.large-file",
        "src/main.rs",
        1,
        Severity::Medium,
    )]);
    let after = summary(vec![finding(
        "new-generated-id",
        "architecture.large-file",
        "src/main.rs",
        1,
        Severity::High,
    )]);

    let diff = diff_summaries(&before, &after);

    assert!(diff.new_findings.is_empty());
    assert!(diff.resolved_findings.is_empty());
    assert_eq!(diff.severity_increased.len(), 1);
}

#[test]
fn compare_reports_new_and_resolved_findings() {
    let before = summary(vec![finding(
        "a",
        "code-marker.todo",
        "src/old.rs",
        2,
        Severity::Low,
    )]);
    let after = summary(vec![finding(
        "b",
        "security.secret-candidate",
        "src/new.rs",
        4,
        Severity::High,
    )]);

    let diff = diff_summaries(&before, &after);

    assert_eq!(diff.new_findings.len(), 1);
    assert_eq!(diff.resolved_findings.len(), 1);
}

#[test]
fn compare_json_renders_valid_json() {
    let diff = diff_summaries(&summary(vec![]), &summary(vec![]));

    let rendered = render_json(&diff).expect("failed to render json");
    let value: serde_json::Value = serde_json::from_str(&rendered).expect("invalid json");

    assert_eq!(value["new_findings"].as_array().unwrap().len(), 0);
}

fn summary(findings: Vec<Finding>) -> ScanSummary {
    let visible_findings_count = findings.len();
    ScanSummary {
        metadata: repopilot::scan::types::ScanMetadata {
            root_path: PathBuf::from("demo"),
            ..Default::default()
        },
        metrics: repopilot::scan::types::ScanMetrics {
            files_analyzed: 1,
            raw_findings_count: visible_findings_count,
            visible_findings_count,
            ..Default::default()
        },
        artifacts: repopilot::scan::types::ScanArtifacts {
            findings,
            ..Default::default()
        },
    }
}

fn finding(id: &str, rule_id: &str, path: &str, line: usize, severity: Severity) -> Finding {
    Finding {
        id: id.to_string(),
        rule_id: rule_id.to_string(),
        recommendation: Finding::recommendation_for_rule_id(rule_id),
        title: "Finding".to_string(),
        description: "Description".to_string(),
        category: FindingCategory::Architecture,
        severity,
        confidence: Default::default(),
        evidence: vec![Evidence {
            path: PathBuf::from(path),
            line_start: line,
            line_end: None,
            snippet: "snippet".to_string(),
        }],
        workspace_package: None,
        docs_url: None,
        provenance: Default::default(),
        risk: Default::default(),
    }
}