use globset::Glob;
use sqlx::SqlitePool;
use std::collections::{HashMap, HashSet};
use crate::context::types::{EvidenceKind, EvidenceRecord};
use crate::error::CoreError;
#[derive(sqlx::FromRow)]
pub(crate) struct SkillDetailRow {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) description: String,
pub(crate) r#type: String,
#[allow(dead_code)]
pub(crate) tags: String,
pub(crate) confidence_score: f64,
pub(crate) file_patterns: Option<String>,
pub(crate) origin: String,
pub(crate) source_repo: Option<String>,
pub(crate) trigger: Option<String>,
pub(crate) check_prompt: Option<String>,
}
pub(crate) fn render_full_rule_with_examples(
row: &SkillDetailRow,
examples: Option<&Vec<crate::context::rule_source::RuleExample>>,
) -> String {
let file_patterns = parse_file_patterns(row.file_patterns.as_deref());
let input = crate::context::rule_render::RuleRenderInput {
id: &row.id,
name: &row.name,
r#type: &row.r#type,
confidence: row.confidence_score,
origin: &row.origin,
source_repo: row.source_repo.as_deref(),
file_patterns: &file_patterns,
description: &row.description,
trigger: row.trigger.as_deref(),
check_prompt: row.check_prompt.as_deref(),
examples: examples.map(Vec::as_slice),
};
let body = crate::context::rule_render::render_code_spec(&input);
render_explicit_recall_application_guidance(row, body)
}
pub(crate) fn rule_preview(description: &str, limit: usize) -> String {
description
.trim()
.chars()
.take(limit)
.map(|c| if c == '\n' || c == '\r' { ' ' } else { c })
.collect()
}
pub fn parse_file_patterns(raw: Option<&str>) -> Vec<String> {
let Some(raw) = raw else {
return Vec::new();
};
let trimmed = raw.trim();
if trimmed.is_empty() {
return Vec::new();
}
serde_json::from_str::<Vec<String>>(trimmed).unwrap_or_default()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AutoServeRuleKind {
Faq,
Pitfall,
Other,
}
pub(crate) fn infer_auto_serve_rule_kind(row: &SkillDetailRow) -> AutoServeRuleKind {
let type_lc = row.r#type.trim().to_ascii_lowercase();
if type_lc == "faq" {
return AutoServeRuleKind::Faq;
}
if type_lc == "pitfall" {
return AutoServeRuleKind::Pitfall;
}
let text = format!(
"{}\n{}\n{}\n{}",
row.r#type,
row.name,
row.description,
row.trigger.as_deref().unwrap_or("")
)
.to_ascii_lowercase();
let title = row.name.trim().to_ascii_lowercase();
if title.ends_with('?')
|| title.starts_with("why ")
|| title.starts_with("how ")
|| title.starts_with("what ")
|| title.starts_with("when ")
|| title.starts_with("where ")
|| title.starts_with("can ")
|| title.starts_with("should ")
|| title.starts_with("do we ")
|| text.contains("faq")
|| text.contains("question:")
|| text.contains("answer:")
{
return AutoServeRuleKind::Faq;
}
if [
"don't ",
"do not ",
"never ",
"must not ",
"avoid ",
"pitfall",
"breaks ",
"regression",
"causes ",
"wrong ",
"instead of ",
]
.iter()
.any(|needle| text.contains(needle))
{
return AutoServeRuleKind::Pitfall;
}
AutoServeRuleKind::Other
}
pub(crate) fn kind_gate_allows_silent_injection(
row: &SkillDetailRow,
strict_skill_ids: &HashSet<String>,
) -> bool {
match infer_auto_serve_rule_kind(row) {
AutoServeRuleKind::Faq => false,
AutoServeRuleKind::Pitfall => strict_skill_ids.contains(&row.id),
AutoServeRuleKind::Other => true,
}
}
pub(crate) fn explicit_recall_kind_gate_multiplier(
row: &SkillDetailRow,
strict_skill_ids: &HashSet<String>,
) -> f64 {
match infer_auto_serve_rule_kind(row) {
AutoServeRuleKind::Faq => 0.20,
AutoServeRuleKind::Pitfall if strict_skill_ids.contains(&row.id) => 1.0,
AutoServeRuleKind::Pitfall => 0.30,
AutoServeRuleKind::Other => 1.0,
}
}
pub(crate) fn explicit_recall_application_kind(row: &SkillDetailRow) -> &'static str {
match infer_auto_serve_rule_kind(row) {
AutoServeRuleKind::Faq => "background",
AutoServeRuleKind::Pitfall => "pitfall_guardrail",
AutoServeRuleKind::Other => "rule",
}
}
pub(crate) fn explicit_recall_application_guidance(row: &SkillDetailRow) -> Option<&'static str> {
match infer_auto_serve_rule_kind(row) {
AutoServeRuleKind::Faq => Some(
"Use this as background behavior/constraint for interpreting the task. Do not force a code change or workaround unless the current change directly asks for it and the evidence matches.",
),
AutoServeRuleKind::Pitfall => Some(
"Apply this only when the current file/task matches the failure mode. Follow the positive replacement; do not copy the forbidden wording as an instruction.",
),
AutoServeRuleKind::Other => None,
}
}
fn render_explicit_recall_application_guidance(row: &SkillDetailRow, body: String) -> String {
let Some(guidance) = explicit_recall_application_guidance(row) else {
return body;
};
let section = format!("### Application guidance\n{guidance}\n");
if let Some(split) = body.find("\n\n") {
let (header, rest) = body.split_at(split);
let rest = rest.trim_start();
format!("{header}\n\n{section}\n{rest}")
} else {
format!("{body}\n\n{section}")
}
}
pub(crate) fn first_matching_pattern(patterns: &[String], target_file: &str) -> Option<String> {
let normalised = target_file.trim_start_matches('/').replace('\\', "/");
patterns.iter().find_map(|pattern| {
Glob::new(pattern).ok().and_then(|glob| {
glob.compile_matcher()
.is_match(&normalised)
.then(|| pattern.clone())
})
})
}
pub(crate) fn has_strict_file_patterns_match(file_patterns: &[String], target_file: &str) -> bool {
let target_file = target_file.trim();
if target_file.is_empty() || target_file == "unknown" {
return false;
}
first_matching_pattern(file_patterns, target_file).is_some()
}
pub(crate) fn has_strict_file_scope_match(
file_patterns_raw: Option<&str>,
target_file: &str,
) -> bool {
let target_file = target_file.trim();
let patterns = parse_file_patterns(file_patterns_raw);
has_strict_file_patterns_match(&patterns, target_file)
}
pub(crate) fn strict_file_match_count_for_ids(
meta_map: &HashMap<String, SkillDetailRow>,
ids: &[String],
target_file: Option<&str>,
) -> i64 {
let Some(target_file) = target_file else {
return 0;
};
let count = ids
.iter()
.filter(|id| {
meta_map.get(id.as_str()).is_some_and(|row| {
has_strict_file_scope_match(row.file_patterns.as_deref(), target_file)
})
})
.count();
i64::try_from(count).unwrap_or(i64::MAX)
}
pub(crate) fn strict_file_match_ids_for_rules(
rules: &[crate::context::rule_source::RuleDocument],
target_file: Option<&str>,
) -> HashSet<String> {
let Some(target_file) = target_file else {
return HashSet::new();
};
rules
.iter()
.filter(|rule| has_strict_file_scope_match(rule.file_patterns.as_deref(), target_file))
.map(|rule| rule.skill_id.clone())
.collect()
}
pub(crate) fn strict_file_match_ids_for_meta(
meta_map: &HashMap<String, SkillDetailRow>,
target_file: Option<&str>,
) -> HashSet<String> {
let Some(target_file) = target_file else {
return HashSet::new();
};
meta_map
.iter()
.filter(|(_, row)| has_strict_file_scope_match(row.file_patterns.as_deref(), target_file))
.map(|(id, _)| id.clone())
.collect()
}
pub(crate) fn build_match_evidence(
file: &str,
similarity: f64,
file_patterns: &[String],
confidence: f64,
) -> Vec<EvidenceRecord> {
let mut evidence = Vec::new();
if file != "unknown" {
if let Some(pattern) = first_matching_pattern(file_patterns, file) {
evidence.push(
EvidenceRecord::new(
EvidenceKind::FilePatternMatch,
format!("target file `{file}` matches file_patterns via `{pattern}`"),
)
.with_source("search_rules")
.with_target(file.to_owned())
.with_matched_value(pattern),
);
} else if file_patterns.is_empty() {
evidence.push(
EvidenceRecord::new(
EvidenceKind::FilePatternMatch,
format!(
"target file `{file}` is eligible because the rule has no file_patterns"
),
)
.with_source("search_rules")
.with_target(file.to_owned())
.with_matched_value("universal"),
);
}
}
evidence.push(
EvidenceRecord::new(
EvidenceKind::RetrievalMatch,
format!("retrieval match score {similarity:.3} with confidence {confidence:.2}"),
)
.with_source("search_rules")
.with_score(similarity)
.with_target(file.to_owned()),
);
evidence
}
pub(crate) fn build_timeline_evidence(
kind: EvidenceKind,
source: &str,
ts: &str,
preview: &str,
) -> EvidenceRecord {
let reason = match kind {
EvidenceKind::RuleCreated => format!("rule created from {source} at {ts}"),
EvidenceKind::RuleUpdated => format!("rule updated from {source} at {ts}"),
EvidenceKind::RuleExample => format!("example captured from {source} at {ts}"),
EvidenceKind::TriggerMatch => format!("trigger text carried forward from {source} at {ts}"),
EvidenceKind::FilePatternMatch => format!("file-pattern match at {ts}"),
EvidenceKind::RetrievalMatch => format!("retrieval match at {ts}"),
EvidenceKind::SemanticSimilarity => format!("semantic match at {ts}"),
EvidenceKind::PastVerdictRecall => format!("past verdict recall at {ts}"),
};
EvidenceRecord::new(kind, reason)
.with_source(source.to_owned())
.with_ts(ts.to_owned())
.with_matched_value(preview.to_owned())
}
pub(crate) async fn fetch_skills_by_ids(
db: &SqlitePool,
ids: &[String],
) -> Result<HashMap<String, SkillDetailRow>, CoreError> {
if ids.is_empty() {
return Ok(HashMap::new());
}
let ids_json =
serde_json::to_string(ids).map_err(|e| CoreError::Internal(format!("encode ids: {e}")))?;
let rows = sqlx::query_as::<_, SkillDetailRow>(
"SELECT id, name, description, type, tags, confidence_score, file_patterns, origin, \
source_repo, `trigger`, check_prompt \
FROM skills WHERE id IN (SELECT value FROM json_each(?1)) AND status = 'active'",
)
.bind(ids_json)
.fetch_all(db)
.await
.map_err(|e| CoreError::Internal(format!("skills lookup failed: {e}")))?;
let mut map = HashMap::with_capacity(rows.len());
for row in rows {
map.insert(row.id.clone(), row);
}
Ok(map)
}
pub(crate) fn truncate_chars(s: &str, limit: usize) -> String {
s.chars().take(limit).collect()
}
pub fn origin_to_kind(origin: &str) -> &'static str {
match origin {
"conversation" => "remember",
"pr_review" => "pr_review",
"extracted" => "extracted",
"manual" => "manual",
"cloud" => "cloud",
"team" => "team",
_ => "created",
}
}