sdd-layer 0.19.0

Spec-Driven Development CLI and agent harness
#[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
    }
}