use crate::{
config::LintConfig,
diagnostics::{Diagnostic, Fix},
file_utils::safe_read_file,
rules::{Validator, ValidatorMetadata},
schemas::claude_md::{
check_readme_duplication, check_token_count, extract_npm_scripts, find_critical_in_middle,
find_generic_instructions, find_negative_without_positive, find_weak_constraints,
},
};
use rust_i18n::t;
use std::path::Path;
const RULE_IDS: &[&str] = &[
"CC-MEM-004",
"CC-MEM-005",
"CC-MEM-006",
"CC-MEM-007",
"CC-MEM-008",
"CC-MEM-009",
"CC-MEM-010",
"CC-MEM-014",
];
pub struct ClaudeMdValidator;
fn is_path_under_cursor_rules(path: &Path) -> bool {
path.components()
.zip(path.components().skip(1))
.any(|(a, b)| {
matches!(
(a, b),
(std::path::Component::Normal(a_os), std::path::Component::Normal(b_os))
if a_os == ".cursor" && b_os == "rules"
)
})
}
fn is_cursor_rules_file(path: &Path) -> bool {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if matches!(filename, ".cursorrules" | ".cursorrules.md") {
return true;
}
(filename.ends_with(".md") || filename.ends_with(".mdc")) && is_path_under_cursor_rules(path)
}
impl Validator for ClaudeMdValidator {
fn metadata(&self) -> ValidatorMetadata {
ValidatorMetadata {
name: self.name(),
rule_ids: RULE_IDS,
}
}
fn validate(&self, path: &Path, content: &str, config: &LintConfig) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let is_claude_md = matches!(filename, "CLAUDE.md" | "CLAUDE.local.md");
let is_cursor_rules = is_cursor_rules_file(path);
if !is_claude_md && !is_cursor_rules {
return diagnostics;
}
if config.is_rule_enabled("CC-MEM-005") && config.rules().generic_instructions {
let generic_insts = find_generic_instructions(content);
for inst in generic_insts {
diagnostics.push(
Diagnostic::warning(
path.to_path_buf(),
inst.line,
inst.column,
"CC-MEM-005",
t!("rules.cc_mem_005.message", text = inst.text.as_str()),
)
.with_suggestion(t!("rules.cc_mem_005.suggestion"))
.with_fix(Fix::delete(
inst.start_byte,
inst.end_byte,
t!("rules.cc_mem_005.fix"),
false,
)),
);
}
}
if config.is_rule_enabled("CC-MEM-009")
&& let Some(exceeded) = check_token_count(content)
{
diagnostics.push(
Diagnostic::warning(
path.to_path_buf(),
1,
0,
"CC-MEM-009",
t!(
"rules.cc_mem_009.message",
tokens = exceeded.estimated_tokens,
limit = exceeded.limit
),
)
.with_suggestion(t!("rules.cc_mem_009.suggestion")),
);
}
if config.is_rule_enabled("CC-MEM-006") {
let negatives = find_negative_without_positive(content);
for neg in negatives {
diagnostics.push(
Diagnostic::warning(
path.to_path_buf(),
neg.line,
neg.column,
"CC-MEM-006",
t!("rules.cc_mem_006.message", text = neg.text.as_str()),
)
.with_suggestion(t!("rules.cc_mem_006.suggestion")),
);
}
}
if config.is_rule_enabled("CC-MEM-007") {
let weak = find_weak_constraints(content);
for w in weak {
let (replacement, safe) = get_weak_constraint_replacement(&w.text);
let mut diagnostic = Diagnostic::warning(
path.to_path_buf(),
w.line,
w.column,
"CC-MEM-007",
t!(
"rules.cc_mem_007.message",
text = w.text.as_str(),
section = w.section.as_str()
),
)
.with_suggestion(t!("rules.cc_mem_007.suggestion"));
if let Some(repl) = replacement {
diagnostic = diagnostic.with_fix(Fix::replace(
w.start_byte,
w.end_byte,
repl,
t!("rules.cc_mem_007.fix", text = w.text.as_str()),
safe,
));
}
diagnostics.push(diagnostic);
}
}
if config.is_rule_enabled("CC-MEM-008") {
let critical = find_critical_in_middle(content);
for c in critical {
diagnostics.push(
Diagnostic::warning(
path.to_path_buf(),
c.line,
c.column,
"CC-MEM-008",
t!(
"rules.cc_mem_008.message",
keyword = c.keyword.as_str(),
percent = format!("{:.0}", c.position_percent)
),
)
.with_suggestion(t!("rules.cc_mem_008.suggestion")),
);
}
}
if config.is_rule_enabled("CC-MEM-004") {
let npm_refs = extract_npm_scripts(content);
if !npm_refs.is_empty() {
if let Some(parent) = path.parent() {
let package_json_path = parent.join("package.json");
if let Ok(pkg_content) = safe_read_file(&package_json_path) {
if let Ok(pkg_json) =
serde_json::from_str::<serde_json::Value>(&pkg_content)
{
let available_scripts: Vec<String> = pkg_json
.get("scripts")
.and_then(|s| s.as_object())
.map(|scripts| scripts.keys().cloned().collect())
.unwrap_or_default();
for npm_ref in npm_refs {
if !available_scripts.contains(&npm_ref.script_name) {
let suggestion = if available_scripts.is_empty() {
t!("rules.cc_mem_004.suggestion_no_scripts").to_string()
} else {
t!(
"rules.cc_mem_004.suggestion_available",
scripts = available_scripts.join(", ")
)
.to_string()
};
diagnostics.push(
Diagnostic::warning(
path.to_path_buf(),
npm_ref.line,
npm_ref.column,
"CC-MEM-004",
t!(
"rules.cc_mem_004.message",
script = npm_ref.script_name.as_str()
),
)
.with_suggestion(suggestion),
);
}
}
}
}
}
}
}
if config.is_rule_enabled("CC-MEM-010") {
if let Some(parent) = path.parent() {
let readme_path = parent.join("README.md");
if let Ok(readme_content) = safe_read_file(&readme_path) {
if let Some(dup) = check_readme_duplication(content, &readme_content) {
diagnostics.push(
Diagnostic::warning(
path.to_path_buf(),
1,
0,
"CC-MEM-010",
t!(
"rules.cc_mem_010.message",
overlap = format!("{:.0}", dup.overlap_percent),
threshold = format!("{:.0}", dup.threshold)
),
)
.with_suggestion(t!("rules.cc_mem_010.suggestion")),
);
}
}
}
}
if config.is_rule_enabled("CC-MEM-014") {
const MAX_RECOMMENDED_LINES: usize = 200;
let non_empty_lines = content
.lines()
.filter(|line| !line.trim().is_empty())
.count();
if non_empty_lines > MAX_RECOMMENDED_LINES {
diagnostics.push(
Diagnostic::info(
path.to_path_buf(),
1,
0,
"CC-MEM-014",
format!(
"CLAUDE.md has {} non-empty lines, exceeding the recommended {} line limit",
non_empty_lines, MAX_RECOMMENDED_LINES
),
)
.with_suggestion(
"Split content into multiple files (e.g. CLAUDE.local.md) or trim to keep under 200 non-empty lines.",
),
);
}
}
diagnostics
}
}
fn get_weak_constraint_replacement(text: &str) -> (Option<&'static str>, bool) {
match text.to_lowercase().as_str() {
"should" => (Some("must"), true),
"try to" => (Some("must"), true),
"consider" => (Some("ensure"), true),
"maybe" => (Some(""), true),
"might want to" => (Some("must"), true),
"could" => (Some("must"), false),
"possibly" => (Some(""), false),
_ => (None, false),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::LintConfig;
use std::fs;
#[test]
fn test_generic_instruction_detected() {
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
assert!(!diagnostics.is_empty());
assert!(diagnostics.iter().any(|d| d.rule == "CC-MEM-005"));
}
#[test]
fn test_skip_agents_files() {
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("AGENTS.md"), content, &LintConfig::default());
assert!(
diagnostics.is_empty(),
"CC-MEM rules should not fire for AGENTS.md"
);
let diagnostics = validator.validate(
Path::new("AGENTS.local.md"),
content,
&LintConfig::default(),
);
assert!(
diagnostics.is_empty(),
"CC-MEM rules should not fire for AGENTS.local.md"
);
let diagnostics = validator.validate(
Path::new("AGENTS.override.md"),
content,
&LintConfig::default(),
);
assert!(
diagnostics.is_empty(),
"CC-MEM rules should not fire for AGENTS.override.md"
);
}
#[test]
fn test_claude_local_md_gets_rules() {
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(
Path::new("CLAUDE.local.md"),
content,
&LintConfig::default(),
);
assert!(
!diagnostics.is_empty(),
"CC-MEM rules should fire for CLAUDE.local.md"
);
assert!(diagnostics.iter().any(|d| d.rule == "CC-MEM-005"));
}
#[test]
fn test_cursor_rules_mdc_gets_rules() {
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(
Path::new(".cursor/rules/typescript.mdc"),
content,
&LintConfig::default(),
);
assert!(!diagnostics.is_empty());
assert!(diagnostics.iter().any(|d| d.rule == "CC-MEM-005"));
}
#[test]
fn test_cursor_rules_md_gets_rules() {
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(
Path::new(".cursor/rules/typescript.md"),
content,
&LintConfig::default(),
);
assert!(!diagnostics.is_empty());
assert!(diagnostics.iter().any(|d| d.rule == "CC-MEM-005"));
}
#[test]
fn test_config_disabled_memory_category() {
let mut config = LintConfig::default();
config.rules_mut().memory = false;
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(Path::new("CLAUDE.md"), content, &config);
assert!(diagnostics.is_empty());
}
#[test]
fn test_config_disabled_specific_rule() {
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec!["CC-MEM-005".to_string()];
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(Path::new("CLAUDE.md"), content, &config);
assert!(diagnostics.is_empty());
}
#[test]
fn test_config_cursor_target_disables_cc_mem_rules() {
use crate::config::TargetTool;
let mut config = LintConfig::default();
config.set_target(TargetTool::Cursor);
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(Path::new("CLAUDE.md"), content, &config);
assert!(diagnostics.is_empty());
}
#[test]
fn test_legacy_generic_instructions_flag() {
let mut config = LintConfig::default();
config.rules_mut().generic_instructions = false;
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(Path::new("CLAUDE.md"), content, &config);
assert!(diagnostics.is_empty());
}
#[test]
fn test_cc_mem_009_token_exceeded() {
let content = "x".repeat(6100); let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-009")
.collect();
assert_eq!(mem009.len(), 1);
assert!(mem009[0].message.contains("exceeds"));
}
#[test]
fn test_cc_mem_009_under_limit() {
let content = "Short content.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-009")
.collect();
assert!(mem009.is_empty());
}
#[test]
fn test_cc_mem_006_negative_without_positive() {
let content = "Never use var in JavaScript.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-006")
.collect();
assert_eq!(mem006.len(), 1);
assert!(mem006[0].message.contains("Never"));
}
#[test]
fn test_cc_mem_006_negative_with_positive() {
let content = "Never use var, instead prefer const.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-006")
.collect();
assert!(mem006.is_empty());
}
#[test]
fn test_cc_mem_007_weak_in_critical() {
let content = "# Critical Rules\n\nYou should follow the coding style.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-007")
.collect();
assert_eq!(mem007.len(), 1);
assert!(mem007[0].message.contains("should"));
}
#[test]
fn test_cc_mem_007_weak_outside_critical() {
let content = "# General Info\n\nYou should follow the coding style.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-007")
.collect();
assert!(mem007.is_empty());
}
#[test]
fn test_cc_mem_008_critical_in_middle() {
let mut lines: Vec<String> = (0..20).map(|i| format!("Line {}", i)).collect();
lines[10] = "This is critical information.".to_string();
let content = lines.join("\n");
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-008")
.collect();
assert_eq!(mem008.len(), 1);
assert!(mem008[0].message.contains("middle zone"));
}
#[test]
fn test_cc_mem_008_critical_at_top() {
let mut lines: Vec<String> = (0..20).map(|i| format!("Line {}", i)).collect();
lines[1] = "This is critical information.".to_string();
let content = lines.join("\n");
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-008")
.collect();
assert!(mem008.is_empty());
}
#[test]
fn test_cc_mem_004_invalid_npm_script() {
use std::io::Write;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let claude_md_path = temp_dir.path().join("CLAUDE.md");
let package_json_path = temp_dir.path().join("package.json");
let mut claude_file = fs::File::create(&claude_md_path).unwrap();
writeln!(claude_file, "Run tests with npm run nonexistent").unwrap();
let mut pkg_file = fs::File::create(&package_json_path).unwrap();
writeln!(
pkg_file,
r#"{{"scripts": {{"test": "jest", "build": "tsc"}}}}"#
)
.unwrap();
let content = fs::read_to_string(&claude_md_path).unwrap();
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(&claude_md_path, &content, &LintConfig::default());
let mem004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-004")
.collect();
assert_eq!(mem004.len(), 1);
assert!(mem004[0].message.contains("nonexistent"));
}
#[test]
fn test_cc_mem_004_valid_npm_script() {
use std::io::Write;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let claude_md_path = temp_dir.path().join("CLAUDE.md");
let package_json_path = temp_dir.path().join("package.json");
let mut claude_file = fs::File::create(&claude_md_path).unwrap();
writeln!(claude_file, "Run tests with npm run test").unwrap();
let mut pkg_file = fs::File::create(&package_json_path).unwrap();
writeln!(pkg_file, r#"{{"scripts": {{"test": "jest"}}}}"#).unwrap();
let content = fs::read_to_string(&claude_md_path).unwrap();
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(&claude_md_path, &content, &LintConfig::default());
let mem004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-004")
.collect();
assert!(mem004.is_empty());
}
#[test]
fn test_cc_mem_010_readme_duplication() {
use std::io::Write;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let claude_md_path = temp_dir.path().join("CLAUDE.md");
let readme_path = temp_dir.path().join("README.md");
let shared_content =
"This project validates agent configurations using Rust for performance.";
let mut claude_file = fs::File::create(&claude_md_path).unwrap();
writeln!(claude_file, "{}", shared_content).unwrap();
let mut readme_file = fs::File::create(&readme_path).unwrap();
writeln!(readme_file, "{}", shared_content).unwrap();
let content = fs::read_to_string(&claude_md_path).unwrap();
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(&claude_md_path, &content, &LintConfig::default());
let mem010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-010")
.collect();
assert_eq!(mem010.len(), 1);
assert!(mem010[0].message.contains("overlap"));
}
#[test]
fn test_cc_mem_010_no_duplication() {
use std::io::Write;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let claude_md_path = temp_dir.path().join("CLAUDE.md");
let readme_path = temp_dir.path().join("README.md");
let mut claude_file = fs::File::create(&claude_md_path).unwrap();
writeln!(
claude_file,
"Project-specific instructions for Claude. Focus on these guidelines."
)
.unwrap();
let mut readme_file = fs::File::create(&readme_path).unwrap();
writeln!(
readme_file,
"Welcome to the project. Installation: npm install. Usage: npm start."
)
.unwrap();
let content = fs::read_to_string(&claude_md_path).unwrap();
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(&claude_md_path, &content, &LintConfig::default());
let mem010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-010")
.collect();
assert!(mem010.is_empty());
}
#[test]
fn test_all_new_rules_disabled_individually() {
let content = r#"# Critical Rules
Don't do this without alternatives.
You should consider this approach.
"#
.to_string()
+ &"x".repeat(6100);
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec![
"CC-MEM-004".to_string(),
"CC-MEM-006".to_string(),
"CC-MEM-007".to_string(),
"CC-MEM-008".to_string(),
"CC-MEM-009".to_string(),
"CC-MEM-010".to_string(),
];
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(Path::new("CLAUDE.md"), &content, &config);
for d in &diagnostics {
assert!(
!d.rule.starts_with("CC-MEM-00") || d.rule == "CC-MEM-005",
"Rule {} should be disabled",
d.rule
);
}
}
#[test]
fn test_cc_mem_005_has_fix() {
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-005")
.collect();
assert!(!mem005.is_empty());
assert!(mem005[0].has_fixes());
let fix = &mem005[0].fixes[0];
assert!(fix.is_deletion());
assert!(!fix.safe);
}
#[test]
fn test_cc_mem_005_fix_byte_positions() {
let content = "Line one.\nBe helpful and accurate.\nLine three.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-005")
.collect();
assert_eq!(mem005.len(), 1);
let fix = &mem005[0].fixes[0];
assert_eq!(fix.start_byte, 10);
assert_eq!(fix.end_byte, 35);
}
#[test]
fn test_cc_mem_005_fix_application() {
let content = "Line one.\nBe helpful and accurate.\nLine three.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-005")
.collect();
assert_eq!(mem005.len(), 1);
let fix = &mem005[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert_eq!(fixed, "Line one.\nLine three.");
}
#[test]
fn test_cc_mem_005_fix_last_line_no_newline() {
let content = "Be helpful and accurate.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-005")
.collect();
assert_eq!(mem005.len(), 1);
let fix = &mem005[0].fixes[0];
assert_eq!(fix.start_byte, 0);
assert_eq!(fix.end_byte, 24);
}
#[test]
fn test_cc_mem_007_has_fix() {
let content = "# Critical Rules\n\nYou should follow the coding style.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-007")
.collect();
assert_eq!(mem007.len(), 1);
assert!(mem007[0].has_fixes());
let fix = &mem007[0].fixes[0];
assert_eq!(fix.replacement, "must");
assert!(fix.safe);
}
#[test]
fn test_cc_mem_007_fix_byte_positions() {
let content = "# Critical Rules\n\nYou should follow the coding style.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-007")
.collect();
assert_eq!(mem007.len(), 1);
let fix = &mem007[0].fixes[0];
assert_eq!(fix.start_byte, 22);
assert_eq!(fix.end_byte, 28);
}
#[test]
fn test_cc_mem_007_fix_application() {
let content = "# Critical Rules\n\nYou should follow the coding style.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-007")
.collect();
assert_eq!(mem007.len(), 1);
let fix = &mem007[0].fixes[0];
let mut fixed = content.to_string();
fixed.replace_range(fix.start_byte..fix.end_byte, &fix.replacement);
assert_eq!(
fixed,
"# Critical Rules\n\nYou must follow the coding style."
);
}
#[test]
fn test_cc_mem_007_fix_replacements() {
let test_cases = vec![
("# Critical Rules\n\nYou should do this.", "must", true),
("# Critical Rules\n\nTry to do this.", "must", true),
("# Critical Rules\n\nConsider doing this.", "ensure", true),
("# Critical Rules\n\nMaybe do this.", "", true),
("# Critical Rules\n\nYou could do this.", "must", false),
];
let validator = ClaudeMdValidator;
for (content, expected_replacement, expected_safe) in test_cases {
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-007")
.collect();
assert_eq!(mem007.len(), 1, "Expected one CC-MEM-007 for: {}", content);
assert!(mem007[0].has_fixes());
let fix = &mem007[0].fixes[0];
assert_eq!(
fix.replacement, expected_replacement,
"Wrong replacement for: {}",
content
);
assert_eq!(fix.safe, expected_safe, "Wrong safe flag for: {}", content);
}
}
#[test]
fn test_cc_mem_007_multiple_weak_words() {
let content = "# Critical Rules\n\nYou should consider doing this.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem007: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-007")
.collect();
assert!(mem007.len() >= 1);
for d in &mem007 {
assert!(d.has_fixes());
}
}
#[test]
fn test_cc_mem_004_all_known_commands() {
let known_commands = [
"/help", "/compact", "/resume", "/memory", "/config", "/doctor",
];
let validator = ClaudeMdValidator;
for cmd in known_commands {
let content = format!("# Commands\n\nUse {} for assistance.", cmd);
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem004: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-004")
.collect();
assert!(
mem004.is_empty(),
"Known command '{}' should not trigger CC-MEM-004",
cmd
);
}
}
#[test]
fn test_cc_mem_005_be_helpful_pattern() {
let content = "Be helpful and accurate when responding.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem005: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-005")
.collect();
assert!(
!mem005.is_empty(),
"Generic 'Be helpful and accurate' should trigger CC-MEM-005"
);
}
#[test]
fn test_cc_mem_006_all_negative_patterns() {
let negative_patterns = [
"Don't write bad code.",
"Never use deprecated APIs.",
"Avoid global variables.",
"Do not skip tests.",
];
let validator = ClaudeMdValidator;
for pattern in negative_patterns {
let content = format!("# Rules\n\n{}", pattern);
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem006: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-006")
.collect();
assert!(
!mem006.is_empty(),
"Negative pattern '{}' should trigger CC-MEM-006",
pattern
);
}
}
#[test]
fn test_cc_mem_008_edge_at_boundary() {
let lines: Vec<String> = (1..=10).map(|i| format!("Line {}", i)).collect();
let content = lines.join("\n");
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem008: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-008")
.collect();
assert!(mem008.is_empty());
}
#[test]
fn test_cc_mem_009_exact_token_limit() {
let long_content = "word ".repeat(3900);
let content = format!("# Project\n\n{}", long_content);
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem009: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-009")
.collect();
assert!(!mem009.is_empty());
}
#[test]
fn test_cc_mem_010_unique_content_no_error() {
let content = "# Project Memory\n\nThis contains unique project-specific instructions.";
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), content, &LintConfig::default());
let mem010: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-010")
.collect();
assert!(mem010.is_empty());
}
#[test]
fn test_cc_mem_014_exceeds_line_limit() {
let lines: Vec<String> = (1..=201).map(|i| format!("Rule number {}", i)).collect();
let content = lines.join("\n");
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-014")
.collect();
assert_eq!(mem014.len(), 1);
assert!(mem014[0].message.contains("201"));
assert!(mem014[0].message.contains("200"));
}
#[test]
fn test_cc_mem_014_under_limit() {
let lines: Vec<String> = (1..=200).map(|i| format!("Rule number {}", i)).collect();
let content = lines.join("\n");
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-014")
.collect();
assert!(mem014.is_empty());
}
#[test]
fn test_cc_mem_014_empty_lines_not_counted() {
let mut lines: Vec<String> = (1..=100).map(|i| format!("Rule number {}", i)).collect();
for _ in 0..200 {
lines.push(String::new());
}
let content = lines.join("\n");
let validator = ClaudeMdValidator;
let diagnostics =
validator.validate(Path::new("CLAUDE.md"), &content, &LintConfig::default());
let mem014: Vec<_> = diagnostics
.iter()
.filter(|d| d.rule == "CC-MEM-014")
.collect();
assert!(mem014.is_empty());
}
#[test]
fn test_all_cc_mem_rules_can_be_disabled() {
let rules = [
"CC-MEM-004",
"CC-MEM-005",
"CC-MEM-006",
"CC-MEM-007",
"CC-MEM-008",
"CC-MEM-009",
"CC-MEM-010",
"CC-MEM-014",
];
for rule in rules {
let mut config = LintConfig::default();
config.rules_mut().disabled_rules = vec![rule.to_string()];
let content = "# Critical Rules\n\nAlways follow best practices. Don't write bad code.\nRun /unknown-cmd.\nYou should do this.";
let validator = ClaudeMdValidator;
let diagnostics = validator.validate(Path::new("CLAUDE.md"), content, &config);
assert!(
!diagnostics.iter().any(|d| d.rule == rule),
"Rule {} should be disabled",
rule
);
}
}
}