use super::{Command, PathBuf, QualityRules, Section, AgentsMdDocument, Path};
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fmt::Write;
pub struct AgentsMdGenerator {
templates: HashMap<ProjectType, Template>,
config: GeneratorConfig,
}
#[derive(Debug, Clone)]
pub struct GeneratorConfig {
pub include_quality: bool,
pub include_security: bool,
pub include_pr_guidelines: bool,
pub max_commands: usize,
pub include_examples: bool,
}
impl Default for GeneratorConfig {
fn default() -> Self {
Self {
include_quality: true,
include_security: true,
include_pr_guidelines: true,
max_commands: 20,
include_examples: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ProjectType {
Rust,
JavaScript,
Python,
Go,
Java,
Generic,
}
#[derive(Debug, Clone)]
pub struct Template {
pub sections: Vec<TemplateSection>,
pub default_commands: Vec<Command>,
}
#[derive(Debug, Clone)]
pub struct TemplateSection {
pub title: String,
pub content: String,
pub variables: Vec<String>,
}
pub struct PmatAnalysis {
pub project_name: String,
pub description: String,
pub project_type: ProjectType,
pub commands: Vec<Command>,
pub quality_metrics: QualityMetrics,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct QualityMetrics {
pub avg_complexity: f64,
pub test_coverage: f64,
pub satd_count: usize,
pub grade: String,
}
pub struct ProjectInfo {
pub root: PathBuf,
pub name: String,
pub version: String,
pub description: String,
pub readme: Option<String>,
}
pub struct Updates {
pub new_commands: Vec<Command>,
pub quality_rules: Option<QualityRules>,
pub new_sections: Vec<Section>,
}
impl Default for AgentsMdGenerator {
fn default() -> Self {
Self::new()
}
}
impl AgentsMdGenerator {
#[must_use]
pub fn new() -> Self {
let mut generator = Self {
templates: HashMap::new(),
config: GeneratorConfig::default(),
};
generator.load_default_templates();
generator
}
#[must_use]
pub fn with_config(config: GeneratorConfig) -> Self {
let mut generator = Self {
templates: HashMap::new(),
config,
};
generator.load_default_templates();
generator
}
fn load_default_templates(&mut self) {
self.templates.insert(ProjectType::Rust, Template {
sections: vec![
TemplateSection {
title: "Project Overview".to_string(),
content: "{project_name}\n{description}".to_string(),
variables: vec!["project_name".to_string(), "description".to_string()],
},
TemplateSection {
title: "Development Setup".to_string(),
content: "```bash\n# Install dependencies\ncargo build --all\n\n# Run tests\ncargo test --all\n```".to_string(),
variables: vec![],
},
],
default_commands: vec![
Command {
name: "Build".to_string(),
command: "cargo build --all".to_string(),
working_dir: None,
env: vec![],
timeout: Some(60),
safe: true,
},
Command {
name: "Test".to_string(),
command: "cargo test --all".to_string(),
working_dir: None,
env: vec![],
timeout: Some(120),
safe: true,
},
],
});
self.templates.insert(
ProjectType::Generic,
Template {
sections: vec![TemplateSection {
title: "Project Overview".to_string(),
content: "{project_name}\n{description}".to_string(),
variables: vec!["project_name".to_string(), "description".to_string()],
}],
default_commands: vec![],
},
);
}
pub fn generate_from_analysis(&self, analysis: &PmatAnalysis) -> Result<String> {
let _template = self
.templates
.get(&analysis.project_type)
.or_else(|| self.templates.get(&ProjectType::Generic))
.context("No template found")?;
let mut output = String::new();
writeln!(output, "# AGENTS.md")?;
writeln!(output)?;
writeln!(output, "## Project Overview")?;
writeln!(
output,
"{}\n{}",
analysis.project_name, analysis.description
)?;
writeln!(output)?;
self.generate_dev_setup(&mut output, analysis)?;
self.generate_testing(&mut output, analysis)?;
if self.config.include_quality {
self.generate_code_style(&mut output, analysis)?;
}
if self.config.include_pr_guidelines {
self.generate_pr_guidelines(&mut output)?;
}
if self.config.include_security {
self.generate_security(&mut output)?;
}
Ok(output)
}
pub fn generate_from_project(&self, project: &ProjectInfo) -> Result<String> {
let analysis = PmatAnalysis {
project_name: project.name.clone(),
description: project.description.clone(),
project_type: self.detect_project_type(&project.root)?,
commands: self.detect_commands(&project.root)?,
quality_metrics: QualityMetrics {
avg_complexity: 10.0,
test_coverage: 80.0,
satd_count: 0,
grade: "A".to_string(),
},
dependencies: vec![],
};
self.generate_from_analysis(&analysis)
}
pub fn update_existing(&self, current: &str, updates: Updates) -> Result<String> {
let parser = super::parser::AgentsMdParser::new();
let mut doc = parser.parse(current)?;
for cmd in updates.new_commands {
if !doc.commands.iter().any(|c| c.name == cmd.name) {
doc.commands.push(cmd);
}
}
if let Some(rules) = updates.quality_rules {
doc.quality_rules = Some(rules);
}
for section in updates.new_sections {
if !doc.sections.iter().any(|s| s.title == section.title) {
doc.sections.push(section);
}
}
self.format_document(&doc)
}
fn format_document(&self, doc: &AgentsMdDocument) -> Result<String> {
let mut output = String::new();
writeln!(output, "# AGENTS.md")?;
writeln!(output)?;
for section in &doc.sections {
self.format_section(&mut output, section, 2)?;
}
Ok(output)
}
#[allow(clippy::only_used_in_recursion)]
fn format_section(&self, output: &mut String, section: &Section, level: usize) -> Result<()> {
let heading = "#".repeat(level);
writeln!(output, "{} {}", heading, section.title)?;
writeln!(output, "{}", section.content)?;
writeln!(output)?;
for subsection in §ion.subsections {
self.format_section(output, subsection, level + 1)?;
}
Ok(())
}
fn generate_dev_setup(&self, output: &mut String, analysis: &PmatAnalysis) -> Result<()> {
writeln!(output, "## Development Setup")?;
writeln!(output, "```bash")?;
let commands = &analysis.commands[..analysis.commands.len().min(5)];
for cmd in commands {
writeln!(output, "# {}", cmd.name)?;
writeln!(output, "{}", cmd.command)?;
writeln!(output)?;
}
writeln!(output, "```")?;
writeln!(output)?;
Ok(())
}
fn generate_testing(&self, output: &mut String, analysis: &PmatAnalysis) -> Result<()> {
writeln!(output, "## Testing Instructions")?;
writeln!(output, "- Run tests before committing")?;
writeln!(
output,
"- Ensure {}%+ coverage maintained",
analysis.quality_metrics.test_coverage as u32
)?;
writeln!(output, "- All functions must have complexity ≤{}", 10)?;
writeln!(output)?;
Ok(())
}
fn generate_code_style(&self, output: &mut String, analysis: &PmatAnalysis) -> Result<()> {
writeln!(output, "## Code Style")?;
writeln!(output, "- Follow project coding standards")?;
writeln!(
output,
"- Current quality grade: {}",
analysis.quality_metrics.grade
)?;
writeln!(output, "- Zero SATD tolerance")?;
writeln!(output)?;
Ok(())
}
fn generate_pr_guidelines(&self, output: &mut String) -> Result<()> {
writeln!(output, "## PR Guidelines")?;
writeln!(output, "- Squash commits with conventional format")?;
writeln!(output, "- Must pass all quality gates")?;
writeln!(output, "- Include tests for new features")?;
writeln!(output)?;
Ok(())
}
fn generate_security(&self, output: &mut String) -> Result<()> {
writeln!(output, "## Security Considerations")?;
writeln!(output, "- No secrets in code")?;
writeln!(output, "- Validate all external input")?;
writeln!(output, "- Use secure defaults")?;
writeln!(output)?;
Ok(())
}
fn detect_project_type(&self, path: &Path) -> Result<ProjectType> {
if path.join("Cargo.toml").exists() {
Ok(ProjectType::Rust)
} else if path.join("package.json").exists() {
Ok(ProjectType::JavaScript)
} else if path.join("setup.py").exists() || path.join("pyproject.toml").exists() {
Ok(ProjectType::Python)
} else if path.join("go.mod").exists() {
Ok(ProjectType::Go)
} else if path.join("pom.xml").exists() || path.join("build.gradle").exists() {
Ok(ProjectType::Java)
} else {
Ok(ProjectType::Generic)
}
}
fn detect_commands(&self, path: &Path) -> Result<Vec<Command>> {
let mut commands = Vec::new();
if path.join("Makefile").exists() {
commands.push(Command {
name: "Build".to_string(),
command: "make build".to_string(),
working_dir: None,
env: vec![],
timeout: Some(60),
safe: true,
});
commands.push(Command {
name: "Test".to_string(),
command: "make test".to_string(),
working_dir: None,
env: vec![],
timeout: Some(120),
safe: true,
});
}
if path.join("package.json").exists() {
commands.push(Command {
name: "Install".to_string(),
command: "npm install".to_string(),
working_dir: None,
env: vec![],
timeout: Some(300),
safe: true,
});
}
Ok(commands)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agents_md::{DocumentMetadata, SectionType};
use std::fs;
use tempfile::TempDir;
#[test]
fn test_generator_creation() {
let generator = AgentsMdGenerator::new();
assert!(generator.config.include_quality);
assert!(generator.config.include_security);
assert_eq!(generator.config.max_commands, 20);
}
#[test]
fn test_generate_from_analysis() {
let generator = AgentsMdGenerator::new();
let analysis = PmatAnalysis {
project_name: "Test Project".to_string(),
description: "A test project".to_string(),
project_type: ProjectType::Rust,
commands: vec![Command {
name: "Build".to_string(),
command: "cargo build".to_string(),
working_dir: None,
env: vec![],
timeout: Some(60),
safe: true,
}],
quality_metrics: QualityMetrics {
avg_complexity: 8.5,
test_coverage: 85.0,
satd_count: 0,
grade: "A".to_string(),
},
dependencies: vec![],
};
let result = generator.generate_from_analysis(&analysis).unwrap();
assert!(result.contains("# AGENTS.md"));
assert!(result.contains("Test Project"));
assert!(result.contains("cargo build"));
assert!(result.contains("## Testing Instructions"));
}
#[test]
fn test_detect_project_type() {
let generator = AgentsMdGenerator::new();
let temp = TempDir::new().unwrap();
fs::write(temp.path().join("Cargo.toml"), "[package]").unwrap();
assert_eq!(
generator.detect_project_type(temp.path()).unwrap(),
ProjectType::Rust
);
fs::remove_file(temp.path().join("Cargo.toml")).unwrap();
fs::write(temp.path().join("package.json"), "{}").unwrap();
assert_eq!(
generator.detect_project_type(temp.path()).unwrap(),
ProjectType::JavaScript
);
}
#[test]
fn test_update_existing() {
let generator = AgentsMdGenerator::new();
let current = r#"# AGENTS.md
## Project Overview
Test Project
## Development Setup
```bash
cargo build
```
"#;
let updates = Updates {
new_commands: vec![Command {
name: "Test".to_string(),
command: "cargo test".to_string(),
working_dir: None,
env: vec![],
timeout: Some(120),
safe: true,
}],
quality_rules: Some(QualityRules {
max_complexity: Some(10),
min_coverage: Some(80.0),
satd_allowed: false,
custom_checks: vec![],
}),
new_sections: vec![],
};
let result = generator.update_existing(current, updates).unwrap();
assert!(result.contains("# AGENTS.md"));
assert!(result.contains("Project Overview"));
}
#[test]
fn test_format_document() {
let generator = AgentsMdGenerator::new();
let doc = AgentsMdDocument {
metadata: DocumentMetadata {
path: PathBuf::from("AGENTS.md"),
modified: std::time::SystemTime::now(),
version: Some("1.0.0".to_string()),
project: Some("Test".to_string()),
},
sections: vec![Section {
section_type: SectionType::Overview,
title: "Project Overview".to_string(),
content: "Test project".to_string(),
subsections: vec![],
}],
commands: vec![],
guidelines: vec![],
quality_rules: None,
};
let result = generator.format_document(&doc).unwrap();
assert!(result.contains("# AGENTS.md"));
assert!(result.contains("## Project Overview"));
assert!(result.contains("Test project"));
}
#[test]
fn test_config_customization() {
let config = GeneratorConfig {
include_quality: false,
include_security: false,
include_pr_guidelines: false,
max_commands: 5,
include_examples: false,
};
let generator = AgentsMdGenerator::with_config(config);
assert!(!generator.config.include_quality);
assert!(!generator.config.include_security);
assert_eq!(generator.config.max_commands, 5);
}
}