diff --git a/archdoc-cli/src/commands/init.rs b/archdoc-cli/src/commands/init.rs index 6c4a81b..932a423 100644 --- a/archdoc-cli/src/commands/init.rs +++ b/archdoc-cli/src/commands/init.rs @@ -1,9 +1,48 @@ use anyhow::Result; use colored::Colorize; +/// Detect project name from pyproject.toml or directory basename. +fn detect_project_name(root: &str) -> String { + let root_path = std::path::Path::new(root); + + // Try pyproject.toml + let pyproject_path = root_path.join("pyproject.toml"); + if let Ok(content) = std::fs::read_to_string(&pyproject_path) { + let mut in_project = false; + for line in content.lines() { + let trimmed = line.trim(); + if trimmed == "[project]" { + in_project = true; + continue; + } + if trimmed.starts_with('[') { + in_project = false; + continue; + } + if in_project && trimmed.starts_with("name") { + if let Some(val) = trimmed.split('=').nth(1) { + let name = val.trim().trim_matches('"').trim_matches('\''); + if !name.is_empty() { + return name.to_string(); + } + } + } + } + } + + // Fallback: directory basename + root_path + .canonicalize() + .ok() + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) + .unwrap_or_else(|| "Project".to_string()) +} + pub fn init_project(root: &str, out: &str) -> Result<()> { println!("{}", "Initializing archdoc project...".cyan().bold()); + let project_name = detect_project_name(root); + let out_path = std::path::Path::new(out); std::fs::create_dir_all(out_path)?; std::fs::create_dir_all(out_path.join("modules"))?; @@ -95,8 +134,10 @@ pub fn init_project(root: &str, out: &str) -> Result<()> { "#; + let architecture_md_content = architecture_md_content.replace("", &project_name); + let architecture_md_path = std::path::Path::new(root).join("ARCHITECTURE.md"); - std::fs::write(&architecture_md_path, architecture_md_content)?; + std::fs::write(&architecture_md_path, &architecture_md_content)?; let config_toml_content = r#"[project] root = "." diff --git a/archdoc-core/src/python_analyzer.rs b/archdoc-core/src/python_analyzer.rs index b474fac..bb053cc 100644 --- a/archdoc-core/src/python_analyzer.rs +++ b/archdoc-core/src/python_analyzer.rs @@ -658,9 +658,9 @@ impl PythonAnalyzer { let doc_summary = if is_init { parsed_module.file_docstring.clone() } else { - // For non-init files, check if there's an __init__.py docstring for this module's parent - init_docstrings.get(&module_id).cloned() - .or_else(|| parsed_module.file_docstring.clone()) + // For non-init files, use file docstring first, then check __init__.py + parsed_module.file_docstring.clone() + .or_else(|| init_docstrings.get(&module_id).cloned()) }; let module = Module { @@ -799,6 +799,25 @@ impl PythonAnalyzer { Ok(()) } + /// Check if a class symbol is a simple data container (dataclass-like). + /// A class is considered a dataclass if it has ≤2 methods (typically __init__ and __repr__/__str__). + fn is_dataclass_like(symbol_id: &str, project_model: &ProjectModel) -> bool { + let symbol = match project_model.symbols.get(symbol_id) { + Some(s) => s, + None => return false, + }; + if symbol.kind != crate::model::SymbolKind::Class { + return false; + } + // Count methods belonging to this class + let class_name = &symbol.qualname; + let method_prefix = format!("{}::{}.", symbol.module_id, class_name); + let method_count = project_model.symbols.values() + .filter(|s| s.kind == crate::model::SymbolKind::Method && s.id.starts_with(&method_prefix)) + .count(); + method_count <= 2 + } + fn compute_metrics(&self, project_model: &mut ProjectModel) -> Result<(), ArchDocError> { // Collect fan-in/fan-out first to avoid borrow issues let mut metrics: std::collections::HashMap = std::collections::HashMap::new(); @@ -815,12 +834,20 @@ impl PythonAnalyzer { metrics.insert(symbol_id.clone(), (fan_in, fan_out)); } + // Pre-compute which symbols are dataclass-like (need immutable borrow) + let dataclass_ids: std::collections::HashSet = metrics.keys() + .filter(|id| Self::is_dataclass_like(id, project_model)) + .cloned() + .collect(); + for (symbol_id, (fan_in, fan_out)) in &metrics { if let Some(symbol) = project_model.symbols.get_mut(symbol_id) { symbol.metrics.fan_in = *fan_in; symbol.metrics.fan_out = *fan_out; - symbol.metrics.is_critical = *fan_in > self.config.thresholds.critical_fan_in + // Don't mark dataclass-like classes as critical — they're just data containers + let exceeds_threshold = *fan_in > self.config.thresholds.critical_fan_in || *fan_out > self.config.thresholds.critical_fan_out; + symbol.metrics.is_critical = exceeds_threshold && !dataclass_ids.contains(symbol_id); } }