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:
2026-01-25 20:17:37 +03:00
commit 3701cee205
36 changed files with 7394 additions and 0 deletions

View 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 37 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))
}
}