init: archlens — architecture diagram generator
Some checks failed
CI / Check / Test (push) Failing after 1m24s

Hex arch + DDD, tree-sitter parsing, Mermaid/ASCII output.
Supports Rust + Python. 92 tests. CI, diff, --check for staleness detection.
This commit is contained in:
2026-06-16 16:13:04 +02:00
commit 35f27d00b0
106 changed files with 6744 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
use std::collections::{HashMap, HashSet};
use std::path::Path;
use serde::Deserialize;
use archlens_domain::{
CodeElement, CodeElementKind, CodeGraph, DomainError, FilePath, ModuleName, Relationship,
RelationshipKind, ports::ProjectAnalyzer,
};
pub struct CargoWorkspaceAnalyzer;
impl Default for CargoWorkspaceAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl CargoWorkspaceAnalyzer {
pub fn new() -> Self {
Self
}
}
#[derive(Deserialize)]
struct WorkspaceToml {
workspace: Option<WorkspaceSection>,
}
#[derive(Deserialize)]
struct WorkspaceSection {
#[serde(default)]
members: Vec<String>,
}
#[derive(Deserialize)]
struct MemberToml {
package: Option<PackageSection>,
#[serde(default)]
dependencies: HashMap<String, toml::Value>,
}
#[derive(Deserialize)]
struct PackageSection {
name: String,
}
impl ProjectAnalyzer for CargoWorkspaceAnalyzer {
fn analyze(&self, root: &Path) -> Result<CodeGraph, DomainError> {
let workspace_toml_path = root.join("Cargo.toml");
let content = std::fs::read_to_string(&workspace_toml_path)
.map_err(|e| DomainError::IoError(e.to_string()))?;
let workspace: WorkspaceToml =
toml::from_str(&content).map_err(|e| DomainError::ConfigError(e.to_string()))?;
let members = workspace.workspace.map(|w| w.members).unwrap_or_default();
let mut graph = CodeGraph::new();
let mut name_set: HashSet<String> = HashSet::new();
let mut member_names: Vec<(String, String)> = Vec::new(); // (member_path, package_name)
for member_path in &members {
let member_cargo = root.join(member_path).join("Cargo.toml");
let member_content = std::fs::read_to_string(&member_cargo)
.map_err(|e| DomainError::IoError(e.to_string()))?;
let member: MemberToml = toml::from_str(&member_content)
.map_err(|e| DomainError::ConfigError(e.to_string()))?;
let package_name = member
.package
.map(|p| p.name)
.unwrap_or_else(|| member_path.clone());
name_set.insert(package_name.clone());
member_names.push((member_path.clone(), package_name));
}
for (member_path, package_name) in &member_names {
let file_path = FilePath::new(&format!("{}/Cargo.toml", member_path))
.map_err(|e| DomainError::IoError(e.to_string()))?;
let mut element =
CodeElement::new(package_name, CodeElementKind::Project, file_path, 1)?;
if let Some(module) = infer_group(member_path) {
element = element.with_module(module);
}
graph.add_element(element);
}
for (member_path, package_name) in &member_names {
let member_cargo = root.join(member_path).join("Cargo.toml");
let member_content = std::fs::read_to_string(&member_cargo)
.map_err(|e| DomainError::IoError(e.to_string()))?;
let member: MemberToml = toml::from_str(&member_content)
.map_err(|e| DomainError::ConfigError(e.to_string()))?;
for dep_name in member.dependencies.keys() {
let normalized = dep_name.replace('_', "-");
if name_set.contains(&normalized)
&& let Ok(rel) =
Relationship::new(package_name, &normalized, RelationshipKind::Composition)
{
graph.add_relationship(rel);
}
}
}
Ok(graph)
}
}
fn infer_group(member_path: &str) -> Option<ModuleName> {
let parts: Vec<&str> = member_path.split('/').collect();
if parts.len() < 3 {
return None;
}
let group = parts[parts.len() - 2];
let capitalized = format!("{}{}", group[..1].to_uppercase(), &group[1..]);
ModuleName::new(&capitalized).ok()
}

View File

@@ -0,0 +1,3 @@
mod cargo_workspace_analyzer;
pub use cargo_workspace_analyzer::CargoWorkspaceAnalyzer;