codemem_engine/enrichment/
api_surface.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_api_surface(
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 ApiStats {
25 public: Vec<String>,
26 private_count: usize,
27 }
28 let mut file_api: HashMap<String, ApiStats> = HashMap::new();
29
30 for node in &all_nodes {
31 if !matches!(
32 node.kind,
33 NodeKind::Function
34 | NodeKind::Method
35 | NodeKind::Class
36 | NodeKind::Interface
37 | NodeKind::Type
38 | NodeKind::Constant
39 ) {
40 continue;
41 }
42 let file_path = match node.payload.get("file_path").and_then(|v| v.as_str()) {
43 Some(fp) => fp.to_string(),
44 None => continue,
45 };
46 let visibility = node
47 .payload
48 .get("visibility")
49 .and_then(|v| v.as_str())
50 .unwrap_or("private");
51
52 let stats = file_api.entry(file_path).or_insert(ApiStats {
53 public: Vec::new(),
54 private_count: 0,
55 });
56 if visibility == "public" {
57 stats.public.push(node.label.clone());
58 } else {
59 stats.private_count += 1;
60 }
61 }
62
63 let mut insights_stored = 0;
64 let mut total_public = 0usize;
65 let mut total_private = 0usize;
66
67 for (file_path, stats) in &file_api {
68 total_public += stats.public.len();
69 total_private += stats.private_count;
70
71 if stats.public.is_empty() {
72 continue;
73 }
74 let names: Vec<&str> = stats.public.iter().take(15).map(|s| s.as_str()).collect();
75 let suffix = if stats.public.len() > 15 {
76 format!(" (and {} more)", stats.public.len() - 15)
77 } else {
78 String::new()
79 };
80 let ratio = stats.public.len() as f64
81 / (stats.public.len() + stats.private_count).max(1) as f64;
82 let content = format!(
83 "API surface: {} — {} public, {} private (ratio {:.0}%). Exports: {}{}",
84 file_path,
85 stats.public.len(),
86 stats.private_count,
87 ratio * 100.0,
88 names.join(", "),
89 suffix
90 );
91 let importance = if ratio > 0.8 { 0.6 } else { 0.4 };
92 if self
93 .store_insight(
94 &content,
95 "api",
96 &[],
97 importance,
98 namespace,
99 &[format!("file:{file_path}")],
100 )
101 .is_some()
102 {
103 insights_stored += 1;
104 }
105 }
106
107 self.save_index();
108
109 Ok(EnrichResult {
110 insights_stored,
111 details: json!({
112 "files_analyzed": file_api.len(),
113 "total_public_symbols": total_public,
114 "total_private_symbols": total_private,
115 "insights_stored": insights_stored,
116 }),
117 })
118 }
119}