difflore-cli 0.2.0

Your AI coding agent learned public code, not your team's private decisions. difflore turns past PR reviews into source-backed local rules.
//! "Proven rule" drilldown: the single rule (repo-scoped, else best-on-machine)
//! with the most accepted-edit proofs, merging signed local fix outcomes with
//! agent-hook accepted outcomes.

use super::super::transform::ProvenRuleCandidate;
use super::proof_counters::{
    LOCAL_ACCEPTED_RECALL_LOOKBACK_DAYS, LOCAL_PROOF_WINDOW_DAYS, normalized_repo_aliases,
};
use crate::support::proven_rule::{ProvenRuleRank, fetch_rule_metadata_for_ids};

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub(in crate::commands::status) struct ProvenRuleDrilldown {
    pub(in crate::commands::status) rule_id: String,
    pub(in crate::commands::status) name: String,
    pub(in crate::commands::status) source_repo: Option<String>,
    pub(in crate::commands::status) accepted_fixes: i64,
    pub(in crate::commands::status) accepted_fix_proofs: i64,
    pub(in crate::commands::status) accepted_hook_outcomes: i64,
    pub(in crate::commands::status) accepted_hook_outcomes_linked_to_prior_recall: i64,
    pub(in crate::commands::status) accepted_hook_outcomes_linked_to_recall_or_edit_proof: i64,
    pub(in crate::commands::status) accepted_hook_outcomes_linked_to_rule_recall: i64,
    pub(in crate::commands::status) accepted_hook_outcomes_linked_to_mcp_rule_serve: i64,
    pub(in crate::commands::status) accepted_hook_outcomes_linked_to_edit_attribution: i64,
    pub(in crate::commands::status) sample_file: Option<String>,
    pub(in crate::commands::status) explain_command: String,
}

#[derive(Debug, Clone, sqlx::FromRow)]
pub(super) struct ProvenRuleDrilldownRow {
    pub(super) rule_id: String,
    pub(super) name: String,
    pub(super) source_repo: Option<String>,
    pub(super) accepted_fix_proofs: i64,
    pub(super) sample_file: Option<String>,
}

pub(in crate::commands::status) async fn local_proven_rule_drilldown(
    db: &difflore_core::SqlitePool,
    repo_aliases: &[String],
) -> Option<ProvenRuleDrilldown> {
    let normalized_aliases = normalized_repo_aliases(repo_aliases);
    if normalized_aliases.is_empty() {
        return None;
    }

    let hook_summaries =
        match difflore_core::cloud::observations::ObservationEmitter::open_default().await {
            Ok(emitter) => emitter
                .accepted_fix_outcome_rule_summaries(
                    LOCAL_PROOF_WINDOW_DAYS,
                    LOCAL_ACCEPTED_RECALL_LOOKBACK_DAYS,
                )
                .await
                .unwrap_or_default(),
            Err(_) => Vec::new(),
        };

    if let Some(rule) =
        fetch_proven_rule_drilldown(db, Some(&normalized_aliases), &hook_summaries).await
    {
        return Some(rule);
    }

    None
}

pub(in crate::commands::status) async fn fetch_proven_rule_drilldown(
    db: &difflore_core::SqlitePool,
    normalized_repos: Option<&[String]>,
    hook_summaries: &[difflore_core::cloud::observations::AcceptedFixOutcomeRuleSummary],
) -> Option<ProvenRuleDrilldown> {
    let mut candidates: std::collections::BTreeMap<String, ProvenRuleCandidate> =
        std::collections::BTreeMap::new();

    for row in fetch_signed_proven_rule_rows(db, normalized_repos).await {
        candidates.insert(
            row.rule_id.clone(),
            ProvenRuleCandidate {
                rule_id: row.rule_id,
                name: row.name,
                source_repo: row.source_repo,
                accepted_fix_proofs: row.accepted_fix_proofs,
                accepted_hook_outcomes: 0,
                accepted_hook_outcomes_linked_to_prior_recall: 0,
                accepted_hook_outcomes_linked_to_recall_or_edit_proof: 0,
                accepted_hook_outcomes_linked_to_rule_recall: 0,
                accepted_hook_outcomes_linked_to_mcp_rule_serve: 0,
                accepted_hook_outcomes_linked_to_edit_attribution: 0,
                sample_file: row.sample_file,
            },
        );
    }

    let hook_ids: Vec<String> = hook_summaries
        .iter()
        .map(|summary| summary.rule_id.clone())
        .collect();
    let metadata = fetch_rule_metadata_for_ids(db, &hook_ids, normalized_repos).await;
    for summary in hook_summaries {
        let Some(rule) = metadata.get(&summary.rule_id) else {
            continue;
        };
        let candidate = candidates
            .entry(summary.rule_id.clone())
            .or_insert_with(|| ProvenRuleCandidate {
                rule_id: summary.rule_id.clone(),
                name: rule.name.clone(),
                source_repo: rule.source_repo.clone(),
                accepted_fix_proofs: 0,
                accepted_hook_outcomes: 0,
                accepted_hook_outcomes_linked_to_prior_recall: 0,
                accepted_hook_outcomes_linked_to_recall_or_edit_proof: 0,
                accepted_hook_outcomes_linked_to_rule_recall: 0,
                accepted_hook_outcomes_linked_to_mcp_rule_serve: 0,
                accepted_hook_outcomes_linked_to_edit_attribution: 0,
                sample_file: None,
            });
        candidate.accepted_hook_outcomes += summary.accepted_outcomes;
        candidate.accepted_hook_outcomes_linked_to_prior_recall += summary.linked_to_prior_recall;
        candidate.accepted_hook_outcomes_linked_to_recall_or_edit_proof +=
            summary.linked_to_prior_recall;
        candidate.accepted_hook_outcomes_linked_to_rule_recall += summary.linked_to_rule_recall;
        candidate.accepted_hook_outcomes_linked_to_mcp_rule_serve +=
            summary.linked_to_mcp_rule_serve;
        candidate.accepted_hook_outcomes_linked_to_edit_attribution +=
            summary.linked_to_edit_attribution;
        if candidate
            .sample_file
            .as_deref()
            .unwrap_or("")
            .trim()
            .is_empty()
            && let Some(file) = summary
                .sample_file
                .as_deref()
                .map(str::trim)
                .filter(|file| !file.is_empty())
        {
            candidate.sample_file = Some(file.to_owned());
        }
    }

    let mut candidates: Vec<_> = candidates
        .into_values()
        .filter(|candidate| candidate.accepted_fix_proofs + candidate.accepted_hook_outcomes > 0)
        .collect();
    candidates.sort_by(|a, b| {
        ProvenRuleRank {
            total: a.accepted_fix_proofs + a.accepted_hook_outcomes,
            linked_to_prior_recall: a.accepted_hook_outcomes_linked_to_prior_recall,
            accepted_fix_proofs: a.accepted_fix_proofs,
            name: &a.name,
        }
        .cmp(&ProvenRuleRank {
            total: b.accepted_fix_proofs + b.accepted_hook_outcomes,
            linked_to_prior_recall: b.accepted_hook_outcomes_linked_to_prior_recall,
            accepted_fix_proofs: b.accepted_fix_proofs,
            name: &b.name,
        })
    });

    candidates
        .into_iter()
        .next()
        .map(|candidate| ProvenRuleDrilldown {
            explain_command: "difflore status --json".to_owned(),
            accepted_fixes: candidate.accepted_fix_proofs + candidate.accepted_hook_outcomes,
            accepted_fix_proofs: candidate.accepted_fix_proofs,
            accepted_hook_outcomes: candidate.accepted_hook_outcomes,
            accepted_hook_outcomes_linked_to_prior_recall: candidate
                .accepted_hook_outcomes_linked_to_prior_recall,
            accepted_hook_outcomes_linked_to_recall_or_edit_proof: candidate
                .accepted_hook_outcomes_linked_to_recall_or_edit_proof,
            accepted_hook_outcomes_linked_to_rule_recall: candidate
                .accepted_hook_outcomes_linked_to_rule_recall,
            accepted_hook_outcomes_linked_to_mcp_rule_serve: candidate
                .accepted_hook_outcomes_linked_to_mcp_rule_serve,
            accepted_hook_outcomes_linked_to_edit_attribution: candidate
                .accepted_hook_outcomes_linked_to_edit_attribution,
            rule_id: candidate.rule_id,
            name: candidate.name,
            source_repo: candidate.source_repo,
            sample_file: candidate.sample_file,
        })
}

async fn fetch_signed_proven_rule_rows(
    db: &difflore_core::SqlitePool,
    normalized_repos: Option<&[String]>,
) -> Vec<ProvenRuleDrilldownRow> {
    let repo_filter = normalized_repos
        .filter(|repos| !repos.is_empty())
        .map(|repos| {
            let placeholders = std::iter::repeat_n("?", repos.len())
                .collect::<Vec<_>>()
                .join(", ");
            format!("AND LOWER(COALESCE(s.source_repo, '')) IN ({placeholders})")
        })
        .unwrap_or_default();
    let sql = format!(
        "SELECT f.rule_id AS rule_id,
                COALESCE(NULLIF(s.name, ''), f.rule_name) AS name,
                s.source_repo AS source_repo,
                COUNT(*) AS accepted_fix_proofs,
                MAX(NULLIF(f.file_path, '')) AS sample_file
         FROM fix_outcomes f
         INNER JOIN skills s ON s.id = f.rule_id
         WHERE f.accepted = 1
           AND f.applied_ok = 1
           AND f.rule_id IS NOT NULL
           AND COALESCE(s.status, 'active') = 'active'
           {repo_filter}
         GROUP BY f.rule_id, COALESCE(NULLIF(s.name, ''), f.rule_name), s.source_repo
         ORDER BY COUNT(*) DESC, MAX(f.created_at) DESC
         LIMIT 20"
    );
    let mut query = sqlx::query_as::<_, ProvenRuleDrilldownRow>(&sql);
    if let Some(repos) = normalized_repos {
        for repo in repos {
            query = query.bind(repo);
        }
    }

    query.fetch_all(db).await.unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::super::test_support::{
        ProvenRuleSeed, insert_proven_rule, insert_skill_only, proven_rule_pool,
    };
    use super::*;

    #[tokio::test]
    async fn local_proven_rule_drilldown_prefers_current_repo_alias() {
        let pool = proven_rule_pool().await;
        insert_proven_rule(
            &pool,
            ProvenRuleSeed {
                id: "router",
                name: "Pin Actions to commit SHAs",
                repo: "tanstack/router",
                file: ".github/workflows/ci.yml",
                accepted: 3,
            },
        )
        .await;
        insert_proven_rule(
            &pool,
            ProvenRuleSeed {
                id: "gin",
                name: "Return 413 for large request bodies",
                repo: "gin-gonic/gin",
                file: "binding/binding.go",
                accepted: 1,
            },
        )
        .await;

        let scoped = fetch_proven_rule_drilldown(
            &pool,
            Some(&[
                "difflore-fixtures/gin".to_owned(),
                "gin-gonic/gin".to_owned(),
            ]),
            &[],
        )
        .await
        .expect("scoped proven rule");
        assert_eq!(scoped.rule_id, "gin");
        assert_eq!(scoped.source_repo.as_deref(), Some("gin-gonic/gin"));
        assert_eq!(scoped.accepted_fixes, 1);
        assert_eq!(scoped.accepted_fix_proofs, 1);
        assert_eq!(scoped.accepted_hook_outcomes, 0);
        assert_eq!(scoped.explain_command, "difflore status --json");

        let scoped_none =
            fetch_proven_rule_drilldown(&pool, Some(&["unknown/repo".to_owned()]), &[])
                .await
                .is_none();
        assert!(scoped_none, "scoped lookup should not leak other repos");
    }

    #[tokio::test]
    async fn local_proven_rule_drilldown_does_not_global_fallback_when_repo_is_known() {
        let pool = proven_rule_pool().await;
        insert_proven_rule(
            &pool,
            ProvenRuleSeed {
                id: "router",
                name: "Pin Actions to commit SHAs",
                repo: "tanstack/router",
                file: ".github/workflows/ci.yml",
                accepted: 3,
            },
        )
        .await;

        let scoped =
            local_proven_rule_drilldown(&pool, &["difflore-fixtures/store".to_owned()]).await;
        assert!(
            scoped.is_none(),
            "status must not advertise a global proven rule that current-repo recall/MCP cannot serve"
        );

        let no_scope = local_proven_rule_drilldown(&pool, &[]).await;
        assert!(
            no_scope.is_none(),
            "status must not advertise unrelated proven rules when no repo scope is known"
        );
    }

    #[tokio::test]
    async fn proven_rule_drilldown_includes_agent_hook_accepted_outcomes() {
        let pool = proven_rule_pool().await;
        insert_skill_only(
            &pool,
            "agent-rule",
            "Prefer structured API parsing",
            "acme/widgets",
        )
        .await;
        let hook_summaries = vec![
            difflore_core::cloud::observations::AcceptedFixOutcomeRuleSummary {
                rule_id: "agent-rule".to_owned(),
                accepted_outcomes: 2,
                linked_to_prior_recall: 1,
                linked_to_rule_recall: 0,
                linked_to_mcp_rule_serve: 1,
                linked_to_edit_attribution: 0,
                sample_file: Some("src/parser.rs".to_owned()),
                latest_occurred_at_ms: 123,
            },
        ];

        let drilldown =
            fetch_proven_rule_drilldown(&pool, Some(&["acme/widgets".to_owned()]), &hook_summaries)
                .await
                .expect("hook proof drilldown");

        assert_eq!(drilldown.rule_id, "agent-rule");
        assert_eq!(drilldown.accepted_fixes, 2);
        assert_eq!(drilldown.accepted_fix_proofs, 0);
        assert_eq!(drilldown.accepted_hook_outcomes, 2);
        assert_eq!(drilldown.accepted_hook_outcomes_linked_to_prior_recall, 1);
        assert_eq!(drilldown.accepted_hook_outcomes_linked_to_mcp_rule_serve, 1);
        assert_eq!(drilldown.sample_file.as_deref(), Some("src/parser.rs"));
    }

    #[tokio::test]
    async fn proven_rule_drilldown_global_fallback_uses_best_available_proof() {
        let pool = proven_rule_pool().await;
        insert_proven_rule(
            &pool,
            ProvenRuleSeed {
                id: "router",
                name: "Pin Actions to commit SHAs",
                repo: "tanstack/router",
                file: ".github/workflows/ci.yml",
                accepted: 3,
            },
        )
        .await;

        let fallback = fetch_proven_rule_drilldown(&pool, None, &[])
            .await
            .expect("global proven rule");
        assert_eq!(fallback.rule_id, "router");
        assert_eq!(fallback.accepted_fixes, 3);
    }
}