feat: use actual project data, real usage examples, dry-run/verbose flags, skip-unchanged optimization

- renderer: render_architecture_md accepts Config, uses project name and current date
- renderer: generate real Python usage examples from analyzed symbols
- writer: skip writing files when content unchanged (optimization)
- cli: add --dry-run flag to generate command (lists files without writing)
- cli: add verbose logging for file/module/symbol generation progress
This commit is contained in:
2026-02-15 03:32:10 +03:00
parent df52f80999
commit 25fdf400fa
7 changed files with 135 additions and 28 deletions

View File

@@ -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<String, anyhow::Error> {
pub fn render_architecture_md(&self, model: &ProjectModel, config: Option<&Config>) -> Result<String, anyhow::Error> {
// 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": "<FILL_MANUALLY: what this project does in 37 lines>",
"created_date": "2026-01-25",
"updated_date": "2026-01-25",
"created_date": &today,
"updated_date": &today,
"key_decisions": ["<FILL_MANUALLY>"],
"non_goals": ["<FILL_MANUALLY>"],
"change_notes": ["<FILL_MANUALLY>"],
"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::<Vec<_>>()
.join(", ");
let example_args = if clean_args.is_empty() { String::new() } else {
clean_args.split(", ").map(|a| {
if a.starts_with('*') { "..." } else { a }
}).collect::<Vec<_>>().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!({

View File

@@ -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());
}