1use std::path::Path;
4
5use crate::engine::{
6 CausalParams, PatternParams, PatternSort, QueryEngine, TraversalParams, WriteEngine,
7};
8use crate::format::{AmemReader, AmemWriter};
9use crate::graph::traversal::TraversalDirection;
10use crate::graph::MemoryGraph;
11use crate::types::{AmemResult, CognitiveEvent, CognitiveEventBuilder, Edge, EdgeType, EventType};
12
13pub fn cmd_create(path: &Path, dimension: usize) -> AmemResult<()> {
15 let graph = MemoryGraph::new(dimension);
16 let writer = AmemWriter::new(dimension);
17 writer.write_to_file(&graph, path)?;
18 println!("Created {}", path.display());
19 Ok(())
20}
21
22pub fn cmd_info(path: &Path, json: bool) -> AmemResult<()> {
24 let graph = AmemReader::read_from_file(path)?;
25 let file_size = std::fs::metadata(path)?.len();
26
27 if json {
28 let info = serde_json::json!({
29 "file": path.display().to_string(),
30 "version": 1,
31 "dimension": graph.dimension(),
32 "nodes": graph.node_count(),
33 "edges": graph.edge_count(),
34 "sessions": graph.session_index().session_count(),
35 "file_size": file_size,
36 "node_types": {
37 "facts": graph.type_index().count(EventType::Fact),
38 "decisions": graph.type_index().count(EventType::Decision),
39 "inferences": graph.type_index().count(EventType::Inference),
40 "corrections": graph.type_index().count(EventType::Correction),
41 "skills": graph.type_index().count(EventType::Skill),
42 "episodes": graph.type_index().count(EventType::Episode),
43 }
44 });
45 println!(
46 "{}",
47 serde_json::to_string_pretty(&info).unwrap_or_default()
48 );
49 } else {
50 println!("File: {}", path.display());
51 println!("Version: 1");
52 println!("Dimension: {}", graph.dimension());
53 println!("Nodes: {}", graph.node_count());
54 println!("Edges: {}", graph.edge_count());
55 println!("Sessions: {}", graph.session_index().session_count());
56 println!("File size: {}", format_size(file_size));
57 println!("Node types:");
58 println!(" Facts: {}", graph.type_index().count(EventType::Fact));
59 println!(
60 " Decisions: {}",
61 graph.type_index().count(EventType::Decision)
62 );
63 println!(
64 " Inferences: {}",
65 graph.type_index().count(EventType::Inference)
66 );
67 println!(
68 " Corrections: {}",
69 graph.type_index().count(EventType::Correction)
70 );
71 println!(" Skills: {}", graph.type_index().count(EventType::Skill));
72 println!(
73 " Episodes: {}",
74 graph.type_index().count(EventType::Episode)
75 );
76 }
77 Ok(())
78}
79
80pub fn cmd_add(
82 path: &Path,
83 event_type: EventType,
84 content: &str,
85 session_id: u32,
86 confidence: f32,
87 supersedes: Option<u64>,
88 json: bool,
89) -> AmemResult<()> {
90 let mut graph = AmemReader::read_from_file(path)?;
91 let write_engine = WriteEngine::new(graph.dimension());
92
93 let id = if let Some(old_id) = supersedes {
94 write_engine.correct(&mut graph, old_id, content, session_id)?
95 } else {
96 let event = CognitiveEventBuilder::new(event_type, content)
97 .session_id(session_id)
98 .confidence(confidence)
99 .build();
100 graph.add_node(event)?
101 };
102
103 let writer = AmemWriter::new(graph.dimension());
104 writer.write_to_file(&graph, path)?;
105
106 if json {
107 println!(
108 "{}",
109 serde_json::json!({"id": id, "type": event_type.name()})
110 );
111 } else {
112 println!(
113 "Added node {} ({}) to {}",
114 id,
115 event_type.name(),
116 path.display()
117 );
118 }
119 Ok(())
120}
121
122pub fn cmd_link(
124 path: &Path,
125 source_id: u64,
126 target_id: u64,
127 edge_type: EdgeType,
128 weight: f32,
129 json: bool,
130) -> AmemResult<()> {
131 let mut graph = AmemReader::read_from_file(path)?;
132 let edge = Edge::new(source_id, target_id, edge_type, weight);
133 graph.add_edge(edge)?;
134
135 let writer = AmemWriter::new(graph.dimension());
136 writer.write_to_file(&graph, path)?;
137
138 if json {
139 println!(
140 "{}",
141 serde_json::json!({"source": source_id, "target": target_id, "type": edge_type.name()})
142 );
143 } else {
144 println!(
145 "Linked {} --{}--> {}",
146 source_id,
147 edge_type.name(),
148 target_id
149 );
150 }
151 Ok(())
152}
153
154pub fn cmd_get(path: &Path, node_id: u64, json: bool) -> AmemResult<()> {
156 let graph = AmemReader::read_from_file(path)?;
157 let node = graph
158 .get_node(node_id)
159 .ok_or(crate::types::AmemError::NodeNotFound(node_id))?;
160
161 let edges_out = graph.edges_from(node_id).len();
162 let edges_in = graph.edges_to(node_id).len();
163
164 if json {
165 let info = serde_json::json!({
166 "id": node.id,
167 "type": node.event_type.name(),
168 "created_at": node.created_at,
169 "session_id": node.session_id,
170 "confidence": node.confidence,
171 "access_count": node.access_count,
172 "decay_score": node.decay_score,
173 "content": node.content,
174 "edges_out": edges_out,
175 "edges_in": edges_in,
176 });
177 println!(
178 "{}",
179 serde_json::to_string_pretty(&info).unwrap_or_default()
180 );
181 } else {
182 println!("Node {}", node.id);
183 println!(" Type: {}", node.event_type.name());
184 println!(" Created: {}", format_timestamp(node.created_at));
185 println!(" Session: {}", node.session_id);
186 println!(" Confidence: {:.2}", node.confidence);
187 println!(" Access count: {}", node.access_count);
188 println!(" Decay score: {:.2}", node.decay_score);
189 println!(" Content: {:?}", node.content);
190 println!(" Edges out: {}", edges_out);
191 println!(" Edges in: {}", edges_in);
192 }
193 Ok(())
194}
195
196#[allow(clippy::too_many_arguments)]
198pub fn cmd_traverse(
199 path: &Path,
200 start_id: u64,
201 edge_types: Vec<EdgeType>,
202 direction: TraversalDirection,
203 max_depth: u32,
204 max_results: usize,
205 min_confidence: f32,
206 json: bool,
207) -> AmemResult<()> {
208 let graph = AmemReader::read_from_file(path)?;
209 let query_engine = QueryEngine::new();
210
211 let et = if edge_types.is_empty() {
212 vec![
213 EdgeType::CausedBy,
214 EdgeType::Supports,
215 EdgeType::Contradicts,
216 EdgeType::Supersedes,
217 EdgeType::RelatedTo,
218 EdgeType::PartOf,
219 EdgeType::TemporalNext,
220 ]
221 } else {
222 edge_types
223 };
224
225 let result = query_engine.traverse(
226 &graph,
227 TraversalParams {
228 start_id,
229 edge_types: et,
230 direction,
231 max_depth,
232 max_results,
233 min_confidence,
234 },
235 )?;
236
237 if json {
238 let nodes_info: Vec<serde_json::Value> = result
239 .visited
240 .iter()
241 .map(|&id| {
242 let depth = result.depths.get(&id).copied().unwrap_or(0);
243 if let Some(node) = graph.get_node(id) {
244 serde_json::json!({
245 "id": id,
246 "depth": depth,
247 "type": node.event_type.name(),
248 "content": node.content,
249 })
250 } else {
251 serde_json::json!({"id": id, "depth": depth})
252 }
253 })
254 .collect();
255 println!(
256 "{}",
257 serde_json::to_string_pretty(&nodes_info).unwrap_or_default()
258 );
259 } else {
260 for &id in &result.visited {
261 let depth = result.depths.get(&id).copied().unwrap_or(0);
262 let indent = " ".repeat(depth as usize);
263 if let Some(node) = graph.get_node(id) {
264 println!(
265 "{}[depth {}] Node {} ({}): {:?}",
266 indent,
267 depth,
268 id,
269 node.event_type.name(),
270 node.content
271 );
272 }
273 }
274 }
275 Ok(())
276}
277
278#[allow(clippy::too_many_arguments)]
280pub fn cmd_search(
281 path: &Path,
282 event_types: Vec<EventType>,
283 session_ids: Vec<u32>,
284 min_confidence: Option<f32>,
285 max_confidence: Option<f32>,
286 created_after: Option<u64>,
287 created_before: Option<u64>,
288 sort_by: PatternSort,
289 limit: usize,
290 json: bool,
291) -> AmemResult<()> {
292 let graph = AmemReader::read_from_file(path)?;
293 let query_engine = QueryEngine::new();
294
295 let results = query_engine.pattern(
296 &graph,
297 PatternParams {
298 event_types,
299 min_confidence,
300 max_confidence,
301 session_ids,
302 created_after,
303 created_before,
304 min_decay_score: None,
305 max_results: limit,
306 sort_by,
307 },
308 )?;
309
310 if json {
311 let nodes: Vec<serde_json::Value> = results
312 .iter()
313 .map(|node| {
314 serde_json::json!({
315 "id": node.id,
316 "type": node.event_type.name(),
317 "confidence": node.confidence,
318 "content": node.content,
319 "session_id": node.session_id,
320 })
321 })
322 .collect();
323 println!(
324 "{}",
325 serde_json::to_string_pretty(&nodes).unwrap_or_default()
326 );
327 } else {
328 for node in &results {
329 println!(
330 "Node {} ({}, confidence: {:.2}): {:?}",
331 node.id,
332 node.event_type.name(),
333 node.confidence,
334 node.content
335 );
336 }
337 println!("\n{} results", results.len());
338 }
339 Ok(())
340}
341
342pub fn cmd_impact(path: &Path, node_id: u64, max_depth: u32, json: bool) -> AmemResult<()> {
344 let graph = AmemReader::read_from_file(path)?;
345 let query_engine = QueryEngine::new();
346
347 let result = query_engine.causal(
348 &graph,
349 CausalParams {
350 node_id,
351 max_depth,
352 dependency_types: vec![EdgeType::CausedBy, EdgeType::Supports],
353 },
354 )?;
355
356 if json {
357 let info = serde_json::json!({
358 "root_id": result.root_id,
359 "direct_dependents": result.dependency_tree.get(&node_id).map(|v| v.len()).unwrap_or(0),
360 "total_dependents": result.dependents.len(),
361 "affected_decisions": result.affected_decisions,
362 "affected_inferences": result.affected_inferences,
363 "dependents": result.dependents,
364 });
365 println!(
366 "{}",
367 serde_json::to_string_pretty(&info).unwrap_or_default()
368 );
369 } else {
370 println!("Impact analysis for node {}", node_id);
371 let direct = result
372 .dependency_tree
373 .get(&node_id)
374 .map(|v| v.len())
375 .unwrap_or(0);
376 println!(" Direct dependents: {}", direct);
377 println!(" Total dependents: {}", result.dependents.len());
378 println!(" Affected decisions: {}", result.affected_decisions);
379 println!(" Affected inferences: {}", result.affected_inferences);
380
381 if !result.dependents.is_empty() {
382 println!("\nDependency tree:");
383 print_dependency_tree(&graph, &result.dependency_tree, node_id, 1);
384 }
385 }
386 Ok(())
387}
388
389fn print_dependency_tree(
390 graph: &MemoryGraph,
391 tree: &std::collections::HashMap<u64, Vec<(u64, EdgeType)>>,
392 node_id: u64,
393 depth: usize,
394) {
395 if let Some(deps) = tree.get(&node_id) {
396 for (dep_id, edge_type) in deps {
397 let indent = " ".repeat(depth);
398 if let Some(node) = graph.get_node(*dep_id) {
399 println!(
400 "{}<- Node {} ({}, {})",
401 indent,
402 dep_id,
403 node.event_type.name(),
404 edge_type.name()
405 );
406 }
407 print_dependency_tree(graph, tree, *dep_id, depth + 1);
408 }
409 }
410}
411
412pub fn cmd_resolve(path: &Path, node_id: u64, json: bool) -> AmemResult<()> {
414 let graph = AmemReader::read_from_file(path)?;
415 let query_engine = QueryEngine::new();
416
417 let resolved = query_engine.resolve(&graph, node_id)?;
418
419 if json {
420 let info = serde_json::json!({
421 "original_id": node_id,
422 "resolved_id": resolved.id,
423 "type": resolved.event_type.name(),
424 "content": resolved.content,
425 });
426 println!(
427 "{}",
428 serde_json::to_string_pretty(&info).unwrap_or_default()
429 );
430 } else {
431 if resolved.id != node_id {
432 let mut chain = vec![node_id];
434 let mut current = node_id;
435 for _ in 0..100 {
436 let mut next = None;
437 for edge in graph.edges_to(current) {
438 if edge.edge_type == EdgeType::Supersedes {
439 next = Some(edge.source_id);
440 break;
441 }
442 }
443 match next {
444 Some(n) => {
445 chain.push(n);
446 current = n;
447 }
448 None => break,
449 }
450 }
451 let chain_str: Vec<String> = chain.iter().map(|id| format!("Node {}", id)).collect();
452 println!("{} (current)", chain_str.join(" -> superseded by -> "));
453 } else {
454 println!("Node {} is already the current version", node_id);
455 }
456 println!("\nCurrent version:");
457 println!(" Node {}", resolved.id);
458 println!(" Type: {}", resolved.event_type.name());
459 println!(" Content: {:?}", resolved.content);
460 }
461 Ok(())
462}
463
464pub fn cmd_sessions(path: &Path, limit: usize, json: bool) -> AmemResult<()> {
466 let graph = AmemReader::read_from_file(path)?;
467 let session_ids = graph.session_index().session_ids();
468
469 if json {
470 let sessions: Vec<serde_json::Value> = session_ids
471 .iter()
472 .rev()
473 .take(limit)
474 .map(|&sid| {
475 serde_json::json!({
476 "session_id": sid,
477 "node_count": graph.session_index().node_count(sid),
478 })
479 })
480 .collect();
481 println!(
482 "{}",
483 serde_json::to_string_pretty(&sessions).unwrap_or_default()
484 );
485 } else {
486 println!("Sessions in {}:", path.display());
487 for &sid in session_ids.iter().rev().take(limit) {
488 let count = graph.session_index().node_count(sid);
489 println!(" Session {}: {} nodes", sid, count);
490 }
491 println!(" Total: {} sessions", session_ids.len());
492 }
493 Ok(())
494}
495
496pub fn cmd_export(
498 path: &Path,
499 nodes_only: bool,
500 session: Option<u32>,
501 pretty: bool,
502) -> AmemResult<()> {
503 let graph = AmemReader::read_from_file(path)?;
504
505 let nodes: Vec<&CognitiveEvent> = if let Some(sid) = session {
506 let ids = graph.session_index().get_session(sid);
507 ids.iter().filter_map(|&id| graph.get_node(id)).collect()
508 } else {
509 graph.nodes().iter().collect()
510 };
511
512 let nodes_json: Vec<serde_json::Value> = nodes
513 .iter()
514 .map(|n| {
515 serde_json::json!({
516 "id": n.id,
517 "event_type": n.event_type.name(),
518 "created_at": n.created_at,
519 "session_id": n.session_id,
520 "confidence": n.confidence,
521 "access_count": n.access_count,
522 "last_accessed": n.last_accessed,
523 "decay_score": n.decay_score,
524 "content": n.content,
525 })
526 })
527 .collect();
528
529 let output = if nodes_only {
530 serde_json::json!({"nodes": nodes_json})
531 } else {
532 let edges_json: Vec<serde_json::Value> = graph
533 .edges()
534 .iter()
535 .map(|e| {
536 serde_json::json!({
537 "source_id": e.source_id,
538 "target_id": e.target_id,
539 "edge_type": e.edge_type.name(),
540 "weight": e.weight,
541 "created_at": e.created_at,
542 })
543 })
544 .collect();
545 serde_json::json!({"nodes": nodes_json, "edges": edges_json})
546 };
547
548 if pretty {
549 println!(
550 "{}",
551 serde_json::to_string_pretty(&output).unwrap_or_default()
552 );
553 } else {
554 println!("{}", serde_json::to_string(&output).unwrap_or_default());
555 }
556 Ok(())
557}
558
559pub fn cmd_import(path: &Path, json_path: &Path) -> AmemResult<()> {
561 let mut graph = AmemReader::read_from_file(path)?;
562 let json_data = std::fs::read_to_string(json_path)?;
563 let parsed: serde_json::Value = serde_json::from_str(&json_data)
564 .map_err(|e| crate::types::AmemError::Compression(e.to_string()))?;
565
566 let mut added_nodes = 0;
567 let mut added_edges = 0;
568
569 if let Some(nodes) = parsed.get("nodes").and_then(|v| v.as_array()) {
570 for node_val in nodes {
571 let event_type = node_val
572 .get("event_type")
573 .and_then(|v| v.as_str())
574 .and_then(EventType::from_name)
575 .unwrap_or(EventType::Fact);
576 let content = node_val
577 .get("content")
578 .and_then(|v| v.as_str())
579 .unwrap_or("");
580 let session_id = node_val
581 .get("session_id")
582 .and_then(|v| v.as_u64())
583 .unwrap_or(0) as u32;
584 let confidence = node_val
585 .get("confidence")
586 .and_then(|v| v.as_f64())
587 .unwrap_or(1.0) as f32;
588
589 let event = CognitiveEventBuilder::new(event_type, content)
590 .session_id(session_id)
591 .confidence(confidence)
592 .build();
593 graph.add_node(event)?;
594 added_nodes += 1;
595 }
596 }
597
598 if let Some(edges) = parsed.get("edges").and_then(|v| v.as_array()) {
599 for edge_val in edges {
600 let source_id = edge_val
601 .get("source_id")
602 .and_then(|v| v.as_u64())
603 .unwrap_or(0);
604 let target_id = edge_val
605 .get("target_id")
606 .and_then(|v| v.as_u64())
607 .unwrap_or(0);
608 let edge_type = edge_val
609 .get("edge_type")
610 .and_then(|v| v.as_str())
611 .and_then(EdgeType::from_name)
612 .unwrap_or(EdgeType::RelatedTo);
613 let weight = edge_val
614 .get("weight")
615 .and_then(|v| v.as_f64())
616 .unwrap_or(1.0) as f32;
617
618 let edge = Edge::new(source_id, target_id, edge_type, weight);
619 if graph.add_edge(edge).is_ok() {
620 added_edges += 1;
621 }
622 }
623 }
624
625 let writer = AmemWriter::new(graph.dimension());
626 writer.write_to_file(&graph, path)?;
627
628 println!("Imported {} nodes and {} edges", added_nodes, added_edges);
629 Ok(())
630}
631
632pub fn cmd_decay(path: &Path, threshold: f32, json: bool) -> AmemResult<()> {
634 let mut graph = AmemReader::read_from_file(path)?;
635 let write_engine = WriteEngine::new(graph.dimension());
636 let current_time = crate::types::now_micros();
637 let report = write_engine.run_decay(&mut graph, current_time)?;
638
639 let writer = AmemWriter::new(graph.dimension());
640 writer.write_to_file(&graph, path)?;
641
642 let low: Vec<u64> = report
643 .low_importance_nodes
644 .iter()
645 .filter(|&&id| {
646 graph
647 .get_node(id)
648 .map(|n| n.decay_score < threshold)
649 .unwrap_or(false)
650 })
651 .copied()
652 .collect();
653
654 if json {
655 let info = serde_json::json!({
656 "nodes_decayed": report.nodes_decayed,
657 "low_importance_count": low.len(),
658 "low_importance_nodes": low,
659 });
660 println!(
661 "{}",
662 serde_json::to_string_pretty(&info).unwrap_or_default()
663 );
664 } else {
665 println!("Decay complete:");
666 println!(" Nodes updated: {}", report.nodes_decayed);
667 println!(
668 " Low importance (below {}): {} nodes",
669 threshold,
670 low.len()
671 );
672 }
673 Ok(())
674}
675
676pub fn cmd_stats(path: &Path, json: bool) -> AmemResult<()> {
678 let graph = AmemReader::read_from_file(path)?;
679 let file_size = std::fs::metadata(path)?.len();
680
681 let node_count = graph.node_count();
682 let edge_count = graph.edge_count();
683 let avg_edges = if node_count > 0 {
684 edge_count as f64 / node_count as f64
685 } else {
686 0.0
687 };
688 let max_edges = graph
689 .nodes()
690 .iter()
691 .map(|n| graph.edges_from(n.id).len())
692 .max()
693 .unwrap_or(0);
694 let session_count = graph.session_index().session_count();
695 let avg_nodes_per_session = if session_count > 0 {
696 node_count as f64 / session_count as f64
697 } else {
698 0.0
699 };
700
701 let mut conf_buckets = [0usize; 5];
703 for node in graph.nodes() {
704 let bucket = ((node.confidence * 5.0).floor() as usize).min(4);
705 conf_buckets[bucket] += 1;
706 }
707
708 if json {
709 let info = serde_json::json!({
710 "nodes": node_count,
711 "edges": edge_count,
712 "avg_edges_per_node": avg_edges,
713 "max_edges_per_node": max_edges,
714 "sessions": session_count,
715 "file_size": file_size,
716 });
717 println!(
718 "{}",
719 serde_json::to_string_pretty(&info).unwrap_or_default()
720 );
721 } else {
722 println!("Graph Statistics:");
723 println!(" Nodes: {}", node_count);
724 println!(" Edges: {}", edge_count);
725 println!(" Avg edges per node: {:.2}", avg_edges);
726 println!(" Max edges per node: {}", max_edges);
727 println!(" Sessions: {}", session_count);
728 println!(" Avg nodes per session: {:.0}", avg_nodes_per_session);
729 println!();
730 println!(" Confidence distribution:");
731 println!(" 0.0-0.2: {} nodes", conf_buckets[0]);
732 println!(" 0.2-0.4: {} nodes", conf_buckets[1]);
733 println!(" 0.4-0.6: {} nodes", conf_buckets[2]);
734 println!(" 0.6-0.8: {} nodes", conf_buckets[3]);
735 println!(" 0.8-1.0: {} nodes", conf_buckets[4]);
736 println!();
737 println!(" Edge type distribution:");
738 for et_val in 0u8..=6 {
739 if let Some(et) = EdgeType::from_u8(et_val) {
740 let count = graph.edges().iter().filter(|e| e.edge_type == et).count();
741 if count > 0 {
742 println!(" {}: {}", et.name(), count);
743 }
744 }
745 }
746 }
747 Ok(())
748}
749
750fn format_size(bytes: u64) -> String {
751 if bytes < 1024 {
752 format!("{} B", bytes)
753 } else if bytes < 1024 * 1024 {
754 format!("{:.1} KB", bytes as f64 / 1024.0)
755 } else if bytes < 1024 * 1024 * 1024 {
756 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
757 } else {
758 format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
759 }
760}
761
762fn format_timestamp(micros: u64) -> String {
763 let secs = (micros / 1_000_000) as i64;
764 let dt = chrono::DateTime::from_timestamp(secs, 0);
765 match dt {
766 Some(dt) => dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(),
767 None => format!("{} us", micros),
768 }
769}