- Config::validate() checks project.root, language, scan.include, python.src_roots, caching.max_cache_age, and scan.max_file_size - Add parse_duration() and parse_file_size() helper functions - Implement DFS-based cycle detection in cycle_detector.rs - Wire cycle detection into renderer critical points section - Add comprehensive unit tests for all new functionality
666 lines
20 KiB
Rust
666 lines
20 KiB
Rust
//! Markdown renderer for ArchDoc
|
||
//!
|
||
//! This module handles generating Markdown documentation from the project model
|
||
//! using templates.
|
||
|
||
use crate::cycle_detector;
|
||
use crate::model::ProjectModel;
|
||
use handlebars::Handlebars;
|
||
|
||
fn sanitize_for_link(filename: &str) -> String {
|
||
filename
|
||
.chars()
|
||
.map(|c| match c {
|
||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||
c => c,
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
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:** 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 -->
|
||
"#
|
||
}
|
||
|
||
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}}
|
||
|
||
## Usage Examples
|
||
|
||
{{#each usage_examples}}
|
||
```python
|
||
{{{this}}}
|
||
```
|
||
|
||
{{/each}}
|
||
"#
|
||
}
|
||
|
||
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_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();
|
||
|
||
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());
|
||
}
|
||
}
|
||
}
|
||
|
||
// Prepare usage examples (for now, just placeholders)
|
||
let usage_examples = vec![
|
||
"// Example usage of module functions\n// TODO: Add real usage examples based on module analysis".to_string()
|
||
];
|
||
|
||
// 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(),
|
||
"db_symbols": db_symbols,
|
||
"http_symbols": http_symbols,
|
||
"queue_symbols": queue_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();
|
||
|
||
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_doc in model.files.values() {
|
||
layout_items.push(serde_json::json!({
|
||
"path": file_doc.path,
|
||
"purpose": "Source file",
|
||
"link": format!("docs/architecture/files/{}.md", sanitize_for_link(&file_doc.path))
|
||
}));
|
||
}
|
||
|
||
// 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}}
|
||
- {{{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))
|
||
}
|
||
|
||
pub fn render_layout_md(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||
// Collect layout information from files
|
||
let mut layout_items = Vec::new();
|
||
|
||
for file_doc in model.files.values() {
|
||
layout_items.push(serde_json::json!({
|
||
"path": file_doc.path,
|
||
"purpose": "Source file",
|
||
"link": format!("files/{}.md", sanitize_for_link(&file_doc.path))
|
||
}));
|
||
}
|
||
|
||
// 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,
|
||
},
|
||
"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}}
|
||
<!-- 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))
|
||
}
|
||
} |