Skip to main content

gobby_code/graph/report/
generation.rs

1use std::collections::HashMap;
2
3use gobby_core::degradation::ServiceState;
4
5use crate::config::Context;
6
7#[cfg(test)]
8use super::RELATES_TO_CODE;
9use super::loading::load_report_snapshot;
10use super::render::{RenderMarkdownInput, render_markdown};
11use super::summary::{
12    normalize_bridge_edges, suggested_questions, summarize_bridge_edges, summarize_graph,
13    summarize_hotspots, target_frequencies,
14};
15use super::time::now_iso8601;
16use super::types::{
17    BridgeEdgeInput, ProjectGraphReport, ProjectGraphReportError, ProjectGraphReportOptions,
18    ReportDegradation, ReportGraphSnapshot,
19};
20
21pub fn generate_report(ctx: &Context) -> Result<ProjectGraphReport, ProjectGraphReportError> {
22    generate_report_with_options(ctx, ProjectGraphReportOptions::default())
23}
24
25pub fn generate_report_with_options(
26    ctx: &Context,
27    options: ProjectGraphReportOptions,
28) -> Result<ProjectGraphReport, ProjectGraphReportError> {
29    let Some(config) = ctx.falkordb.as_ref() else {
30        return Err(ProjectGraphReportError::GraphServiceNotConfigured);
31    };
32
33    let connection_config = config.connection_config();
34    let options = options.normalized();
35    let result = gobby_core::falkor::with_graph(
36        Some(&connection_config),
37        &config.graph_name,
38        ReportGraphSnapshot::default(),
39        |client| load_report_snapshot(client, &ctx.project_id, options.top_n),
40    );
41
42    match result {
43        Ok((snapshot, ServiceState::Available)) => Ok(generate_report_from_snapshot_with_options(
44            &ctx.project_id,
45            now_iso8601(),
46            snapshot,
47            options,
48        )),
49        Ok((_, ServiceState::NotConfigured)) => {
50            Err(ProjectGraphReportError::GraphServiceNotConfigured)
51        }
52        Ok((_, ServiceState::Unreachable { message })) => {
53            Err(ProjectGraphReportError::GraphServiceUnreachable { message })
54        }
55        Err(error) => Err(ProjectGraphReportError::GraphQueryFailed {
56            message: format!("{error:#}"),
57        }),
58    }
59}
60
61pub fn empty_report(project_id: impl Into<String>) -> ProjectGraphReport {
62    generate_report_from_snapshot(project_id, now_iso8601(), ReportGraphSnapshot::default())
63}
64
65pub(super) fn generate_report_from_snapshot(
66    project_id: impl Into<String>,
67    generated_at: impl Into<String>,
68    snapshot: ReportGraphSnapshot,
69) -> ProjectGraphReport {
70    generate_report_from_snapshot_with_options(
71        project_id,
72        generated_at,
73        snapshot,
74        ProjectGraphReportOptions::default().normalized(),
75    )
76}
77
78fn generate_report_from_snapshot_with_options(
79    project_id: impl Into<String>,
80    generated_at: impl Into<String>,
81    snapshot: ReportGraphSnapshot,
82    options: ProjectGraphReportOptions,
83) -> ProjectGraphReport {
84    let project_id = project_id.into();
85    let generated_at = generated_at.into();
86    let node_by_id = snapshot
87        .nodes
88        .iter()
89        .map(|node| (node.id.as_str(), node))
90        .collect::<HashMap<_, _>>();
91
92    let summary = snapshot
93        .summary
94        .clone()
95        .unwrap_or_else(|| summarize_graph(&snapshot.nodes, &snapshot.code_edges));
96    let hotspots = snapshot.hotspots.clone().unwrap_or_else(|| {
97        summarize_hotspots(&snapshot.nodes, &snapshot.code_edges, options.top_n)
98    });
99    let unresolved_targets = snapshot.unresolved_targets.clone().unwrap_or_else(|| {
100        target_frequencies(
101            &snapshot.code_edges,
102            &node_by_id,
103            "unresolved",
104            options.top_n,
105        )
106    });
107    let external_targets = snapshot.external_targets.clone().unwrap_or_else(|| {
108        target_frequencies(&snapshot.code_edges, &node_by_id, "external", options.top_n)
109    });
110
111    let (bridge_edges, mut degradation_details): (_, Vec<ReportDegradation>) =
112        match snapshot.bridge_edges {
113            BridgeEdgeInput::Available(edges) => (normalize_bridge_edges(edges), vec![]),
114            #[cfg(test)]
115            BridgeEdgeInput::Unavailable(reason) => (
116                vec![],
117                vec![ReportDegradation {
118                    input: RELATES_TO_CODE.to_string(),
119                    required: false,
120                    detail: reason,
121                }],
122            ),
123        };
124    let bridge_summary = summarize_bridge_edges(&bridge_edges);
125    degradation_details.sort_by(|left, right| left.input.cmp(&right.input));
126
127    let suggested_investigation_questions = suggested_questions(
128        &hotspots,
129        &unresolved_targets,
130        &external_targets,
131        bridge_summary.as_ref(),
132        &degradation_details,
133    );
134    let markdown = render_markdown(RenderMarkdownInput {
135        project_id: &project_id,
136        generated_at: &generated_at,
137        summary: &summary,
138        hotspots: &hotspots,
139        unresolved_targets: &unresolved_targets,
140        external_targets: &external_targets,
141        bridge_summary: bridge_summary.as_ref(),
142        degradation_details: &degradation_details,
143        top_n: options.top_n,
144    });
145
146    ProjectGraphReport {
147        project_id,
148        generated_at,
149        summary,
150        hotspots,
151        unresolved_targets,
152        external_targets,
153        bridge_summary,
154        bridge_edges,
155        degradation_details,
156        suggested_investigation_questions,
157        markdown,
158    }
159}