use chrono::{DateTime, Utc};
use crate::db::DbConnection;
use crate::models::DomainError;
use crate::pack::PackEntityRef;
use super::{
GuardAction, GuardMatch, MatchResolution, PreflightMemoryMatch, RuleSource,
trauma_guard_command_terms, trauma_guard_content_match,
};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PreflightAdvice {
pub memories: Vec<PreflightMemoryMatch>,
pub rules: Vec<GuardMatch>,
}
pub fn load_preflight_advice(
connection: &DbConnection,
workspace_id: &str,
command: &str,
reference_time: DateTime<Utc>,
) -> Result<PreflightAdvice, DomainError> {
let terms = trauma_guard_command_terms(command);
if terms.is_empty() {
return Ok(PreflightAdvice::default());
}
let corpus =
crate::core::ask::load_command_advice_corpus(connection, workspace_id, reference_time)?;
let mut advice = PreflightAdvice::default();
let mut rules = Vec::new();
for candidate in corpus.candidates {
let Some(matched) = trauma_guard_content_match(
&candidate.memory_id,
&candidate.kind,
&candidate.content,
candidate.provenance_uri.as_deref(),
&terms,
) else {
continue;
};
if let Some(source) = corpus.native_sources.get(&candidate.memory_id) {
let PackEntityRef::Rule(rule_id) = &source.entity else {
continue;
};
let rule_id = rule_id.to_string();
rules.push((
matched.score,
GuardMatch {
rule_id: rule_id.clone(),
pattern: format!("text-overlap:{}", matched.matched_terms.join(",")),
action: GuardAction::Warn,
message: matched.content,
source: RuleSource::ProceduralRule { rule_id },
resolution: MatchResolution::Matched,
},
));
} else {
advice.memories.push(matched);
}
}
advice.memories.sort_by(|left, right| {
right
.score
.total_cmp(&left.score)
.then_with(|| left.memory_id.cmp(&right.memory_id))
});
rules.sort_by(|(left_score, left), (right_score, right)| {
right_score
.total_cmp(left_score)
.then_with(|| left.rule_id.cmp(&right.rule_id))
});
advice.rules = rules.into_iter().map(|(_, rule)| rule).collect();
Ok(advice)
}
#[cfg(test)]
#[path = "preflight_advice_tests.rs"]
mod tests;