#![cfg_attr(coverage_nightly, coverage(off))]
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
static ref GENERIC_PATTERNS: Vec<Regex> = vec![
Regex::new(r"^The .+ parameter").expect("internal error"),
Regex::new(r"^\w+ parameter$").expect("internal error"),
Regex::new(r"^\w+ value$").expect("internal error"),
Regex::new(r"^Input (for|value)").expect("internal error"),
Regex::new(r"^Output (for|value)").expect("internal error"),
Regex::new(r"^[A-Z][a-z]+$").expect("internal error"),
Regex::new(r"^Path to \w+$").expect("internal error"),
Regex::new(r"^Name for \w+$").expect("internal error"),
Regex::new(r"^\w+ for \w+$").expect("internal error"),
];
static ref LAZY_WORDS: Vec<&'static str> = vec![
"parameter",
"value",
"input",
"output",
"option",
"setting",
];
}
pub fn is_generic_description(desc: &str) -> bool {
if desc.is_empty() {
return true;
}
let desc = desc.trim();
if is_too_short_or_matches_pattern(desc) {
return true;
}
let words: Vec<&str> = desc.split_whitespace().collect();
if words.len() < 3 {
return true;
}
if has_too_many_lazy_words(&words) || has_low_word_uniqueness(&words) {
return true;
}
if has_detail_indicators(desc) && desc.len() > 30 {
return false;
}
false
}
fn is_too_short_or_matches_pattern(desc: &str) -> bool {
if desc.len() < 15 {
return true;
}
for pattern in GENERIC_PATTERNS.iter() {
if pattern.is_match(desc) {
return true;
}
}
false
}
fn has_too_many_lazy_words(words: &[&str]) -> bool {
let lazy_count = words
.iter()
.filter(|w| LAZY_WORDS.contains(&w.to_lowercase().as_str()))
.count();
(lazy_count as f64) / (words.len() as f64) > 0.5
}
fn has_detail_indicators(desc: &str) -> bool {
desc.contains("(") || desc.contains("[") || desc.contains(":") || desc.contains("e.g.")
|| desc.contains("default")
|| desc.contains("example")
}
fn has_low_word_uniqueness(words: &[&str]) -> bool {
let lowercase_words: Vec<String> = words.iter().map(|w| w.to_lowercase()).collect();
let unique_words: std::collections::HashSet<&String> = lowercase_words.iter().collect();
(unique_words.len() as f64) / (words.len() as f64) < 0.4
}
pub fn suggest_improvements(desc: &str) -> Vec<String> {
let mut suggestions = Vec::new();
if desc.len() < 15 {
suggestions
.push("Make description at least 15 characters with specific details".to_string());
}
if desc.split_whitespace().count() < 3 {
suggestions.push("Add more context - aim for at least 3 words with details".to_string());
}
if !desc.contains("(") && !desc.contains("[") && !desc.contains(":") {
suggestions.push("Add examples, defaults, or constraints: '(default: ...)', '[required]', 'level: A, B, C'".to_string());
}
if desc.starts_with("The ") && desc.ends_with(" parameter") {
suggestions.push(format!(
"Instead of '{}', explain what it does and any constraints",
desc
));
}
if desc.split_whitespace().count() < 5 {
suggestions
.push("Explain: What is it? What does it do? What are valid values?".to_string());
}
suggestions
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_is_generic() {
assert!(is_generic_description(""));
}
#[test]
fn test_short_is_generic() {
assert!(is_generic_description("Name"));
assert!(is_generic_description("Template"));
assert!(is_generic_description("Short"));
}
#[test]
fn test_the_parameter_pattern() {
assert!(is_generic_description("The name parameter"));
assert!(is_generic_description("The template parameter"));
assert!(is_generic_description("The output parameter"));
}
#[test]
fn test_noun_parameter_pattern() {
assert!(is_generic_description("Name parameter"));
assert!(is_generic_description("Template parameter"));
}
#[test]
fn test_value_patterns() {
assert!(is_generic_description("Name value"));
assert!(is_generic_description("Input value"));
assert!(is_generic_description("Output value"));
assert!(is_generic_description("Input for testing"));
assert!(is_generic_description("Output for results"));
}
#[test]
fn test_path_patterns() {
assert!(is_generic_description("Path to file"));
assert!(is_generic_description("Path to directory"));
assert!(is_generic_description("Name for project"));
}
#[test]
fn test_specific_descriptions_not_generic() {
assert!(!is_generic_description(
"Agent project name (lowercase, alphanumeric, hyphens only)"
));
assert!(!is_generic_description(
"Quality level: standard (fast), high (thorough), extreme (comprehensive)"
));
assert!(!is_generic_description(
"Path to ROADMAP.md file for validation (default: ./ROADMAP.md)"
));
assert!(!is_generic_description(
"Output directory where the agent project will be created (default: current directory)"
));
assert!(!is_generic_description(
"Dry-run mode: preview changes without creating files"
));
}
#[test]
fn test_domain_specific_terms_allowed() {
assert!(!is_generic_description(
"ROADMAP.md file path (default: ./ROADMAP.md in project root)"
));
assert!(!is_generic_description(
"Cyclomatic complexity threshold (default: 8)"
));
assert!(!is_generic_description(
"SATD annotation pattern (e.g., TODO, FIXME, HACK)"
));
}
#[test]
fn test_suggest_improvements() {
let suggestions = suggest_improvements("Name");
assert!(!suggestions.is_empty());
assert!(suggestions.iter().any(|s| s.contains("15 characters")));
}
#[test]
fn test_suggest_improvements_for_the_parameter() {
let suggestions = suggest_improvements("The name parameter");
assert!(suggestions.iter().any(|s| s.contains("Instead of")));
}
}