use difflore_core::context::retrieval::ScoredRuleChunk;
use difflore_core::context::types::{PastVerdict, PastVerdictScope};
use difflore_core::skills::SearchSkillMeta;
use crate::style::{self, sym};
use crate::support::util::project_path;
use super::{
CloudRecallResult, CommandContext, DiagnosticItem, DiagnosticStep, LocalRecallResult,
LocalRuleHit, RecallDiagnostics, candidate_pool_size, local_rule_title,
more_specific_query_example, query_looks_broad, recall_command, strict_pattern_match_any_file,
strict_scope_files, truncate_one_line,
};
const RECALL_INDEX_EMBEDDING_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(2500);
pub(super) async fn recall_local_rules(
ctx: &CommandContext,
intent: &str,
file: Option<&str>,
diff_files: &[String],
top_k: usize,
) -> LocalRecallResult {
let top_k = crate::support::util::clamp_with_warn("--top-k", top_k, 1, 50, false);
let db = &ctx.db;
let rules = match difflore_core::context::rule_source::load_rules_from_db(db).await {
Ok(rules) => rules,
Err(error) => {
eprintln!(
"{} failed to load local rules: {error}",
style::err(sym::ERR)
);
return LocalRecallResult {
rules_indexed: 0,
repo_full_name: None,
matches: Vec::new(),
file_scope_fallback: false,
trace: super::RecallTrace::default(),
};
}
};
let mut rules_indexed = 0usize;
let configured_gitlab_hosts = difflore_core::ingest::gitlab::auth::configured_hosts().await;
let detected_repo_full_names =
difflore_core::infra::git::detect_repo_full_names_with_gitlab_hosts(
&project_path(),
&configured_gitlab_hosts,
);
let repo_full_names = difflore_core::skills::expand_repo_scopes_with_source_aliases(
db,
&detected_repo_full_names,
)
.await
.unwrap_or(detected_repo_full_names);
let repo_full_name = repo_full_names.first().cloned();
if repo_full_name.is_none() {
return LocalRecallResult {
rules_indexed,
repo_full_name: None,
matches: Vec::new(),
file_scope_fallback: false,
trace: super::RecallTrace::default(),
};
}
let repo_scopes: Vec<String> = repo_full_names.clone();
let index_pool = match difflore_core::context::index_db::get_pool_for_cwd().await {
Ok(pool) => pool,
Err(error) => {
eprintln!(
"{} failed to open local index DB: {error}",
style::err(sym::ERR)
);
return LocalRecallResult {
rules_indexed,
repo_full_name: None,
matches: Vec::new(),
file_scope_fallback: false,
trace: super::RecallTrace::default(),
};
}
};
match difflore_core::context::orchestrator::ensure_rules_indexed_for_repo_scopes_with_embedding_timeout(
db,
&index_pool,
&repo_scopes,
Some(RECALL_INDEX_EMBEDDING_TIMEOUT),
)
.await
{
Ok(count) => rules_indexed = count,
Err(error) => {
eprintln!(
"{} failed to refresh local rule index: {error}",
style::err(sym::ERR)
);
}
}
let embedding_diag =
difflore_core::context::index_db::gather_embedding_diagnostics(&index_pool).await;
if should_force_rebuild_semantic_index(&embedding_diag, rules_indexed) {
match difflore_core::context::orchestrator::rebuild_rules_index_for_repo_scopes(
db,
&index_pool,
&repo_scopes,
Some(RECALL_INDEX_EMBEDDING_TIMEOUT),
)
.await
{
Ok(count) => rules_indexed = count,
Err(error) => {
eprintln!(
"{} failed to rebuild semantic rule index: {error}",
style::err(sym::ERR)
);
}
}
}
let query = match file {
Some(file) => format!("{file} {intent}"),
None => intent.to_owned(),
};
let ranking_inputs = difflore_core::context::rule_source::load_rule_ranking_inputs(db).await;
let target_scope = if diff_files.is_empty() {
file.map(difflore_core::context::retrieval::TargetScope::File)
} else {
Some(difflore_core::context::retrieval::TargetScope::Changeset(
diff_files,
))
};
let pool_k = candidate_pool_size(top_k);
let scored = match crate::commands::recall::search::retrieve_rules_for_search(
&index_pool,
&query,
intent,
pool_k,
ranking_inputs.confidence_map.as_ref(),
ranking_inputs.age_days_map.as_ref(),
ranking_inputs.effectiveness_map.as_ref(),
target_scope,
repo_scopes.as_slice(),
)
.await
{
Ok(scored) => scored,
Err(error) => {
eprintln!(
"{} local rule retrieval failed: {error}",
style::err(sym::ERR)
);
Vec::new()
}
};
let candidates_retrieved = scored.len();
let mut scored = crate::commands::recall::search::merge_exact_title_matches(
&rules,
intent,
repo_scopes.as_slice(),
scored,
pool_k,
);
let candidates_after_exact_merge = scored.len();
difflore_core::context::retrieval::apply_intent_alignment_gate(&mut scored, intent);
let candidates_after_intent_gate = scored.len();
difflore_core::context::retrieval::apply_explicit_recall_threshold(&mut scored);
let candidates_after_relevance_gate = scored.len();
let ids: Vec<String> = scored.iter().map(|hit| hit.skill_id.clone()).collect();
let metas = difflore_core::skills::fetch_search_meta(db, &ids).await;
let mut hits = build_local_hits(&scored, &metas);
let metadata_missing_dropped = scored.len().saturating_sub(hits.len());
let scope_files = strict_scope_files(file, diff_files);
dedupe_local_hits(&mut hits);
hits.truncate(top_k);
hydrate_full_rule_bodies(db, &mut hits).await;
let file_scope_fallback = content_only_file_scope_fallback(&hits, &scope_files);
let returned = hits.len();
LocalRecallResult {
rules_indexed,
repo_full_name,
matches: hits,
file_scope_fallback,
trace: super::RecallTrace {
repo_scopes,
candidate_limit: pool_k,
candidates_retrieved,
candidates_after_exact_merge,
candidates_after_intent_gate,
candidates_after_relevance_gate,
metadata_missing_dropped,
returned,
},
}
}
fn should_force_rebuild_semantic_index(
diag: &difflore_core::context::EmbeddingDiagnostics,
rules_indexed: usize,
) -> bool {
if rules_indexed == 0 || difflore_core::context::index_db::embedding_provider_recently_down() {
return false;
}
let active_semantic =
diag.active_profile.starts_with("cloud:") || diag.active_profile.starts_with("byok:");
active_semantic
&& matches!(
diag.degraded_reason.as_deref(),
Some("index_not_built" | "profile_mismatch" | "dimension_mismatch")
)
}
pub(super) fn dedupe_local_hits(hits: &mut Vec<LocalRuleHit>) {
let mut seen = std::collections::HashSet::new();
hits.retain(|hit| seen.insert(local_hit_dedupe_key(hit)));
}
fn local_hit_dedupe_key(hit: &LocalRuleHit) -> String {
let mut patterns: Vec<String> = hit
.file_patterns
.iter()
.map(|pattern| pattern.trim().to_ascii_lowercase())
.filter(|pattern| !pattern.is_empty())
.collect();
patterns.sort();
patterns.dedup();
format!(
"{}\u{1f}{}\u{1f}{}",
hit.source_repo
.as_deref()
.unwrap_or("")
.trim()
.to_ascii_lowercase(),
hit.title.trim().to_ascii_lowercase(),
patterns.join("\u{1e}")
)
}
pub(super) fn content_only_file_scope_fallback(
hits: &[LocalRuleHit],
scope_files: &[String],
) -> bool {
!scope_files.is_empty()
&& !hits.is_empty()
&& !hits
.iter()
.any(|hit| strict_pattern_match_any_file(&hit.file_patterns, scope_files))
}
pub(super) fn build_local_hits(
scored: &[ScoredRuleChunk],
metas: &std::collections::HashMap<String, SearchSkillMeta>,
) -> Vec<LocalRuleHit> {
let max_score = scored
.iter()
.map(|hit| hit.score)
.fold(f64::NEG_INFINITY, f64::max);
scored
.iter()
.filter_map(|hit| {
let meta = metas.get(&hit.skill_id)?;
let origin = meta.origin.clone();
let source_rank = origin
.as_deref()
.map(difflore_core::context::retrieval::source_rank);
let rank_score = if max_score > 0.0 {
hit.score / max_score
} else {
0.0
};
let (bad, fix) = extract_rule_examples(&hit.content);
Some(LocalRuleHit {
id: hit.skill_id.clone(),
title: local_rule_title(&hit.content, &hit.skill_id),
preview: truncate_one_line(&hit.content, 200),
bad,
fix,
rank_score,
raw_score: hit.score,
confidence: hit.confidence,
file_patterns: meta.file_patterns.clone(),
source_repo: meta.source_repo.clone(),
origin,
source_rank,
body: None,
})
})
.collect()
}
pub(super) async fn hydrate_full_rule_bodies(
db: &difflore_core::SqlitePool,
hits: &mut [LocalRuleHit],
) {
if hits.is_empty() {
return;
}
let ids: Vec<String> = hits.iter().map(|hit| hit.id.clone()).collect();
let mut bodies = difflore_core::context::retrieval::render_full_rule_bodies(db, &ids)
.await
.unwrap_or_default();
for hit in hits.iter_mut() {
let Some(rendered) = bodies.remove(&hit.id) else {
continue;
};
let db_bad = rendered.first_bad_code();
let db_fix = rendered.first_good_code();
if db_bad.is_some() || db_fix.is_some() {
let (bad_line, fix_line) =
divergent_example_lines(db_bad.as_deref(), db_fix.as_deref());
if bad_line.is_some() {
hit.bad = bad_line;
}
if fix_line.is_some() {
hit.fix = fix_line;
}
}
hit.body = Some(rendered);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum ExampleSide {
Bad,
Fix,
}
pub(super) fn classify_example_heading(line: &str) -> Option<ExampleSide> {
let trimmed = line.trim();
let decorated = trimmed.starts_with('#')
|| trimmed.starts_with('*')
|| trimmed.starts_with('-')
|| trimmed.starts_with('>')
|| trimmed.contains('❌')
|| trimmed.contains('✅');
let stripped: String = trimmed
.trim_start_matches(['#', '*', '-', '>', ' '])
.chars()
.map(|c| {
if c == '❌' || c == '✅' || c == '*' || c == '`' {
' '
} else {
c
}
})
.collect();
let lower = stripped.to_ascii_lowercase();
let tokens: Vec<&str> = lower
.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|t| !t.is_empty())
.collect();
let [first, rest @ ..] = tokens.as_slice() else {
return None;
};
let rest_is_qualifier_only = rest.iter().all(|t| {
matches!(
*t,
"example" | "examples" | "code" | "way" | "approach" | "pattern"
)
});
if !decorated && !rest_is_qualifier_only {
return None;
}
match *first {
"bad" | "wrong" | "incorrect" | "anti" | "antipattern" => Some(ExampleSide::Bad),
"good" | "correct" | "right" | "fix" | "fixed" | "better" => Some(ExampleSide::Fix),
_ => None,
}
}
pub(super) fn meaningful_example_code_lines(block: &str) -> Vec<String> {
let mut lines = Vec::new();
for raw in block.lines() {
let trimmed = raw.trim();
if is_markdown_section_break(trimmed) {
break;
}
if trimmed.is_empty()
|| trimmed.starts_with("```")
|| trimmed.starts_with("~~~")
|| trimmed == "-"
|| trimmed == "*"
{
continue;
}
lines.push(trimmed.to_owned());
}
lines
}
#[cfg(test)]
pub(super) fn first_example_code_line(block: &str) -> Option<String> {
meaningful_example_code_lines(block).into_iter().next()
}
pub(super) fn is_markdown_section_break(trimmed: &str) -> bool {
if trimmed == "---" || trimmed == "***" || trimmed == "___" {
return true;
}
let hashes = trimmed.chars().take_while(|&c| c == '#').count();
(1..=6).contains(&hashes)
&& trimmed[hashes..]
.chars()
.next()
.is_some_and(char::is_whitespace)
}
pub(super) fn extract_rule_examples(content: &str) -> (Option<String>, Option<String>) {
let lines: Vec<&str> = content.lines().collect();
let mut headings: Vec<(usize, ExampleSide)> = Vec::new();
for (idx, line) in lines.iter().enumerate() {
if let Some(side) = classify_example_heading(line) {
headings.push((idx, side));
}
}
if headings.is_empty() {
return (None, None);
}
let mut bad_block: Option<String> = None;
let mut fix_block: Option<String> = None;
for (n, &(start, side)) in headings.iter().enumerate() {
let end = headings
.get(n + 1)
.map_or(lines.len(), |&(next_start, _)| next_start);
let block = lines[start + 1..end].join("\n");
match side {
ExampleSide::Bad if bad_block.is_none() => bad_block = Some(block),
ExampleSide::Fix if fix_block.is_none() => fix_block = Some(block),
_ => {}
}
}
divergent_example_lines(bad_block.as_deref(), fix_block.as_deref())
}
pub(super) fn divergent_example_lines(
bad_block: Option<&str>,
fix_block: Option<&str>,
) -> (Option<String>, Option<String>) {
let bad_lines = bad_block.map(meaningful_example_code_lines);
let fix_lines = fix_block.map(meaningful_example_code_lines);
let bad_first = bad_lines.as_ref().and_then(|l| l.first().cloned());
let fix_first = fix_lines.as_ref().and_then(|l| l.first().cloned());
let (Some(bad_lines), Some(fix_lines)) = (bad_lines, fix_lines) else {
return (bad_first, fix_first);
};
let (Some(bad_head), Some(fix_head)) = (bad_first.clone(), fix_first.clone()) else {
return (bad_first, fix_first);
};
if bad_head.trim() != fix_head.trim() {
return (Some(bad_head), Some(fix_head));
}
let mut i = 0;
while i < bad_lines.len() && i < fix_lines.len() && bad_lines[i].trim() == fix_lines[i].trim() {
i += 1;
}
match (bad_lines.get(i), fix_lines.get(i)) {
(Some(bad_div), Some(fix_div)) => (Some(bad_div.clone()), Some(fix_div.clone())),
(None, Some(fix_div)) => (Some(bad_head), Some(fix_div.clone())),
(Some(_) | None, None) => (Some(bad_head), Some(fix_head)),
}
}
const STARTER_RELEVANCE_FLOOR: f64 = 0.12;
pub(super) fn filter_starter_by_relevance(
hits: Vec<LocalRuleHit>,
floor: f64,
) -> Vec<LocalRuleHit> {
hits.into_iter()
.filter(|hit| hit.raw_score >= floor)
.collect()
}
pub(super) async fn cross_repo_starter_hits(
ctx: &CommandContext,
intent: &str,
file: &str,
top_k: usize,
) -> Vec<LocalRuleHit> {
let db = &ctx.db;
let Ok(starter_pool) =
difflore_core::context::orchestrator::ensure_cross_repo_starter_indexed(db).await
else {
return Vec::new();
};
let query = format!("{file} {intent}");
let ranking_inputs = difflore_core::context::rule_source::load_rule_ranking_inputs(db).await;
let pool_k = candidate_pool_size(top_k);
let Ok(scored) = crate::commands::recall::search::retrieve_rules_for_search(
&starter_pool,
&query,
intent,
pool_k,
ranking_inputs.confidence_map.as_ref(),
ranking_inputs.age_days_map.as_ref(),
ranking_inputs.effectiveness_map.as_ref(),
Some(difflore_core::context::retrieval::TargetScope::File(file)),
&[],
)
.await
else {
return Vec::new();
};
let ids: Vec<String> = scored.iter().map(|hit| hit.skill_id.clone()).collect();
let metas = difflore_core::skills::fetch_search_meta(db, &ids).await;
let hits = build_local_hits(&scored, &metas);
let mut hits = filter_starter_by_relevance(hits, STARTER_RELEVANCE_FLOOR);
hits.truncate(top_k);
hits
}
pub(super) async fn record_local_recall(
ctx: &CommandContext,
local: &LocalRecallResult,
intent: &str,
file: Option<&str>,
diff_files: &[String],
top_k: usize,
session_id: &str,
) {
if local.matches.is_empty() {
return;
}
let db = &ctx.db;
let query = match file {
Some(file) => format!("{file} {intent}"),
None => intent.to_owned(),
};
let scope_files = strict_scope_files(file, diff_files);
let recalls: Vec<_> = local
.matches
.iter()
.enumerate()
.map(
|(index, hit)| difflore_core::observability::rule_outcomes::RuleRecallInput {
rule_id: hit.id.as_str(),
session_id: Some(session_id),
repo_full_name: local.repo_full_name.as_deref(),
file_path: file,
query_text: query.as_str(),
rank: index as i64 + 1,
top_k: top_k as i64,
strict_file_match: strict_pattern_match_any_file(&hit.file_patterns, &scope_files),
},
)
.collect();
let _ = difflore_core::observability::rule_outcomes::record_recalled_with_context(db, &recalls)
.await;
let ids: Vec<String> = local.matches.iter().map(|hit| hit.id.clone()).collect();
emit_rule_fired_observation(ctx, &ids, intent, file, session_id).await;
}
pub(super) fn build_zero_match_diagnostics(
local: &LocalRecallResult,
cloud: &CloudRecallResult,
intent: &str,
file: Option<&str>,
) -> RecallDiagnostics {
let mut possible_causes = Vec::new();
let mut next_steps = Vec::new();
let no_scope = local.repo_full_name.is_none();
let empty_corpus = !no_scope && local.rules_indexed == 0;
if no_scope {
possible_causes.push(DiagnosticItem {
code: "repo_scope_missing",
message: "No supported origin/upstream git remote was detected; local recall scopes rules by repo, so an unscoped checkout retrieves nothing. This is by design, not empty local memory.".to_owned(),
});
} else if empty_corpus {
possible_causes.push(DiagnosticItem {
code: "local_corpus_empty",
message: "No accepted local rules are indexed for this repo yet, so offline recall has nothing to retrieve.".to_owned(),
});
} else {
possible_causes.push(DiagnosticItem {
code: "repo_scoped_no_overlap",
message: format!(
"{} local rule{} exist for this repo scope, but none overlapped the query strongly enough.",
local.rules_indexed,
if local.rules_indexed == 1 { "" } else { "s" },
),
});
}
if let Some(file) = file.map(str::trim).filter(|file| !file.is_empty()) {
possible_causes.push(DiagnosticItem {
code: "file_path_hint",
message: format!(
"`{file}` was used only as a path hint; no repo rule was semantically close enough for this query."
),
});
next_steps.push(DiagnosticStep {
command: Some("difflore status".to_owned()),
message:
"inspect the local rules for this repo, then retry with review-specific wording"
.to_owned(),
});
} else {
possible_causes.push(DiagnosticItem {
code: "no_file_hint",
message: "No target file was supplied, so recall could not use path hints to break close relevance ties.".to_owned(),
});
next_steps.push(DiagnosticStep {
command: Some(recall_command(intent, Some("path/to/file"))),
message: "add --file <path> as a ranking hint for close matches".to_owned(),
});
}
if query_looks_broad(intent) {
possible_causes.push(DiagnosticItem {
code: "query_too_broad",
message: "The query is broad; recall works best with review-language details like API names, failure modes, or the convention being checked.".to_owned(),
});
next_steps.push(DiagnosticStep {
command: Some(recall_command(
&more_specific_query_example(intent, file),
file,
)),
message: "retry with a more specific review phrase".to_owned(),
});
}
if !cloud.logged_in {
possible_causes.push(DiagnosticItem {
code: "cloud_not_logged_in",
message: "Cloud PR review memory is available after sign-in.".to_owned(),
});
} else if cloud.repo_full_name.is_none() {
possible_causes.push(DiagnosticItem {
code: "cloud_repo_scope_missing",
message: "Cloud PR review memory needs a supported repo remote.".to_owned(),
});
} else {
possible_causes.push(DiagnosticItem {
code: "cloud_no_overlap",
message: "Cloud PR review rules did not find an imported PR review verdict for this repo, file, and query.".to_owned(),
});
}
if no_scope {
next_steps.push(DiagnosticStep {
command: Some("git remote -v".to_owned()),
message: "local recall is repo-scoped; add a supported origin/upstream git remote (or run inside a repo that has one) so this checkout has memory to retrieve".to_owned(),
});
} else if empty_corpus {
next_steps.push(DiagnosticStep {
command: Some("difflore import-reviews --max-prs 50".to_owned()),
message: "create local rules from recent PR review history".to_owned(),
});
} else {
next_steps.push(DiagnosticStep {
command: Some("difflore status".to_owned()),
message: "inspect local memory readiness and the current next action".to_owned(),
});
next_steps.push(DiagnosticStep {
command: Some("difflore import-reviews --max-prs 50".to_owned()),
message:
"mine more review history if the current repo has no memory for this topic yet"
.to_owned(),
});
}
if empty_corpus {
prioritize_empty_corpus_steps(&mut next_steps);
}
RecallDiagnostics {
summary: "No local rules or cloud review memories matched; recall ran, but the available memory did not overlap this scope.".to_owned(),
possible_causes,
next_steps,
}
}
pub(super) fn prioritize_empty_corpus_steps(next_steps: &mut [DiagnosticStep]) {
next_steps.sort_by_key(|step| match step.command.as_deref() {
Some("difflore import-reviews --max-prs 50") => 0,
_ => 3,
});
}
pub(super) async fn emit_rule_fired_observation(
ctx: &CommandContext,
rule_ids: &[String],
intent: &str,
file: Option<&str>,
session_id: &str,
) {
if rule_ids.is_empty() {
return;
}
let client = ctx.cloud().await;
let event = difflore_core::cloud::observations::ObservationEvent::RuleFired {
rule_ids: rule_ids.iter().take(10).cloned().collect(),
file_path: file.map(ToOwned::to_owned),
intent: Some(intent.to_owned()),
session_id: session_id.to_owned(),
fired_at: chrono::Utc::now(),
};
let _ = difflore_core::cloud::observations::enqueue_and_flush_default(event, client).await;
}
pub(super) async fn recall_cloud_review_memory(
ctx: &CommandContext,
intent: &str,
file: Option<&str>,
top_k: usize,
) -> CloudRecallResult {
let client = ctx.cloud().await;
let has_saved_token = client.is_logged_in();
let configured_gitlab_hosts = difflore_core::ingest::gitlab::auth::configured_hosts().await;
let detected_repo_full_names =
difflore_core::infra::git::detect_repo_full_names_with_gitlab_hosts(
&project_path(),
&configured_gitlab_hosts,
);
let repo_full_names = difflore_core::skills::expand_repo_scopes_with_source_aliases(
&ctx.db,
&detected_repo_full_names,
)
.await
.unwrap_or(detected_repo_full_names);
let repo_full_name = repo_full_names.first().cloned();
if !has_saved_token {
return CloudRecallResult {
logged_in: false,
repo_full_name,
scope: PastVerdictScope::Personal.as_str(),
team_id: None,
verdicts: Vec::new(),
};
}
let cloud_status = difflore_core::cloud::sync::fetch_cloud_status(client).await;
if !cloud_status.logged_in {
return CloudRecallResult {
logged_in: false,
repo_full_name,
scope: PastVerdictScope::Personal.as_str(),
team_id: None,
verdicts: Vec::new(),
};
}
let team_id = cloud_status.team_id.clone();
let scope = if team_id.is_some() {
PastVerdictScope::Team
} else {
PastVerdictScope::Personal
};
let top_k = crate::support::util::clamp_with_warn("--top-k", top_k, 1, 10, false);
if repo_full_names.is_empty() {
return CloudRecallResult {
logged_in: true,
repo_full_name,
scope: scope.as_str(),
team_id,
verdicts: Vec::new(),
};
}
let repos: Vec<String> = repo_full_names.iter().take(4).cloned().collect();
let probes = repos.iter().map(|repo| {
recall_cloud_repo_verdicts(client, intent, file, top_k, repo, scope, team_id.as_deref())
});
let groups = futures_util::future::join_all(probes).await;
let mut seen = std::collections::HashSet::new();
let mut verdicts: Vec<PastVerdict> = Vec::new();
for group in groups {
for v in group {
if seen.insert(v.extraction_id.clone()) {
verdicts.push(v);
}
}
}
verdicts.sort_by(|a, b| {
b.similarity
.partial_cmp(&a.similarity)
.unwrap_or(std::cmp::Ordering::Equal)
});
verdicts.truncate(top_k);
CloudRecallResult {
logged_in: true,
repo_full_name,
scope: scope.as_str(),
team_id,
verdicts,
}
}
pub(super) async fn recall_cloud_repo_verdicts(
client: &difflore_core::cloud::client::CloudClient,
intent: &str,
file: Option<&str>,
top_k: usize,
repo: &str,
scope: PastVerdictScope,
team_id: Option<&str>,
) -> Vec<PastVerdict> {
difflore_core::context::retrieval::retrieve_past_verdicts_by_text_with_team(
client,
intent,
Some(repo),
scope,
top_k as u32,
file,
team_id,
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
fn hit(id: &str, title: &str, source_repo: Option<&str>, patterns: &[&str]) -> LocalRuleHit {
LocalRuleHit {
id: id.to_owned(),
title: title.to_owned(),
preview: String::new(),
bad: None,
fix: None,
rank_score: 1.0,
raw_score: 1.0,
confidence: 0.9,
file_patterns: patterns
.iter()
.map(|pattern| (*pattern).to_owned())
.collect(),
source_repo: source_repo.map(str::to_owned),
origin: None,
source_rank: None,
body: None,
}
}
#[test]
fn dedupe_local_hits_collapses_same_source_title_and_patterns() {
let mut hits = vec![
hit(
"keep",
"Try to avoid using 'any'",
Some("acme/web"),
&["src/**/*.ts"],
),
hit(
"drop",
" try to avoid using 'any' ",
Some("ACME/WEB"),
&["SRC/**/*.ts"],
),
hit(
"other-source",
"Try to avoid using 'any'",
Some("acme/api"),
&["src/**/*.ts"],
),
hit(
"other-pattern",
"Try to avoid using 'any'",
Some("acme/web"),
&["app/**/*.ts"],
),
];
dedupe_local_hits(&mut hits);
let ids: Vec<&str> = hits.iter().map(|hit| hit.id.as_str()).collect();
assert_eq!(ids, ["keep", "other-source", "other-pattern"]);
}
}