#[derive(Debug, Clone)]
pub struct RiskResult {
pub level: &'static str,
pub factors: Vec<&'static str>,
pub checkpoints: Vec<&'static str>,
}
const RISK_TERMS: &[(&str, &[&str])] = &[
(
"security_sensitive",
&[
"auth",
"autenticacao",
"autorizacao",
"login",
"permission",
"permissao",
"role",
"perfil",
"token",
"tokens",
"access token",
"api key",
"api-key",
"apikey",
"secret",
"secrets",
"segredo",
"credencial",
"credenciais",
"credential",
"credentials",
"oauth",
"sso",
],
),
(
"data_sensitive",
&[
"pii",
"cpf",
"email",
"endereco",
"address",
"payment",
"pagamento",
"card",
"cartao",
"personal data",
"dados pessoais",
"dados sensiveis",
"gdpr",
"lgpd",
],
),
(
"migration",
&[
"migration",
"migracao",
"migrate",
"schema",
"backfill",
"database",
"banco",
"db",
],
),
(
"external_dependency",
&[
"webhook",
"integration",
"integracao",
"api externa",
"provider",
"providers",
"provedor",
"provedores",
"agy",
"antigravity",
"openai",
"anthropic",
"claude",
"gemini",
"opencode",
"codex",
"third party",
"terceiro",
"stripe",
"salesforce",
],
),
(
"cross_layer",
&[
"frontend",
"backend",
"database",
"banco",
"api",
"fullstack",
],
),
(
"performance_sensitive",
&["performance", "latency", "cache", "scale", "load", "slow"],
),
(
"infra",
&[
"terraform",
"kubernetes",
"deploy",
"worker",
"queue",
"fila",
"infra",
],
),
(
"feature_flag",
&[
"feature flag",
"flag",
"remote config",
"config remota",
"rollout",
],
),
];
pub fn classify(text: &str) -> RiskResult {
let lowered = text.to_lowercase();
let mut hits = Vec::new();
for (risk, words) in RISK_TERMS {
if words.iter().any(|term| term_matches(&lowered, term)) {
hits.push(*risk);
}
}
let mut score = hits.len() as i32;
if hits.iter().any(|item| {
matches!(
*item,
"security_sensitive" | "data_sensitive" | "migration" | "infra"
)
}) {
score += 2;
}
let level = if score >= 5 {
"critical"
} else if score >= 3 {
"high"
} else if score >= 1 {
"medium"
} else {
"low"
};
let mut checkpoints = vec!["PRD", "Tech Spec", "Merge"];
if matches!(level, "medium" | "high" | "critical") {
checkpoints.push("Refinement");
}
if matches!(level, "high" | "critical") || hits.contains(&"infra") {
checkpoints.push("Deploy");
}
RiskResult {
level,
factors: hits,
checkpoints,
}
}
pub fn term_matches(text: &str, term: &str) -> bool {
if !term
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == ' ')
{
text.contains(term)
} else {
let mut start = 0;
while let Some(offset) = text[start..].find(term) {
let begin = start + offset;
let end = begin + term.len();
let before_ok = begin == 0
|| !text[..begin]
.chars()
.next_back()
.is_some_and(|ch| ch.is_ascii_alphanumeric());
let after_ok = end == text.len()
|| !text[end..]
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_alphanumeric());
if before_ok && after_ok {
return true;
}
start = end;
}
false
}
}