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