Skip to main content

memnite_ingest/
crumbex.rs

1use std::fs;
2use std::path::Path;
3
4use memnite_core::Scope;
5
6use crate::error::IngestError;
7use crate::source::{IngestSource, IngestedMemory};
8
9/// Adapter for a crumbex `graph.json` (`crumbex graph --format json`). Maps the
10/// static-architecture report into one memory per hub (god module), hotspot
11/// (central module, PageRank), import cycle, and community (Louvain cluster).
12/// Reads the file as a data contract — it never runs crumbex. `root` is the path
13/// to the JSON file itself.
14pub struct CrumbexGraphSource;
15
16#[derive(serde::Deserialize)]
17struct GraphReportDe {
18    #[serde(default)]
19    god_modules: Vec<(String, usize)>,
20    #[serde(default)]
21    central_modules: Vec<(String, f64)>,
22    #[serde(default)]
23    cycles: Vec<Vec<String>>,
24    #[serde(default)]
25    communities: Vec<Vec<String>>,
26}
27
28impl IngestSource for CrumbexGraphSource {
29    fn name(&self) -> &str {
30        "crumbex"
31    }
32
33    fn collect(&self, root: &Path) -> Result<Vec<IngestedMemory>, IngestError> {
34        let content = fs::read_to_string(root)?;
35        let report: GraphReportDe =
36            serde_json::from_str(&content).map_err(|e| IngestError::Parse(e.to_string()))?;
37
38        let mut out = Vec::new();
39
40        for (file, indeg) in &report.god_modules {
41            out.push(IngestedMemory {
42                source_key: format!("god-module::{file}"),
43                title: format!("Hub: {file}"),
44                body: format!(
45                    "{file} es un hub arquitectónico: importado por {indeg} archivos (blast radius). Editarlo impacta ~{indeg} módulos."
46                ),
47                mem_type: "hub".to_string(),
48                topic_key: Some("architecture".to_string()),
49                scope: Scope::Repo,
50            });
51        }
52
53        for (file, score) in &report.central_modules {
54            out.push(IngestedMemory {
55                source_key: format!("central-module::{file}"),
56                title: format!("Hotspot: {file}"),
57                body: format!(
58                    "{file} es central en el grafo (PageRank {score:.4}): centralidad transitiva alta — los cambios se propagan por el grafo aunque pocos módulos lo importen directo."
59                ),
60                mem_type: "hotspot".to_string(),
61                topic_key: Some("architecture".to_string()),
62                scope: Scope::Repo,
63            });
64        }
65
66        for members in &report.cycles {
67            if members.is_empty() {
68                continue;
69            }
70            let n = members.len();
71            let title = if n <= 2 {
72                format!("Cycle: {}", members.join(", "))
73            } else {
74                format!("Cycle: {}, {} (+{} más)", members[0], members[1], n - 2)
75            };
76            let chain = format!("{} → {}", members.join(" → "), members[0]);
77            out.push(IngestedMemory {
78                source_key: format!("cycle::{}", members.join("|")),
79                title,
80                body: format!(
81                    "Ciclo de import ({n} archivos): {chain}. Dependencia circular, candidato a refactor."
82                ),
83                mem_type: "cycle".to_string(),
84                topic_key: Some("architecture".to_string()),
85                scope: Scope::Repo,
86            });
87        }
88
89        for members in &report.communities {
90            if members.is_empty() {
91                continue;
92            }
93            let n = members.len();
94            let anchor = &members[0];
95            out.push(IngestedMemory {
96                source_key: format!("community::{anchor}"),
97                title: format!("Cluster: {anchor} (+{} más)", n - 1),
98                body: format!(
99                    "Subsistema de {n} archivos acoplados (comunidad Louvain): {}.",
100                    members.join(", ")
101                ),
102                mem_type: "cluster".to_string(),
103                topic_key: Some("architecture".to_string()),
104                scope: Scope::Repo,
105            });
106        }
107
108        Ok(out)
109    }
110}