1use std::path::{Path, PathBuf};
7
8use crate::cli::output::{format_size, Styled};
9use crate::cli::repl_complete::COMMANDS;
10use crate::engine::query::{
11 CallDirection, CallGraphParams, CouplingParams, DependencyParams, ImpactParams, MatchMode,
12 ProphecyParams, QueryEngine, SimilarityParams, SymbolLookupParams,
13};
14use crate::format::{AcbReader, AcbWriter};
15use crate::graph::CodeGraph;
16use crate::parse::parser::{ParseOptions, Parser};
17use crate::semantic::analyzer::{AnalyzeOptions, SemanticAnalyzer};
18
19pub struct ReplState {
21 pub graph: Option<CodeGraph>,
23 pub graph_path: Option<PathBuf>,
25}
26
27impl Default for ReplState {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl ReplState {
34 pub fn new() -> Self {
35 Self {
36 graph: None,
37 graph_path: None,
38 }
39 }
40
41 fn require_graph(&self) -> Option<&CodeGraph> {
43 if let Some(ref g) = self.graph {
44 Some(g)
45 } else {
46 let s = Styled::auto();
47 eprintln!(
48 " {} No graph loaded. Use {} or {}",
49 s.info(),
50 s.bold("/load <file.acb>"),
51 s.bold("/compile <dir>")
52 );
53 None
54 }
55 }
56}
57
58pub fn execute(input: &str, state: &mut ReplState) -> Result<bool, Box<dyn std::error::Error>> {
60 let input = input.trim();
61 if input.is_empty() {
62 return Ok(false);
63 }
64
65 let input = input.strip_prefix('/').unwrap_or(input);
67
68 if input.is_empty() {
70 cmd_help();
71 return Ok(false);
72 }
73
74 let mut parts = input.splitn(2, ' ');
76 let cmd = parts.next().unwrap_or("");
77 let args = parts.next().unwrap_or("").trim();
78
79 match cmd {
80 "exit" | "quit" => return Ok(true),
81 "help" | "h" | "?" => cmd_help(),
82 "clear" | "cls" => cmd_clear(),
83 "compile" | "build" => cmd_compile(args, state)?,
84 "info" => cmd_info(args, state)?,
85 "load" => cmd_load(args, state)?,
86 "query" | "q" => cmd_query(args, state)?,
87 "get" => cmd_get(args, state)?,
88 "units" | "ls" => cmd_units(state)?,
89 _ => {
90 let s = Styled::auto();
91 if let Some(suggestion) = crate::cli::repl_complete::suggest_command(cmd) {
92 eprintln!(
93 " {} Unknown command '/{cmd}'. Did you mean {}?",
94 s.warn(),
95 s.bold(suggestion)
96 );
97 } else {
98 eprintln!(
99 " {} Unknown command '/{cmd}'. Type {} for commands.",
100 s.warn(),
101 s.bold("/help"),
102 );
103 }
104 }
105 }
106
107 Ok(false)
108}
109
110fn cmd_help() {
112 let s = Styled::auto();
113 eprintln!();
114 eprintln!(" {}", s.bold("Commands:"));
115 eprintln!();
116 for (cmd, desc) in COMMANDS {
117 eprintln!(" {:<22} {}", s.cyan(cmd), s.dim(desc));
118 }
119 eprintln!();
120 eprintln!(
121 " {}",
122 s.dim("Tip: Tab completion works for commands, query types, and .acb files.")
123 );
124 eprintln!();
125}
126
127fn cmd_clear() {
129 eprint!("\x1b[2J\x1b[H");
130}
131
132fn cmd_compile(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
134 let s = Styled::auto();
135
136 if args.is_empty() {
137 eprintln!(" {} Usage: {}", s.info(), s.bold("/compile <directory>"));
138 return Ok(());
139 }
140
141 let tokens: Vec<&str> = args.split_whitespace().collect();
142 let dir_path = Path::new(tokens[0]);
143
144 if !dir_path.exists() || !dir_path.is_dir() {
145 eprintln!(
146 " {} Not a valid directory: {}",
147 s.fail(),
148 dir_path.display()
149 );
150 return Ok(());
151 }
152
153 let out_name = dir_path
154 .file_name()
155 .map(|n| n.to_string_lossy().to_string())
156 .unwrap_or_else(|| "output".to_string());
157 let out_path = PathBuf::from(format!("{}.acb", out_name));
158
159 eprintln!();
160 eprintln!(
161 " {} Compiling {} {} {}",
162 s.info(),
163 s.bold(&dir_path.display().to_string()),
164 s.arrow(),
165 s.cyan(&out_path.display().to_string()),
166 );
167
168 let parser = Parser::new();
169 let parse_result = parser.parse_directory(dir_path, &ParseOptions::default())?;
170 eprintln!(
171 " {} Parsed {} files ({} units)",
172 s.ok(),
173 parse_result.stats.files_parsed,
174 parse_result.units.len(),
175 );
176
177 let analyzer = SemanticAnalyzer::new();
178 let graph = analyzer.analyze(parse_result.units, &AnalyzeOptions::default())?;
179
180 let writer = AcbWriter::with_default_dimension();
181 writer.write_to_file(&graph, &out_path)?;
182
183 let file_size = std::fs::metadata(&out_path).map(|m| m.len()).unwrap_or(0);
184 eprintln!(
185 " {} Compiled: {} units, {} edges ({})",
186 s.ok(),
187 s.bold(&graph.unit_count().to_string()),
188 graph.edge_count(),
189 s.dim(&format_size(file_size)),
190 );
191
192 state.graph_path = Some(out_path.clone());
194 state.graph = Some(graph);
195 eprintln!(
196 " {} Graph loaded. Try: {}",
197 s.info(),
198 s.cyan("/query symbol --name <search>")
199 );
200 eprintln!();
201
202 Ok(())
203}
204
205fn cmd_load(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
207 let s = Styled::auto();
208
209 if args.is_empty() {
210 eprintln!(" {} Usage: {}", s.info(), s.bold("/load <file.acb>"));
211 return Ok(());
212 }
213
214 let path = PathBuf::from(args.split_whitespace().next().unwrap_or(args));
215 if !path.exists() {
216 eprintln!(" {} File not found: {}", s.fail(), path.display());
217 return Ok(());
218 }
219
220 let graph = AcbReader::read_from_file(&path)?;
221 eprintln!(
222 " {} Loaded {} ({} units, {} edges)",
223 s.ok(),
224 s.bold(&path.display().to_string()),
225 graph.unit_count(),
226 graph.edge_count(),
227 );
228
229 state.graph_path = Some(path);
230 state.graph = Some(graph);
231 Ok(())
232}
233
234fn cmd_info(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
236 let s = Styled::auto();
237
238 let graph = if args.is_empty() {
239 match state.require_graph() {
240 Some(g) => g,
241 None => return Ok(()),
242 }
243 } else {
244 let path = PathBuf::from(args.split_whitespace().next().unwrap_or(args));
245 let g = AcbReader::read_from_file(&path)?;
246 state.graph_path = Some(path);
247 state.graph = Some(g);
248 state.graph.as_ref().unwrap()
249 };
250
251 let file_label = state
252 .graph_path
253 .as_ref()
254 .map(|p| p.display().to_string())
255 .unwrap_or_else(|| "(in-memory)".to_string());
256
257 eprintln!();
258 eprintln!(" {} {}", s.info(), s.bold(&file_label));
259 eprintln!(
260 " Units: {}",
261 s.bold(&graph.unit_count().to_string())
262 );
263 eprintln!(
264 " Edges: {}",
265 s.bold(&graph.edge_count().to_string())
266 );
267 eprintln!(
268 " Languages: {}",
269 s.bold(&graph.languages().len().to_string())
270 );
271 for lang in graph.languages() {
272 let count = graph.units().iter().filter(|u| u.language == *lang).count();
273 eprintln!(
274 " {} {} {}",
275 s.arrow(),
276 s.cyan(&format!("{:12}", lang)),
277 s.dim(&format!("{} units", count))
278 );
279 }
280 eprintln!();
281
282 Ok(())
283}
284
285fn cmd_query(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
287 let s = Styled::auto();
288 let graph = match state.require_graph() {
289 Some(g) => g,
290 None => return Ok(()),
291 };
292
293 let engine = QueryEngine::new();
294 let tokens: Vec<&str> = args.split_whitespace().collect();
295
296 if tokens.is_empty() {
297 eprintln!(
298 " {} Usage: {}",
299 s.info(),
300 s.bold("/query <type> [--name <n>] [--unit-id <id>] [--depth <d>] [--limit <l>]")
301 );
302 eprintln!(
303 " {} Types: symbol, deps, rdeps, impact, calls, similar, prophecy, stability, coupling",
304 s.dim(" ")
305 );
306 return Ok(());
307 }
308
309 let query_type = tokens[0];
310 let mut name: Option<String> = None;
311 let mut unit_id: Option<u64> = None;
312 let mut depth: u32 = 3;
313 let mut limit: usize = 20;
314
315 let mut i = 1;
317 while i < tokens.len() {
318 match tokens[i] {
319 "--name" | "-n" if i + 1 < tokens.len() => {
320 name = Some(tokens[i + 1].to_string());
321 i += 2;
322 }
323 "--unit-id" | "-u" if i + 1 < tokens.len() => {
324 unit_id = tokens[i + 1].parse().ok();
325 i += 2;
326 }
327 "--depth" | "-d" if i + 1 < tokens.len() => {
328 depth = tokens[i + 1].parse().unwrap_or(3);
329 i += 2;
330 }
331 "--limit" | "-l" if i + 1 < tokens.len() => {
332 limit = tokens[i + 1].parse().unwrap_or(20);
333 i += 2;
334 }
335 _ => {
336 if query_type == "symbol" && name.is_none() {
338 name = Some(tokens[i].to_string());
339 } else if unit_id.is_none() {
340 unit_id = tokens[i].parse().ok();
341 }
342 i += 1;
343 }
344 }
345 }
346
347 match query_type {
348 "symbol" | "sym" | "s" => {
349 let search = match name {
350 Some(n) => n,
351 None => {
352 eprintln!(" {} --name is required for symbol queries", s.fail());
353 return Ok(());
354 }
355 };
356 let params = SymbolLookupParams {
357 name: search.clone(),
358 mode: MatchMode::Contains,
359 limit,
360 ..Default::default()
361 };
362 let results = engine.symbol_lookup(graph, params)?;
363 eprintln!(
364 "\n Symbol lookup: {} ({} results)\n",
365 s.bold(&format!("\"{}\"", search)),
366 results.len()
367 );
368 for (i, unit) in results.iter().enumerate() {
369 eprintln!(
370 " {:>3}. {} {} {}",
371 s.dim(&format!("#{}", i + 1)),
372 s.bold(&unit.qualified_name),
373 s.dim(&format!("({})", unit.unit_type)),
374 s.dim(&format!("[id:{}]", unit.id))
375 );
376 }
377 eprintln!();
378 }
379
380 "deps" | "dep" | "d" => {
381 let uid = match unit_id {
382 Some(u) => u,
383 None => {
384 eprintln!(" {} --unit-id is required for deps queries", s.fail());
385 return Ok(());
386 }
387 };
388 let params = DependencyParams {
389 unit_id: uid,
390 max_depth: depth,
391 edge_types: vec![],
392 include_transitive: true,
393 };
394 let result = engine.dependency_graph(graph, params)?;
395 let root = graph
396 .get_unit(uid)
397 .map(|u| u.qualified_name.as_str())
398 .unwrap_or("?");
399 eprintln!(
400 "\n Dependencies of {} ({} found)\n",
401 s.bold(root),
402 result.nodes.len()
403 );
404 for node in &result.nodes {
405 let name = graph
406 .get_unit(node.unit_id)
407 .map(|u| u.qualified_name.as_str())
408 .unwrap_or("?");
409 let indent = " ".repeat(node.depth as usize);
410 eprintln!(" {}{} {}", indent, s.arrow(), s.cyan(name));
411 }
412 eprintln!();
413 }
414
415 "rdeps" | "rdep" | "r" => {
416 let uid = match unit_id {
417 Some(u) => u,
418 None => {
419 eprintln!(" {} --unit-id is required for rdeps queries", s.fail());
420 return Ok(());
421 }
422 };
423 let params = DependencyParams {
424 unit_id: uid,
425 max_depth: depth,
426 edge_types: vec![],
427 include_transitive: true,
428 };
429 let result = engine.reverse_dependency(graph, params)?;
430 let root = graph
431 .get_unit(uid)
432 .map(|u| u.qualified_name.as_str())
433 .unwrap_or("?");
434 eprintln!(
435 "\n Reverse deps of {} ({} found)\n",
436 s.bold(root),
437 result.nodes.len()
438 );
439 for node in &result.nodes {
440 let name = graph
441 .get_unit(node.unit_id)
442 .map(|u| u.qualified_name.as_str())
443 .unwrap_or("?");
444 let indent = " ".repeat(node.depth as usize);
445 eprintln!(" {}{} {}", indent, s.arrow(), s.cyan(name));
446 }
447 eprintln!();
448 }
449
450 "impact" | "imp" | "i" => {
451 let uid = match unit_id {
452 Some(u) => u,
453 None => {
454 eprintln!(" {} --unit-id is required for impact queries", s.fail());
455 return Ok(());
456 }
457 };
458 let params = ImpactParams {
459 unit_id: uid,
460 max_depth: depth,
461 edge_types: vec![],
462 };
463 let result = engine.impact_analysis(graph, params)?;
464 let root = graph
465 .get_unit(uid)
466 .map(|u| u.qualified_name.as_str())
467 .unwrap_or("?");
468
469 let risk_label = if result.overall_risk >= 0.7 {
470 s.red("HIGH")
471 } else if result.overall_risk >= 0.4 {
472 s.yellow("MEDIUM")
473 } else {
474 s.green("LOW")
475 };
476
477 eprintln!("\n Impact of {} (risk: {})\n", s.bold(root), risk_label,);
478 for imp in &result.impacted {
479 let name = graph
480 .get_unit(imp.unit_id)
481 .map(|u| u.qualified_name.as_str())
482 .unwrap_or("?");
483 let risk_sym = if imp.risk_score >= 0.7 {
484 s.fail()
485 } else if imp.risk_score >= 0.4 {
486 s.warn()
487 } else {
488 s.ok()
489 };
490 eprintln!(
491 " {} {} {} risk:{:.2}",
492 risk_sym,
493 s.cyan(name),
494 s.dim(&format!("(depth {})", imp.depth)),
495 imp.risk_score,
496 );
497 }
498 eprintln!();
499 }
500
501 "calls" | "call" | "c" => {
502 let uid = match unit_id {
503 Some(u) => u,
504 None => {
505 eprintln!(" {} --unit-id is required for calls queries", s.fail());
506 return Ok(());
507 }
508 };
509 let params = CallGraphParams {
510 unit_id: uid,
511 direction: CallDirection::Both,
512 max_depth: depth,
513 };
514 let result = engine.call_graph(graph, params)?;
515 let root = graph
516 .get_unit(uid)
517 .map(|u| u.qualified_name.as_str())
518 .unwrap_or("?");
519 eprintln!(
520 "\n Call graph for {} ({} nodes)\n",
521 s.bold(root),
522 result.nodes.len()
523 );
524 for (nid, d) in &result.nodes {
525 let name = graph
526 .get_unit(*nid)
527 .map(|u| u.qualified_name.as_str())
528 .unwrap_or("?");
529 let indent = " ".repeat(*d as usize);
530 eprintln!(" {}{} {}", indent, s.arrow(), s.cyan(name));
531 }
532 eprintln!();
533 }
534
535 "similar" | "sim" => {
536 let uid = match unit_id {
537 Some(u) => u,
538 None => {
539 eprintln!(" {} --unit-id is required for similar queries", s.fail());
540 return Ok(());
541 }
542 };
543 let params = SimilarityParams {
544 unit_id: uid,
545 top_k: limit,
546 min_similarity: 0.0,
547 };
548 let results = engine.similarity(graph, params)?;
549 let root = graph
550 .get_unit(uid)
551 .map(|u| u.qualified_name.as_str())
552 .unwrap_or("?");
553 eprintln!(
554 "\n Similar to {} ({} matches)\n",
555 s.bold(root),
556 results.len()
557 );
558 for (i, m) in results.iter().enumerate() {
559 let name = graph
560 .get_unit(m.unit_id)
561 .map(|u| u.qualified_name.as_str())
562 .unwrap_or("?");
563 eprintln!(
564 " {:>3}. {} {}",
565 s.dim(&format!("#{}", i + 1)),
566 s.cyan(name),
567 s.yellow(&format!("{:.1}%", m.score * 100.0)),
568 );
569 }
570 eprintln!();
571 }
572
573 "prophecy" | "predict" | "p" => {
574 let params = ProphecyParams {
575 top_k: limit,
576 min_risk: 0.0,
577 };
578 let result = engine.prophecy(graph, params)?;
579 eprintln!(
580 "\n {} Prophecy ({} predictions)\n",
581 s.info(),
582 result.predictions.len()
583 );
584 if result.predictions.is_empty() {
585 eprintln!(" {} Codebase looks stable!", s.ok());
586 }
587 for pred in &result.predictions {
588 let name = graph
589 .get_unit(pred.unit_id)
590 .map(|u| u.qualified_name.as_str())
591 .unwrap_or("?");
592 let risk_sym = if pred.risk_score >= 0.7 {
593 s.fail()
594 } else if pred.risk_score >= 0.4 {
595 s.warn()
596 } else {
597 s.ok()
598 };
599 eprintln!(
600 " {} {} {}: {}",
601 risk_sym,
602 s.cyan(name),
603 s.dim(&format!("(risk {:.2})", pred.risk_score)),
604 pred.reason,
605 );
606 }
607 eprintln!();
608 }
609
610 "stability" | "stab" => {
611 let uid = match unit_id {
612 Some(u) => u,
613 None => {
614 eprintln!(" {} --unit-id is required for stability queries", s.fail());
615 return Ok(());
616 }
617 };
618 let result = engine.stability_analysis(graph, uid)?;
619 let root = graph
620 .get_unit(uid)
621 .map(|u| u.qualified_name.as_str())
622 .unwrap_or("?");
623 let score_color = if result.overall_score >= 0.7 {
624 s.green(&format!("{:.2}", result.overall_score))
625 } else if result.overall_score >= 0.4 {
626 s.yellow(&format!("{:.2}", result.overall_score))
627 } else {
628 s.red(&format!("{:.2}", result.overall_score))
629 };
630 eprintln!("\n Stability of {}: {}\n", s.bold(root), score_color);
631 for factor in &result.factors {
632 eprintln!(
633 " {} {} = {:.2}: {}",
634 s.arrow(),
635 s.bold(&factor.name),
636 factor.value,
637 s.dim(&factor.description),
638 );
639 }
640 eprintln!();
641 }
642
643 "coupling" | "couple" => {
644 let params = CouplingParams {
645 unit_id,
646 min_strength: 0.0,
647 };
648 let results = engine.coupling_detection(graph, params)?;
649 eprintln!("\n Coupling analysis ({} pairs)\n", results.len());
650 if results.is_empty() {
651 eprintln!(" {} No tightly coupled pairs detected.", s.ok());
652 }
653 for c in &results {
654 let name_a = graph
655 .get_unit(c.unit_a)
656 .map(|u| u.qualified_name.as_str())
657 .unwrap_or("?");
658 let name_b = graph
659 .get_unit(c.unit_b)
660 .map(|u| u.qualified_name.as_str())
661 .unwrap_or("?");
662 eprintln!(
663 " {} {} {} {} {}",
664 s.warn(),
665 s.cyan(name_a),
666 s.dim("<->"),
667 s.cyan(name_b),
668 s.yellow(&format!("{:.0}%", c.strength * 100.0)),
669 );
670 }
671 eprintln!();
672 }
673
674 other => {
675 let known = [
676 "symbol",
677 "deps",
678 "rdeps",
679 "impact",
680 "calls",
681 "similar",
682 "prophecy",
683 "stability",
684 "coupling",
685 ];
686 eprintln!(
687 " {} Unknown query type: {}. Available: {}",
688 s.fail(),
689 other,
690 known.join(", ")
691 );
692 }
693 }
694
695 Ok(())
696}
697
698fn cmd_get(args: &str, state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
700 let s = Styled::auto();
701 let graph = match state.require_graph() {
702 Some(g) => g,
703 None => return Ok(()),
704 };
705
706 let uid: u64 = match args.split_whitespace().next().and_then(|s| s.parse().ok()) {
707 Some(id) => id,
708 None => {
709 eprintln!(" {} Usage: {}", s.info(), s.bold("/get <unit-id>"));
710 return Ok(());
711 }
712 };
713
714 let unit = match graph.get_unit(uid) {
715 Some(u) => u,
716 None => {
717 eprintln!(" {} Unit {} not found", s.fail(), uid);
718 return Ok(());
719 }
720 };
721
722 let outgoing = graph.edges_from(uid);
723 let incoming = graph.edges_to(uid);
724
725 eprintln!();
726 eprintln!(" {} {}", s.info(), s.bold(&format!("Unit {}", unit.id)));
727 eprintln!(" Name: {}", s.cyan(&unit.name));
728 eprintln!(" Qualified name: {}", s.bold(&unit.qualified_name));
729 eprintln!(" Type: {}", unit.unit_type);
730 eprintln!(" Language: {}", unit.language);
731 eprintln!(
732 " File: {}",
733 s.cyan(&unit.file_path.display().to_string())
734 );
735 eprintln!(" Span: {}", unit.span);
736 eprintln!(" Complexity: {}", unit.complexity);
737 eprintln!(" Stability: {:.2}", unit.stability_score);
738
739 if let Some(sig) = &unit.signature {
740 eprintln!(" Signature: {}", s.dim(sig));
741 }
742 if let Some(doc) = &unit.doc_summary {
743 eprintln!(" Doc: {}", s.dim(doc));
744 }
745
746 if !outgoing.is_empty() {
747 eprintln!("\n {} Outgoing edges ({})", s.arrow(), outgoing.len());
748 for edge in &outgoing {
749 let target_name = graph
750 .get_unit(edge.target_id)
751 .map(|u| u.qualified_name.as_str())
752 .unwrap_or("?");
753 eprintln!(
754 " {} {} {}",
755 s.arrow(),
756 s.cyan(target_name),
757 s.dim(&format!("({})", edge.edge_type))
758 );
759 }
760 }
761 if !incoming.is_empty() {
762 eprintln!("\n {} Incoming edges ({})", s.arrow(), incoming.len());
763 for edge in &incoming {
764 let source_name = graph
765 .get_unit(edge.source_id)
766 .map(|u| u.qualified_name.as_str())
767 .unwrap_or("?");
768 eprintln!(
769 " {} {} {}",
770 s.arrow(),
771 s.cyan(source_name),
772 s.dim(&format!("({})", edge.edge_type))
773 );
774 }
775 }
776 eprintln!();
777
778 Ok(())
779}
780
781fn cmd_units(state: &mut ReplState) -> Result<(), Box<dyn std::error::Error>> {
783 let s = Styled::auto();
784 let graph = match state.require_graph() {
785 Some(g) => g,
786 None => return Ok(()),
787 };
788
789 eprintln!("\n {} units in graph:\n", graph.unit_count());
790 for unit in graph.units() {
791 eprintln!(
792 " {:>5} {} {} {}",
793 s.dim(&format!("[{}]", unit.id)),
794 s.bold(&unit.qualified_name),
795 s.dim(&format!("({})", unit.unit_type)),
796 s.dim(&format!("c:{}", unit.complexity)),
797 );
798 }
799 eprintln!();
800
801 Ok(())
802}