codemem_engine/enrichment/
doc_coverage.rs1use super::EnrichResult;
4use crate::CodememEngine;
5use codemem_core::{CodememError, NodeKind};
6use serde_json::json;
7use std::collections::HashMap;
8
9impl CodememEngine {
10 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}