use std::collections::{BTreeMap, HashMap};
use crate::classify::errors::{ClassifyError, Result};
use crate::classify::pipeline::ClassificationPipeline;
use crate::classify::taxonomy::TaxonomyRegistry;
use crate::classify::tiers::regex_tier::RegexMatcher;
use crate::classify::tiers::ClassificationResult;
use crate::classify::trace::{RuleTrace, TraceTier, TracedVerdict};
use crate::core::models::ClassificationMethod;
use super::pipeline_db::CommitRow;
#[derive(Debug, Clone, Default)]
pub(crate) struct RepoCategoryMap {
map: HashMap<String, String>,
}
impl RepoCategoryMap {
pub(crate) fn checked(raw: &HashMap<String, String>, known: &[String]) -> Result<Self> {
let sorted: BTreeMap<&String, &String> = raw.iter().collect();
let globs: Vec<&str> = sorted
.keys()
.filter(|k| k.contains('*'))
.map(|k| k.as_str())
.collect();
if !globs.is_empty() {
return Err(ClassifyError::Config(format!(
"classification.repo_categories keys are exact repository names, not globs: {}",
globs.join(", ")
)));
}
let mut map = HashMap::with_capacity(raw.len());
let mut unknown = Vec::new();
for (repo, category) in sorted {
match known.iter().find(|k| k.eq_ignore_ascii_case(category)) {
Some(name) => {
map.insert(repo.clone(), name.clone());
}
None => unknown.push(format!("repository '{repo}' -> category '{category}'")),
}
}
if !unknown.is_empty() {
return Err(ClassifyError::Config(format!(
"classification.repo_categories names categories this config does not know: {}",
unknown.join(", ")
)));
}
Ok(Self { map })
}
pub(crate) fn traced(
&self,
repo: &str,
is_merge: bool,
message: &str,
taxonomy: &TaxonomyRegistry,
) -> Option<TracedVerdict> {
if is_merge {
return None;
}
let category = self.map.get(repo)?;
let verdict = ClassificationResult {
top_level: taxonomy.resolve(category),
category: category.clone(),
subcategory: None,
confidence: 1.0,
method: ClassificationMethod::RepoMap,
ticket_id: RegexMatcher::extract_ticket_id(message),
complexity: None,
};
let trace = RuleTrace::new(TraceTier::RepoMap, format!("repo_map:{repo}"));
Some(TracedVerdict { verdict, trace })
}
pub(super) fn verdicts(
&self,
commits: &[CommitRow],
taxonomy: &TaxonomyRegistry,
) -> HashMap<i64, ClassificationResult> {
if self.map.is_empty() {
return HashMap::new();
}
commits
.iter()
.filter_map(|c| {
self.traced(&c.repository, c.is_merge, &c.message, taxonomy)
.map(|t| (c.id, t.verdict))
})
.collect()
}
}
impl ClassificationPipeline {
pub(crate) fn repo_category_map(&self) -> Result<RepoCategoryMap> {
let raw = match self.config.classification.as_ref() {
Some(c) if !c.repo_categories.is_empty() => &c.repo_categories,
_ => return Ok(RepoCategoryMap::default()),
};
let known = match self.llm_categories()? {
Some(defs) => defs.into_iter().map(|d| d.name).collect(),
None => self.known_categories()?,
};
RepoCategoryMap::checked(raw, &known)
}
}