rename: archdoc → wtismycode (WTIsMyCode)
This commit is contained in:
961
wtismycode-core/src/renderer.rs
Normal file
961
wtismycode-core/src/renderer.rs
Normal file
@@ -0,0 +1,961 @@
|
||||
//! Markdown renderer for WTIsMyCode
|
||||
//!
|
||||
//! This module handles generating Markdown documentation from the project model
|
||||
//! using templates.
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::cycle_detector;
|
||||
use crate::model::{ProjectModel, SymbolKind};
|
||||
use chrono::Utc;
|
||||
use handlebars::Handlebars;
|
||||
|
||||
fn sanitize_for_link(filename: &str) -> String {
|
||||
let cleaned = filename.strip_prefix("./").unwrap_or(filename);
|
||||
cleaned.replace('/', "__")
|
||||
}
|
||||
|
||||
pub struct Renderer {
|
||||
templates: Handlebars<'static>,
|
||||
}
|
||||
|
||||
impl Default for Renderer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// Register module documentation template
|
||||
handlebars.register_template_string("module_md", Self::module_md_template())
|
||||
.expect("Failed to register module_md template");
|
||||
|
||||
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:** wtismycode (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}}
|
||||
|
||||
### Storage Integrations
|
||||
{{#each storage_integrations}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
### AI/ML Integrations
|
||||
{{#each ai_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 -->
|
||||
"#
|
||||
}
|
||||
|
||||
fn module_md_template() -> &'static str {
|
||||
r#"# Module: {{{module_name}}}
|
||||
|
||||
{{{module_summary}}}
|
||||
|
||||
## Symbols
|
||||
|
||||
{{#each symbols}}
|
||||
### {{{name}}}
|
||||
|
||||
{{{signature}}}
|
||||
|
||||
{{{docstring}}}
|
||||
|
||||
**Type:** {{{kind}}}
|
||||
|
||||
**Metrics:**
|
||||
- Fan-in: {{{fan_in}}}
|
||||
- Fan-out: {{{fan_out}}}
|
||||
{{#if is_critical}}
|
||||
- Critical: Yes
|
||||
{{/if}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Imports
|
||||
{{#each imports}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
### Outbound Modules
|
||||
{{#each outbound_modules}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
### Inbound Modules
|
||||
{{#each inbound_modules}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
## Integrations
|
||||
|
||||
{{#if has_db_integrations}}
|
||||
### Database Integrations
|
||||
{{#each db_symbols}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if has_http_integrations}}
|
||||
### HTTP/API Integrations
|
||||
{{#each http_symbols}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if has_queue_integrations}}
|
||||
### Queue Integrations
|
||||
{{#each queue_symbols}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if has_storage_integrations}}
|
||||
### Storage Integrations
|
||||
{{#each storage_symbols}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if has_ai_integrations}}
|
||||
### AI/ML Integrations
|
||||
{{#each ai_symbols}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
## Usage Examples
|
||||
|
||||
{{#each usage_examples}}
|
||||
```python
|
||||
{{{this}}}
|
||||
```
|
||||
|
||||
{{/each}}
|
||||
"#
|
||||
}
|
||||
|
||||
pub fn render_architecture_md(&self, model: &ProjectModel, config: Option<&Config>) -> 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();
|
||||
let mut storage_integrations = Vec::new();
|
||||
let mut ai_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));
|
||||
}
|
||||
if symbol.integrations_flags.storage {
|
||||
storage_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||
}
|
||||
if symbol.integrations_flags.ai {
|
||||
ai_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine project name: config > pyproject.toml > directory name > fallback
|
||||
let project_name = config
|
||||
.and_then(|c| {
|
||||
if c.project.name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(c.project.name.clone())
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
// Try pyproject.toml
|
||||
config.and_then(|c| {
|
||||
let pyproject_path = std::path::Path::new(&c.project.root).join("pyproject.toml");
|
||||
std::fs::read_to_string(&pyproject_path).ok().and_then(|content| {
|
||||
// Simple TOML parsing for [project] name = "..."
|
||||
let mut in_project = false;
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed == "[project]" {
|
||||
in_project = true;
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with('[') {
|
||||
in_project = false;
|
||||
continue;
|
||||
}
|
||||
if in_project && trimmed.starts_with("name") {
|
||||
if let Some(val) = trimmed.split('=').nth(1) {
|
||||
let name = val.trim().trim_matches('"').trim_matches('\'');
|
||||
if !name.is_empty() {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
config.map(|c| {
|
||||
std::path::Path::new(&c.project.root)
|
||||
.canonicalize()
|
||||
.ok()
|
||||
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
|
||||
.unwrap_or_else(|| "Project".to_string())
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| "Project".to_string());
|
||||
|
||||
let today = Utc::now().format("%Y-%m-%d").to_string();
|
||||
|
||||
// Collect layout items grouped by top-level directory
|
||||
let mut dir_files: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
|
||||
for file_doc in model.files.values() {
|
||||
let path = file_doc.path.strip_prefix("./").unwrap_or(&file_doc.path);
|
||||
let top_dir = path.split('/').next().unwrap_or(path);
|
||||
// If file is at root level (no '/'), use the filename itself
|
||||
let top = if path.contains('/') {
|
||||
format!("{}/", top_dir)
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
dir_files.entry(top).or_default().push(path.to_string());
|
||||
}
|
||||
let mut layout_items = Vec::new();
|
||||
for (dir, files) in &dir_files {
|
||||
let file_count = files.len();
|
||||
let purpose = if dir.ends_with('/') {
|
||||
format!("{} files", file_count)
|
||||
} else {
|
||||
"Root file".to_string()
|
||||
};
|
||||
layout_items.push(serde_json::json!({
|
||||
"path": dir,
|
||||
"purpose": purpose,
|
||||
"link": format!("docs/architecture/files/{}.md", sanitize_for_link(dir.trim_end_matches('/')))
|
||||
}));
|
||||
}
|
||||
|
||||
// Collect module items for template
|
||||
let mut modules_list = Vec::new();
|
||||
for (module_id, module) in &model.modules {
|
||||
modules_list.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", sanitize_for_link(module_id))
|
||||
}));
|
||||
}
|
||||
|
||||
// Collect critical points
|
||||
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 {
|
||||
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 {
|
||||
high_fan_out.push(serde_json::json!({
|
||||
"symbol": symbol_id,
|
||||
"count": symbol.metrics.fan_out,
|
||||
"critical": symbol.metrics.is_critical,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let cycles: Vec<_> = cycle_detector::detect_cycles(model)
|
||||
.iter()
|
||||
.map(|cycle| {
|
||||
serde_json::json!({
|
||||
"cycle_path": format!("{} → {}", cycle.join(" → "), cycle.first().unwrap_or(&String::new()))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Project statistics
|
||||
let project_description = format!(
|
||||
"Python project with {} modules, {} files, and {} symbols.",
|
||||
model.modules.len(), model.files.len(), model.symbols.len()
|
||||
);
|
||||
|
||||
// Prepare data for template
|
||||
let data = serde_json::json!({
|
||||
"project_name": project_name,
|
||||
"project_description": project_description,
|
||||
"created_date": &today,
|
||||
"updated_date": &today,
|
||||
"key_decisions": ["<FILL_MANUALLY>"],
|
||||
"non_goals": ["<FILL_MANUALLY>"],
|
||||
"change_notes": ["<FILL_MANUALLY>"],
|
||||
"db_integrations": db_integrations,
|
||||
"http_integrations": http_integrations,
|
||||
"queue_integrations": queue_integrations,
|
||||
"storage_integrations": storage_integrations,
|
||||
"ai_integrations": ai_integrations,
|
||||
"rails_summary": "\n\nNo tooling information available.\n",
|
||||
"layout_items": layout_items,
|
||||
"modules": modules_list,
|
||||
"high_fan_in": high_fan_in,
|
||||
"high_fan_out": high_fan_out,
|
||||
"cycles": cycles,
|
||||
});
|
||||
|
||||
self.templates.render("architecture_md", &data)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to render architecture.md: {}", e))
|
||||
}
|
||||
|
||||
pub fn render_module_md(&self, model: &ProjectModel, module_id: &str) -> Result<String, anyhow::Error> {
|
||||
// Find the module in the project model
|
||||
let module = model.modules.get(module_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Module {} not found", module_id))?;
|
||||
|
||||
// Collect symbols for this module
|
||||
let mut symbols = Vec::new();
|
||||
for symbol_id in &module.symbols {
|
||||
if let Some(symbol) = model.symbols.get(symbol_id) {
|
||||
symbols.push(serde_json::json!({
|
||||
"name": symbol.qualname,
|
||||
"signature": symbol.signature,
|
||||
"docstring": symbol.docstring_first_line.as_deref().unwrap_or("No documentation available"),
|
||||
"kind": format!("{:?}", symbol.kind),
|
||||
"fan_in": symbol.metrics.fan_in,
|
||||
"fan_out": symbol.metrics.fan_out,
|
||||
"is_critical": symbol.metrics.is_critical,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Collect integration information for this module
|
||||
let mut db_symbols = Vec::new();
|
||||
let mut http_symbols = Vec::new();
|
||||
let mut queue_symbols = Vec::new();
|
||||
let mut storage_symbols = Vec::new();
|
||||
let mut ai_symbols = Vec::new();
|
||||
|
||||
for symbol_id in &module.symbols {
|
||||
if let Some(symbol) = model.symbols.get(symbol_id) {
|
||||
if symbol.integrations_flags.db {
|
||||
db_symbols.push(symbol.qualname.clone());
|
||||
}
|
||||
if symbol.integrations_flags.http {
|
||||
http_symbols.push(symbol.qualname.clone());
|
||||
}
|
||||
if symbol.integrations_flags.queue {
|
||||
queue_symbols.push(symbol.qualname.clone());
|
||||
}
|
||||
if symbol.integrations_flags.storage {
|
||||
storage_symbols.push(symbol.qualname.clone());
|
||||
}
|
||||
if symbol.integrations_flags.ai {
|
||||
ai_symbols.push(symbol.qualname.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate usage examples from public symbols
|
||||
let mut usage_examples = Vec::new();
|
||||
for symbol_id in &module.symbols {
|
||||
if let Some(symbol) = model.symbols.get(symbol_id) {
|
||||
let short_name = symbol.qualname.rsplit('.').next().unwrap_or(&symbol.qualname);
|
||||
match symbol.kind {
|
||||
SymbolKind::Function | SymbolKind::AsyncFunction => {
|
||||
// Extract args from signature: "def foo(a, b)" -> "a, b"
|
||||
let args = symbol.signature
|
||||
.find('(')
|
||||
.and_then(|start| symbol.signature.rfind(')').map(|end| (start, end)))
|
||||
.map(|(s, e)| &symbol.signature[s+1..e])
|
||||
.unwrap_or("");
|
||||
let clean_args = args.split(',')
|
||||
.map(|a| a.split(':').next().unwrap_or("").trim())
|
||||
.filter(|a| !a.is_empty() && *a != "self" && *a != "cls")
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let example_args = if clean_args.is_empty() { String::new() } else {
|
||||
clean_args.split(", ").map(|a| {
|
||||
if a.starts_with('*') { "..." } else { a }
|
||||
}).collect::<Vec<_>>().join(", ")
|
||||
};
|
||||
let prefix = if symbol.kind == SymbolKind::AsyncFunction { "await " } else { "" };
|
||||
usage_examples.push(format!(
|
||||
"from {} import {}\nresult = {}{}({})",
|
||||
module_id, short_name, prefix, short_name, example_args
|
||||
));
|
||||
}
|
||||
SymbolKind::Class => {
|
||||
// Find __init__ method to get constructor args
|
||||
let init_name = format!("{}.__init__", short_name);
|
||||
let init_args = module.symbols.iter()
|
||||
.find_map(|sid| {
|
||||
model.symbols.get(sid).and_then(|s| {
|
||||
if s.qualname == init_name || s.id == init_name {
|
||||
// Extract args from __init__ signature
|
||||
let args = s.signature
|
||||
.find('(')
|
||||
.and_then(|start| s.signature.rfind(')').map(|end| (start, end)))
|
||||
.map(|(st, en)| &s.signature[st+1..en])
|
||||
.unwrap_or("");
|
||||
let clean = args.split(',')
|
||||
.map(|a| a.split(':').next().unwrap_or("").split('=').next().unwrap_or("").trim())
|
||||
.filter(|a| !a.is_empty() && *a != "self" && *a != "cls" && !a.starts_with('*'))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
Some(clean)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
usage_examples.push(format!(
|
||||
"from {} import {}\ninstance = {}({})",
|
||||
module_id, short_name, short_name, init_args
|
||||
));
|
||||
}
|
||||
SymbolKind::Method => {
|
||||
// Skip methods - they're shown via class usage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if usage_examples.is_empty() {
|
||||
usage_examples.push(format!("import {}", module_id));
|
||||
}
|
||||
|
||||
// Prepare data for template
|
||||
let data = serde_json::json!({
|
||||
"module_name": module_id,
|
||||
"module_summary": module.doc_summary.as_deref().unwrap_or("No summary available"),
|
||||
"symbols": symbols,
|
||||
"imports": model.files.get(&module.files[0]).map(|f| f.imports.clone()).unwrap_or_default(),
|
||||
"outbound_modules": module.outbound_modules,
|
||||
"inbound_modules": module.inbound_modules,
|
||||
"has_db_integrations": !db_symbols.is_empty(),
|
||||
"has_http_integrations": !http_symbols.is_empty(),
|
||||
"has_queue_integrations": !queue_symbols.is_empty(),
|
||||
"has_storage_integrations": !storage_symbols.is_empty(),
|
||||
"has_ai_integrations": !ai_symbols.is_empty(),
|
||||
"db_symbols": db_symbols,
|
||||
"http_symbols": http_symbols,
|
||||
"queue_symbols": queue_symbols,
|
||||
"storage_symbols": storage_symbols,
|
||||
"ai_symbols": ai_symbols,
|
||||
"usage_examples": usage_examples,
|
||||
});
|
||||
|
||||
self.templates.render("module_md", &data)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to render module.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();
|
||||
let mut storage_integrations = Vec::new();
|
||||
let mut ai_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));
|
||||
}
|
||||
if symbol.integrations_flags.storage {
|
||||
storage_integrations.push(format!("{} in {}", symbol_id, symbol.file_id));
|
||||
}
|
||||
if symbol.integrations_flags.ai {
|
||||
ai_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,
|
||||
"storage_integrations": storage_integrations,
|
||||
"ai_integrations": ai_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}}
|
||||
|
||||
### Storage Integrations
|
||||
{{#each storage_integrations}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
### AI/ML Integrations
|
||||
{{#each ai_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 items grouped by top-level directory
|
||||
let mut dir_files: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
|
||||
for file_doc in model.files.values() {
|
||||
let path = file_doc.path.strip_prefix("./").unwrap_or(&file_doc.path);
|
||||
let top_dir = path.split('/').next().unwrap_or(path);
|
||||
let top = if path.contains('/') {
|
||||
format!("{}/", top_dir)
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
dir_files.entry(top).or_default().push(path.to_string());
|
||||
}
|
||||
let mut layout_items = Vec::new();
|
||||
for (dir, files) in &dir_files {
|
||||
let file_count = files.len();
|
||||
let purpose = if dir.ends_with('/') {
|
||||
format!("{} files", file_count)
|
||||
} else {
|
||||
"Root file".to_string()
|
||||
};
|
||||
layout_items.push(serde_json::json!({
|
||||
"path": dir,
|
||||
"purpose": purpose,
|
||||
"link": format!("docs/architecture/files/{}.md", sanitize_for_link(dir.trim_end_matches('/')))
|
||||
}));
|
||||
}
|
||||
|
||||
// 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", sanitize_for_link(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": cycle_detector::detect_cycles(model)
|
||||
.iter()
|
||||
.map(|cycle| {
|
||||
serde_json::json!({
|
||||
"cycle_path": format!("{} → {}", cycle.join(" → "), cycle.first().unwrap_or(&String::new()))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
});
|
||||
|
||||
// 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}}
|
||||
- {{{cycle_path}}}
|
||||
{{/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))
|
||||
}
|
||||
|
||||
pub fn render_layout_md(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||
// Collect layout items grouped by top-level directory
|
||||
let mut dir_files: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
|
||||
for file_doc in model.files.values() {
|
||||
let path = file_doc.path.strip_prefix("./").unwrap_or(&file_doc.path);
|
||||
let top_dir = path.split('/').next().unwrap_or(path);
|
||||
let top = if path.contains('/') {
|
||||
format!("{}/", top_dir)
|
||||
} else {
|
||||
path.to_string()
|
||||
};
|
||||
dir_files.entry(top).or_default().push(path.to_string());
|
||||
}
|
||||
let mut layout_items = Vec::new();
|
||||
for (dir, files) in &dir_files {
|
||||
let file_count = files.len();
|
||||
let purpose = if dir.ends_with('/') {
|
||||
format!("{} files", file_count)
|
||||
} else {
|
||||
"Root file".to_string()
|
||||
};
|
||||
layout_items.push(serde_json::json!({
|
||||
"path": dir,
|
||||
"purpose": purpose,
|
||||
"link": format!("files/{}.md", sanitize_for_link(dir.trim_end_matches('/')))
|
||||
}));
|
||||
}
|
||||
|
||||
// Prepare data for layout template
|
||||
let data = serde_json::json!({
|
||||
"layout_items": layout_items,
|
||||
});
|
||||
|
||||
// Create template for layout.md
|
||||
let layout_template = r#"# Repository layout
|
||||
|
||||
<!-- MANUAL:BEGIN -->
|
||||
## Manual overrides
|
||||
- `src/app/` — <FILL_MANUALLY>
|
||||
<!-- MANUAL:END -->
|
||||
|
||||
---
|
||||
|
||||
## Detected structure
|
||||
<!-- ARCHDOC:BEGIN section=layout_detected -->
|
||||
> Generated. Do not edit inside this block.
|
||||
| Path | Purpose | Link |
|
||||
|------|---------|------|
|
||||
{{#each layout_items}}
|
||||
| {{{path}}} | {{{purpose}}} | [details]({{{link}}}) |
|
||||
{{/each}}
|
||||
<!-- ARCHDOC:END section=layout_detected -->
|
||||
"#;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_template_string("layout_md", layout_template)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to register layout_md template: {}", e))?;
|
||||
|
||||
handlebars.render("layout_md", &data)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to render layout.md: {}", e))
|
||||
}
|
||||
|
||||
pub fn render_symbol_details(&self, model: &ProjectModel, symbol_id: &str) -> Result<String, anyhow::Error> {
|
||||
// Find the symbol in the project model
|
||||
let symbol = model.symbols.get(symbol_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Symbol {} not found", symbol_id))?;
|
||||
|
||||
// Prepare data for symbol template
|
||||
let data = serde_json::json!({
|
||||
"symbol_id": symbol_id,
|
||||
"qualname": symbol.qualname,
|
||||
"kind": format!("{:?}", symbol.kind),
|
||||
"signature": symbol.signature,
|
||||
"docstring": symbol.docstring_first_line.as_deref().unwrap_or("No documentation available"),
|
||||
"purpose": symbol.purpose,
|
||||
"integrations": {
|
||||
"http": symbol.integrations_flags.http,
|
||||
"db": symbol.integrations_flags.db,
|
||||
"queue": symbol.integrations_flags.queue,
|
||||
"storage": symbol.integrations_flags.storage,
|
||||
"ai": symbol.integrations_flags.ai,
|
||||
},
|
||||
"metrics": {
|
||||
"fan_in": symbol.metrics.fan_in,
|
||||
"fan_out": symbol.metrics.fan_out,
|
||||
"is_critical": symbol.metrics.is_critical,
|
||||
"cycle_participant": symbol.metrics.cycle_participant,
|
||||
},
|
||||
"outbound_calls": symbol.outbound_calls,
|
||||
"inbound_calls": symbol.inbound_calls,
|
||||
});
|
||||
|
||||
// Create template for symbol details
|
||||
let symbol_template = r#"<a id="{{symbol_id}}"></a>
|
||||
|
||||
### `{{qualname}}`
|
||||
- **Kind:** {{kind}}
|
||||
- **Signature:** `{{{signature}}}`
|
||||
- **Docstring:** `{{{docstring}}}`
|
||||
|
||||
#### What it does
|
||||
<!-- ARCHDOC:BEGIN section=purpose -->
|
||||
{{{purpose}}}
|
||||
<!-- ARCHDOC:END section=purpose -->
|
||||
|
||||
#### Relations
|
||||
<!-- ARCHDOC:BEGIN section=relations -->
|
||||
**Outbound calls (best-effort):**
|
||||
{{#each outbound_calls}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
|
||||
**Inbound (used by) (best-effort):**
|
||||
{{#each inbound_calls}}
|
||||
- {{{this}}}
|
||||
{{/each}}
|
||||
<!-- ARCHDOC:END section=relations -->
|
||||
|
||||
#### Integrations (heuristic)
|
||||
<!-- ARCHDOC:BEGIN section=integrations -->
|
||||
- HTTP: {{#if integrations.http}}yes{{else}}no{{/if}}
|
||||
- DB: {{#if integrations.db}}yes{{else}}no{{/if}}
|
||||
- Queue/Tasks: {{#if integrations.queue}}yes{{else}}no{{/if}}
|
||||
- Storage: {{#if integrations.storage}}yes{{else}}no{{/if}}
|
||||
- AI/ML: {{#if integrations.ai}}yes{{else}}no{{/if}}
|
||||
<!-- ARCHDOC:END section=integrations -->
|
||||
|
||||
#### Risk / impact
|
||||
<!-- ARCHDOC:BEGIN section=impact -->
|
||||
- fan-in: {{{metrics.fan_in}}}
|
||||
- fan-out: {{{metrics.fan_out}}}
|
||||
- cycle participant: {{#if metrics.cycle_participant}}yes{{else}}no{{/if}}
|
||||
- critical: {{#if metrics.is_critical}}yes{{else}}no{{/if}}
|
||||
<!-- ARCHDOC:END section=impact -->
|
||||
|
||||
<!-- MANUAL:BEGIN -->
|
||||
#### Manual notes
|
||||
<FILL_MANUALLY>
|
||||
<!-- MANUAL:END -->
|
||||
"#;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_template_string("symbol_details", symbol_template)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to register symbol_details template: {}", e))?;
|
||||
|
||||
handlebars.render("symbol_details", &data)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to render symbol details: {}", e))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user