use std::collections::BTreeSet;
use std::fmt::Write as _;
use super::system_model::{Edge, RuntimeMode, ServiceKind, SystemModel};
const VALID_HEADERS: [&str; 3] = ["flowchart", "graph", "sequenceDiagram"];
pub(crate) fn render_architecture_diagrams(model: &SystemModel) -> Option<String> {
let mut blocks: Vec<String> = Vec::new();
if let Some(topology) = render_topology_flowchart(model) {
blocks.push(topology);
}
if let Some(flow) = render_runtime_flow_sequence(model) {
blocks.push(flow);
}
blocks.retain(|block| is_valid_mermaid(block));
if blocks.is_empty() {
return None;
}
let mut section = String::new();
section.push_str("## Architecture Diagrams\n\n");
section.push_str(
"These diagrams are derived from the workspace `Cargo.toml` topology — \
member crates, their workspace-internal dependency edges, the service \
boundaries their feature gates pull in, and the standalone-vs-daemon runtime \
branch. They describe structure, not per-symbol call graphs.\n\n",
);
section.push_str(
"Solid edges are workspace-internal crate dependencies. Dotted edges are \
service boundaries, labelled by dependency strength: **required** (the command \
cannot run without it), **degraded-ok** (required product infrastructure with \
degraded command behavior when the service is absent), **optional** (engaged \
only when that routing is selected), and **always** (always-on transport).\n\n",
);
for block in blocks {
section.push_str(&block);
if !section.ends_with('\n') {
section.push('\n');
}
section.push('\n');
}
Some(section)
}
pub(crate) fn render_service_matrix(model: &SystemModel) -> Option<String> {
if model.services.is_empty() {
return None;
}
let mut section = String::from("## Services\n\n");
section.push_str(
"Derived deterministically from the workspace's Cargo features and service \
boundaries. **Requirement** classifies each service: the PostgreSQL hub is hard-required; \
FalkorDB, Qdrant, and the embedding API are required product infrastructure with degraded \
command behavior when absent (search drops a ranking signal, never the whole result); the \
daemon is optional AI routing; the ghook inbox is always-on transport; and the parsing / \
media toolchains gate AST and multimodal ingest.\n\n",
);
section.push_str("| Service | Requirement | Pulled in by |\n");
section.push_str("| --- | --- | --- |\n");
for service in &model.services {
let pulled_in_by = if service.pulled_in_by.is_empty() {
"workspace".to_string()
} else {
service.pulled_in_by.join("; ")
};
let _ = writeln!(
section,
"| {} | {} | {} |",
service.name,
service_requirement(service.kind),
pulled_in_by
);
}
section.push('\n');
Some(section)
}
fn service_requirement(kind: ServiceKind) -> &'static str {
match kind {
ServiceKind::Postgres => "Required (index-backed commands)",
ServiceKind::Falkor | ServiceKind::Qdrant | ServiceKind::EmbeddingApi => {
"Required, degraded behavior when absent"
}
ServiceKind::Daemon => "Optional (AI routing)",
ServiceKind::GhookInbox => "Always-on (hook transport)",
ServiceKind::TreeSitter | ServiceKind::DocumentToolchain | ServiceKind::MediaToolchain => {
"Toolchain (degraded behavior when absent)"
}
}
}
fn render_topology_flowchart(model: &SystemModel) -> Option<String> {
if model.crates.is_empty() {
return None;
}
let mut body = String::from("flowchart TD\n");
body.push_str(" subgraph crates [\"Workspace crates\"]\n");
for krate in &model.crates {
let shape = if krate.is_binary && !krate.is_lib {
("([\"", "\"])")
} else {
("[\"", "\"]")
};
let _ = writeln!(
body,
" {}{}{}{}",
node_id(&krate.name),
shape.0,
mermaid_label(&krate.name),
shape.1
);
}
body.push_str(" end\n");
if !model.services.is_empty() {
body.push_str(" subgraph services [\"Service boundaries\"]\n");
for service in &model.services {
let _ = writeln!(
body,
" {}[(\"{}\")]",
service_node_id(service.kind),
mermaid_label(&service.name)
);
}
body.push_str(" end\n");
}
for Edge { from, to } in &model.edges {
let _ = writeln!(body, " {} --> {}", node_id(from), node_id(to));
}
let crate_names: BTreeSet<&str> = model.crates.iter().map(|c| c.name.as_str()).collect();
let mut service_edges: BTreeSet<(String, ServiceKind)> = BTreeSet::new();
for service in &model.services {
for provenance in &service.pulled_in_by {
if let Some(name) = provenance_crate(provenance, &crate_names) {
service_edges.insert((name.to_string(), service.kind));
}
}
}
for (krate, kind) in &service_edges {
let _ = writeln!(
body,
" {} -.->|\"{}\"| {}",
node_id(krate),
service_edge_label(*kind),
service_node_id(*kind)
);
}
if has_mode(model, RuntimeMode::Standalone) || has_mode(model, RuntimeMode::DaemonAttached) {
body.push_str(" subgraph runtime [\"Runtime routing\"]\n");
body.push_str(" cli{{\"AI routing decision\"}}\n");
if has_mode(model, RuntimeMode::Standalone) {
body.push_str(
" cli -->|standalone| standalone[\"Direct to datastores / API\"]\n",
);
}
if has_mode(model, RuntimeMode::DaemonAttached) {
body.push_str(" cli -->|daemon| daemonmode[\"Delegate to Gobby daemon\"]\n");
}
body.push_str(" end\n");
}
body.push_str(" classDef service fill:#eef,stroke:#557,stroke-width:1px;\n");
for service in &model.services {
let _ = writeln!(body, " class {} service;", service_node_id(service.kind));
}
let block = fence(&body);
is_valid_mermaid(&block).then_some(block)
}
fn render_runtime_flow_sequence(model: &SystemModel) -> Option<String> {
if let Some(block) = render_ai_generation_flow(model) {
return Some(block);
}
render_ghook_enqueue_flow(model)
}
fn render_ai_generation_flow(model: &SystemModel) -> Option<String> {
let has_embedding = model
.services
.iter()
.any(|s| s.kind == ServiceKind::EmbeddingApi);
let has_daemon = model.services.iter().any(|s| s.kind == ServiceKind::Daemon);
if !has_embedding || !has_daemon {
return None;
}
let actor = ai_feature_crate(model).unwrap_or("CLI");
let mut body = String::from("sequenceDiagram\n");
let _ = writeln!(body, " participant CLI as {}", mermaid_label(actor));
body.push_str(" participant Core as gobby-core (ai_context)\n");
body.push_str(" participant Daemon as Gobby daemon\n");
body.push_str(" participant API as Embedding API\n");
body.push_str(" CLI->>Core: resolve AiContext + routing decision\n");
body.push_str(" alt daemon-attached\n");
body.push_str(" Core->>Daemon: delegate generation\n");
body.push_str(" Daemon-->>Core: completion\n");
body.push_str(" else standalone (direct)\n");
body.push_str(" Core->>API: embed / complete (OpenAI-compatible)\n");
body.push_str(" API-->>Core: vectors / completion\n");
body.push_str(" end\n");
body.push_str(" Core-->>CLI: grounded result\n");
let block = fence(&body);
is_valid_mermaid(&block).then_some(block)
}
fn render_ghook_enqueue_flow(model: &SystemModel) -> Option<String> {
let has_inbox = model
.services
.iter()
.any(|s| s.kind == ServiceKind::GhookInbox);
if !has_inbox {
return None;
}
let has_daemon = model.services.iter().any(|s| s.kind == ServiceKind::Daemon);
let mut body = String::from("sequenceDiagram\n");
body.push_str(" participant Hook as ghook\n");
body.push_str(" participant Inbox as ~/.gobby/hooks/inbox\n");
if has_daemon {
body.push_str(" participant Daemon as Gobby daemon\n");
}
body.push_str(" Hook->>Inbox: enqueue hook envelope\n");
body.push_str(" Inbox-->>Hook: durable (survives daemon down)\n");
if has_daemon {
body.push_str(" Hook->>Daemon: best-effort POST\n");
body.push_str(" Daemon-->>Hook: ack (optional)\n");
}
let block = fence(&body);
is_valid_mermaid(&block).then_some(block)
}
fn has_mode(model: &SystemModel, mode: RuntimeMode) -> bool {
model.runtime_modes.contains(&mode)
}
fn ai_feature_crate(model: &SystemModel) -> Option<&str> {
model
.features_by_crate
.iter()
.find(|(_, feats)| feats.iter().any(|f| f == "ai"))
.map(|(name, _)| name.as_str())
}
fn provenance_crate<'a>(provenance: &'a str, crate_names: &BTreeSet<&str>) -> Option<&'a str> {
let name = provenance.split(" (").next()?.trim();
crate_names.contains(name).then_some(name)
}
fn node_id(name: &str) -> String {
let mut out = String::from("c_");
for ch in name.chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch);
} else {
out.push('_');
}
}
out
}
fn service_node_id(kind: ServiceKind) -> &'static str {
match kind {
ServiceKind::Postgres => "svc_postgres",
ServiceKind::Falkor => "svc_falkor",
ServiceKind::Qdrant => "svc_qdrant",
ServiceKind::EmbeddingApi => "svc_embedding",
ServiceKind::Daemon => "svc_daemon",
ServiceKind::GhookInbox => "svc_inbox",
ServiceKind::TreeSitter => "svc_treesitter",
ServiceKind::DocumentToolchain => "svc_documents",
ServiceKind::MediaToolchain => "svc_media",
}
}
fn service_edge_label(kind: ServiceKind) -> &'static str {
match kind {
ServiceKind::Postgres => "required",
ServiceKind::Falkor | ServiceKind::Qdrant | ServiceKind::EmbeddingApi => "degraded-ok",
ServiceKind::Daemon => "optional",
ServiceKind::GhookInbox => "always",
ServiceKind::TreeSitter | ServiceKind::DocumentToolchain | ServiceKind::MediaToolchain => {
"toolchain"
}
}
}
fn mermaid_label(text: &str) -> String {
text.replace('\\', "\\\\")
.replace('"', """)
.replace('[', "[")
.replace(']', "]")
.replace('(', "(")
.replace(')', ")")
.replace('{', "{")
.replace('}', "}")
.replace('|', "|")
}
fn fence(body: &str) -> String {
let trimmed = body.trim_end_matches('\n');
format!("```mermaid\n{trimmed}\n```\n")
}
pub(crate) fn is_valid_mermaid(block: &str) -> bool {
let lines: Vec<&str> = block.lines().collect();
if lines.len() < 3 {
return false;
}
if lines[0].trim() != "```mermaid" {
return false;
}
let Some(close_idx) = lines.iter().rposition(|l| l.trim() == "```") else {
return false;
};
if close_idx == 0 {
return false;
}
if lines[1..close_idx]
.iter()
.any(|l| l.trim_start().starts_with("```"))
{
return false;
}
if lines[close_idx + 1..].iter().any(|l| !l.trim().is_empty()) {
return false;
}
let interior = &lines[1..close_idx];
let mut content = interior.iter().filter(|l| !l.trim().is_empty());
let Some(header) = content.next() else {
return false;
};
let Some(header_token) = header.split_whitespace().next() else {
return false;
};
if !VALID_HEADERS.contains(&header_token) {
return false;
}
if content.next().is_none() {
return false;
}
balanced_delimiters(interior)
}
fn balanced_delimiters(lines: &[&str]) -> bool {
let (mut paren, mut bracket, mut brace) = (0i32, 0i32, 0i32);
let mut in_quote = false;
for line in lines {
for ch in line.chars() {
if ch == '"' {
in_quote = !in_quote;
continue;
}
if in_quote {
continue;
}
match ch {
'(' => paren += 1,
')' => paren -= 1,
'[' => bracket += 1,
']' => bracket -= 1,
'{' => brace += 1,
'}' => brace -= 1,
_ => {}
}
if paren < 0 || bracket < 0 || brace < 0 {
return false;
}
}
if in_quote {
return false;
}
}
paren == 0 && bracket == 0 && brace == 0
}
pub(crate) struct ConceptualFlowStep {
pub(crate) id: String,
pub(crate) label: String,
pub(crate) role: Option<String>,
}
pub(crate) fn render_conceptual_flow(
steps: &[ConceptualFlowStep],
ordered_from_docs: bool,
degraded: bool,
) -> Option<String> {
if steps.len() < 2 {
return None;
}
let mut body = String::from("flowchart LR\n");
for step in steps {
let label = match step.role.as_deref() {
Some(role) if !role.is_empty() => format!("{} — {role}", step.label),
_ => step.label.clone(),
};
let _ = writeln!(body, " {}[\"{}\"]", step.id, mermaid_label(&label));
}
for pair in steps.windows(2) {
let _ = writeln!(body, " {} --> {}", pair[0].id, pair[1].id);
}
let block = fence(&body);
if !is_valid_mermaid(&block) {
return None;
}
let mut section = String::from("## Conceptual flow\n\n");
let provenance = if ordered_from_docs {
"ordered by the data flow documented in the sources"
} else {
"in the order these subsystems are grouped on this page"
};
let _ = write!(
section,
"> _Conceptual flow_ — how this page's subsystems behave together, \
{provenance}. Grounded in the member module/file summaries below; it is a \
behavior sketch, not a per-symbol call or import graph.\n\n"
);
if degraded {
section.push_str(
"> _Degraded:_ one or more subsystems had no indexed summary, so it \
appears by name only.\n\n",
);
}
section.push_str(&block);
if !section.ends_with('\n') {
section.push('\n');
}
section.push('\n');
Some(section)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::codewiki::system_model::{Crate, ServiceBoundary};
use std::collections::BTreeMap;
fn step(id: &str, label: &str, role: Option<&str>) -> ConceptualFlowStep {
ConceptualFlowStep {
id: id.to_string(),
label: label.to_string(),
role: role.map(str::to_string),
}
}
#[test]
fn conceptual_flow_chains_members_with_roles_caption_and_degradation() {
let steps = vec![
step("s0", "walker", Some("discovers files")),
step("s1", "parser", Some("extracts the AST")),
step("s2", "indexer", None),
];
let section = render_conceptual_flow(&steps, true, true).expect("flow section");
assert!(section.contains("## Conceptual flow"), "{section}");
assert!(section.contains("```mermaid"), "{section}");
assert!(section.contains("flowchart LR"), "{section}");
assert!(
section.contains("not a per-symbol call or import graph"),
"{section}"
);
assert!(
section.contains("s0[\"walker — discovers files\"]"),
"{section}"
);
assert!(section.contains("s2[\"indexer\"]"), "{section}");
assert!(section.contains("_Degraded:_"), "{section}");
assert!(section.contains("s0 --> s1"), "{section}");
assert!(section.contains("s1 --> s2"), "{section}");
assert!(
section.contains("ordered by the data flow documented"),
"{section}"
);
let fence = section
.split_once("```mermaid")
.map(|(_, rest)| format!("```mermaid{rest}"))
.map(|block| block.trim_end().to_string())
.expect("mermaid fence present");
assert!(is_valid_mermaid(&fence), "{fence}");
}
#[test]
fn conceptual_flow_omitted_below_two_stages() {
let steps = vec![step("s0", "only", None)];
assert!(render_conceptual_flow(&steps, false, false).is_none());
}
fn sample_model() -> SystemModel {
let krate = |name: &str, path: &str, bin: bool, lib: bool| Crate {
name: name.to_string(),
path: path.to_string(),
is_binary: bin,
is_lib: lib,
};
let edge = |from: &str, to: &str| Edge {
from: from.to_string(),
to: to.to_string(),
};
let boundary = |name: &str, kind: ServiceKind, pulled: &[&str]| ServiceBoundary {
name: name.to_string(),
kind,
pulled_in_by: pulled.iter().map(|s| s.to_string()).collect(),
};
let mut features_by_crate = BTreeMap::new();
features_by_crate.insert(
"gobby-code".to_string(),
vec!["ai".to_string(), "postgres".to_string()],
);
SystemModel {
crates: vec![
krate("gobby-code", "crates/gcode", true, false),
krate("gobby-core", "crates/gcore", false, true),
krate("gobby-hooks", "crates/ghook", true, false),
krate("gobby-wiki", "crates/gwiki", true, false),
],
edges: vec![
edge("gobby-code", "gobby-core"),
edge("gobby-hooks", "gobby-core"),
edge("gobby-wiki", "gobby-core"),
],
services: vec![
boundary(
"PostgreSQL hub",
ServiceKind::Postgres,
&["gobby-code (feature: postgres)"],
),
boundary(
"Embedding API",
ServiceKind::EmbeddingApi,
&["gobby-code (feature: ai)"],
),
boundary(
"Gobby daemon",
ServiceKind::Daemon,
&[
"gobby-code (feature: ai)",
"workspace (gobby_core::daemon_url, always)",
],
),
boundary(
"ghook inbox",
ServiceKind::GhookInbox,
&["gobby-hooks (always)"],
),
],
runtime_modes: vec![RuntimeMode::Standalone, RuntimeMode::DaemonAttached],
features_by_crate,
notes: Vec::new(),
}
}
#[test]
fn topology_diagram_contains_crate_nodes_and_dependency_edges() {
let model = sample_model();
let block = render_topology_flowchart(&model).expect("topology rendered for full model");
assert!(is_valid_mermaid(&block), "topology must be valid:\n{block}");
assert!(block.starts_with("```mermaid\n"));
assert!(block.trim_end().ends_with("```"));
assert!(block.contains("flowchart TD"));
for name in ["gobby-code", "gobby-core", "gobby-wiki", "gobby-hooks"] {
assert!(
block.contains(name),
"missing crate node `{name}`:\n{block}"
);
}
let from = node_id("gobby-code");
let to = node_id("gobby-core");
assert!(
block.contains(&format!("{from} --> {to}")),
"missing edge gobby-code -> gobby-core:\n{block}"
);
assert!(block.contains("PostgreSQL hub"));
assert!(block.contains("classDef service"));
assert!(block.contains("standalone"));
assert!(block.contains("daemon"));
}
#[test]
fn service_edges_carry_fixed_dependency_strength_labels() {
let model = sample_model();
let block = render_topology_flowchart(&model).expect("topology rendered for full model");
assert!(
is_valid_mermaid(&block),
"labelled topology must stay valid:\n{block}"
);
let code = node_id("gobby-code");
assert!(
block.contains(&format!(
"{code} -.->|\"required\"| {}",
service_node_id(ServiceKind::Postgres)
)),
"postgres edge must be labelled required:\n{block}"
);
assert!(
block.contains(&format!(
"{code} -.->|\"degraded-ok\"| {}",
service_node_id(ServiceKind::EmbeddingApi)
)),
"embedding edge must be labelled degraded-ok:\n{block}"
);
}
#[test]
fn diagram_section_caption_explains_required_but_degraded_framing() {
let model = sample_model();
let section = render_architecture_diagrams(&model).expect("section for full model");
assert!(
section.contains("required product infrastructure with degraded command behavior"),
"caption must carry the required-but-degraded framing:\n{section}"
);
}
#[test]
fn service_matrix_lists_services_with_fixed_requirement_classes() {
let model = sample_model();
let matrix = render_service_matrix(&model).expect("matrix for full model");
assert!(matrix.contains("## Services"));
assert!(matrix.contains("| Service | Requirement | Pulled in by |"));
assert!(matrix.contains("PostgreSQL hub"));
assert!(matrix.contains("Required (index-backed commands)"));
assert!(matrix.contains("Required, degraded behavior when absent"));
assert!(
matrix.contains("required product infrastructure with degraded command behavior"),
"matrix intro must carry the required-but-degraded framing:\n{matrix}"
);
}
#[test]
fn service_matrix_empty_when_model_reaches_no_services() {
let mut model = sample_model();
model.services.clear();
assert!(render_service_matrix(&model).is_none());
}
#[test]
fn runtime_flow_sequence_is_valid_and_model_seeded() {
let model = sample_model();
let block = render_runtime_flow_sequence(&model).expect("ai flow for ai-feature model");
assert!(is_valid_mermaid(&block), "sequence must be valid:\n{block}");
assert!(block.contains("sequenceDiagram"));
assert!(block.contains("gobby-code"));
assert!(block.contains("Gobby daemon"));
assert!(block.contains("Embedding API"));
}
#[test]
fn ghook_flow_used_when_no_ai_boundary() {
let mut model = sample_model();
model.features_by_crate.clear();
model
.services
.retain(|s| s.kind == ServiceKind::GhookInbox || s.kind == ServiceKind::Daemon);
let block = render_runtime_flow_sequence(&model).expect("ghook flow present");
assert!(is_valid_mermaid(&block), "{block}");
assert!(block.contains("ghook"));
assert!(block.contains("inbox"));
}
#[test]
fn empty_model_draws_nothing_but_is_not_an_error() {
let model = SystemModel {
crates: Vec::new(),
edges: Vec::new(),
services: Vec::new(),
runtime_modes: vec![RuntimeMode::Standalone, RuntimeMode::DaemonAttached],
features_by_crate: BTreeMap::new(),
notes: vec!["cannot read workspace manifest".to_string()],
};
assert!(render_topology_flowchart(&model).is_none());
assert!(render_runtime_flow_sequence(&model).is_none());
assert!(render_architecture_diagrams(&model).is_none());
}
#[test]
fn section_render_includes_prose_and_only_valid_fences() {
let model = sample_model();
let section = render_architecture_diagrams(&model).expect("section rendered");
assert!(section.contains("## Architecture Diagrams"));
assert!(section.contains("derived from the workspace"));
let fences: Vec<&str> = section
.match_indices("```mermaid")
.map(|(_, s)| s)
.collect();
assert!(fences.len() >= 2, "expected topology + flow fences");
let opens = section.matches("```mermaid").count();
let total_fences = section.matches("```").count();
assert_eq!(total_fences, opens * 2, "every fence is closed:\n{section}");
}
#[test]
fn validator_accepts_minimal_flowchart() {
let block = "```mermaid\nflowchart TD\n a --> b\n```\n";
assert!(is_valid_mermaid(block));
}
#[test]
fn validator_accepts_sequence_diagram() {
let block = "```mermaid\nsequenceDiagram\n A->>B: msg\n B-->>A: reply\n```\n";
assert!(is_valid_mermaid(block));
}
#[test]
fn validator_rejects_unrecognized_header() {
let block = "```mermaid\nbananas\n a --> b\n```\n";
assert!(!is_valid_mermaid(block));
}
#[test]
fn validator_rejects_valid_header_prefix() {
let block = "```mermaid\nflowcharting TD\n a --> b\n```\n";
assert!(!is_valid_mermaid(block));
}
#[test]
fn validator_rejects_unclosed_fence() {
let block = "```mermaid\nflowchart TD\n a --> b\n";
assert!(!is_valid_mermaid(block));
}
#[test]
fn validator_rejects_empty_diagram() {
let block = "```mermaid\nflowchart TD\n```\n";
assert!(!is_valid_mermaid(block));
}
#[test]
fn validator_rejects_unbalanced_node_shape() {
let block = "```mermaid\nflowchart TD\n a[\"b --> c\n```\n";
assert!(!is_valid_mermaid(block));
}
#[test]
fn validator_rejects_nested_fence() {
let block = "```mermaid\nflowchart TD\n```mermaid\n a --> b\n```\n";
assert!(!is_valid_mermaid(block));
}
#[test]
fn validator_rejects_content_after_close() {
let block = "```mermaid\nflowchart TD\n a --> b\n```\nstray text\n";
assert!(!is_valid_mermaid(block));
}
}