Skip to main content

codemem_engine/enrichment/
api_surface.rs

1//! API surface analysis: public vs private symbol counts per file.
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 the public API surface of each module/package.
11    ///
12    /// Counts public vs private symbols per file and stores Insight memories
13    /// summarizing the public API.
14    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        // Count public vs private symbols per file
24        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}