memnite_ingest/
crumbex.rs1use std::fs;
2use std::path::Path;
3
4use memnite_core::Scope;
5
6use crate::error::IngestError;
7use crate::source::{IngestSource, IngestedMemory};
8
9pub 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}