diff --git a/archdoc-cli/src/commands/check.rs b/archdoc-cli/src/commands/check.rs index 633a0a6..52f072d 100644 --- a/archdoc-cli/src/commands/check.rs +++ b/archdoc-cli/src/commands/check.rs @@ -10,7 +10,7 @@ pub fn check_docs_consistency(root: &str, config: &Config) -> Result<()> { let model = analyze_project(root, config)?; let renderer = archdoc_core::renderer::Renderer::new(); - let _generated = renderer.render_architecture_md(&model)?; + let _generated = renderer.render_architecture_md(&model, None)?; let architecture_md_path = std::path::Path::new(root).join(&config.project.entry_file); if !architecture_md_path.exists() { diff --git a/archdoc-cli/src/commands/generate.rs b/archdoc-cli/src/commands/generate.rs index bddc0fc..72142e3 100644 --- a/archdoc-cli/src/commands/generate.rs +++ b/archdoc-cli/src/commands/generate.rs @@ -55,7 +55,46 @@ pub fn analyze_project(root: &str, config: &Config) -> Result { Ok(model) } -pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool) -> Result<()> { +pub fn dry_run_docs(model: &ProjectModel, out: &str, config: &Config) -> Result<()> { + println!("{}", "Dry run — no files will be written.".cyan().bold()); + println!(); + + let out_path = std::path::Path::new(out); + let arch_path = std::path::Path::new(".").join("ARCHITECTURE.md"); + + // ARCHITECTURE.md + let exists = arch_path.exists(); + println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, arch_path.display()); + + // layout.md + let layout_path = out_path.join("layout.md"); + let exists = layout_path.exists(); + println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, layout_path.display()); + + // Module docs + for module_id in model.modules.keys() { + let p = out_path.join("modules").join(format!("{}.md", sanitize_filename(module_id))); + let exists = p.exists(); + println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, p.display()); + } + + // File docs + for file_doc in model.files.values() { + let p = out_path.join("files").join(format!("{}.md", sanitize_filename(&file_doc.path))); + let exists = p.exists(); + println!(" {} {}", if exists { "UPDATE" } else { "CREATE" }, p.display()); + } + + let _ = config; // used for future extensions + println!(); + println!("{} {} file(s) would be generated/updated", + "✓".green().bold(), + 2 + model.modules.len() + model.files.len()); + + Ok(()) +} + +pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool, _config: &Config) -> Result<()> { println!("{}", "Generating documentation...".cyan()); let out_path = std::path::Path::new(out); @@ -74,6 +113,9 @@ pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool) -> Result<( // Generate module docs for module_id in model.modules.keys() { let module_doc_path = modules_path.join(format!("{}.md", sanitize_filename(module_id))); + if verbose { + println!(" Generating module doc: {}", module_id); + } match renderer.render_module_md(model, module_id) { Ok(module_content) => { std::fs::write(&module_doc_path, module_content)?; @@ -88,6 +130,9 @@ pub fn generate_docs(model: &ProjectModel, out: &str, verbose: bool) -> Result<( // Generate file docs for file_doc in model.files.values() { + if verbose { + println!(" Generating file doc: {}", file_doc.path); + } let file_doc_path = files_path.join(format!("{}.md", sanitize_filename(&file_doc.path))); let mut file_content = format!("# File: {}\n\n", file_doc.path); diff --git a/archdoc-cli/src/main.rs b/archdoc-cli/src/main.rs index e5f4421..a67c1f6 100644 --- a/archdoc-cli/src/main.rs +++ b/archdoc-cli/src/main.rs @@ -34,6 +34,9 @@ enum Commands { out: String, #[arg(short, long, default_value = "archdoc.toml")] config: String, + /// Show what would be generated without writing files + #[arg(long)] + dry_run: bool, }, /// Check if documentation is up to date Check { @@ -58,10 +61,14 @@ fn main() -> Result<()> { Commands::Init { root, out } => { commands::init::init_project(root, out)?; } - Commands::Generate { root, out, config } => { + Commands::Generate { root, out, config, dry_run } => { let config = commands::generate::load_config(config)?; let model = commands::generate::analyze_project(root, &config)?; - commands::generate::generate_docs(&model, out, cli.verbose)?; + if *dry_run { + commands::generate::dry_run_docs(&model, out, &config)?; + } else { + commands::generate::generate_docs(&model, out, cli.verbose, &config)?; + } output::print_generate_summary(&model); } Commands::Check { root, config } => { diff --git a/archdoc-core/src/renderer.rs b/archdoc-core/src/renderer.rs index d7c594d..49a51d3 100644 --- a/archdoc-core/src/renderer.rs +++ b/archdoc-core/src/renderer.rs @@ -3,8 +3,10 @@ //! This module handles generating Markdown documentation from the project model //! using templates. +use crate::config::Config; use crate::cycle_detector; -use crate::model::ProjectModel; +use crate::model::{ProjectModel, SymbolKind}; +use chrono::Utc; use handlebars::Handlebars; fn sanitize_for_link(filename: &str) -> String { @@ -236,7 +238,7 @@ impl Renderer { "# } - pub fn render_architecture_md(&self, model: &ProjectModel) -> Result { + pub fn render_architecture_md(&self, model: &ProjectModel, config: Option<&Config>) -> Result { // Collect integration information let mut db_integrations = Vec::new(); let mut http_integrations = Vec::new(); @@ -254,19 +256,40 @@ impl Renderer { } } + // Determine project name: config > directory name > fallback + let project_name = config + .and_then(|c| { + if c.project.name.is_empty() { + None + } else { + Some(c.project.name.clone()) + } + }) + .or_else(|| { + config.map(|c| { + std::path::Path::new(&c.project.root) + .canonicalize() + .ok() + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) + .unwrap_or_else(|| "Project".to_string()) + }) + }) + .unwrap_or_else(|| "Project".to_string()); + + let today = Utc::now().format("%Y-%m-%d").to_string(); + // Prepare data for template let data = serde_json::json!({ - "project_name": "New Project", + "project_name": project_name, "project_description": "", - "created_date": "2026-01-25", - "updated_date": "2026-01-25", + "created_date": &today, + "updated_date": &today, "key_decisions": [""], "non_goals": [""], "change_notes": [""], "db_integrations": db_integrations, "http_integrations": http_integrations, "queue_integrations": queue_integrations, - // TODO: Fill with more actual data from model }); self.templates.render("architecture_md", &data) @@ -313,10 +336,50 @@ impl Renderer { } } - // Prepare usage examples (for now, just placeholders) - let usage_examples = vec![ - "// Example usage of module functions\n// TODO: Add real usage examples based on module analysis".to_string() - ]; + // Generate usage examples from public symbols + let mut usage_examples = Vec::new(); + for symbol_id in &module.symbols { + if let Some(symbol) = model.symbols.get(symbol_id) { + let short_name = symbol.qualname.rsplit('.').next().unwrap_or(&symbol.qualname); + match symbol.kind { + SymbolKind::Function | SymbolKind::AsyncFunction => { + // Extract args from signature: "def foo(a, b)" -> "a, b" + let args = symbol.signature + .find('(') + .and_then(|start| symbol.signature.rfind(')').map(|end| (start, end))) + .map(|(s, e)| &symbol.signature[s+1..e]) + .unwrap_or(""); + let clean_args = args.split(',') + .map(|a| a.split(':').next().unwrap_or("").trim()) + .filter(|a| !a.is_empty() && *a != "self" && *a != "cls") + .collect::>() + .join(", "); + let example_args = if clean_args.is_empty() { String::new() } else { + clean_args.split(", ").map(|a| { + if a.starts_with('*') { "..." } else { a } + }).collect::>().join(", ") + }; + let prefix = if symbol.kind == SymbolKind::AsyncFunction { "await " } else { "" }; + usage_examples.push(format!( + "from {} import {}\nresult = {}{}({})", + module_id, short_name, prefix, short_name, example_args + )); + } + SymbolKind::Class => { + usage_examples.push(format!( + "from {} import {}\ninstance = {}()", + module_id, short_name, short_name + )); + } + SymbolKind::Method => { + // Skip methods - they're shown via class usage + } + } + } + } + if usage_examples.is_empty() { + usage_examples.push(format!("import {}", module_id)); + } // Prepare data for template let data = serde_json::json!({ diff --git a/archdoc-core/src/writer.rs b/archdoc-core/src/writer.rs index f1a2f82..ae2f0af 100644 --- a/archdoc-core/src/writer.rs +++ b/archdoc-core/src/writer.rs @@ -70,17 +70,13 @@ impl DiffAwareWriter { // Check if content has changed let content_changed = existing_content != new_content; - // Write updated content + // Only write if content actually changed (optimization) if content_changed { let updated_content = self.update_timestamp(new_content)?; fs::write(file_path, updated_content) .map_err(ArchDocError::Io)?; - } else { - // Content hasn't changed, but we might still need to update timestamp - // TODO: Implement timestamp update logic based on config - fs::write(file_path, new_content) - .map_err(ArchDocError::Io)?; } + // If not changed, skip writing entirely } Ok(()) @@ -118,17 +114,13 @@ impl DiffAwareWriter { // Check if content has changed let content_changed = existing_content != new_content; - // Write updated content + // Only write if content actually changed (optimization) if content_changed { let updated_content = self.update_timestamp(new_content)?; fs::write(file_path, updated_content) .map_err(ArchDocError::Io)?; - } else { - // Content hasn't changed, but we might still need to update timestamp - // TODO: Implement timestamp update logic based on config - fs::write(file_path, new_content) - .map_err(ArchDocError::Io)?; } + // If not changed, skip writing entirely } else { eprintln!("Warning: No symbol marker found for {} in {}", symbol_id, file_path.display()); } diff --git a/archdoc-core/tests/full_pipeline.rs b/archdoc-core/tests/full_pipeline.rs index 87bf60d..cabb319 100644 --- a/archdoc-core/tests/full_pipeline.rs +++ b/archdoc-core/tests/full_pipeline.rs @@ -142,7 +142,7 @@ fn test_renderer_produces_output() { let config = Config::default(); let model = ProjectModel::new(); let renderer = Renderer::new(); - let result = renderer.render_architecture_md(&model); + let result = renderer.render_architecture_md(&model, None); assert!(result.is_ok(), "Renderer should produce output for empty model"); } diff --git a/archdoc-core/tests/renderer_tests.rs b/archdoc-core/tests/renderer_tests.rs index 5c40db1..2d2fb87 100644 --- a/archdoc-core/tests/renderer_tests.rs +++ b/archdoc-core/tests/renderer_tests.rs @@ -70,7 +70,7 @@ fn test_render_with_integrations() { let renderer = Renderer::new(); // Render architecture documentation - let result = renderer.render_architecture_md(&project_model); + let result = renderer.render_architecture_md(&project_model, None); assert!(result.is_ok()); let rendered_content = result.unwrap();