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,328 @@
use archlens_domain::{
CodeElement, CodeElementKind, CodeGraph, DiagramLevel, FilePath, ModuleName, Relationship,
RelationshipKind, Visibility, ports::DiagramRenderer,
};
use archlens_mermaid::MermaidRenderer;
fn build_type_level_graph() -> CodeGraph {
let mut graph = CodeGraph::new();
graph.add_element(
CodeElement::new(
"OrderService",
CodeElementKind::Class,
FilePath::new("src/service.rs").unwrap(),
1,
)
.unwrap(),
);
graph.add_element(
CodeElement::new(
"Order",
CodeElementKind::Struct,
FilePath::new("src/order.rs").unwrap(),
1,
)
.unwrap(),
);
graph.add_relationship(
Relationship::new("OrderService", "Order", RelationshipKind::Composition).unwrap(),
);
graph
}
#[test]
fn renders_class_diagram_with_elements_and_composition() {
let renderer = MermaidRenderer::new();
let output = renderer.render(&build_type_level_graph()).unwrap();
let content = output.files()[0].content();
assert!(content.contains("classDiagram"));
assert!(content.contains("class OrderService"));
assert!(content.contains("class Order"));
assert!(content.contains("OrderService --> Order"));
}
#[test]
fn inheritance_uses_different_arrow() {
let mut graph = CodeGraph::new();
graph.add_element(
CodeElement::new(
"Animal",
CodeElementKind::Class,
FilePath::new("src/animal.rs").unwrap(),
1,
)
.unwrap(),
);
graph.add_element(
CodeElement::new(
"Dog",
CodeElementKind::Class,
FilePath::new("src/dog.rs").unwrap(),
1,
)
.unwrap(),
);
graph.add_relationship(
Relationship::new("Dog", "Animal", RelationshipKind::Inheritance).unwrap(),
);
let renderer = MermaidRenderer::new();
let output = renderer.render(&graph).unwrap();
let content = output.files()[0].content();
assert!(content.contains("Dog <|-- Animal"));
}
#[test]
fn elements_show_kind_and_generics() {
let mut graph = CodeGraph::new();
graph.add_element(
CodeElement::new(
"Repository",
CodeElementKind::Trait,
FilePath::new("src/repo.rs").unwrap(),
1,
)
.unwrap()
.with_generics(vec!["T".to_string()]),
);
let renderer = MermaidRenderer::new();
let output = renderer.render(&graph).unwrap();
let content = output.files()[0].content();
assert!(content.contains("class Repository~T~"));
}
#[test]
fn private_elements_show_visibility_annotation() {
let mut graph = CodeGraph::new();
graph.add_element(
CodeElement::new(
"InternalHelper",
CodeElementKind::Class,
FilePath::new("src/helper.rs").unwrap(),
1,
)
.unwrap()
.with_visibility(Visibility::Private),
);
let renderer = MermaidRenderer::new();
let output = renderer.render(&graph).unwrap();
let content = output.files()[0].content();
assert!(content.contains("class InternalHelper"));
assert!(content.contains("<<private>>"));
}
#[test]
fn renders_module_level_flowchart() {
let mut graph = CodeGraph::new();
graph.add_element(
CodeElement::new(
"OrderService",
CodeElementKind::Class,
FilePath::new("src/orders/service.rs").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Orders").unwrap()),
);
graph.add_element(
CodeElement::new(
"BillingService",
CodeElementKind::Class,
FilePath::new("src/billing/service.rs").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Billing").unwrap()),
);
graph.add_relationship(
Relationship::new(
"OrderService",
"BillingService",
RelationshipKind::Composition,
)
.unwrap(),
);
let renderer = MermaidRenderer::with_level(DiagramLevel::Module);
let output = renderer.render(&graph).unwrap();
let content = output.files()[0].content();
assert!(content.contains("graph TD"));
assert!(content.contains("Orders"));
assert!(content.contains("Billing"));
assert!(content.contains("Orders --> Billing"));
}
#[test]
fn empty_graph_produces_valid_diagram() {
let renderer = MermaidRenderer::new();
let output = renderer.render(&CodeGraph::new()).unwrap();
let content = output.files()[0].content();
assert!(content.contains("classDiagram"));
}
#[test]
fn project_level_renders_subgraphs_for_grouped_projects() {
let mut graph = CodeGraph::new();
graph.add_element(
CodeElement::new(
"myapp-domain",
CodeElementKind::Project,
FilePath::new("crates/domain/Cargo.toml").unwrap(),
1,
)
.unwrap(),
);
graph.add_element(
CodeElement::new(
"myapp-sqlite",
CodeElementKind::Project,
FilePath::new("crates/adapters/sqlite/Cargo.toml").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Adapters").unwrap()),
);
graph.add_element(
CodeElement::new(
"myapp-nats",
CodeElementKind::Project,
FilePath::new("crates/adapters/nats/Cargo.toml").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Adapters").unwrap()),
);
graph.add_relationship(
Relationship::new(
"myapp-sqlite",
"myapp-domain",
RelationshipKind::Composition,
)
.unwrap(),
);
let renderer = MermaidRenderer::with_level(DiagramLevel::Project);
let output = renderer.render(&graph).unwrap();
let content = output.files()[0].content();
assert!(content.contains("graph TD"));
assert!(content.contains("subgraph Adapters"));
assert!(content.contains("myapp-sqlite"));
assert!(content.contains("myapp-nats"));
assert!(content.contains("myapp-domain"));
assert!(content.contains("-->"));
}
#[test]
fn type_level_groups_elements_by_module() {
let mut graph = CodeGraph::new();
graph.add_element(
CodeElement::new(
"OrderService",
CodeElementKind::Class,
FilePath::new("src/orders/service.rs").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Orders").unwrap()),
);
graph.add_element(
CodeElement::new(
"Order",
CodeElementKind::Struct,
FilePath::new("src/orders/order.rs").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Orders").unwrap()),
);
graph.add_element(
CodeElement::new(
"Invoice",
CodeElementKind::Struct,
FilePath::new("src/billing/invoice.rs").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Billing").unwrap()),
);
let renderer = MermaidRenderer::new();
let output = renderer.render(&graph).unwrap();
let content = output.files()[0].content();
assert!(content.contains("namespace Orders"));
assert!(content.contains("namespace Billing"));
assert!(content.contains("OrderService"));
assert!(content.contains("Invoice"));
}
#[test]
fn module_level_aggregates_cross_module_deps_into_single_arrow() {
let mut graph = CodeGraph::new();
graph.add_element(
CodeElement::new(
"OrderService",
CodeElementKind::Class,
FilePath::new("src/orders/service.rs").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Orders").unwrap()),
);
graph.add_element(
CodeElement::new(
"OrderRepo",
CodeElementKind::Trait,
FilePath::new("src/orders/repo.rs").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Orders").unwrap()),
);
graph.add_element(
CodeElement::new(
"DbPool",
CodeElementKind::Struct,
FilePath::new("src/infra/db.rs").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Infra").unwrap()),
);
graph.add_element(
CodeElement::new(
"SqliteRepo",
CodeElementKind::Struct,
FilePath::new("src/infra/sqlite.rs").unwrap(),
1,
)
.unwrap()
.with_module(ModuleName::new("Infra").unwrap()),
);
// Two types in Orders depend on two types in Infra
graph.add_relationship(
Relationship::new("OrderService", "DbPool", RelationshipKind::Composition).unwrap(),
);
graph.add_relationship(
Relationship::new("OrderRepo", "SqliteRepo", RelationshipKind::Composition).unwrap(),
);
let renderer = MermaidRenderer::with_level(DiagramLevel::Module);
let output = renderer.render(&graph).unwrap();
let content = output.files()[0].content();
let arrow_count = content.matches("Orders --> Infra").count();
assert_eq!(
arrow_count, 1,
"should have exactly one aggregated arrow, got:\n{content}"
);
}