Skip to main content

codemem_engine/enrichment/
doc_coverage.rs

1//! Documentation coverage analysis for public symbols.
2
3use super::EnrichResult;
4use crate::CodememEngine;
5use codemem_core::{CodememError, NodeKind};
6use serde_json::json;
7use std::collections::HashMap;
8
9impl CodememEngine {
10    /// Analyze documentation coverage for public symbols.
11    ///
12    /// Checks if each public symbol has a non-empty `doc_comment` in its payload.
13    /// Stores Insight memories for files with low documentation coverage.
14    pub fn enrich_doc_coverage(
15        &self,
16        namespace: Option<&str>,
17    ) -> Result<EnrichResult, CodememError> {
18        let all_nodes = {
19            let graph = self.lock_graph()?;
20            graph.get_all_nodes()
21        };
22
23        struct DocStats {
24            documented: usize,
25            undocumented: Vec<String>,
26        }
27        let mut file_docs: HashMap<String, DocStats> = HashMap::new();
28
29        for node in &all_nodes {
30            if !matches!(
31                node.kind,
32                NodeKind::Function
33                    | NodeKind::Method
34                    | NodeKind::Class
35                    | NodeKind::Interface
36                    | NodeKind::Type
37            ) {
38                continue;
39            }
40            let visibility = node
41                .payload
42                .get("visibility")
43                .and_then(|v| v.as_str())
44                .unwrap_or("private");
45            if visibility != "public" {
46                continue;
47            }
48            let file_path = match node.payload.get("file_path").and_then(|v| v.as_str()) {
49                Some(fp) => fp.to_string(),
50                None => continue,
51            };
52            let has_doc = node
53                .payload
54                .get("doc_comment")
55                .and_then(|v| v.as_str())
56                .map(|s| !s.trim().is_empty())
57                .unwrap_or(false);
58
59            let stats = file_docs.entry(file_path).or_insert(DocStats {
60                documented: 0,
61                undocumented: Vec::new(),
62            });
63            if has_doc {
64                stats.documented += 1;
65            } else {
66                stats.undocumented.push(node.label.clone());
67            }
68        }
69
70        let mut insights_stored = 0;
71        let mut total_documented = 0usize;
72        let mut total_undocumented = 0usize;
73
74        for (file_path, stats) in &file_docs {
75            total_documented += stats.documented;
76            total_undocumented += stats.undocumented.len();
77
78            let total = stats.documented + stats.undocumented.len();
79            if total == 0 {
80                continue;
81            }
82            let coverage = stats.documented as f64 / total as f64;
83            if coverage < 0.5 && !stats.undocumented.is_empty() {
84                let names: Vec<&str> = stats
85                    .undocumented
86                    .iter()
87                    .take(10)
88                    .map(|s| s.as_str())
89                    .collect();
90                let suffix = if stats.undocumented.len() > 10 {
91                    format!(" (and {} more)", stats.undocumented.len() - 10)
92                } else {
93                    String::new()
94                };
95                let content = format!(
96                    "Undocumented public API: {} — {:.0}% coverage ({}/{} documented). Missing: {}{}",
97                    file_path,
98                    coverage * 100.0,
99                    stats.documented,
100                    total,
101                    names.join(", "),
102                    suffix
103                );
104                let importance = if coverage < 0.2 { 0.7 } else { 0.5 };
105                if self
106                    .store_insight(
107                        &content,
108                        "documentation",
109                        &[],
110                        importance,
111                        namespace,
112                        &[format!("file:{file_path}")],
113                    )
114                    .is_some()
115                {
116                    insights_stored += 1;
117                }
118            }
119        }
120
121        self.save_index();
122
123        let total = total_documented + total_undocumented;
124        let overall_coverage = if total > 0 {
125            total_documented as f64 / total as f64
126        } else {
127            1.0
128        };
129
130        Ok(EnrichResult {
131            insights_stored,
132            details: json!({
133                "files_analyzed": file_docs.len(),
134                "total_public_documented": total_documented,
135                "total_public_undocumented": total_undocumented,
136                "overall_coverage": format!("{:.1}%", overall_coverage * 100.0),
137                "insights_stored": insights_stored,
138            }),
139        })
140    }
141}