1use crate::core::knowledge::ProjectKnowledge;
2use crate::core::memory_policy::MemoryPolicy;
3use crate::core::property_graph::{CodeGraph, Edge, EdgeKind, Node, NodeKind};
4
5use super::admission::AdmittedPackage;
6use super::composition;
7#[cfg(test)]
8use super::content::PackageContent;
9use super::content::{GraphLayer, KnowledgeLayer, PatternsLayer, SessionLayer};
10use super::graph_model::ContextGraph;
11use super::manifest::{PackageLayer, PackageManifest};
12
13#[derive(Debug, Clone, Default)]
14pub struct LoadReport {
15 pub package_name: String,
16 pub package_version: String,
17 pub knowledge_facts_merged: u32,
18 pub knowledge_facts_skipped: u32,
19 pub knowledge_patterns_merged: u32,
20 pub knowledge_insights_merged: u32,
21 pub graph_nodes_imported: u32,
22 pub graph_edges_imported: u32,
23 pub gotchas_imported: u32,
24 pub patterns_imported: u32,
25 pub session_findings_merged: u32,
26 pub session_decisions_merged: u32,
27 pub v2_nodes_added: u32,
28 pub v2_nodes_updated: u32,
29 pub v2_edges_added: u32,
30 pub v2_edges_merged: u32,
31 pub v2_conflicts: Vec<String>,
32 pub warnings: Vec<String>,
33}
34
35impl std::fmt::Display for LoadReport {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 writeln!(
38 f,
39 "Package: {} v{}",
40 self.package_name, self.package_version
41 )?;
42 if self.knowledge_facts_merged > 0 || self.knowledge_facts_skipped > 0 {
43 writeln!(
44 f,
45 " Knowledge: {} facts merged, {} skipped (duplicates)",
46 self.knowledge_facts_merged, self.knowledge_facts_skipped
47 )?;
48 }
49 if self.knowledge_patterns_merged > 0 {
50 writeln!(
51 f,
52 " Patterns: {} imported",
53 self.knowledge_patterns_merged
54 )?;
55 }
56 if self.knowledge_insights_merged > 0 {
57 writeln!(
58 f,
59 " Insights: {} imported",
60 self.knowledge_insights_merged
61 )?;
62 }
63 if self.graph_nodes_imported > 0 || self.graph_edges_imported > 0 {
64 writeln!(
65 f,
66 " Graph: {} nodes, {} edges imported",
67 self.graph_nodes_imported, self.graph_edges_imported
68 )?;
69 }
70 if self.patterns_imported > 0 {
71 writeln!(
72 f,
73 " Patterns: {} imported (standalone)",
74 self.patterns_imported
75 )?;
76 }
77 if self.gotchas_imported > 0 {
78 writeln!(f, " Gotchas: {} imported", self.gotchas_imported)?;
79 }
80 if self.session_findings_merged > 0 || self.session_decisions_merged > 0 {
81 writeln!(
82 f,
83 " Session: {} findings, {} decisions imported",
84 self.session_findings_merged, self.session_decisions_merged
85 )?;
86 }
87 if self.v2_nodes_added > 0 || self.v2_nodes_updated > 0 {
88 writeln!(
89 f,
90 " Graph v2: {} nodes added, {} updated, {} edges added, {} merged",
91 self.v2_nodes_added,
92 self.v2_nodes_updated,
93 self.v2_edges_added,
94 self.v2_edges_merged,
95 )?;
96 }
97 for c in &self.v2_conflicts {
98 writeln!(f, " CONFLICT: {c}")?;
99 }
100 for w in &self.warnings {
101 writeln!(f, " WARNING: {w}")?;
102 }
103 Ok(())
104 }
105}
106
107pub fn load_admitted_package(
111 package: &AdmittedPackage,
112 project_root: &str,
113) -> Result<LoadReport, String> {
114 let manifest = package.manifest();
115 let content = package.content();
116 if manifest.has_layer(PackageLayer::Checkpoint) || content.checkpoint.is_some() {
117 return Err(
118 "checkpoint packages require the explicit checkpoint-aware SDK seed path; generic load is unsupported"
119 .into(),
120 );
121 }
122 let mut report = LoadReport {
123 package_name: manifest.name.clone(),
124 package_version: manifest.version.clone(),
125 ..Default::default()
126 };
127
128 if let Some(ref min_ver) = manifest.compatibility.min_lean_ctx_version {
129 let current = env!("CARGO_PKG_VERSION");
130 if version_lt(current, min_ver) {
131 report.warnings.push(format!(
132 "package requires lean-ctx >= {min_ver}, current is {current}"
133 ));
134 }
135 }
136
137 if !manifest.dependencies.is_empty() {
138 for dep in &manifest.dependencies {
139 if !dep.optional {
140 report.warnings.push(format!(
141 "unresolved dependency: {} {}",
142 dep.name, dep.version_req
143 ));
144 }
145 }
146 }
147
148 if let Some(ref kl) = content.knowledge
149 && let Err(e) = merge_knowledge(kl, project_root, manifest, &mut report)
150 {
151 report
152 .warnings
153 .push(format!("knowledge import failed: {e}"));
154 }
155
156 if let Some(ref gl) = content.graph
157 && let Err(e) = import_graph(gl, project_root, &mut report)
158 {
159 report.warnings.push(format!("graph import failed: {e}"));
160 }
161
162 if let Some(ref patterns) = content.patterns
163 && let Err(e) = import_patterns(patterns, project_root, manifest, &mut report)
164 {
165 report.warnings.push(format!("patterns import failed: {e}"));
166 }
167
168 if let Some(ref gotchas) = content.gotchas {
169 import_gotchas(gotchas, project_root, &mut report);
170 }
171
172 if let Some(ref session) = content.session {
173 import_session(session, project_root, manifest, &mut report);
174 }
175
176 if let Some(ref incoming_graph) = content.context_graph {
177 import_v2_graph(incoming_graph, project_root, &mut report);
178 }
179
180 Ok(report)
181}
182
183#[cfg(test)]
186pub(crate) fn load_package(
187 manifest: &PackageManifest,
188 content: &PackageContent,
189 project_root: &str,
190) -> Result<LoadReport, String> {
191 let package = AdmittedPackage::admit(manifest.clone(), content.clone())
192 .map_err(|refusal| refusal.to_string())?;
193 load_admitted_package(&package, project_root)
194}
195
196fn import_v2_graph(incoming: &ContextGraph, project_root: &str, report: &mut LoadReport) {
197 let project_hash = crate::core::project_hash::hash_project_root(project_root);
198 let data_dir = match crate::core::data_dir::lean_ctx_data_dir() {
199 Ok(d) => d,
200 Err(e) => {
201 report
202 .warnings
203 .push(format!("v2 graph: data dir unavailable: {e}"));
204 return;
205 }
206 };
207 let graph_path = data_dir
208 .join("context_graph")
209 .join(format!("{project_hash}.json"));
210
211 let graph_path_str = graph_path.to_string_lossy().to_string();
212 let mut local_graph = if let Ok(data) = std::fs::read_to_string(&graph_path_str) {
213 serde_json::from_str::<ContextGraph>(&data).unwrap_or_default()
214 } else {
215 ContextGraph::default()
216 };
217
218 let merge_report = composition::merge_graphs(&mut local_graph, incoming);
219
220 report.v2_nodes_added = merge_report.nodes_added;
221 report.v2_nodes_updated = merge_report.nodes_updated;
222 report.v2_edges_added = merge_report.edges_added;
223 report.v2_edges_merged = merge_report.edges_merged;
224 report.v2_conflicts = merge_report.conflicts;
225
226 if merge_report.nodes_added > 0
227 || merge_report.nodes_updated > 0
228 || merge_report.edges_added > 0
229 {
230 match serde_json::to_string_pretty(&local_graph) {
231 Ok(json) => {
232 if let Some(parent) = graph_path.parent() {
233 let _ = std::fs::create_dir_all(parent);
234 }
235 if let Err(e) = std::fs::write(&graph_path_str, json) {
236 report.warnings.push(format!("v2 graph save failed: {e}"));
237 }
238 }
239 Err(e) => {
240 report
241 .warnings
242 .push(format!("v2 graph serialize failed: {e}"));
243 }
244 }
245 }
246}
247
248fn merge_knowledge(
249 layer: &KnowledgeLayer,
250 project_root: &str,
251 manifest: &PackageManifest,
252 report: &mut LoadReport,
253) -> Result<(), String> {
254 let policy = MemoryPolicy::default();
255 let source_tag = format!("{}@{}", manifest.name, manifest.version);
256
257 ProjectKnowledge::mutate_locked(project_root, |knowledge| {
261 for fact in &layer.facts {
262 let exists = knowledge
263 .facts
264 .iter()
265 .any(|f| f.category == fact.category && f.key == fact.key && f.value == fact.value);
266
267 if exists {
268 report.knowledge_facts_skipped += 1;
269 continue;
270 }
271
272 knowledge.remember(
273 &fact.category,
274 &fact.key,
275 &fact.value,
276 &fact.source_session,
277 fact.confidence.min(0.8),
278 &policy,
279 );
280 if let Some(last) = knowledge.facts.last_mut() {
281 last.imported_from = Some(source_tag.clone());
282 }
283 report.knowledge_facts_merged += 1;
284 }
285
286 for pattern in &layer.patterns {
287 let exists = knowledge.patterns.iter().any(|p| {
288 p.pattern_type == pattern.pattern_type && p.description == pattern.description
289 });
290
291 if !exists {
292 knowledge.patterns.push(pattern.clone());
293 report.knowledge_patterns_merged += 1;
294 }
295 }
296
297 for insight in &layer.insights {
298 let exists = knowledge
299 .history
300 .iter()
301 .any(|h| h.summary == insight.summary);
302
303 if !exists {
304 knowledge.history.push(insight.clone());
305 report.knowledge_insights_merged += 1;
306 }
307 }
308 })
309 .map(|_| ())
310}
311
312fn import_graph(
313 layer: &GraphLayer,
314 project_root: &str,
315 report: &mut LoadReport,
316) -> Result<(), String> {
317 let graph = CodeGraph::open(project_root).map_err(|e| format!("graph open: {e}"))?;
318
319 for node_export in &layer.nodes {
320 let node = Node {
321 id: None,
322 kind: NodeKind::parse(&node_export.kind),
323 name: node_export.name.clone(),
324 file_path: node_export.file_path.clone(),
325 line_start: node_export.line_start,
326 line_end: node_export.line_end,
327 metadata: node_export.metadata.clone(),
328 };
329
330 match graph.upsert_node(&node) {
331 Ok(_) => report.graph_nodes_imported += 1,
332 Err(e) => {
333 report
334 .warnings
335 .push(format!("node import failed ({}): {e}", node_export.name));
336 }
337 }
338 }
339
340 for edge_export in &layer.edges {
341 let source = find_node_for_edge(&graph, &edge_export.source_path, &edge_export.source_name);
342 let target = find_node_for_edge(&graph, &edge_export.target_path, &edge_export.target_name);
343
344 match (source, target) {
345 (Some(src), Some(tgt)) => {
346 let Some(src_id) = src.id else {
347 report.warnings.push(format!(
348 "edge skipped: source node has no id ({}:{})",
349 edge_export.source_path, edge_export.source_name
350 ));
351 continue;
352 };
353 let Some(tgt_id) = tgt.id else {
354 report.warnings.push(format!(
355 "edge skipped: target node has no id ({}:{})",
356 edge_export.target_path, edge_export.target_name
357 ));
358 continue;
359 };
360
361 let edge = Edge {
362 id: None,
363 source_id: src_id,
364 target_id: tgt_id,
365 kind: EdgeKind::parse(&edge_export.kind),
366 metadata: edge_export.metadata.clone(),
367 };
368
369 match graph.upsert_edge(&edge) {
370 Ok(()) => report.graph_edges_imported += 1,
371 Err(e) => {
372 report.warnings.push(format!(
373 "edge import failed ({} -> {}): {e}",
374 edge_export.source_name, edge_export.target_name
375 ));
376 }
377 }
378 }
379 _ => {
380 report.warnings.push(format!(
381 "edge skipped: node not found ({} -> {})",
382 edge_export.source_name, edge_export.target_name
383 ));
384 }
385 }
386 }
387
388 Ok(())
389}
390
391fn find_node_for_edge(graph: &CodeGraph, file_path: &str, name: &str) -> Option<Node> {
393 if let Ok(Some(node)) = graph.get_node_by_symbol(name, file_path) {
394 return Some(node);
395 }
396 if let Ok(Some(node)) = graph.get_node_by_path(file_path) {
397 return Some(node);
398 }
399 None
400}
401
402fn import_patterns(
403 layer: &PatternsLayer,
404 project_root: &str,
405 _manifest: &PackageManifest,
406 report: &mut LoadReport,
407) -> Result<(), String> {
408 ProjectKnowledge::mutate_locked(project_root, |knowledge| {
411 for pattern in &layer.patterns {
412 let exists = knowledge.patterns.iter().any(|p| {
413 p.pattern_type == pattern.pattern_type && p.description == pattern.description
414 });
415
416 if !exists {
417 knowledge.patterns.push(pattern.clone());
418 report.patterns_imported += 1;
419 }
420 }
421 })
422 .map(|_| ())
423}
424
425fn import_gotchas(
426 layer: &super::content::GotchasLayer,
427 project_root: &str,
428 report: &mut LoadReport,
429) {
430 use crate::core::gotcha_tracker::{
431 Gotcha, GotchaCategory, GotchaSeverity, GotchaSource, GotchaStore,
432 };
433
434 let mut store = GotchaStore::load(project_root);
435 let before = store.gotchas.len();
436
437 for g in &layer.gotchas {
438 let dup = store.gotchas.iter().any(|e| e.id == g.id);
439 if dup {
440 continue;
441 }
442
443 let category = GotchaCategory::from_str_loose(&g.category);
444 let severity = match g.severity.as_str() {
445 "critical" => GotchaSeverity::Critical,
446 "warning" => GotchaSeverity::Warning,
447 _ => GotchaSeverity::Info,
448 };
449
450 let mut gotcha = Gotcha::new(
451 category,
452 severity,
453 &g.trigger,
454 &g.resolution,
455 GotchaSource::AgentReported {
456 session_id: "package-import".into(),
457 },
458 "package-import",
459 );
460 g.id.clone_into(&mut gotcha.id);
461 g.file_patterns.clone_into(&mut gotcha.file_patterns);
462 gotcha.confidence = g.confidence.min(0.8);
463
464 store.gotchas.push(gotcha);
465 }
466
467 report.gotchas_imported = (store.gotchas.len() - before) as u32;
468 if let Err(e) = store.save(project_root) {
469 report.warnings.push(format!("gotcha save failed: {e}"));
470 }
471}
472
473pub(crate) fn version_lt(current: &str, required: &str) -> bool {
474 let parse = |v: &str| -> Vec<u32> {
475 v.split('.')
476 .map(|s| s.parse::<u32>().unwrap_or(0))
477 .collect()
478 };
479 let c = parse(current);
480 let r = parse(required);
481 for i in 0..c.len().max(r.len()) {
482 let cv = c.get(i).copied().unwrap_or(0);
483 let rv = r.get(i).copied().unwrap_or(0);
484 if cv < rv {
485 return true;
486 }
487 if cv > rv {
488 return false;
489 }
490 }
491 false
492}
493
494fn import_session(
495 layer: &SessionLayer,
496 project_root: &str,
497 manifest: &PackageManifest,
498 report: &mut LoadReport,
499) {
500 let policy = MemoryPolicy::default();
501 let source_tag = format!("{}@{} (session)", manifest.name, manifest.version);
502
503 let outcome = ProjectKnowledge::mutate_locked(project_root, |knowledge| {
506 for finding in &layer.findings {
507 let key = finding.file.as_deref().unwrap_or("general");
508 let exists = knowledge
509 .facts
510 .iter()
511 .any(|f| f.category == "session_finding" && f.value == finding.summary);
512 if !exists {
513 knowledge.remember(
514 "session_finding",
515 key,
516 &finding.summary,
517 &source_tag,
518 0.6,
519 &policy,
520 );
521 report.session_findings_merged += 1;
522 }
523 }
524
525 for decision in &layer.decisions {
526 let value = if let Some(ref rationale) = decision.rationale {
527 format!("{} (rationale: {})", decision.summary, rationale)
528 } else {
529 decision.summary.clone()
530 };
531 let exists = knowledge
532 .facts
533 .iter()
534 .any(|f| f.category == "session_decision" && f.value == decision.summary);
535 if !exists {
536 knowledge.remember(
537 "session_decision",
538 "decision",
539 &value,
540 &source_tag,
541 0.7,
542 &policy,
543 );
544 report.session_decisions_merged += 1;
545 }
546 }
547 });
548
549 if let Err(e) = outcome {
550 report
551 .warnings
552 .push(format!("session knowledge save failed: {e}"));
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::core::context_package::content::*;
560 use crate::core::context_package::manifest::*;
561 use chrono::Utc;
562
563 fn test_manifest(layers: Vec<PackageLayer>) -> PackageManifest {
564 PackageManifest {
565 schema_version: 1,
566 conformance_level: None,
567 kind: crate::core::context_package::manifest::PackageKind::default(),
568 name: "test-pkg".into(),
569 version: "1.0.0".into(),
570 description: "test".into(),
571 author: None,
572 scope: None,
573 created_at: Utc::now(),
574 updated_at: None,
575 layers,
576 dependencies: vec![],
577 tags: vec![],
578 visibility: None,
579 integrity: PackageIntegrity {
580 sha256: "a".repeat(64),
581 content_hash: "b".repeat(64),
582 byte_size: 100,
583 },
584 provenance: PackageProvenance {
585 tool: "test".into(),
586 tool_version: "0.0.1".into(),
587 project_hash: None,
588 source_session_id: None,
589 },
590 compatibility: CompatibilitySpec::default(),
591 stats: PackageStats::default(),
592 signature: None,
593 graph_summary: None,
594 marketplace: None,
595 redistributable: None,
596 }
597 }
598
599 #[test]
600 fn version_lt_basic_comparisons() {
601 assert!(version_lt("3.5.0", "3.6.0"));
602 assert!(!version_lt("3.6.0", "3.5.0"));
603 assert!(!version_lt("3.6.0", "3.6.0"));
604 assert!(version_lt("3.6.14", "3.6.15"));
605 assert!(version_lt("2.0.0", "3.0.0"));
606 }
607
608 #[test]
609 fn compatibility_warning_when_version_too_low() {
610 let _isolated = crate::core::data_dir::isolated_data_dir();
611 let mut manifest = test_manifest(vec![PackageLayer::Knowledge]);
612 manifest.compatibility.min_lean_ctx_version = Some("99.0.0".into());
613
614 let content = PackageContent {
615 knowledge: Some(KnowledgeLayer {
616 facts: vec![],
617 patterns: vec![],
618 insights: vec![],
619 exported_at: Utc::now(),
620 }),
621 ..Default::default()
622 };
623
624 let dir = tempfile::tempdir().unwrap();
625 let report = load_package(&manifest, &content, dir.path().to_str().unwrap()).unwrap();
626 assert!(
627 report
628 .warnings
629 .iter()
630 .any(|w| w.contains("requires lean-ctx >= 99.0.0"))
631 );
632 }
633
634 #[test]
635 fn dependency_warning_for_required_deps() {
636 let _isolated = crate::core::data_dir::isolated_data_dir();
637 let mut manifest = test_manifest(vec![PackageLayer::Knowledge]);
638 manifest.dependencies.push(PackageDependency {
639 name: "missing-pkg".into(),
640 version_req: "^1.0".into(),
641 optional: false,
642 });
643
644 let content = PackageContent {
645 knowledge: Some(KnowledgeLayer {
646 facts: vec![],
647 patterns: vec![],
648 insights: vec![],
649 exported_at: Utc::now(),
650 }),
651 ..Default::default()
652 };
653
654 let dir = tempfile::tempdir().unwrap();
655 let report = load_package(&manifest, &content, dir.path().to_str().unwrap()).unwrap();
656 assert!(
657 report
658 .warnings
659 .iter()
660 .any(|w| w.contains("unresolved dependency: missing-pkg"))
661 );
662 }
663
664 #[test]
665 fn optional_dependency_no_warning() {
666 let _isolated = crate::core::data_dir::isolated_data_dir();
667 let mut manifest = test_manifest(vec![PackageLayer::Knowledge]);
668 manifest.dependencies.push(PackageDependency {
669 name: "optional-pkg".into(),
670 version_req: "^1.0".into(),
671 optional: true,
672 });
673
674 let content = PackageContent {
675 knowledge: Some(KnowledgeLayer {
676 facts: vec![],
677 patterns: vec![],
678 insights: vec![],
679 exported_at: Utc::now(),
680 }),
681 ..Default::default()
682 };
683
684 let dir = tempfile::tempdir().unwrap();
685 let report = load_package(&manifest, &content, dir.path().to_str().unwrap()).unwrap();
686 assert!(!report.warnings.iter().any(|w| w.contains("optional-pkg")));
687 }
688
689 #[test]
690 fn load_report_display_format() {
691 let report = LoadReport {
692 package_name: "my-pkg".into(),
693 package_version: "1.0.0".into(),
694 knowledge_facts_merged: 5,
695 knowledge_facts_skipped: 2,
696 knowledge_patterns_merged: 3,
697 knowledge_insights_merged: 1,
698 graph_nodes_imported: 10,
699 graph_edges_imported: 8,
700 gotchas_imported: 4,
701 patterns_imported: 2,
702 session_findings_merged: 3,
703 session_decisions_merged: 1,
704 v2_nodes_added: 0,
705 v2_nodes_updated: 0,
706 v2_edges_added: 0,
707 v2_edges_merged: 0,
708 v2_conflicts: vec![],
709 warnings: vec!["test warning".into()],
710 };
711
712 let display = format!("{report}");
713 assert!(display.contains("my-pkg v1.0.0"));
714 assert!(display.contains("5 facts merged"));
715 assert!(display.contains("10 nodes"));
716 assert!(display.contains("2 imported (standalone)"));
717 assert!(display.contains("WARNING: test warning"));
718 }
719
720 #[test]
721 fn v2_graph_import_creates_local_graph() {
722 let _isolated = crate::core::data_dir::isolated_data_dir();
723 use crate::core::context_package::graph_model::{ContextEdge, ContextGraph, ContextNode};
724
725 let mut graph = ContextGraph::new();
726 graph.add_node(ContextNode::fact("n1", "test fact", "arch"));
727 graph.add_node(ContextNode::gotcha("n2", "trigger", "resolution"));
728 graph.add_edge(ContextEdge {
729 from: "n1".into(),
730 to: "n2".into(),
731 edge_type: "has_gotcha".into(),
732 weight: 0.9,
733 coactivations: 5,
734 metadata: None,
735 });
736
737 let content = PackageContent {
738 context_graph: Some(graph),
739 ..Default::default()
740 };
741
742 let mut manifest = test_manifest(vec![]);
743 manifest.schema_version = 2;
744 manifest.conformance_level = Some(2);
745
746 let dir = tempfile::tempdir().unwrap();
747 let report = load_package(&manifest, &content, dir.path().to_str().unwrap()).unwrap();
748
749 assert_eq!(report.v2_nodes_added, 2);
750 assert_eq!(report.v2_edges_added, 1);
751 }
752
753 #[test]
754 fn v2_load_report_display_includes_graph() {
755 let report = LoadReport {
756 package_name: "v2-pkg".into(),
757 package_version: "2.0.0".into(),
758 v2_nodes_added: 15,
759 v2_nodes_updated: 3,
760 v2_edges_added: 20,
761 v2_edges_merged: 5,
762 v2_conflicts: vec!["conflict A".into()],
763 ..Default::default()
764 };
765
766 let display = format!("{report}");
767 assert!(display.contains("15 nodes added"));
768 assert!(display.contains("3 updated"));
769 assert!(display.contains("20 edges added"));
770 assert!(display.contains("5 merged"));
771 assert!(display.contains("CONFLICT: conflict A"));
772 }
773
774 #[test]
775 fn v2_graph_import_merges_with_existing() {
776 let _isolated = crate::core::data_dir::isolated_data_dir();
777 use crate::core::context_package::graph_model::{ContextGraph, ContextNode};
778
779 let _env = crate::core::data_dir::test_env_lock();
781 let mut first_graph = ContextGraph::new();
782 first_graph.add_node(ContextNode::fact("shared", "original", "cat"));
783
784 let first_content = PackageContent {
785 context_graph: Some(first_graph),
786 ..Default::default()
787 };
788
789 let mut manifest = test_manifest(vec![]);
790 manifest.schema_version = 2;
791
792 let dir = tempfile::tempdir().unwrap();
793 let r1 = load_package(&manifest, &first_content, dir.path().to_str().unwrap()).unwrap();
794 assert!(r1.warnings.is_empty(), "{:?}", r1.warnings);
795 assert_eq!(r1.v2_nodes_added, 1);
796
797 let mut second_graph = ContextGraph::new();
798 let mut node = ContextNode::fact("shared", "updated", "cat");
799 node.activation = 0.9;
800 second_graph.add_node(node);
801 second_graph.add_node(ContextNode::fact("new_node", "new content", "cat"));
802
803 let second_content = PackageContent {
804 context_graph: Some(second_graph),
805 ..Default::default()
806 };
807
808 let r2 = load_package(&manifest, &second_content, dir.path().to_str().unwrap()).unwrap();
809 assert!(r2.warnings.is_empty(), "{:?}", r2.warnings);
810 assert_eq!(r2.v2_nodes_added, 1);
811 assert_eq!(r2.v2_nodes_updated, 1);
812 }
813}