Add initial project structure and core functionality for ArchDoc
- Created `.gitignore` files for various directories to exclude unnecessary files. - Added `PLAN.md` to outline the project goals and architecture documentation generation. - Implemented the `archdoc-cli` with a command-line interface for initializing and generating documentation. - Developed the `archdoc-core` library for analyzing Python projects and generating architecture documentation. - Included caching mechanisms to optimize repeated analysis. - Established a comprehensive test suite to ensure functionality and error handling. - Updated `README.md` to provide an overview and installation instructions for ArchDoc.
This commit is contained in:
369
archdoc-core/src/renderer.rs
Normal file
369
archdoc-core/src/renderer.rs
Normal file
@@ -0,0 +1,369 @@
|
||||
//! Markdown renderer for ArchDoc
|
||||
//!
|
||||
//! This module handles generating Markdown documentation from the project model
|
||||
//! using templates.
|
||||
|
||||
use crate::model::ProjectModel;
|
||||
use handlebars::Handlebars;
|
||||
|
||||
pub struct Renderer {
|
||||
templates: Handlebars<'static>,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new() -> Self {
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
// Register templates
|
||||
handlebars.register_template_string("architecture_md", Self::architecture_md_template())
|
||||
.expect("Failed to register architecture_md template");
|
||||
|
||||
// TODO: Register other templates
|
||||
|
||||
Self {
|
||||
templates: handlebars,
|
||||
}
|
||||
}
|
||||
|
||||
fn architecture_md_template() -> &'static str {
|
||||
r#"# ARCHITECTURE — {{{project_name}}}
|
||||
|
||||
<!-- MANUAL:BEGIN -->
|
||||
## Project summary
|
||||
**Name:** {{{project_name}}}
|
||||
**Description:** {{{project_description}}}
|
||||
|
||||
## Key decisions (manual)
|
||||
{{#each key_decisions}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
## Non-goals (manual)
|
||||
{{#each non_goals}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
<!-- MANUAL:END -->
|
||||
|
||||
---
|
||||
|
||||
## Document metadata
|
||||
- **Created:** {{{created_date}}}
|
||||
- **Updated:** {{{updated_date}}}
|
||||
- **Generated by:** archdoc (cli) v0.1
|
||||
|
||||
---
|
||||
|
||||
## Integrations
|
||||
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||
> Generated. Do not edit inside this block.
|
||||
|
||||
### Database Integrations
|
||||
{{#each db_integrations}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
### HTTP/API Integrations
|
||||
{{#each http_integrations}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
### Queue Integrations
|
||||
{{#each queue_integrations}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
<!-- ARCHDOC:END section=integrations -->
|
||||
|
||||
---
|
||||
|
||||
## Rails / Tooling
|
||||
<!-- ARCHDOC:BEGIN section=rails -->
|
||||
> Generated. Do not edit inside this block.
|
||||
{{{rails_summary}}}
|
||||
<!-- ARCHDOC:END section=rails -->
|
||||
|
||||
---
|
||||
|
||||
## Repository layout (top-level)
|
||||
<!-- ARCHDOC:BEGIN section=layout -->
|
||||
> Generated. Do not edit inside this block.
|
||||
| Path | Purpose | Link |
|
||||
|------|---------|------|
|
||||
{{#each layout_items}}
|
||||
| {{{path}}} | {{{purpose}}} | [details]({{{link}}}) |
|
||||
{{/each}}
|
||||
<!-- ARCHDOC:END section=layout -->
|
||||
|
||||
---
|
||||
|
||||
## Modules index
|
||||
<!-- ARCHDOC:BEGIN section=modules_index -->
|
||||
> Generated. Do not edit inside this block.
|
||||
| Module | Symbols | Inbound | Outbound | Link |
|
||||
|--------|---------|---------|----------|------|
|
||||
{{#each modules}}
|
||||
| {{{name}}} | {{{symbol_count}}} | {{{inbound_count}}} | {{{outbound_count}}} | [details]({{{link}}}) |
|
||||
{{/each}}
|
||||
<!-- ARCHDOC:END section=modules_index -->
|
||||
|
||||
---
|
||||
|
||||
## Critical dependency points
|
||||
<!-- ARCHDOC:BEGIN section=critical_points -->
|
||||
> Generated. Do not edit inside this block.
|
||||
### High Fan-in (Most Called)
|
||||
| Symbol | Fan-in | Critical |
|
||||
|--------|--------|----------|
|
||||
{{#each high_fan_in}}
|
||||
| {{{symbol}}} | {{{count}}} | {{{critical}}} |
|
||||
{{/each}}
|
||||
|
||||
### High Fan-out (Calls Many)
|
||||
| Symbol | Fan-out | Critical |
|
||||
|--------|---------|----------|
|
||||
{{#each high_fan_out}}
|
||||
| {{{symbol}}} | {{{count}}} | {{{critical}}} |
|
||||
{{/each}}
|
||||
|
||||
### Module Cycles
|
||||
{{#each cycles}}
|
||||
- {{{cycle_path}}}
|
||||
{{/each}}
|
||||
<!-- ARCHDOC:END section=critical_points -->
|
||||
|
||||
---
|
||||
|
||||
<!-- MANUAL:BEGIN -->
|
||||
## Change notes (manual)
|
||||
{{#each change_notes}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
<!-- MANUAL:END -->
|
||||
"#
|
||||
}
|
||||
|
||||
pub fn render_architecture_md(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||
// Collect integration information
|
||||
let mut db_integrations = Vec::new();
|
||||
let mut http_integrations = Vec::new();
|
||||
let mut queue_integrations = Vec::new();
|
||||
|
||||
for (symbol_id, symbol) in &model.symbols {
|
||||
if symbol.integrations_flags.db {
|
||||
db_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||
}
|
||||
if symbol.integrations_flags.http {
|
||||
http_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||
}
|
||||
if symbol.integrations_flags.queue {
|
||||
queue_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare data for template
|
||||
let data = serde_json::json!({
|
||||
"project_name": "New Project",
|
||||
"project_description": "<FILL_MANUALLY: what this project does in 3–7 lines>",
|
||||
"created_date": "2026-01-25",
|
||||
"updated_date": "2026-01-25",
|
||||
"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)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to render architecture.md: {}", e))
|
||||
}
|
||||
|
||||
pub fn render_integrations_section(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||
// Collect integration information
|
||||
let mut db_integrations = Vec::new();
|
||||
let mut http_integrations = Vec::new();
|
||||
let mut queue_integrations = Vec::new();
|
||||
|
||||
for (symbol_id, symbol) in &model.symbols {
|
||||
if symbol.integrations_flags.db {
|
||||
db_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||
}
|
||||
if symbol.integrations_flags.http {
|
||||
http_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||
}
|
||||
if symbol.integrations_flags.queue {
|
||||
queue_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare data for integrations section
|
||||
let data = serde_json::json!({
|
||||
"db_integrations": db_integrations,
|
||||
"http_integrations": http_integrations,
|
||||
"queue_integrations": queue_integrations,
|
||||
});
|
||||
|
||||
// Create a smaller template just for the integrations section
|
||||
let integrations_template = r#"
|
||||
|
||||
### Database Integrations
|
||||
{{#each db_integrations}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
### HTTP/API Integrations
|
||||
{{#each http_integrations}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
### Queue Integrations
|
||||
{{#each queue_integrations}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
"#;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_template_string("integrations", integrations_template)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to register integrations template: {}", e))?;
|
||||
|
||||
handlebars.render("integrations", &data)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to render integrations section: {}", e))
|
||||
}
|
||||
|
||||
pub fn render_rails_section(&self, _model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||
// For now, return a simple placeholder
|
||||
Ok("\n\nNo tooling information available.\n".to_string())
|
||||
}
|
||||
|
||||
pub fn render_layout_section(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||
// Collect layout information from files
|
||||
let mut layout_items = Vec::new();
|
||||
|
||||
for (file_id, file_doc) in &model.files {
|
||||
layout_items.push(serde_json::json!({
|
||||
"path": file_doc.path,
|
||||
"purpose": "Source file",
|
||||
"link": format!("docs/architecture/files/{}.md", file_id)
|
||||
}));
|
||||
}
|
||||
|
||||
// Prepare data for layout section
|
||||
let data = serde_json::json!({
|
||||
"layout_items": layout_items,
|
||||
});
|
||||
|
||||
// Create a smaller template just for the layout section
|
||||
let layout_template = r#"
|
||||
|
||||
| Path | Purpose | Link |
|
||||
|------|---------|------|
|
||||
{{#each layout_items}}
|
||||
| {{{path}}} | {{{purpose}}} | [details]({{{link}}}) |
|
||||
{{/each}}
|
||||
"#;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_template_string("layout", layout_template)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to register layout template: {}", e))?;
|
||||
|
||||
handlebars.render("layout", &data)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to render layout section: {}", e))
|
||||
}
|
||||
|
||||
pub fn render_modules_index_section(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||
// Collect module information
|
||||
let mut modules = Vec::new();
|
||||
|
||||
for (module_id, module) in &model.modules {
|
||||
modules.push(serde_json::json!({
|
||||
"name": module_id,
|
||||
"symbol_count": module.symbols.len(),
|
||||
"inbound_count": module.inbound_modules.len(),
|
||||
"outbound_count": module.outbound_modules.len(),
|
||||
"link": format!("docs/architecture/modules/{}.md", module_id)
|
||||
}));
|
||||
}
|
||||
|
||||
// Prepare data for modules index section
|
||||
let data = serde_json::json!({
|
||||
"modules": modules,
|
||||
});
|
||||
|
||||
// Create a smaller template just for the modules index section
|
||||
let modules_template = r#"
|
||||
|
||||
| Module | Symbols | Inbound | Outbound | Link |
|
||||
|--------|---------|---------|----------|------|
|
||||
{{#each modules}}
|
||||
| {{{name}}} | {{{symbol_count}}} | {{{inbound_count}}} | {{{outbound_count}}} | [details]({{{link}}}) |
|
||||
{{/each}}
|
||||
"#;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_template_string("modules_index", modules_template)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to register modules_index template: {}", e))?;
|
||||
|
||||
handlebars.render("modules_index", &data)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to render modules index section: {}", e))
|
||||
}
|
||||
|
||||
pub fn render_critical_points_section(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||
// Collect critical points information
|
||||
let mut high_fan_in = Vec::new();
|
||||
let mut high_fan_out = Vec::new();
|
||||
|
||||
for (symbol_id, symbol) in &model.symbols {
|
||||
if symbol.metrics.fan_in > 5 { // Threshold for high fan-in
|
||||
high_fan_in.push(serde_json::json!({
|
||||
"symbol": symbol_id,
|
||||
"count": symbol.metrics.fan_in,
|
||||
"critical": symbol.metrics.is_critical,
|
||||
}));
|
||||
}
|
||||
if symbol.metrics.fan_out > 5 { // Threshold for high fan-out
|
||||
high_fan_out.push(serde_json::json!({
|
||||
"symbol": symbol_id,
|
||||
"count": symbol.metrics.fan_out,
|
||||
"critical": symbol.metrics.is_critical,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare data for critical points section
|
||||
let data = serde_json::json!({
|
||||
"high_fan_in": high_fan_in,
|
||||
"high_fan_out": high_fan_out,
|
||||
"cycles": Vec::<String>::new(), // TODO: Implement cycle detection
|
||||
});
|
||||
|
||||
// Create a smaller template just for the critical points section
|
||||
let critical_points_template = r#"
|
||||
|
||||
### High Fan-in (Most Called)
|
||||
| Symbol | Fan-in | Critical |
|
||||
|--------|--------|----------|
|
||||
{{#each high_fan_in}}
|
||||
| {{{symbol}}} | {{{count}}} | {{{critical}}} |
|
||||
{{/each}}
|
||||
|
||||
### High Fan-out (Calls Many)
|
||||
| Symbol | Fan-out | Critical |
|
||||
|--------|---------|----------|
|
||||
{{#each high_fan_out}}
|
||||
| {{{symbol}}} | {{{count}}} | {{{critical}}} |
|
||||
{{/each}}
|
||||
|
||||
### Module Cycles
|
||||
{{#each cycles}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
"#;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_template_string("critical_points", critical_points_template)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to register critical_points template: {}", e))?;
|
||||
|
||||
handlebars.render("critical_points", &data)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to render critical points section: {}", e))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user