Skip to main content

lean_ctx/core/context_package/
loader.rs

1use crate::core::knowledge::ProjectKnowledge;
2use crate::core::memory_policy::MemoryPolicy;
3use crate::core::property_graph::{CodeGraph, Edge, EdgeKind, Node, NodeKind};
4
5use super::content::{GraphLayer, KnowledgeLayer, PackageContent};
6use super::manifest::PackageManifest;
7
8#[derive(Debug, Clone, Default)]
9pub struct LoadReport {
10    pub package_name: String,
11    pub package_version: String,
12    pub knowledge_facts_merged: u32,
13    pub knowledge_facts_skipped: u32,
14    pub knowledge_patterns_merged: u32,
15    pub graph_nodes_imported: u32,
16    pub graph_edges_imported: u32,
17    pub gotchas_imported: u32,
18    pub warnings: Vec<String>,
19}
20
21impl std::fmt::Display for LoadReport {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        writeln!(
24            f,
25            "Package: {} v{}",
26            self.package_name, self.package_version
27        )?;
28        if self.knowledge_facts_merged > 0 || self.knowledge_facts_skipped > 0 {
29            writeln!(
30                f,
31                "  Knowledge: {} facts merged, {} skipped (duplicates)",
32                self.knowledge_facts_merged, self.knowledge_facts_skipped
33            )?;
34        }
35        if self.knowledge_patterns_merged > 0 {
36            writeln!(
37                f,
38                "  Patterns:  {} imported",
39                self.knowledge_patterns_merged
40            )?;
41        }
42        if self.graph_nodes_imported > 0 || self.graph_edges_imported > 0 {
43            writeln!(
44                f,
45                "  Graph:     {} nodes, {} edges imported",
46                self.graph_nodes_imported, self.graph_edges_imported
47            )?;
48        }
49        if self.gotchas_imported > 0 {
50            writeln!(f, "  Gotchas:   {} imported", self.gotchas_imported)?;
51        }
52        for w in &self.warnings {
53            writeln!(f, "  WARNING: {w}")?;
54        }
55        Ok(())
56    }
57}
58
59pub fn load_package(
60    manifest: &PackageManifest,
61    content: &PackageContent,
62    project_root: &str,
63) -> Result<LoadReport, String> {
64    let mut report = LoadReport {
65        package_name: manifest.name.clone(),
66        package_version: manifest.version.clone(),
67        ..Default::default()
68    };
69
70    if let Some(ref kl) = content.knowledge {
71        merge_knowledge(kl, project_root, manifest, &mut report)?;
72    }
73
74    if let Some(ref gl) = content.graph {
75        import_graph(gl, project_root, &mut report)?;
76    }
77
78    if let Some(ref gotchas) = content.gotchas {
79        import_gotchas(gotchas, project_root, &mut report);
80    }
81
82    Ok(report)
83}
84
85fn merge_knowledge(
86    layer: &KnowledgeLayer,
87    project_root: &str,
88    manifest: &PackageManifest,
89    report: &mut LoadReport,
90) -> Result<(), String> {
91    let mut knowledge = ProjectKnowledge::load_or_create(project_root);
92    let policy = MemoryPolicy::default();
93    let source_tag = format!("{}@{}", manifest.name, manifest.version);
94
95    for fact in &layer.facts {
96        let exists = knowledge
97            .facts
98            .iter()
99            .any(|f| f.category == fact.category && f.key == fact.key && f.value == fact.value);
100
101        if exists {
102            report.knowledge_facts_skipped += 1;
103            continue;
104        }
105
106        knowledge.remember(
107            &fact.category,
108            &fact.key,
109            &fact.value,
110            &fact.source_session,
111            fact.confidence.min(0.8),
112            &policy,
113        );
114        if let Some(last) = knowledge.facts.last_mut() {
115            last.imported_from = Some(source_tag.clone());
116        }
117        report.knowledge_facts_merged += 1;
118    }
119
120    for pattern in &layer.patterns {
121        let exists = knowledge.patterns.iter().any(|p| {
122            p.pattern_type == pattern.pattern_type && p.description == pattern.description
123        });
124
125        if !exists {
126            knowledge.patterns.push(pattern.clone());
127            report.knowledge_patterns_merged += 1;
128        }
129    }
130
131    knowledge.save()?;
132    Ok(())
133}
134
135fn import_graph(
136    layer: &GraphLayer,
137    project_root: &str,
138    report: &mut LoadReport,
139) -> Result<(), String> {
140    let graph = CodeGraph::open(project_root).map_err(|e| format!("graph open: {e}"))?;
141
142    for node_export in &layer.nodes {
143        let node = Node {
144            id: None,
145            kind: NodeKind::parse(&node_export.kind),
146            name: node_export.name.clone(),
147            file_path: node_export.file_path.clone(),
148            line_start: node_export.line_start,
149            line_end: node_export.line_end,
150            metadata: node_export.metadata.clone(),
151        };
152
153        match graph.upsert_node(&node) {
154            Ok(_) => report.graph_nodes_imported += 1,
155            Err(e) => {
156                report
157                    .warnings
158                    .push(format!("node import failed ({}): {e}", node_export.name));
159            }
160        }
161    }
162
163    for edge_export in &layer.edges {
164        let source = graph
165            .get_node_by_path(&edge_export.source_path)
166            .map_err(|e| e.to_string())?;
167        let target = graph
168            .get_node_by_path(&edge_export.target_path)
169            .map_err(|e| e.to_string())?;
170
171        if let (Some(src), Some(tgt)) = (source, target) {
172            let edge = Edge {
173                id: None,
174                source_id: src.id.unwrap_or(0),
175                target_id: tgt.id.unwrap_or(0),
176                kind: EdgeKind::parse(&edge_export.kind),
177                metadata: edge_export.metadata.clone(),
178            };
179
180            match graph.upsert_edge(&edge) {
181                Ok(()) => report.graph_edges_imported += 1,
182                Err(e) => {
183                    report.warnings.push(format!(
184                        "edge import failed ({} -> {}): {e}",
185                        edge_export.source_name, edge_export.target_name
186                    ));
187                }
188            }
189        }
190    }
191
192    Ok(())
193}
194
195fn import_gotchas(
196    layer: &super::content::GotchasLayer,
197    project_root: &str,
198    report: &mut LoadReport,
199) {
200    use crate::core::gotcha_tracker::{
201        Gotcha, GotchaCategory, GotchaSeverity, GotchaSource, GotchaStore,
202    };
203
204    let mut store = GotchaStore::load(project_root);
205    let before = store.gotchas.len();
206
207    for g in &layer.gotchas {
208        let dup = store.gotchas.iter().any(|e| e.id == g.id);
209        if dup {
210            continue;
211        }
212
213        let category = GotchaCategory::from_str_loose(&g.category);
214        let severity = match g.severity.as_str() {
215            "critical" => GotchaSeverity::Critical,
216            "warning" => GotchaSeverity::Warning,
217            _ => GotchaSeverity::Info,
218        };
219
220        let mut gotcha = Gotcha::new(
221            category,
222            severity,
223            &g.trigger,
224            &g.resolution,
225            GotchaSource::AgentReported {
226                session_id: "package-import".into(),
227            },
228            "package-import",
229        );
230        g.id.clone_into(&mut gotcha.id);
231        g.file_patterns.clone_into(&mut gotcha.file_patterns);
232        gotcha.confidence = g.confidence.min(0.8);
233
234        store.gotchas.push(gotcha);
235    }
236
237    report.gotchas_imported = (store.gotchas.len() - before) as u32;
238    let _ = store.save(project_root);
239}