style: cargo fmt
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -107,6 +107,7 @@ name = "archlens-ascii"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"archlens-domain",
|
"archlens-domain",
|
||||||
|
"archlens-rendering-primitives",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@@ -128,6 +129,7 @@ name = "archlens-d2"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"archlens-domain",
|
"archlens-domain",
|
||||||
|
"archlens-rendering-primitives",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -153,6 +155,7 @@ name = "archlens-html"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"archlens-domain",
|
"archlens-domain",
|
||||||
|
"archlens-rendering-primitives",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@@ -163,6 +166,7 @@ name = "archlens-mermaid"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"archlens-domain",
|
"archlens-domain",
|
||||||
|
"archlens-rendering-primitives",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@@ -177,6 +181,13 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "archlens-rendering-primitives"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"archlens-domain",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archlens-stdout-writer"
|
name = "archlens-stdout-writer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ fn render_module(graph: &CodeGraph) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (src, tgt) in graph.module_edges().keys() {
|
for (src, tgt) in graph.module_edges().keys() {
|
||||||
lines.push(format!("{} -> {}", sanitize_identifier(src), sanitize_identifier(tgt)));
|
lines.push(format!(
|
||||||
|
"{} -> {}",
|
||||||
|
sanitize_identifier(src),
|
||||||
|
sanitize_identifier(tgt)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
@@ -113,7 +117,11 @@ fn render_project(graph: &CodeGraph) -> String {
|
|||||||
let mod_id = sanitize_identifier(module);
|
let mod_id = sanitize_identifier(module);
|
||||||
lines.push(format!("{mod_id}: {{"));
|
lines.push(format!("{mod_id}: {{"));
|
||||||
for el in elements {
|
for el in elements {
|
||||||
lines.push(format!(" {}: {}", sanitize_identifier(el.name()), el.name()));
|
lines.push(format!(
|
||||||
|
" {}: {}",
|
||||||
|
sanitize_identifier(el.name()),
|
||||||
|
el.name()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
lines.push("}".to_string());
|
lines.push("}".to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use archlens_domain::{
|
use archlens_domain::{CodeGraph, DomainError, RenderOutput, RenderedFile, ports::DiagramRenderer};
|
||||||
CodeGraph, DomainError, RenderOutput, RenderedFile, ports::DiagramRenderer,
|
|
||||||
};
|
|
||||||
use archlens_rendering_primitives::non_import_rels;
|
use archlens_rendering_primitives::non_import_rels;
|
||||||
|
|
||||||
pub struct HtmlRenderer;
|
pub struct HtmlRenderer;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use archlens_domain::{
|
use archlens_domain::{Relationship, RelationshipKind};
|
||||||
Relationship, RelationshipKind,
|
|
||||||
};
|
|
||||||
use archlens_rendering_primitives::{non_import_rels, sanitize_identifier};
|
use archlens_rendering_primitives::{non_import_rels, sanitize_identifier};
|
||||||
|
|
||||||
fn rel(src: &str, tgt: &str, kind: RelationshipKind) -> Relationship {
|
fn rel(src: &str, tgt: &str, kind: RelationshipKind) -> Relationship {
|
||||||
@@ -16,7 +14,11 @@ fn non_import_rels_excludes_import_relationships() {
|
|||||||
];
|
];
|
||||||
let filtered: Vec<_> = non_import_rels(&rels).collect();
|
let filtered: Vec<_> = non_import_rels(&rels).collect();
|
||||||
assert_eq!(filtered.len(), 2);
|
assert_eq!(filtered.len(), 2);
|
||||||
assert!(filtered.iter().all(|r| r.kind() != RelationshipKind::Import));
|
assert!(
|
||||||
|
filtered
|
||||||
|
.iter()
|
||||||
|
.all(|r| r.kind() != RelationshipKind::Import)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ const RUST_PRIMITIVES: &[&str] = &[
|
|||||||
"Self",
|
"Self",
|
||||||
];
|
];
|
||||||
|
|
||||||
use archlens_domain::{
|
use archlens_domain::{CodeElement, CodeElementKind, Relationship, RelationshipKind, Visibility};
|
||||||
CodeElement, CodeElementKind, Relationship, RelationshipKind, Visibility,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::extraction_context::ExtractionContext;
|
use crate::extraction_context::ExtractionContext;
|
||||||
use crate::language_extractor::LanguageExtractor;
|
use crate::language_extractor::LanguageExtractor;
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ impl<'a> DiffDiagram<'a> {
|
|||||||
let current = rendered.files().first().map(|f| f.content()).unwrap_or("");
|
let current = rendered.files().first().map(|f| f.content()).unwrap_or("");
|
||||||
|
|
||||||
let current_lines: std::collections::HashSet<&str> = current.lines().collect();
|
let current_lines: std::collections::HashSet<&str> = current.lines().collect();
|
||||||
let existing_lines: std::collections::HashSet<&str> = self.existing_content.lines().collect();
|
let existing_lines: std::collections::HashSet<&str> =
|
||||||
|
self.existing_content.lines().collect();
|
||||||
|
|
||||||
let added: Vec<String> = current_lines
|
let added: Vec<String> = current_lines
|
||||||
.difference(&existing_lines)
|
.difference(&existing_lines)
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ fn project_level_returns_project_analyzer_graph() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = use_case
|
let result = use_case
|
||||||
.execute(Path::new("."), &AnalysisConfig::default(), DiagramLevel::Project)
|
.execute(
|
||||||
|
Path::new("."),
|
||||||
|
&AnalysisConfig::default(),
|
||||||
|
DiagramLevel::Project,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.graph.elements().len(), 1);
|
assert_eq!(result.graph.elements().len(), 1);
|
||||||
@@ -47,7 +51,11 @@ fn project_level_without_analyzer_returns_error() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let err = use_case
|
let err = use_case
|
||||||
.execute(Path::new("."), &AnalysisConfig::default(), DiagramLevel::Project)
|
.execute(
|
||||||
|
Path::new("."),
|
||||||
|
&AnalysisConfig::default(),
|
||||||
|
DiagramLevel::Project,
|
||||||
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(err.to_string().contains("no project analyzer"));
|
assert!(err.to_string().contains("no project analyzer"));
|
||||||
@@ -63,13 +71,15 @@ fn type_level_uses_source_analyzer_not_project() {
|
|||||||
let analyzer = FakeSourceAnalyzer::new().with_result(
|
let analyzer = FakeSourceAnalyzer::new().with_result(
|
||||||
"src/order.rs",
|
"src/order.rs",
|
||||||
AnalysisResult::new(
|
AnalysisResult::new(
|
||||||
vec![CodeElement::new(
|
vec![
|
||||||
|
CodeElement::new(
|
||||||
"Order",
|
"Order",
|
||||||
CodeElementKind::Struct,
|
CodeElementKind::Struct,
|
||||||
FilePath::new("src/order.rs").unwrap(),
|
FilePath::new("src/order.rs").unwrap(),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
.unwrap()],
|
.unwrap(),
|
||||||
|
],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
),
|
),
|
||||||
@@ -93,7 +103,11 @@ fn type_level_uses_source_analyzer_not_project() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = use_case
|
let result = use_case
|
||||||
.execute(Path::new("."), &AnalysisConfig::default(), DiagramLevel::Type)
|
.execute(
|
||||||
|
Path::new("."),
|
||||||
|
&AnalysisConfig::default(),
|
||||||
|
DiagramLevel::Type,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Source element present, project element NOT merged (Type level skips merge)
|
// Source element present, project element NOT merged (Type level skips merge)
|
||||||
@@ -110,7 +124,11 @@ fn module_level_without_project_analyzer_succeeds() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = use_case
|
let result = use_case
|
||||||
.execute(Path::new("."), &AnalysisConfig::default(), DiagramLevel::Module)
|
.execute(
|
||||||
|
Path::new("."),
|
||||||
|
&AnalysisConfig::default(),
|
||||||
|
DiagramLevel::Module,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(result.graph.elements().is_empty());
|
assert!(result.graph.elements().is_empty());
|
||||||
@@ -128,12 +146,14 @@ fn warnings_from_source_analysis_are_propagated() {
|
|||||||
AnalysisResult::new(
|
AnalysisResult::new(
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
vec![AnalysisWarning::new(
|
vec![
|
||||||
|
AnalysisWarning::new(
|
||||||
FilePath::new("src/broken.rs").unwrap(),
|
FilePath::new("src/broken.rs").unwrap(),
|
||||||
5,
|
5,
|
||||||
"unparseable block",
|
"unparseable block",
|
||||||
)
|
)
|
||||||
.unwrap()],
|
.unwrap(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -144,7 +164,11 @@ fn warnings_from_source_analysis_are_propagated() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let result = use_case
|
let result = use_case
|
||||||
.execute(Path::new("."), &AnalysisConfig::default(), DiagramLevel::Module)
|
.execute(
|
||||||
|
Path::new("."),
|
||||||
|
&AnalysisConfig::default(),
|
||||||
|
DiagramLevel::Module,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(result.warnings.len(), 1);
|
assert_eq!(result.warnings.len(), 1);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
mod fakes;
|
mod fakes;
|
||||||
|
|
||||||
use archlens_domain::{CodeGraph, NormalizedGraph, ports::DiagramRenderer};
|
|
||||||
use archlens_application::use_cases::check_freshness::CheckFreshness;
|
use archlens_application::use_cases::check_freshness::CheckFreshness;
|
||||||
|
use archlens_domain::{CodeGraph, NormalizedGraph, ports::DiagramRenderer};
|
||||||
use fakes::FakeDiagramRenderer;
|
use fakes::FakeDiagramRenderer;
|
||||||
|
|
||||||
fn empty_graph() -> NormalizedGraph {
|
fn empty_graph() -> NormalizedGraph {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
mod fakes;
|
mod fakes;
|
||||||
|
|
||||||
use archlens_domain::{CodeGraph, NormalizedGraph, ports::DiagramRenderer};
|
|
||||||
use archlens_application::use_cases::diff_diagram::DiffDiagram;
|
use archlens_application::use_cases::diff_diagram::DiffDiagram;
|
||||||
|
use archlens_domain::{CodeGraph, NormalizedGraph, ports::DiagramRenderer};
|
||||||
use fakes::FakeDiagramRenderer;
|
use fakes::FakeDiagramRenderer;
|
||||||
|
|
||||||
fn empty_graph() -> NormalizedGraph {
|
fn empty_graph() -> NormalizedGraph {
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ impl FakeProjectAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn empty() -> Self {
|
pub fn empty() -> Self {
|
||||||
Self { graph: CodeGraph::new() }
|
Self {
|
||||||
|
graph: CodeGraph::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ fn graph_with_one_module() -> NormalizedGraph {
|
|||||||
let analyzer = FakeSourceAnalyzer::new().with_result(
|
let analyzer = FakeSourceAnalyzer::new().with_result(
|
||||||
"/p/src/orders/order.rs",
|
"/p/src/orders/order.rs",
|
||||||
AnalysisResult::new(
|
AnalysisResult::new(
|
||||||
vec![CodeElement::new(
|
vec![
|
||||||
|
CodeElement::new(
|
||||||
"Order",
|
"Order",
|
||||||
CodeElementKind::Struct,
|
CodeElementKind::Struct,
|
||||||
FilePath::new("/p/src/orders/order.rs").unwrap(),
|
FilePath::new("/p/src/orders/order.rs").unwrap(),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
.unwrap()],
|
.unwrap(),
|
||||||
|
],
|
||||||
vec![],
|
vec![],
|
||||||
vec![],
|
vec![],
|
||||||
),
|
),
|
||||||
@@ -44,10 +46,20 @@ fn graph_with_one_module() -> NormalizedGraph {
|
|||||||
|
|
||||||
fn graph_with_violation() -> NormalizedGraph {
|
fn graph_with_violation() -> NormalizedGraph {
|
||||||
let mut cg = CodeGraph::new();
|
let mut cg = CodeGraph::new();
|
||||||
let a = CodeElement::new("A", CodeElementKind::Struct, FilePath::new("a.rs").unwrap(), 1)
|
let a = CodeElement::new(
|
||||||
|
"A",
|
||||||
|
CodeElementKind::Struct,
|
||||||
|
FilePath::new("a.rs").unwrap(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_module(ModuleName::new("Alpha").unwrap());
|
.with_module(ModuleName::new("Alpha").unwrap());
|
||||||
let b = CodeElement::new("B", CodeElementKind::Struct, FilePath::new("b.rs").unwrap(), 1)
|
let b = CodeElement::new(
|
||||||
|
"B",
|
||||||
|
CodeElementKind::Struct,
|
||||||
|
FilePath::new("b.rs").unwrap(),
|
||||||
|
1,
|
||||||
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_module(ModuleName::new("Beta").unwrap());
|
.with_module(ModuleName::new("Beta").unwrap());
|
||||||
cg.add_element(a);
|
cg.add_element(a);
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ mod cli;
|
|||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
use archlens_application::use_cases::{
|
use archlens_application::use_cases::{
|
||||||
build_code_graph::BuildCodeGraph,
|
build_code_graph::BuildCodeGraph, check_freshness::CheckFreshness, diff_diagram::DiffDiagram,
|
||||||
check_freshness::CheckFreshness,
|
|
||||||
diff_diagram::DiffDiagram,
|
|
||||||
generate_diagram::GenerateDiagram,
|
generate_diagram::GenerateDiagram,
|
||||||
};
|
};
|
||||||
use archlens_ascii::AsciiRenderer;
|
use archlens_ascii::AsciiRenderer;
|
||||||
@@ -96,7 +94,11 @@ pub fn run(args: Cli) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_diagram_output(output: &RenderOutput, output_path: Option<&str>, split: bool) -> Result<()> {
|
fn write_diagram_output(
|
||||||
|
output: &RenderOutput,
|
||||||
|
output_path: Option<&str>,
|
||||||
|
split: bool,
|
||||||
|
) -> Result<()> {
|
||||||
if split {
|
if split {
|
||||||
let dir = std::path::PathBuf::from(output_path.unwrap_or("."));
|
let dir = std::path::PathBuf::from(output_path.unwrap_or("."));
|
||||||
std::fs::create_dir_all(&dir)?;
|
std::fs::create_dir_all(&dir)?;
|
||||||
|
|||||||
1518
docs/superpowers/plans/2026-06-17-deepening-interfaces.md
Normal file
1518
docs/superpowers/plans/2026-06-17-deepening-interfaces.md
Normal file
File diff suppressed because it is too large
Load Diff
207
docs/superpowers/specs/2026-06-17-deepening-interfaces-design.md
Normal file
207
docs/superpowers/specs/2026-06-17-deepening-interfaces-design.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Deepening Application and Adapter Interfaces
|
||||||
|
|
||||||
|
**Date:** 2026-06-17
|
||||||
|
**Status:** Approved
|
||||||
|
**Scope:** Four architectural deepening refactors across the application and adapter layers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Four architectural friction points identified via architecture review:
|
||||||
|
|
||||||
|
1. I/O leaks across the seam into the application layer (`GenerateDiagram`, `CheckFreshness`, `DiffDiagram`)
|
||||||
|
2. Orchestration logic (`build_graph`) lives in the presentation layer instead of application
|
||||||
|
3. Renderer adapters duplicate import filtering and identifier sanitization
|
||||||
|
4. `LanguageExtractor` trait's single `analyze()` method gives no structure to implementors
|
||||||
|
|
||||||
|
All four refactors deepen existing modules — shrinking their interfaces and concentrating behavior — without adding new capabilities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Candidate 1: Pull I/O out of the application layer
|
||||||
|
|
||||||
|
**Files:** `crates/application/src/use_cases/generate_diagram.rs`, `check_freshness.rs`, `diff_diagram.rs`
|
||||||
|
|
||||||
|
### GenerateDiagram
|
||||||
|
|
||||||
|
Remove `output_dir: Option<PathBuf>` and `format_ext: String` fields. Remove `write_split`, `write_file_to_dir` free functions, and `check_violations_only()` method.
|
||||||
|
|
||||||
|
`execute()` returns `Result<GenerateDiagramResult, DomainError>`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct GenerateDiagramResult {
|
||||||
|
pub violations: Vec<RuleViolation>, // domain type, not pre-formatted strings
|
||||||
|
pub output: RenderOutput, // caller writes; split mode produces N files
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Split-by-module rendering stays inside `execute()`: it builds a `RenderOutput` with an overview file plus one file per module. Presentation receives the `RenderOutput` and hands it to `OutputWriter`.
|
||||||
|
|
||||||
|
`check_violations_only()` is removed. Callers needing early-exit on violations (strict mode, watch mode) call `execute()` and inspect `result.violations`.
|
||||||
|
|
||||||
|
### CheckFreshness
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// before
|
||||||
|
pub existing_path: &'a std::path::Path,
|
||||||
|
|
||||||
|
// after
|
||||||
|
pub existing_content: &'a str,
|
||||||
|
```
|
||||||
|
|
||||||
|
`execute()` becomes a pure string comparison. Presentation reads the file with `std::fs::read_to_string()` before constructing the struct.
|
||||||
|
|
||||||
|
### DiffDiagram
|
||||||
|
|
||||||
|
Same pattern: `existing_path: &Path` → `existing_content: &str`. Presentation reads the file.
|
||||||
|
|
||||||
|
### Presentation impact
|
||||||
|
|
||||||
|
- Reads files before calling `CheckFreshness` / `DiffDiagram`
|
||||||
|
- After `GenerateDiagram::execute()`: inspects `result.violations` for strict-mode bail, prints violations, calls `output_writer.write(&result.output)`
|
||||||
|
- `write_split` and `write_file_to_dir` free functions move into presentation (or are inlined)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Candidate 2: Sink graph-building orchestration into the application layer
|
||||||
|
|
||||||
|
**Files:** `crates/application/src/use_cases/build_code_graph.rs` (new), `crates/presentation/src/lib.rs`
|
||||||
|
|
||||||
|
### New use case: BuildCodeGraph
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct BuildCodeGraph<F, S> {
|
||||||
|
pub discovery: F,
|
||||||
|
pub source_analyzer: S,
|
||||||
|
pub project_analyzer: Option<Box<dyn ProjectAnalyzer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BuildCodeGraphResult {
|
||||||
|
pub graph: NormalizedGraph,
|
||||||
|
pub warnings: Vec<AnalysisWarning>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FileDiscovery, S: SourceAnalyzer> BuildCodeGraph<F, S> {
|
||||||
|
pub fn execute(
|
||||||
|
self,
|
||||||
|
root: &Path,
|
||||||
|
config: &AnalysisConfig,
|
||||||
|
level: DiagramLevel,
|
||||||
|
) -> Result<BuildCodeGraphResult, DomainError>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Logic inside `execute()`:
|
||||||
|
- `DiagramLevel::Project` → call `project_analyzer.expect("project analyzer required for Project level").analyze(root)`, return via `NormalizedGraph::from_project()`, empty warnings
|
||||||
|
- `DiagramLevel::Module | Type` → call `AnalyzeCodebase::execute()`, collect warnings; at `Module` level, if `project_analyzer` is `Some`, merge its edges into the graph
|
||||||
|
|
||||||
|
### Presentation impact
|
||||||
|
|
||||||
|
`build_graph()` in `presentation/src/lib.rs` drops from ~70 lines to ~25:
|
||||||
|
|
||||||
|
1. Apply CLI overrides to `AnalysisConfig` (scope, excludes, include_tests, changed_files) — stays in presentation, CLI-derived
|
||||||
|
2. Detect project analyzer: `if cargo_toml.exists() { Some(CargoWorkspaceAnalyzer) } else if pyproject.exists() { Some(PythonProjectAnalyzer) } else { None }`
|
||||||
|
3. Construct `BuildCodeGraph` and call `execute(root, config, level)`
|
||||||
|
4. Handle warnings (`eprintln`, strict bail) — stays in presentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Candidate 3: Shared renderer primitives
|
||||||
|
|
||||||
|
**Files:** `crates/adapters/rendering-primitives/src/lib.rs` (new crate), all four renderer adapter crates
|
||||||
|
|
||||||
|
### New crate: rendering-primitives
|
||||||
|
|
||||||
|
Depends only on `archlens_domain`. Exports two functions:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Returns all relationships excluding Import kind.
|
||||||
|
pub fn non_import_rels(rels: &[Relationship]) -> impl Iterator<Item = &Relationship> {
|
||||||
|
rels.iter().filter(|r| r.kind() != RelationshipKind::Import)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitize an identifier by replacing common separator characters with underscore.
|
||||||
|
/// Handles `::`, `-`, `.`, and space.
|
||||||
|
pub fn sanitize_identifier(name: &str) -> String {
|
||||||
|
name.replace("::", "_").replace(['-', '.', ' '], "_")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each of the four renderer adapter crates (`mermaid`, `d2`, `ascii`, `html-viewer`) adds `rendering-primitives` as a dependency.
|
||||||
|
|
||||||
|
Mermaid's `sanitize_id` strips only `[-.]` (not `::`) — intentionally different because Mermaid handles qualified names via `display_name()`. It keeps its local variant; the shared `sanitize_identifier` is used by D2 and HTML. The universal win is `non_import_rels`, which replaces the identical filter expression in all four renderers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Candidate 4: Typed LanguageExtractor pipeline
|
||||||
|
|
||||||
|
**Files:** `crates/adapters/tree-sitter/src/language_extractor.rs`, `rust/mod.rs`, `python/mod.rs`, `tree_sitter_analyzer.rs`
|
||||||
|
|
||||||
|
### Revised LanguageExtractor trait
|
||||||
|
|
||||||
|
Replace the current single-method trait with a 3-method trait plus a `run_extraction` free function:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait LanguageExtractor {
|
||||||
|
fn tree_sitter_language(&self) -> tree_sitter::Language;
|
||||||
|
fn extract_types(&self, root: &Node, source: &str, ctx: &mut ExtractionContext);
|
||||||
|
fn extract_relationships(&self, root: &Node, source: &str, ctx: &mut ExtractionContext);
|
||||||
|
fn extract_imports(&self, root: &Node, source: &str, ctx: &mut ExtractionContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_extraction(
|
||||||
|
extractor: &dyn LanguageExtractor,
|
||||||
|
source: &str,
|
||||||
|
file_path: &FilePath,
|
||||||
|
) -> Result<AnalysisResult, DomainError>
|
||||||
|
```
|
||||||
|
|
||||||
|
`run_extraction` owns: create parser, set language, parse, call all 3 methods in order on the root node, return `ctx.into_result()`.
|
||||||
|
|
||||||
|
### Extractor changes
|
||||||
|
|
||||||
|
`RustExtractor` and `PythonExtractor` implement the new trait. Existing free functions are reorganised into the 3 methods — no logic changes:
|
||||||
|
- `extract_types` → wraps `collect_types`
|
||||||
|
- `extract_relationships` → wraps `collect_relationships`
|
||||||
|
- `extract_imports` → wraps `collect_mod_declarations` + `collect_use_imports`
|
||||||
|
|
||||||
|
`TreeSitterAnalyzer::analyze_file()` replaces `extractor.analyze(source, path)` with `run_extraction(extractor, source, path)`.
|
||||||
|
|
||||||
|
Adding CSharp = implement 4 methods. No guesswork about what's required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution order
|
||||||
|
|
||||||
|
1. Candidate 1 — cleanest standalone win; establishes the pattern (pure use cases return data)
|
||||||
|
2. Candidate 2 — independent of Candidate 1 but benefits from the pattern being in place; `BuildCodeGraph` and `GenerateDiagram` are separate use cases with no shared code
|
||||||
|
3. Candidates 3 and 4 — independent of 1+2 and of each other; can run in parallel
|
||||||
|
|
||||||
|
`FileOutputWriter` (Directory variant) already iterates over all `RenderedFile`s in a `RenderOutput`, so split-by-module output producing N files in one `RenderOutput` requires no changes to the writer.
|
||||||
|
|
||||||
|
## TDD behaviors to verify (by candidate)
|
||||||
|
|
||||||
|
### Candidate 1
|
||||||
|
- `GenerateDiagram::execute()` returns `RenderOutput` with rendered content (no filesystem side effects)
|
||||||
|
- `GenerateDiagram::execute()` returns `Vec<RuleViolation>` when rules are violated
|
||||||
|
- `GenerateDiagram::execute()` with `split_by_module=true` returns `RenderOutput` with multiple files (overview + per-module)
|
||||||
|
- `CheckFreshness::execute()` returns `true` when rendered content equals `existing_content`
|
||||||
|
- `CheckFreshness::execute()` returns `false` when they differ
|
||||||
|
- `DiffDiagram::execute()` returns correct added/removed lines
|
||||||
|
|
||||||
|
### Candidate 2
|
||||||
|
- `BuildCodeGraph::execute()` at `Project` level delegates to `ProjectAnalyzer`, returns its graph
|
||||||
|
- `BuildCodeGraph::execute()` at `Module` level merges project edges when `project_analyzer` is `Some`
|
||||||
|
- `BuildCodeGraph::execute()` at `Type` level does not merge edges even when `project_analyzer` is `Some`
|
||||||
|
- `BuildCodeGraph::execute()` with `None` project analyzer at `Module` level skips merge cleanly
|
||||||
|
- `BuildCodeGraph::execute()` propagates `AnalysisWarning`s from `AnalyzeCodebase`
|
||||||
|
|
||||||
|
### Candidate 3
|
||||||
|
- `non_import_rels` excludes `Import` relationships, passes through `Inheritance` and `Composition`
|
||||||
|
- `sanitize_identifier` replaces `::`, `-`, `.`, space with `_`
|
||||||
|
|
||||||
|
### Candidate 4
|
||||||
|
- `run_extraction` calls `extract_types`, `extract_relationships`, `extract_imports` in order
|
||||||
|
- `RustExtractor` extracts struct/enum/trait types via `extract_types`
|
||||||
|
- `PythonExtractor` extracts classes via `extract_types`
|
||||||
Reference in New Issue
Block a user