use super::population::CommitRow;
use crate::classify::{ClassificationEngine, TraceTier, TracedVerdict};
use crate::core::config::Config;
pub(crate) struct Resolved {
pub tier: TraceTier,
pub rule_id: String,
pub category: String,
pub confidence: f64,
pub carried: bool,
pub superseded: bool,
}
pub(crate) struct CarryPolicy {
use_llm: bool,
llm_threshold: f64,
llm_scope: crate::core::config::LlmFallbackScope,
llm_categories: Option<Vec<String>>,
external: bool,
}
impl CarryPolicy {
pub(crate) fn from_config(config: &Config) -> crate::classify::Result<Self> {
let c = config.classification.as_ref();
let llm_categories = crate::classify::ClassificationPipeline::new(config.clone())
.llm_categories()?
.map(|cats| cats.into_iter().map(|c| c.name).collect());
Ok(Self {
llm_categories,
use_llm: config.llm.is_some() || c.is_some_and(|c| c.use_llm),
llm_threshold: c.map_or(0.65, |c| c.llm_fallback_threshold),
llm_scope: c.map(|c| c.llm_fallback_scope).unwrap_or_default(),
external: c.is_some_and(|c| !c.no_external && !c.sources.is_empty()),
})
}
fn reaches(
&self,
stored: TraceTier,
stored_category: &str,
t: &TracedVerdict,
is_merge: bool,
) -> bool {
match stored {
TraceTier::Manual => true,
TraceTier::Llm if is_merge => false,
TraceTier::Llm => {
let in_set = self
.llm_categories
.as_ref()
.is_none_or(|cats| cats.iter().any(|n| n == stored_category));
self.use_llm
&& in_set
&& crate::classify::pipeline_llm::llm_eligible(
self.llm_scope,
&t.verdict,
self.llm_threshold,
)
}
TraceTier::RepoCategory => false,
TraceTier::ExternalSource => self.external,
_ => false,
}
}
}
fn stored_override(method: &str, traced: TraceTier) -> Option<(TraceTier, &'static str)> {
match method {
"manual" => Some((TraceTier::Manual, "manual_override")),
"llm_fallback" => Some((TraceTier::Llm, "llm")),
"repo_category_fallback" => Some((TraceTier::RepoCategory, "repo_category")),
"external_source" if !matches!(traced, TraceTier::JiraProject | TraceTier::IssueType) => {
Some((TraceTier::ExternalSource, "external_source"))
}
_ => None,
}
}
pub(crate) fn resolve_verdicts(
engine: &ClassificationEngine,
policy: &CarryPolicy,
commits: &[&CommitRow],
) -> (Vec<Resolved>, u64) {
let pairs: Vec<(&str, bool)> = commits
.iter()
.map(|c| (c.message.as_str(), c.is_merge))
.collect();
let traced = engine.classify_batch_traced(&pairs);
let mut drifted = 0u64;
let resolved = commits
.iter()
.zip(traced)
.map(|(c, t)| {
let mut superseded = false;
if let Some((cat, conf, method)) = &c.stored {
match stored_override(method, t.trace.tier) {
Some((tier, rule)) if policy.reaches(tier, cat, &t, c.is_merge) => {
return Resolved {
tier,
rule_id: rule.to_string(),
category: cat.clone(),
confidence: *conf,
carried: true,
superseded: false,
};
}
Some(_) => superseded = true,
None if cat != &t.verdict.category => drifted += 1,
None => {}
}
}
Resolved {
tier: t.trace.tier,
rule_id: t.trace.rule_id,
category: t.verdict.category,
confidence: t.verdict.confidence,
carried: false,
superseded,
}
})
.collect();
(resolved, drifted)
}