Skip to main content

llm_wiki/ops/
graph.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4
5use crate::engine::EngineState;
6use crate::graph;
7
8/// Rendered graph output plus the associated report.
9pub struct GraphResult {
10    /// Rendered graph string (Mermaid, DOT, or llms format).
11    pub rendered: String,
12    /// Metadata about the generated graph.
13    pub report: graph::GraphReport,
14}
15
16/// Parameters for `graph_build`.
17pub struct GraphParams<'a> {
18    /// Output format: `"mermaid"`, `"dot"`, or `"llms"`.
19    pub format: Option<&'a str>,
20    /// Slug of the root node for a subgraph traversal.
21    pub root: Option<String>,
22    /// Maximum hops from root.
23    pub depth: Option<usize>,
24    /// Comma-separated page types to include.
25    pub type_filter: Option<&'a str>,
26    /// Filter edges by this relation label.
27    pub relation: Option<String>,
28    /// File path to write output to; `None` for returning only.
29    pub output: Option<&'a str>,
30    /// If true, merge all mounted wikis into a single graph.
31    pub cross_wiki: bool,
32}
33
34/// Build and render the concept graph according to `params`.
35pub fn graph_build(
36    engine: &EngineState,
37    wiki_name: &str,
38    params: &GraphParams<'_>,
39) -> Result<GraphResult> {
40    let space = engine.space(wiki_name)?;
41    let resolved = space.resolved_config(&engine.config);
42
43    let fmt = params.format.unwrap_or(&resolved.graph.format);
44    let types: Vec<String> = params
45        .type_filter
46        .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
47        .unwrap_or_default();
48
49    let filter = graph::GraphFilter {
50        root: params.root.clone(),
51        depth: params.depth.or(Some(resolved.graph.depth as usize)),
52        types,
53        relation: params.relation.clone(),
54    };
55    let g: Arc<graph::WikiGraph> = if params.cross_wiki {
56        // Build each space graph through its cache, then merge
57        let mut per_space: Vec<(&str, Arc<graph::WikiGraph>)> = Vec::new();
58        for (name, sp) in engine.spaces.iter() {
59            if let Ok(searcher) = sp.index_manager.searcher() {
60                let g = graph::get_or_build_graph(
61                    &sp.index_schema,
62                    &sp.type_registry,
63                    &sp.index_manager,
64                    &sp.graph_cache,
65                    &searcher,
66                    &filter,
67                )?;
68                per_space.push((name.as_str(), g));
69            }
70        }
71        Arc::new(graph::merge_cached_graphs(&per_space, &filter)?)
72    } else {
73        let searcher = space.index_manager.searcher()?;
74        graph::get_or_build_graph(
75            &space.index_schema,
76            &space.type_registry,
77            &space.index_manager,
78            &space.graph_cache,
79            &searcher,
80            &filter,
81        )?
82    };
83
84    let rendered = match fmt {
85        "dot" => graph::render_dot(&g),
86        "llms" => graph::render_llms(&g),
87        _ => graph::render_mermaid(&g),
88    };
89
90    let out = if let Some(out_path) = params.output {
91        let content = if out_path.ends_with(".md") {
92            graph::wrap_graph_md(&rendered, fmt, &filter)
93        } else {
94            rendered.clone()
95        };
96        std::fs::write(out_path, &content)?;
97        out_path.to_string()
98    } else {
99        "stdout".to_string()
100    };
101
102    Ok(GraphResult {
103        rendered,
104        report: graph::GraphReport {
105            nodes: g.node_count(),
106            edges: g.edge_count(),
107            output: out,
108        },
109    })
110}