use std::cmp::Ordering;
use std::collections::HashMap;
#[derive(Debug, Clone, sqlx::FromRow)]
pub(crate) struct RuleMetadataRow {
pub(crate) rule_id: String,
pub(crate) name: String,
pub(crate) source_repo: Option<String>,
}
pub(crate) async fn fetch_rule_metadata_for_ids(
db: &difflore_core::SqlitePool,
rule_ids: &[String],
normalized_repos: Option<&[String]>,
) -> HashMap<String, RuleMetadataRow> {
let ids: Vec<&str> = rule_ids
.iter()
.map(String::as_str)
.map(str::trim)
.filter(|id| !id.is_empty())
.collect();
if ids.is_empty() {
return HashMap::new();
}
let id_placeholders = std::iter::repeat_n("?", ids.len())
.collect::<Vec<_>>()
.join(", ");
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(source_repo, '')) IN ({placeholders})")
})
.unwrap_or_default();
let sql = format!(
"SELECT id AS rule_id,
COALESCE(NULLIF(name, ''), id) AS name,
source_repo AS source_repo
FROM skills
WHERE id IN ({id_placeholders})
AND COALESCE(status, 'active') = 'active'
{repo_filter}"
);
let mut query = sqlx::query_as::<_, RuleMetadataRow>(&sql);
for id in ids {
query = query.bind(id);
}
if let Some(repos) = normalized_repos {
for repo in repos {
query = query.bind(repo);
}
}
query
.fetch_all(db)
.await
.unwrap_or_default()
.into_iter()
.map(|row| (row.rule_id.clone(), row))
.collect()
}
pub(crate) async fn accepted_link_summary_from_target_repo_summaries(
db: &difflore_core::SqlitePool,
hook_summaries: &[difflore_core::cloud::observations::AcceptedFixOutcomeRuleSummary],
) -> difflore_core::cloud::observations::AcceptedRecallLinkSummary {
let rule_ids: Vec<String> = hook_summaries
.iter()
.map(|summary| summary.rule_id.clone())
.collect();
let metadata = fetch_rule_metadata_for_ids(db, &rule_ids, None).await;
let mut out = difflore_core::cloud::observations::AcceptedRecallLinkSummary::default();
for summary in hook_summaries {
if !metadata.contains_key(&summary.rule_id) {
continue;
}
out.accepted_outcomes += summary.accepted_outcomes;
out.linked_to_prior_recall += summary.linked_to_prior_recall;
out.linked_to_rule_recall += summary.linked_to_rule_recall;
out.linked_to_mcp_rule_serve += summary.linked_to_mcp_rule_serve;
out.linked_to_edit_attribution += summary.linked_to_edit_attribution;
}
out
}
pub(crate) async fn accepted_link_summary_from_default_observation_store(
db: &difflore_core::SqlitePool,
normalized_repos: &[String],
window_days: i64,
recall_lookback_days: i64,
) -> difflore_core::cloud::observations::AcceptedRecallLinkSummary {
if normalized_repos.is_empty() {
return difflore_core::cloud::observations::AcceptedRecallLinkSummary::default();
}
let hook_summaries =
match difflore_core::cloud::observations::ObservationEmitter::open_default().await {
Ok(emitter) => emitter
.accepted_fix_outcome_rule_summaries_for_repos(
window_days,
recall_lookback_days,
normalized_repos,
)
.await
.unwrap_or_default(),
Err(_) => Vec::new(),
};
accepted_link_summary_from_target_repo_summaries(db, &hook_summaries).await
}
pub(crate) struct ProvenRuleRank<'a> {
pub(crate) total: i64,
pub(crate) linked_to_prior_recall: i64,
pub(crate) accepted_fix_proofs: i64,
pub(crate) name: &'a str,
}
impl ProvenRuleRank<'_> {
pub(crate) fn cmp(&self, other: &Self) -> Ordering {
other
.total
.cmp(&self.total)
.then(
other
.linked_to_prior_recall
.cmp(&self.linked_to_prior_recall),
)
.then(other.accepted_fix_proofs.cmp(&self.accepted_fix_proofs))
.then(self.name.cmp(other.name))
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn skills_pool() -> difflore_core::SqlitePool {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.expect("open sqlite");
sqlx::query(
"CREATE TABLE skills (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
source_repo TEXT,
status TEXT
)",
)
.execute(&pool)
.await
.expect("create skills");
pool
}
async fn insert_skill(pool: &difflore_core::SqlitePool, id: &str, status: &str) {
sqlx::query(
"INSERT INTO skills (id, name, description, source_repo, status)
VALUES (?1, ?1, '', 'owner/repo', ?2)",
)
.bind(id)
.bind(status)
.execute(pool)
.await
.expect("insert skill");
}
#[tokio::test]
async fn accepted_link_summary_counts_target_repo_scoped_known_rules() {
let pool = skills_pool().await;
insert_skill(&pool, "r-known", "active").await;
insert_skill(&pool, "r-disabled", "disabled").await;
let summaries = vec![
difflore_core::cloud::observations::AcceptedFixOutcomeRuleSummary {
rule_id: "r-known".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/lib.rs".to_owned()),
latest_occurred_at_ms: 10,
},
difflore_core::cloud::observations::AcceptedFixOutcomeRuleSummary {
rule_id: "r-disabled".to_owned(),
accepted_outcomes: 9,
..Default::default()
},
difflore_core::cloud::observations::AcceptedFixOutcomeRuleSummary {
rule_id: "r-unknown".to_owned(),
accepted_outcomes: 8,
..Default::default()
},
];
let summary = accepted_link_summary_from_target_repo_summaries(&pool, &summaries).await;
assert_eq!(summary.accepted_outcomes, 2);
assert_eq!(summary.linked_to_prior_recall, 1);
assert_eq!(summary.linked_to_mcp_rule_serve, 1);
}
}