greppy/cli/
trace.rs

1//! Trace command implementation
2//!
3//! Provides the `greppy trace` CLI command for tracing symbol invocations,
4//! references, data flow, and dead code analysis.
5//!
6//! @module cli/trace
7
8use crate::ai::trace_prompts::is_natural_language_query;
9use crate::ai::{claude::ClaudeClient, gemini::GeminiClient};
10use crate::auth::{self, Provider};
11use crate::core::error::{Error, Result};
12use crate::core::project::Project;
13use crate::trace::context::FileCache;
14use crate::trace::output::{
15    create_formatter, ChainStep, DeadCodeResult, DeadSymbol, FlowAction, FlowResult, FlowStep,
16    ImpactResult, InvocationPath, ModuleResult, OutputFormat, PatternMatch, PatternResult,
17    PotentialCaller, ReferenceInfo, ReferenceKind, RefsResult, RiskLevel, ScopeResult,
18    ScopeVariable, StatsResult, TraceResult,
19};
20use crate::trace::{
21    find_dead_symbols, find_refs, load_index, trace_index_exists, trace_index_path,
22    trace_symbol_by_name, RefKind, SemanticIndex, SymbolKind,
23};
24use clap::Args;
25use regex::Regex;
26use serde::Serialize;
27use std::collections::{HashMap, HashSet};
28use std::env;
29use std::path::{Path, PathBuf};
30use tracing::{debug, info, warn};
31
32/// Combined results for multi-operation JSON output
33#[derive(Debug, Default, Serialize)]
34pub struct CombinedResults {
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub trace: Option<TraceResult>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub refs: Option<RefsResult>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub callers: Option<TraceResult>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub callees: Option<TraceResult>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub type_usage: Option<RefsResult>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub module: Option<ModuleResult>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub pattern: Option<PatternResult>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub flow: Option<FlowResult>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub impact: Option<ImpactResult>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub scope: Option<ScopeResult>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub dead_code: Option<DeadCodeResult>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub stats: Option<StatsResult>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub cycles: Option<ModuleResult>,
61}
62
63// =============================================================================
64// HELPERS
65// =============================================================================
66
67/// Convert SymbolKind to string
68fn symbol_kind_str(kind: SymbolKind) -> &'static str {
69    match kind {
70        SymbolKind::Function => "function",
71        SymbolKind::Method => "method",
72        SymbolKind::Class => "class",
73        SymbolKind::Struct => "struct",
74        SymbolKind::Enum => "enum",
75        SymbolKind::Interface => "interface",
76        SymbolKind::TypeAlias => "type_alias",
77        SymbolKind::Constant => "constant",
78        SymbolKind::Variable => "variable",
79        SymbolKind::Module => "module",
80        SymbolKind::Unknown => "unknown",
81    }
82}
83
84/// Convert output ReferenceKind to string
85fn reference_kind_str(kind: ReferenceKind) -> &'static str {
86    match kind {
87        ReferenceKind::Read => "read",
88        ReferenceKind::Write => "write",
89        ReferenceKind::Call => "call",
90        ReferenceKind::TypeAnnotation => "type",
91        ReferenceKind::Import => "import",
92        ReferenceKind::Export => "export",
93    }
94}
95
96// =============================================================================
97// ARGS
98// =============================================================================
99
100/// Arguments for the trace command
101#[derive(Args, Debug)]
102#[command(after_help = "EXAMPLES:
103    greppy trace validateUser              Trace invocation paths
104    greppy trace -d validateUser           Direct mode (no AI reranking)
105    greppy trace --refs userId             Find all references
106    greppy trace --refs userId -c 2        Find refs with 2 lines context
107    greppy trace --refs userId --in src/   Limit to src/ directory
108    greppy trace --reads userId            Find reads only
109    greppy trace --writes userId           Find writes only
110    greppy trace --callers fetchData       Show what calls this
111    greppy trace --callees fetchData       Show what this calls
112    greppy trace --type UserProfile        Trace type usage
113    greppy trace --module @/lib/auth       Trace module imports/exports
114    greppy trace --pattern \"TODO:.*\"       Find pattern occurrences
115    greppy trace --flow userInput          Trace data flow
116    greppy trace --impact login            Analyze change impact
117    greppy trace --scope src/api.ts:42     Show scope at location
118    greppy trace --dead                    Find unused code
119    greppy trace --dead --xref             Dead code with potential callers
120    greppy trace --stats                   Show codebase statistics
121    greppy trace --cycles                  Find circular dependencies
122
123COMPOSABLE FLAGS (run multiple operations at once):
124    greppy trace --dead --stats            Dead code + statistics
125    greppy trace --dead --stats --in src/  Filtered to src/ directory
126    greppy trace --dead --stats --summary  Condensed one-line summaries
127    greppy trace --refs foo --impact foo   References + impact analysis
128    greppy trace --dead --cycles           Dead code + circular deps
129
130FILTERING:
131    greppy trace --dead --in src/auth      Filter to path
132    greppy trace --dead --symbol-type fn   Filter by type (fn, struct, etc)
133    greppy trace --dead --name \"test.*\"    Filter by name pattern
134
135OUTPUT FORMATS:
136    greppy trace --refs userId --json      JSON output for tooling
137    greppy trace --dead --stats --json     Combined JSON for multi-op
138    greppy trace --refs userId --plain     Plain text (no colors)
139    greppy trace --refs userId --csv       CSV output
140    greppy trace --refs userId --dot       DOT graph format
141    greppy trace --refs userId --markdown  Markdown output")]
142pub struct TraceArgs {
143    /// Symbol to trace (function, class, method, variable)
144    pub symbol: Option<String>,
145
146    /// Direct mode (no AI reranking)
147    #[arg(short = 'd', long)]
148    pub direct: bool,
149
150    /// Trace all references to symbol
151    #[arg(long, value_name = "SYMBOL")]
152    pub refs: Option<String>,
153
154    /// Trace reads only
155    #[arg(long, value_name = "SYMBOL")]
156    pub reads: Option<String>,
157
158    /// Trace writes only
159    #[arg(long, value_name = "SYMBOL")]
160    pub writes: Option<String>,
161
162    /// Show what calls this symbol
163    #[arg(long, value_name = "SYMBOL")]
164    pub callers: Option<String>,
165
166    /// Show what this symbol calls
167    #[arg(long, value_name = "SYMBOL")]
168    pub callees: Option<String>,
169
170    /// Trace type usage
171    #[arg(long = "type", value_name = "TYPE")]
172    pub type_name: Option<String>,
173
174    /// Trace module imports/exports
175    #[arg(long, value_name = "MODULE")]
176    pub module: Option<String>,
177
178    /// Trace pattern occurrences (regex)
179    #[arg(long, value_name = "REGEX")]
180    pub pattern: Option<String>,
181
182    /// Trace data flow
183    #[arg(long, value_name = "SYMBOL")]
184    pub flow: Option<String>,
185
186    /// Analyze impact of changing symbol
187    #[arg(long, value_name = "SYMBOL")]
188    pub impact: Option<String>,
189
190    /// Show scope at location (file:line)
191    #[arg(long, value_name = "LOCATION")]
192    pub scope: Option<String>,
193
194    /// Find dead/unused code
195    #[arg(long)]
196    pub dead: bool,
197
198    /// Cross-reference dead code (show potential callers)
199    #[arg(long)]
200    pub xref: bool,
201
202    /// Show codebase statistics
203    #[arg(long)]
204    pub stats: bool,
205
206    /// Find circular dependencies
207    #[arg(long)]
208    pub cycles: bool,
209
210    /// Filter by reference kind (read, write, call, type, import, export)
211    #[arg(long, value_name = "KIND")]
212    pub kind: Option<String>,
213
214    /// Limit search to path/directory
215    #[arg(long, value_name = "PATH")]
216    pub r#in: Option<PathBuf>,
217
218    /// Filter by symbol type (function, method, class, variable, type, interface)
219    #[arg(long, value_name = "TYPE")]
220    pub symbol_type: Option<String>,
221
222    /// Filter by name pattern (regex)
223    #[arg(long, value_name = "PATTERN")]
224    pub name: Option<String>,
225
226    /// Group results by (file, kind, scope)
227    #[arg(long, value_name = "GROUP")]
228    pub group_by: Option<String>,
229
230    /// Output as JSON
231    #[arg(long)]
232    pub json: bool,
233
234    /// Output as plain text (no colors)
235    #[arg(long)]
236    pub plain: bool,
237
238    /// Output as CSV
239    #[arg(long)]
240    pub csv: bool,
241
242    /// Output as DOT graph
243    #[arg(long)]
244    pub dot: bool,
245
246    /// Output as Markdown
247    #[arg(long)]
248    pub markdown: bool,
249
250    /// Interactive TUI mode
251    #[arg(long)]
252    pub tui: bool,
253
254    /// Maximum trace depth
255    #[arg(long, default_value = "10")]
256    pub max_depth: usize,
257
258    /// Lines of code context to show (before and after)
259    #[arg(long, short = 'c', default_value = "0")]
260    pub context: u32,
261
262    /// Maximum number of results to show
263    #[arg(long, short = 'n')]
264    pub limit: Option<usize>,
265
266    /// Show only counts, not full results
267    #[arg(long)]
268    pub count: bool,
269
270    /// Summary mode: condensed output for multi-op commands
271    #[arg(long)]
272    pub summary: bool,
273
274    /// Project path (default: current directory)
275    #[arg(short, long)]
276    pub project: Option<PathBuf>,
277}
278
279impl TraceArgs {
280    /// Determine the output format from args
281    fn output_format(&self) -> OutputFormat {
282        if self.json {
283            OutputFormat::Json
284        } else if self.csv {
285            OutputFormat::Csv
286        } else if self.dot {
287            OutputFormat::Dot
288        } else if self.markdown {
289            OutputFormat::Markdown
290        } else if self.plain {
291            OutputFormat::Plain
292        } else {
293            OutputFormat::Ascii
294        }
295    }
296
297    /// Get all operations to perform (supports composable flags)
298    fn operations(&self) -> Vec<TraceOperation> {
299        let mut ops = Vec::new();
300
301        // Boolean flags (can combine)
302        if self.dead {
303            ops.push(TraceOperation::DeadCode);
304        }
305        if self.stats {
306            ops.push(TraceOperation::Stats);
307        }
308        if self.cycles {
309            ops.push(TraceOperation::Cycles);
310        }
311
312        // Symbol-based operations (can combine multiple)
313        if let Some(ref loc) = self.scope {
314            ops.push(TraceOperation::Scope(loc.clone()));
315        }
316        if let Some(ref sym) = self.impact {
317            ops.push(TraceOperation::Impact(sym.clone()));
318        }
319        if let Some(ref sym) = self.flow {
320            ops.push(TraceOperation::Flow(sym.clone()));
321        }
322        if let Some(ref pattern) = self.pattern {
323            ops.push(TraceOperation::Pattern(pattern.clone()));
324        }
325        if let Some(ref module) = self.module {
326            ops.push(TraceOperation::Module(module.clone()));
327        }
328        if let Some(ref type_name) = self.type_name {
329            ops.push(TraceOperation::Type(type_name.clone()));
330        }
331        if let Some(ref sym) = self.callers {
332            ops.push(TraceOperation::Callers(sym.clone()));
333        }
334        if let Some(ref sym) = self.callees {
335            ops.push(TraceOperation::Callees(sym.clone()));
336        }
337        if let Some(ref sym) = self.reads {
338            ops.push(TraceOperation::Refs {
339                symbol: sym.clone(),
340                kind: Some(ReferenceKind::Read),
341            });
342        }
343        if let Some(ref sym) = self.writes {
344            ops.push(TraceOperation::Refs {
345                symbol: sym.clone(),
346                kind: Some(ReferenceKind::Write),
347            });
348        }
349        if let Some(ref sym) = self.refs {
350            ops.push(TraceOperation::Refs {
351                symbol: sym.clone(),
352                kind: self.parse_kind_filter(),
353            });
354        }
355        if let Some(ref sym) = self.symbol {
356            ops.push(TraceOperation::Trace(sym.clone()));
357        }
358
359        ops
360    }
361
362    /// Parse --kind filter into ReferenceKind
363    fn parse_kind_filter(&self) -> Option<ReferenceKind> {
364        self.kind
365            .as_ref()
366            .and_then(|k| match k.to_lowercase().as_str() {
367                "read" => Some(ReferenceKind::Read),
368                "write" => Some(ReferenceKind::Write),
369                "call" => Some(ReferenceKind::Call),
370                "type" => Some(ReferenceKind::TypeAnnotation),
371                "import" => Some(ReferenceKind::Import),
372                "export" => Some(ReferenceKind::Export),
373                _ => None,
374            })
375    }
376}
377
378/// The trace operation to perform
379#[derive(Debug)]
380enum TraceOperation {
381    Trace(String),
382    Refs {
383        symbol: String,
384        kind: Option<ReferenceKind>,
385    },
386    Callers(String),
387    Callees(String),
388    Type(String),
389    Module(String),
390    Pattern(String),
391    Flow(String),
392    Impact(String),
393    Scope(String),
394    DeadCode,
395    Stats,
396    Cycles,
397}
398
399/// Universal filter for trace operations
400#[derive(Debug, Clone, Default)]
401pub struct TraceFilter {
402    /// Filter by file/folder path (contains match)
403    pub path: Option<String>,
404    /// Filter by symbol type (function, method, class, etc.)
405    pub symbol_type: Option<String>,
406    /// Filter by name pattern (regex)
407    pub name_pattern: Option<regex::Regex>,
408}
409
410impl TraceFilter {
411    /// Check if a symbol passes the filter
412    pub fn matches_symbol(&self, name: &str, kind: &str, file_path: &str) -> bool {
413        // Path filter
414        if let Some(ref path) = self.path {
415            if !file_path.contains(path) {
416                return false;
417            }
418        }
419        // Symbol type filter
420        if let Some(ref stype) = self.symbol_type {
421            if !kind.to_lowercase().contains(&stype.to_lowercase()) {
422                return false;
423            }
424        }
425        // Name pattern filter
426        if let Some(ref pattern) = self.name_pattern {
427            if !pattern.is_match(name) {
428                return false;
429            }
430        }
431        true
432    }
433
434    /// Check if a file path passes the filter
435    pub fn matches_path(&self, file_path: &str) -> bool {
436        if let Some(ref path) = self.path {
437            return file_path.contains(path);
438        }
439        true
440    }
441}
442
443impl TraceArgs {
444    /// Build a universal filter from args
445    pub fn build_filter(&self) -> TraceFilter {
446        TraceFilter {
447            path: self.r#in.as_ref().map(|p| p.to_string_lossy().to_string()),
448            symbol_type: self.symbol_type.clone(),
449            name_pattern: self.name.as_ref().and_then(|p| regex::Regex::new(p).ok()),
450        }
451    }
452}
453
454// =============================================================================
455// COMMAND EXECUTION
456// =============================================================================
457
458/// Run the trace command
459pub async fn run(args: TraceArgs) -> Result<()> {
460    let project_path = args
461        .project
462        .clone()
463        .unwrap_or_else(|| env::current_dir().expect("Failed to get current directory"));
464
465    let project = Project::detect(&project_path)?;
466    let format = args.output_format();
467    let formatter = create_formatter(format);
468
469    // Check for TUI mode
470    if args.tui {
471        return run_tui(&args, &project).await;
472    }
473
474    // Get all operations (composable flags)
475    let operations = args.operations();
476    debug!(?operations, "Trace operations");
477
478    // Build universal filter from args
479    let filter = args.build_filter();
480
481    // No operations specified
482    if operations.is_empty() {
483        eprintln!("Usage: greppy trace <symbol>");
484        eprintln!("       greppy trace --refs <symbol>");
485        eprintln!("       greppy trace --dead");
486        eprintln!("       greppy trace --stats");
487        eprintln!("       greppy trace --dead --stats  (composable!)");
488        eprintln!("Run 'greppy trace --help' for more options.");
489        return Err(Error::SearchError {
490            message: "No symbol or operation specified".to_string(),
491        });
492    }
493
494    let multi_op = operations.len() > 1;
495    let summary_mode = args.summary;
496    let json_multi_op = args.json && multi_op;
497
498    // For JSON multi-op mode, collect results into combined struct
499    let mut combined = CombinedResults::default();
500
501    // Execute each operation
502    for (i, operation) in operations.iter().enumerate() {
503        // Print section header for multi-operation mode or summary mode (not for JSON)
504        if (multi_op || summary_mode) && !json_multi_op {
505            if i > 0 {
506                println!();
507            }
508            let header = operation_header(operation);
509            println!("{}", "═".repeat(79));
510            println!("{}", header);
511            println!("{}", "═".repeat(79));
512        }
513
514        match operation {
515            TraceOperation::Trace(symbol) => {
516                info!(symbol = %symbol, "Tracing symbol invocations");
517                let result =
518                    trace_symbol_cmd(&project, symbol, args.max_depth, args.direct, &filter)
519                        .await?;
520                if json_multi_op {
521                    combined.trace = Some(result);
522                } else if summary_mode {
523                    println!(
524                        "  Paths: {}  Entry points: {}",
525                        result.invocation_paths.len(),
526                        result.entry_points
527                    );
528                } else {
529                    println!("{}", formatter.format_trace(&result));
530                }
531            }
532            TraceOperation::Refs { symbol, kind } => {
533                info!(symbol = %symbol, ?kind, "Finding references");
534                let result = find_refs_cmd(&project, symbol, *kind, &args, &filter).await?;
535                if json_multi_op {
536                    combined.refs = Some(result);
537                } else if args.count || summary_mode {
538                    println!(
539                        "  References: {}  Files: {}",
540                        result.total_refs,
541                        result.by_file.len()
542                    );
543                } else {
544                    println!("{}", formatter.format_refs(&result));
545                }
546            }
547            TraceOperation::Callers(symbol) => {
548                info!(symbol = %symbol, "Finding callers");
549                let result = find_callers_cmd(&project, symbol, args.max_depth, &filter).await?;
550                if json_multi_op {
551                    combined.callers = Some(result);
552                } else if summary_mode {
553                    println!(
554                        "  Callers: {}  Paths: {}",
555                        result.entry_points,
556                        result.invocation_paths.len()
557                    );
558                } else {
559                    println!("{}", formatter.format_trace(&result));
560                }
561            }
562            TraceOperation::Callees(symbol) => {
563                info!(symbol = %symbol, "Finding callees");
564                let result = find_callees_cmd(&project, symbol, args.max_depth, &filter).await?;
565                if json_multi_op {
566                    combined.callees = Some(result);
567                } else if summary_mode {
568                    println!("  Callees: {}", result.invocation_paths.len());
569                } else {
570                    println!("{}", formatter.format_trace(&result));
571                }
572            }
573            TraceOperation::Type(type_name) => {
574                info!(type_name = %type_name, "Tracing type usage");
575                let result = find_refs_cmd(
576                    &project,
577                    type_name,
578                    Some(ReferenceKind::TypeAnnotation),
579                    &args,
580                    &filter,
581                )
582                .await?;
583                if json_multi_op {
584                    combined.type_usage = Some(result);
585                } else if summary_mode {
586                    println!("  Type usages: {}", result.total_refs);
587                } else {
588                    println!("{}", formatter.format_refs(&result));
589                }
590            }
591            TraceOperation::Module(module) => {
592                info!(module = %module, "Tracing module");
593                let result = trace_module_cmd(&project, module, &filter).await?;
594                if json_multi_op {
595                    combined.module = Some(result);
596                } else if summary_mode {
597                    println!(
598                        "  Exports: {}  Imported by: {}  Deps: {}",
599                        result.exports.len(),
600                        result.imported_by.len(),
601                        result.dependencies.len()
602                    );
603                } else {
604                    println!("{}", formatter.format_module(&result));
605                }
606            }
607            TraceOperation::Pattern(pattern) => {
608                info!(pattern = %pattern, "Tracing pattern");
609                let result = trace_pattern_cmd(&project, pattern, &args, &filter).await?;
610                if json_multi_op {
611                    combined.pattern = Some(result);
612                } else if summary_mode {
613                    println!(
614                        "  Matches: {}  Files: {}",
615                        result.total_matches,
616                        result.by_file.len()
617                    );
618                } else {
619                    println!("{}", formatter.format_pattern(&result));
620                }
621            }
622            TraceOperation::Flow(symbol) => {
623                info!(symbol = %symbol, "Tracing data flow");
624                let result = trace_flow_cmd(&project, symbol, &args, &filter).await?;
625                if json_multi_op {
626                    combined.flow = Some(result);
627                } else if summary_mode {
628                    let total_steps: usize = result.flow_paths.iter().map(|p| p.len()).sum();
629                    println!(
630                        "  Flow paths: {}  Steps: {}",
631                        result.flow_paths.len(),
632                        total_steps
633                    );
634                } else {
635                    println!("{}", formatter.format_flow(&result));
636                }
637            }
638            TraceOperation::Impact(symbol) => {
639                info!(symbol = %symbol, "Analyzing impact");
640                let result = analyze_impact_cmd(&project, symbol, args.max_depth, &filter).await?;
641                if json_multi_op {
642                    combined.impact = Some(result);
643                } else if summary_mode {
644                    println!(
645                        "  Direct callers: {}  Transitive: {}  Entry points: {}  Risk: {:?}",
646                        result.direct_callers.len(),
647                        result.transitive_callers.len(),
648                        result.affected_entry_points.len(),
649                        result.risk_level
650                    );
651                } else {
652                    println!("{}", formatter.format_impact(&result));
653                }
654            }
655            TraceOperation::Scope(location) => {
656                info!(location = %location, "Analyzing scope");
657                let result = analyze_scope_cmd(&project, location, &filter).await?;
658                if json_multi_op {
659                    combined.scope = Some(result);
660                } else if summary_mode {
661                    println!(
662                        "  Scope: {}  Variables: {}  Imports: {}",
663                        result.enclosing_scope.as_deref().unwrap_or("global"),
664                        result.local_variables.len(),
665                        result.imports.len()
666                    );
667                } else {
668                    println!("{}", formatter.format_scope(&result));
669                }
670            }
671            TraceOperation::DeadCode => {
672                info!("Finding dead code");
673                let result = find_dead_code_cmd(&project, args.limit, &filter, args.xref).await?;
674                if json_multi_op {
675                    combined.dead_code = Some(result);
676                } else if args.count || summary_mode {
677                    let kinds: Vec<_> = result
678                        .by_kind
679                        .iter()
680                        .map(|(k, v)| format!("{}={}", k, v))
681                        .collect();
682                    println!(
683                        "  Dead symbols: {}  ({})",
684                        result.total_dead,
685                        kinds.join(", ")
686                    );
687                } else {
688                    println!("{}", formatter.format_dead_code(&result));
689                }
690            }
691            TraceOperation::Stats => {
692                info!("Computing statistics");
693                let result = compute_stats_cmd(&project, &filter).await?;
694                if json_multi_op {
695                    combined.stats = Some(result);
696                } else if summary_mode {
697                    println!(
698                        "  Files: {}  Symbols: {}  Refs: {}  Edges: {}",
699                        result.total_files,
700                        result.total_symbols,
701                        result.total_references,
702                        result.total_edges
703                    );
704                } else {
705                    println!("{}", formatter.format_stats(&result));
706                }
707            }
708            TraceOperation::Cycles => {
709                info!("Finding circular dependencies");
710                let result = find_cycles_cmd(&project, &filter).await?;
711                if json_multi_op {
712                    combined.cycles = Some(result);
713                } else if summary_mode {
714                    println!("  Circular deps: {}", result.circular_deps.len());
715                } else {
716                    println!("{}", formatter.format_module(&result));
717                }
718            }
719        }
720    }
721
722    // Output combined JSON for multi-op JSON mode
723    if json_multi_op {
724        println!(
725            "{}",
726            serde_json::to_string_pretty(&combined)
727                .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
728        );
729    }
730
731    Ok(())
732}
733
734/// Generate header text for an operation in multi-op mode
735fn operation_header(op: &TraceOperation) -> String {
736    match op {
737        TraceOperation::Trace(s) => format!("TRACE: {}", s),
738        TraceOperation::Refs { symbol, kind } => {
739            if let Some(k) = kind {
740                format!("REFERENCES: {} ({:?})", symbol, k)
741            } else {
742                format!("REFERENCES: {}", symbol)
743            }
744        }
745        TraceOperation::Callers(s) => format!("CALLERS: {}", s),
746        TraceOperation::Callees(s) => format!("CALLEES: {}", s),
747        TraceOperation::Type(s) => format!("TYPE USAGE: {}", s),
748        TraceOperation::Module(s) => format!("MODULE: {}", s),
749        TraceOperation::Pattern(s) => format!("PATTERN: {}", s),
750        TraceOperation::Flow(s) => format!("DATA FLOW: {}", s),
751        TraceOperation::Impact(s) => format!("IMPACT ANALYSIS: {}", s),
752        TraceOperation::Scope(s) => format!("SCOPE: {}", s),
753        TraceOperation::DeadCode => "DEAD CODE ANALYSIS".to_string(),
754        TraceOperation::Stats => "CODEBASE STATISTICS".to_string(),
755        TraceOperation::Cycles => "CIRCULAR DEPENDENCIES".to_string(),
756    }
757}
758
759// =============================================================================
760// INDEX LOADING
761// =============================================================================
762
763/// Load the semantic index for a project
764fn load_semantic_index(project: &Project) -> Result<SemanticIndex> {
765    let index_path = trace_index_path(&project.root);
766
767    if !trace_index_exists(&project.root) {
768        return Err(Error::IndexError {
769            message: format!(
770                "Trace index not found. Run 'greppy index' first.\nExpected at: {}",
771                index_path.display()
772            ),
773        });
774    }
775
776    load_index(&index_path).map_err(|e| Error::IndexError {
777        message: format!("Failed to load trace index: {}", e),
778    })
779}
780
781// =============================================================================
782// PHASE 1: CODE CONTEXT ENGINE
783// =============================================================================
784
785/// Get code context for a reference
786fn get_code_context(cache: &mut FileCache, file: &Path, line: u32, context_lines: u32) -> String {
787    if context_lines == 0 {
788        // Just get the single line
789        cache
790            .get_line(file, line)
791            .map(|l| l.trim().to_string())
792            .unwrap_or_else(|| format!("// line {}", line))
793    } else {
794        // Get context with surrounding lines
795        cache
796            .get_context(file, line, context_lines, context_lines)
797            .map(|ctx| ctx.format(false))
798            .unwrap_or_else(|| format!("// line {}", line))
799    }
800}
801
802// =============================================================================
803// PHASE 2: COMPLETE REFERENCE SYSTEM
804// =============================================================================
805
806/// Find references to a symbol with full context
807async fn find_refs_cmd(
808    project: &Project,
809    symbol: &str,
810    kind_filter: Option<ReferenceKind>,
811    args: &TraceArgs,
812    filter: &TraceFilter,
813) -> Result<RefsResult> {
814    debug!(symbol = %symbol, ?kind_filter, ?filter, "find_refs");
815
816    let index = load_semantic_index(project)?;
817    let mut cache = FileCache::new(&project.root);
818
819    let mut references = Vec::new();
820    let mut by_kind: HashMap<String, usize> = HashMap::new();
821    let mut by_file: HashMap<String, usize> = HashMap::new();
822
823    // Find symbol IDs matching the name
824    let symbol_ids = index.symbols_by_name(symbol).cloned().unwrap_or_default();
825
826    // Get definition location from first matching symbol
827    let defined_at = symbol_ids.first().and_then(|&id| {
828        let sym = index.symbol(id)?;
829        let file = index.file_path(sym.file_id)?;
830        Some(format!("{}:{}", file.display(), sym.start_line))
831    });
832
833    // Get symbol kind
834    let symbol_kind = symbol_ids
835        .first()
836        .and_then(|&id| index.symbol(id))
837        .map(|s| symbol_kind_str(s.symbol_kind()).to_string());
838
839    // Find all references to all matching symbols (via Reference table)
840    for &sym_id in &symbol_ids {
841        let refs = find_refs(&index, sym_id);
842
843        for ref_ctx in refs {
844            // Convert RefKind to ReferenceKind
845            let kind = match ref_ctx.reference.ref_kind() {
846                RefKind::Read => ReferenceKind::Read,
847                RefKind::Write => ReferenceKind::Write,
848                RefKind::Call => ReferenceKind::Call,
849                RefKind::TypeAnnotation => ReferenceKind::TypeAnnotation,
850                RefKind::Import => ReferenceKind::Import,
851                RefKind::Export => ReferenceKind::Export,
852                RefKind::Construction => ReferenceKind::Call, // Treat construction as call-like
853                RefKind::Inheritance | RefKind::Decorator | RefKind::Unknown => ReferenceKind::Read,
854            };
855
856            // Apply kind filter
857            if let Some(filter_kind) = kind_filter {
858                if kind != filter_kind {
859                    continue;
860                }
861            }
862
863            // Get file path
864            let file_path = index
865                .file_path(ref_ctx.file_id)
866                .map(|p| p.to_path_buf())
867                .unwrap_or_default();
868            let file = file_path.to_string_lossy().to_string();
869
870            // Apply universal filter (path, type, name)
871            if !filter.matches_path(&file) {
872                continue;
873            }
874
875            // Find enclosing symbol
876            let enclosing_symbol = find_enclosing_symbol(&index, ref_ctx.file_id, ref_ctx.line);
877
878            // Get code context
879            let context = get_code_context(&mut cache, &file_path, ref_ctx.line, args.context);
880
881            // Count by kind and file
882            *by_kind
883                .entry(reference_kind_str(kind).to_string())
884                .or_insert(0) += 1;
885            *by_file.entry(file.clone()).or_insert(0) += 1;
886
887            references.push(ReferenceInfo {
888                file,
889                line: ref_ctx.line,
890                column: ref_ctx.column,
891                kind,
892                context,
893                enclosing_symbol,
894            });
895        }
896    }
897
898    // ALWAYS search tokens by name (catches variables, params, field names)
899    if let Some(token_ids) = index.tokens_by_name(symbol) {
900        for &token_id in token_ids {
901            if let Some(token) = index.token(token_id) {
902                let file_path = index
903                    .file_path(token.file_id)
904                    .map(|p| p.to_path_buf())
905                    .unwrap_or_default();
906                let file = file_path.to_string_lossy().to_string();
907
908                // Skip if we already have this location
909                let already_have = references
910                    .iter()
911                    .any(|r| r.file == file && r.line == token.line && r.column == token.column);
912
913                if already_have {
914                    continue;
915                }
916
917                // Apply path filter
918                if let Some(ref in_path) = args.r#in {
919                    if !file.contains(&in_path.to_string_lossy().to_string()) {
920                        continue;
921                    }
922                }
923
924                let kind = match token.token_kind() {
925                    crate::trace::TokenKind::Call => ReferenceKind::Call,
926                    _ => ReferenceKind::Read,
927                };
928
929                // Apply kind filter
930                if let Some(filter_kind) = kind_filter {
931                    if kind != filter_kind {
932                        continue;
933                    }
934                }
935
936                let enclosing_symbol = find_enclosing_symbol(&index, token.file_id, token.line);
937                let context = get_code_context(&mut cache, &file_path, token.line, args.context);
938
939                *by_kind
940                    .entry(reference_kind_str(kind).to_string())
941                    .or_insert(0) += 1;
942                *by_file.entry(file.clone()).or_insert(0) += 1;
943
944                references.push(ReferenceInfo {
945                    file,
946                    line: token.line,
947                    column: token.column,
948                    kind,
949                    context,
950                    enclosing_symbol,
951                });
952            }
953        }
954    }
955
956    // Sort by file and line
957    references.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
958
959    // Apply limit
960    if let Some(limit) = args.limit {
961        references.truncate(limit);
962    }
963
964    Ok(RefsResult {
965        symbol: symbol.to_string(),
966        defined_at,
967        symbol_kind,
968        total_refs: references.len(),
969        references,
970        by_kind,
971        by_file,
972    })
973}
974
975/// Find the enclosing symbol for a given location
976fn find_enclosing_symbol(index: &SemanticIndex, file_id: u16, line: u32) -> Option<String> {
977    let mut best: Option<(&crate::trace::Symbol, u32)> = None;
978
979    for symbol in &index.symbols {
980        if symbol.file_id == file_id && symbol.start_line <= line && symbol.end_line >= line {
981            let size = symbol.end_line - symbol.start_line;
982            match best {
983                None => best = Some((symbol, size)),
984                Some((_, best_size)) if size < best_size => best = Some((symbol, size)),
985                _ => {}
986            }
987        }
988    }
989
990    best.and_then(|(sym, _)| index.symbol_name(sym).map(|s| s.to_string()))
991}
992
993// =============================================================================
994// PHASE 3: CALL GRAPH WITH FULL PRECISION
995// =============================================================================
996
997/// Find what calls a symbol (callers/incoming)
998async fn find_callers_cmd(
999    project: &Project,
1000    symbol: &str,
1001    max_depth: usize,
1002    filter: &TraceFilter,
1003) -> Result<TraceResult> {
1004    let _ = filter; // TODO: apply filter to results
1005    debug!(symbol = %symbol, "find_callers");
1006
1007    let index = load_semantic_index(project)?;
1008
1009    let symbol_ids = index.symbols_by_name(symbol).cloned().unwrap_or_default();
1010    if symbol_ids.is_empty() {
1011        return Ok(TraceResult {
1012            symbol: symbol.to_string(),
1013            defined_at: None,
1014            kind: "unknown".to_string(),
1015            invocation_paths: Vec::new(),
1016            total_paths: 0,
1017            entry_points: 0,
1018        });
1019    }
1020
1021    let mut paths = Vec::new();
1022    let mut visited = HashSet::new();
1023
1024    for &sym_id in &symbol_ids {
1025        collect_callers_recursive(
1026            &index,
1027            sym_id,
1028            &mut paths,
1029            &mut visited,
1030            Vec::new(),
1031            max_depth,
1032        );
1033    }
1034
1035    let defined_at = symbol_ids.first().and_then(|&id| {
1036        let sym = index.symbol(id)?;
1037        let file = index.file_path(sym.file_id)?;
1038        Some(format!("{}:{}", file.display(), sym.start_line))
1039    });
1040
1041    let kind = symbol_ids
1042        .first()
1043        .and_then(|&id| index.symbol(id))
1044        .map(|s| symbol_kind_str(s.symbol_kind()).to_string())
1045        .unwrap_or_else(|| "function".to_string());
1046
1047    let entry_points = paths
1048        .iter()
1049        .map(|p| &p.entry_point)
1050        .collect::<HashSet<_>>()
1051        .len();
1052
1053    Ok(TraceResult {
1054        symbol: symbol.to_string(),
1055        defined_at,
1056        kind,
1057        invocation_paths: paths.clone(),
1058        total_paths: paths.len(),
1059        entry_points,
1060    })
1061}
1062
1063fn collect_callers_recursive(
1064    index: &SemanticIndex,
1065    sym_id: u32,
1066    paths: &mut Vec<InvocationPath>,
1067    visited: &mut HashSet<u32>,
1068    current_chain: Vec<ChainStep>,
1069    max_depth: usize,
1070) {
1071    if current_chain.len() >= max_depth {
1072        return;
1073    }
1074
1075    let callers = index.callers(sym_id);
1076    if callers.is_empty() && !current_chain.is_empty() {
1077        // End of chain - record path
1078        if let Some(sym) = index.symbol(sym_id) {
1079            let name = index.symbol_name(sym).unwrap_or("<unknown>");
1080            let file = index
1081                .file_path(sym.file_id)
1082                .map(|p| p.to_string_lossy().to_string())
1083                .unwrap_or_default();
1084
1085            let mut chain = current_chain.clone();
1086            chain.push(ChainStep {
1087                symbol: name.to_string(),
1088                file: file.clone(),
1089                line: sym.start_line,
1090                column: None,
1091                context: None,
1092            });
1093
1094            paths.push(InvocationPath {
1095                entry_point: format!("{} ({})", name, file),
1096                entry_kind: symbol_kind_str(sym.symbol_kind()).to_string(),
1097                chain,
1098            });
1099        }
1100        return;
1101    }
1102
1103    for &caller_id in callers {
1104        if visited.contains(&caller_id) {
1105            continue;
1106        }
1107        visited.insert(caller_id);
1108
1109        if let Some(caller) = index.symbol(caller_id) {
1110            let name = index.symbol_name(caller).unwrap_or("<unknown>");
1111            let file = index
1112                .file_path(caller.file_id)
1113                .map(|p| p.to_string_lossy().to_string())
1114                .unwrap_or_default();
1115
1116            // Find call line from edge
1117            let call_line = index
1118                .edges
1119                .iter()
1120                .find(|e| e.from_symbol == caller_id && e.to_symbol == sym_id)
1121                .map(|e| e.line)
1122                .unwrap_or(caller.start_line);
1123
1124            let mut new_chain = current_chain.clone();
1125            new_chain.push(ChainStep {
1126                symbol: name.to_string(),
1127                file,
1128                line: call_line,
1129                column: None,
1130                context: None,
1131            });
1132
1133            collect_callers_recursive(index, caller_id, paths, visited, new_chain, max_depth);
1134        }
1135    }
1136}
1137
1138/// Find what a symbol calls (callees/outgoing)
1139async fn find_callees_cmd(
1140    project: &Project,
1141    symbol: &str,
1142    max_depth: usize,
1143    filter: &TraceFilter,
1144) -> Result<TraceResult> {
1145    let _ = filter; // TODO: apply filter to results
1146    debug!(symbol = %symbol, "find_callees");
1147
1148    let index = load_semantic_index(project)?;
1149
1150    let symbol_ids = index.symbols_by_name(symbol).cloned().unwrap_or_default();
1151    if symbol_ids.is_empty() {
1152        return Ok(TraceResult {
1153            symbol: symbol.to_string(),
1154            defined_at: None,
1155            kind: "unknown".to_string(),
1156            invocation_paths: Vec::new(),
1157            total_paths: 0,
1158            entry_points: 0,
1159        });
1160    }
1161
1162    let mut paths = Vec::new();
1163
1164    for &sym_id in &symbol_ids {
1165        let mut visited = HashSet::new();
1166        collect_callees_recursive(
1167            &index,
1168            sym_id,
1169            &mut paths,
1170            &mut visited,
1171            Vec::new(),
1172            max_depth,
1173        );
1174    }
1175
1176    let defined_at = symbol_ids.first().and_then(|&id| {
1177        let sym = index.symbol(id)?;
1178        let file = index.file_path(sym.file_id)?;
1179        Some(format!("{}:{}", file.display(), sym.start_line))
1180    });
1181
1182    let kind = symbol_ids
1183        .first()
1184        .and_then(|&id| index.symbol(id))
1185        .map(|s| symbol_kind_str(s.symbol_kind()).to_string())
1186        .unwrap_or_else(|| "function".to_string());
1187
1188    Ok(TraceResult {
1189        symbol: symbol.to_string(),
1190        defined_at,
1191        kind,
1192        invocation_paths: paths.clone(),
1193        total_paths: paths.len(),
1194        entry_points: 1,
1195    })
1196}
1197
1198fn collect_callees_recursive(
1199    index: &SemanticIndex,
1200    sym_id: u32,
1201    paths: &mut Vec<InvocationPath>,
1202    visited: &mut HashSet<u32>,
1203    current_chain: Vec<ChainStep>,
1204    max_depth: usize,
1205) {
1206    if current_chain.len() >= max_depth {
1207        return;
1208    }
1209
1210    visited.insert(sym_id);
1211
1212    let callees = index.callees(sym_id);
1213
1214    // Add current symbol to chain
1215    let mut chain = current_chain.clone();
1216    if let Some(sym) = index.symbol(sym_id) {
1217        let name = index.symbol_name(sym).unwrap_or("<unknown>");
1218        let file = index
1219            .file_path(sym.file_id)
1220            .map(|p| p.to_string_lossy().to_string())
1221            .unwrap_or_default();
1222
1223        chain.push(ChainStep {
1224            symbol: name.to_string(),
1225            file: file.clone(),
1226            line: sym.start_line,
1227            column: None,
1228            context: None,
1229        });
1230
1231        if callees.is_empty() && !chain.is_empty() {
1232            // End of chain
1233            paths.push(InvocationPath {
1234                entry_point: chain.first().map(|c| c.symbol.clone()).unwrap_or_default(),
1235                entry_kind: symbol_kind_str(sym.symbol_kind()).to_string(),
1236                chain: chain.clone(),
1237            });
1238            return;
1239        }
1240    }
1241
1242    for &callee_id in callees {
1243        if visited.contains(&callee_id) {
1244            continue;
1245        }
1246        collect_callees_recursive(index, callee_id, paths, visited, chain.clone(), max_depth);
1247    }
1248}
1249
1250// =============================================================================
1251// PHASE 4: IMPACT ANALYSIS (REAL DATA)
1252// =============================================================================
1253
1254/// Analyze impact of changing a symbol
1255async fn analyze_impact_cmd(
1256    project: &Project,
1257    symbol: &str,
1258    max_depth: usize,
1259    filter: &TraceFilter,
1260) -> Result<ImpactResult> {
1261    let _ = filter; // TODO: apply filter to results
1262    debug!(symbol = %symbol, "analyze_impact");
1263
1264    let index = load_semantic_index(project)?;
1265
1266    // Parse file:symbol format if present
1267    let sym_name = if symbol.contains(':') {
1268        symbol.splitn(2, ':').nth(1).unwrap_or(symbol)
1269    } else {
1270        symbol
1271    };
1272
1273    let symbol_ids = index.symbols_by_name(sym_name).cloned().unwrap_or_default();
1274
1275    if symbol_ids.is_empty() {
1276        return Ok(ImpactResult {
1277            symbol: symbol.to_string(),
1278            file: String::new(),
1279            defined_at: None,
1280            direct_callers: Vec::new(),
1281            direct_caller_count: 0,
1282            transitive_callers: Vec::new(),
1283            transitive_caller_count: 0,
1284            affected_entry_points: Vec::new(),
1285            files_affected: Vec::new(),
1286            risk_level: RiskLevel::Low,
1287        });
1288    }
1289
1290    let first_id = symbol_ids[0];
1291    let defined_at = index.symbol(first_id).and_then(|sym| {
1292        let file = index.file_path(sym.file_id)?;
1293        Some(format!("{}:{}", file.display(), sym.start_line))
1294    });
1295
1296    let file = index
1297        .symbol(first_id)
1298        .and_then(|s| index.file_path(s.file_id))
1299        .map(|p| p.to_string_lossy().to_string())
1300        .unwrap_or_default();
1301
1302    // Collect direct callers (deduplicated)
1303    let mut direct_callers_set = HashSet::new();
1304    let mut direct_caller_files = HashSet::new();
1305
1306    for &sym_id in &symbol_ids {
1307        for &caller_id in index.callers(sym_id) {
1308            if let Some(caller) = index.symbol(caller_id) {
1309                let name = index.symbol_name(caller).unwrap_or("<unknown>");
1310                let caller_file = index
1311                    .file_path(caller.file_id)
1312                    .map(|p| p.to_string_lossy().to_string())
1313                    .unwrap_or_default();
1314
1315                let caller_str = format!("{} ({}:{})", name, caller_file, caller.start_line);
1316                direct_callers_set.insert(caller_str);
1317                direct_caller_files.insert(caller_file);
1318            }
1319        }
1320    }
1321    let direct_callers: Vec<_> = direct_callers_set.into_iter().collect();
1322
1323    // Collect transitive callers via BFS (deduplicated)
1324    let mut transitive_callers_set = HashSet::new();
1325    let mut visited = HashSet::new();
1326    let mut queue: Vec<(u32, usize)> = symbol_ids.iter().map(|&id| (id, 0)).collect();
1327    let mut affected_entry_points_set = HashSet::new();
1328    let mut all_files = HashSet::new();
1329
1330    while let Some((current, depth)) = queue.pop() {
1331        if depth > max_depth || visited.contains(&current) {
1332            continue;
1333        }
1334        visited.insert(current);
1335
1336        if let Some(sym) = index.symbol(current) {
1337            let sym_file = index
1338                .file_path(sym.file_id)
1339                .map(|p| p.to_string_lossy().to_string())
1340                .unwrap_or_default();
1341            all_files.insert(sym_file.clone());
1342
1343            if sym.is_entry_point() {
1344                let name = index.symbol_name(sym).unwrap_or("<unknown>");
1345                affected_entry_points_set.insert(format!("{} ({})", name, sym_file));
1346            }
1347
1348            if depth > 1 {
1349                let name = index.symbol_name(sym).unwrap_or("<unknown>");
1350                transitive_callers_set.insert(format!("{} (depth {})", name, depth));
1351            }
1352        }
1353
1354        for &caller_id in index.callers(current) {
1355            if !visited.contains(&caller_id) {
1356                queue.push((caller_id, depth + 1));
1357            }
1358        }
1359    }
1360    let transitive_callers: Vec<_> = transitive_callers_set.into_iter().collect();
1361    let affected_entry_points: Vec<_> = affected_entry_points_set.into_iter().collect();
1362
1363    // Determine risk level
1364    let risk_level = if affected_entry_points.len() > 10 || all_files.len() > 50 {
1365        RiskLevel::Critical
1366    } else if affected_entry_points.len() > 5 || all_files.len() > 20 {
1367        RiskLevel::High
1368    } else if direct_callers.len() > 5 || all_files.len() > 5 {
1369        RiskLevel::Medium
1370    } else {
1371        RiskLevel::Low
1372    };
1373
1374    Ok(ImpactResult {
1375        symbol: symbol.to_string(),
1376        file,
1377        defined_at,
1378        direct_callers: direct_callers.clone(),
1379        direct_caller_count: direct_callers.len(),
1380        transitive_callers: transitive_callers.clone(),
1381        transitive_caller_count: transitive_callers.len(),
1382        affected_entry_points,
1383        files_affected: all_files.into_iter().collect(),
1384        risk_level,
1385    })
1386}
1387
1388// =============================================================================
1389// PHASE 5: TYPE TRACING (handled by refs with TypeAnnotation filter)
1390// =============================================================================
1391
1392// Type tracing is implemented via find_refs_cmd with ReferenceKind::TypeAnnotation
1393
1394// =============================================================================
1395// PHASE 6: MODULE TRACING
1396// =============================================================================
1397
1398/// Trace module imports/exports
1399async fn trace_module_cmd(
1400    project: &Project,
1401    module: &str,
1402    filter: &TraceFilter,
1403) -> Result<ModuleResult> {
1404    let _ = filter; // TODO: apply filter to results
1405    debug!(module = %module, "trace_module");
1406
1407    let index = load_semantic_index(project)?;
1408
1409    // Find files that match the module pattern
1410    let module_files: Vec<_> = index
1411        .files
1412        .iter()
1413        .enumerate()
1414        .filter(|(_, path)| path.to_string_lossy().contains(module))
1415        .collect();
1416
1417    let mut exports = Vec::new();
1418    let mut imported_by = Vec::new();
1419    let mut dependencies = Vec::new();
1420
1421    for (file_id, _file_path) in &module_files {
1422        // Find exported symbols from this file
1423        for symbol in index.symbols_in_file(*file_id as u16) {
1424            if symbol.is_exported() {
1425                let name = index.symbol_name(symbol).unwrap_or("<unknown>");
1426                exports.push(format!(
1427                    "{} ({})",
1428                    name,
1429                    symbol_kind_str(symbol.symbol_kind())
1430                ));
1431            }
1432        }
1433
1434        // Find imports of this module (tokens with Import kind referencing this file)
1435        // This is approximate - we look for symbols from this file being referenced elsewhere
1436        for symbol in index.symbols_in_file(*file_id as u16) {
1437            for reference in index.references_to(symbol.id) {
1438                if reference.ref_kind() == RefKind::Import {
1439                    if let Some(token) = index.token(reference.token_id) {
1440                        if token.file_id != *file_id as u16 {
1441                            let importer_file = index
1442                                .file_path(token.file_id)
1443                                .map(|p| p.to_string_lossy().to_string())
1444                                .unwrap_or_default();
1445                            let name = index.symbol_name(symbol).unwrap_or("<unknown>");
1446                            imported_by.push(format!("{} imports {}", importer_file, name));
1447                        }
1448                    }
1449                }
1450            }
1451        }
1452    }
1453
1454    // Find what this module depends on (imports from other modules)
1455    for (file_id, _) in &module_files {
1456        for token in index.tokens_in_file(*file_id as u16) {
1457            if token.token_kind() == crate::trace::TokenKind::Import {
1458                let name = index.token_name(token).unwrap_or("<unknown>");
1459                if !dependencies.contains(&name.to_string()) {
1460                    dependencies.push(name.to_string());
1461                }
1462            }
1463        }
1464    }
1465
1466    let file_path = module_files
1467        .first()
1468        .map(|(_, p)| p.to_string_lossy().to_string())
1469        .unwrap_or_else(|| module.to_string());
1470
1471    Ok(ModuleResult {
1472        module: module.to_string(),
1473        file_path,
1474        exports,
1475        imported_by,
1476        dependencies,
1477        circular_deps: Vec::new(), // Populated by cycles command
1478    })
1479}
1480
1481/// Find circular dependencies
1482async fn find_cycles_cmd(project: &Project, filter: &TraceFilter) -> Result<ModuleResult> {
1483    debug!("find_cycles filter={:?}", filter);
1484
1485    let index = load_semantic_index(project)?;
1486
1487    // Helper to check if file passes filter
1488    let file_passes = |file_id: u16| -> bool {
1489        if let Some(path) = index.file_path(file_id) {
1490            filter.matches_path(&path.to_string_lossy())
1491        } else {
1492            false
1493        }
1494    };
1495
1496    // Build file dependency graph (filtered)
1497    let mut file_deps: HashMap<u16, HashSet<u16>> = HashMap::new();
1498
1499    for edge in &index.edges {
1500        if let (Some(from_sym), Some(to_sym)) =
1501            (index.symbol(edge.from_symbol), index.symbol(edge.to_symbol))
1502        {
1503            if from_sym.file_id != to_sym.file_id {
1504                // Only include if at least one file passes the filter (or no filter)
1505                let from_passes = file_passes(from_sym.file_id);
1506                let to_passes = file_passes(to_sym.file_id);
1507
1508                if from_passes || to_passes || filter.path.is_none() {
1509                    file_deps
1510                        .entry(from_sym.file_id)
1511                        .or_default()
1512                        .insert(to_sym.file_id);
1513                }
1514            }
1515        }
1516    }
1517
1518    // Find cycles using DFS
1519    let mut cycles = Vec::new();
1520    let mut visited = HashSet::new();
1521    let mut rec_stack = HashSet::new();
1522    let mut path = Vec::new();
1523
1524    for &file_id in file_deps.keys() {
1525        find_cycles_dfs(
1526            file_id,
1527            &file_deps,
1528            &mut visited,
1529            &mut rec_stack,
1530            &mut path,
1531            &mut cycles,
1532            &index,
1533            filter,
1534        );
1535    }
1536
1537    Ok(ModuleResult {
1538        module: "Circular Dependencies".to_string(),
1539        file_path: String::new(),
1540        exports: Vec::new(),
1541        imported_by: Vec::new(),
1542        dependencies: Vec::new(),
1543        circular_deps: cycles,
1544    })
1545}
1546
1547fn find_cycles_dfs(
1548    node: u16,
1549    graph: &HashMap<u16, HashSet<u16>>,
1550    visited: &mut HashSet<u16>,
1551    rec_stack: &mut HashSet<u16>,
1552    path: &mut Vec<u16>,
1553    cycles: &mut Vec<String>,
1554    index: &SemanticIndex,
1555    filter: &TraceFilter,
1556) {
1557    visited.insert(node);
1558    rec_stack.insert(node);
1559    path.push(node);
1560
1561    if let Some(neighbors) = graph.get(&node) {
1562        for &neighbor in neighbors {
1563            if !visited.contains(&neighbor) {
1564                find_cycles_dfs(
1565                    neighbor, graph, visited, rec_stack, path, cycles, index, filter,
1566                );
1567            } else if rec_stack.contains(&neighbor) {
1568                // Found a cycle
1569                let cycle_start = path.iter().position(|&n| n == neighbor).unwrap_or(0);
1570                let cycle_path: Vec<_> = path[cycle_start..]
1571                    .iter()
1572                    .filter_map(|&fid| {
1573                        index
1574                            .file_path(fid)
1575                            .map(|p| p.to_string_lossy().to_string())
1576                    })
1577                    .collect();
1578
1579                if !cycle_path.is_empty() {
1580                    // Only include cycle if at least one file in the cycle passes the filter
1581                    let cycle_passes =
1582                        filter.path.is_none() || cycle_path.iter().any(|p| filter.matches_path(p));
1583
1584                    if cycle_passes {
1585                        cycles.push(cycle_path.join(" -> ") + " -> " + &cycle_path[0]);
1586                    }
1587                }
1588            }
1589        }
1590    }
1591
1592    path.pop();
1593    rec_stack.remove(&node);
1594}
1595
1596// =============================================================================
1597// PHASE 7: DATA FLOW TRACING
1598// =============================================================================
1599
1600/// Trace data flow for a variable
1601async fn trace_flow_cmd(
1602    project: &Project,
1603    symbol: &str,
1604    args: &TraceArgs,
1605    filter: &TraceFilter,
1606) -> Result<FlowResult> {
1607    debug!(symbol = %symbol, "trace_flow filter={:?}", filter);
1608
1609    let index = load_semantic_index(project)?;
1610    let mut cache = FileCache::new(&project.root);
1611
1612    let mut flow_paths = Vec::new();
1613    let mut current_path = Vec::new();
1614
1615    // Find all tokens with this name
1616    if let Some(token_ids) = index.tokens_by_name(symbol) {
1617        // Group by file and sort by line
1618        let mut by_file: HashMap<u16, Vec<&crate::trace::Token>> = HashMap::new();
1619        for &token_id in token_ids {
1620            if let Some(token) = index.token(token_id) {
1621                // Apply path filter
1622                if let Some(path) = index.file_path(token.file_id) {
1623                    if !filter.matches_path(&path.to_string_lossy()) {
1624                        continue;
1625                    }
1626                }
1627                by_file.entry(token.file_id).or_default().push(token);
1628            }
1629        }
1630
1631        for (file_id, mut tokens) in by_file {
1632            tokens.sort_by_key(|t| (t.line, t.column));
1633
1634            let file_path = index
1635                .file_path(file_id)
1636                .map(|p| p.to_path_buf())
1637                .unwrap_or_default();
1638            let file = file_path.to_string_lossy().to_string();
1639
1640            for token in tokens {
1641                // Map token kinds to flow actions
1642                // Note: TokenKind doesn't have Assignment/Return, so we infer from context
1643                let action = match token.token_kind() {
1644                    crate::trace::TokenKind::Call => FlowAction::PassToFunction,
1645                    crate::trace::TokenKind::Property => FlowAction::Read,
1646                    crate::trace::TokenKind::Identifier => FlowAction::Read, // Could be assign or read
1647                    _ => FlowAction::Read,
1648                };
1649
1650                // First occurrence is considered a definition
1651                let actual_action = if current_path.is_empty() {
1652                    FlowAction::Define
1653                } else {
1654                    action
1655                };
1656
1657                let expression = get_code_context(&mut cache, &file_path, token.line, 0);
1658
1659                current_path.push(FlowStep {
1660                    variable: symbol.to_string(),
1661                    action: actual_action,
1662                    file: file.clone(),
1663                    line: token.line,
1664                    expression,
1665                });
1666            }
1667        }
1668    }
1669
1670    if !current_path.is_empty() {
1671        flow_paths.push(current_path);
1672    }
1673
1674    // Apply limit
1675    if let Some(limit) = args.limit {
1676        for path in &mut flow_paths {
1677            path.truncate(limit);
1678        }
1679    }
1680
1681    Ok(FlowResult {
1682        symbol: symbol.to_string(),
1683        flow_paths,
1684    })
1685}
1686
1687// =============================================================================
1688// PHASE 8: PATTERN SEARCH
1689// =============================================================================
1690
1691/// Search for regex pattern in codebase
1692async fn trace_pattern_cmd(
1693    project: &Project,
1694    pattern: &str,
1695    args: &TraceArgs,
1696    filter: &TraceFilter,
1697) -> Result<PatternResult> {
1698    debug!(pattern = %pattern, "trace_pattern filter={:?}", filter);
1699
1700    let regex = Regex::new(pattern).map_err(|e| Error::SearchError {
1701        message: format!("Invalid regex pattern: {}", e),
1702    })?;
1703
1704    let index = load_semantic_index(project)?;
1705    let mut cache = FileCache::new(&project.root);
1706
1707    let mut matches = Vec::new();
1708
1709    for (file_id, file_path) in index.files.iter().enumerate() {
1710        // Apply universal path filter
1711        let file_str = file_path.to_string_lossy();
1712        if !filter.matches_path(&file_str) {
1713            continue;
1714        }
1715
1716        // Read file and search
1717        if let Some(line_count) = cache.line_count(file_path) {
1718            for line_num in 1..=line_count as u32 {
1719                if let Some(line_content) = cache.get_line(file_path, line_num) {
1720                    if let Some(mat) = regex.find(&line_content) {
1721                        let context = if args.context > 0 {
1722                            cache
1723                                .get_context(file_path, line_num, args.context, args.context)
1724                                .map(|ctx| ctx.format(false))
1725                                .unwrap_or_else(|| line_content.clone())
1726                        } else {
1727                            line_content.trim().to_string()
1728                        };
1729
1730                        let enclosing = find_enclosing_symbol(&index, file_id as u16, line_num);
1731
1732                        matches.push(PatternMatch {
1733                            file: file_path.to_string_lossy().to_string(),
1734                            line: line_num,
1735                            column: mat.start() as u16,
1736                            matched_text: mat.as_str().to_string(),
1737                            context,
1738                            enclosing_symbol: enclosing,
1739                        });
1740
1741                        // Apply limit
1742                        if let Some(limit) = args.limit {
1743                            if matches.len() >= limit {
1744                                break;
1745                            }
1746                        }
1747                    }
1748                }
1749            }
1750        }
1751
1752        // Check limit
1753        if let Some(limit) = args.limit {
1754            if matches.len() >= limit {
1755                break;
1756            }
1757        }
1758    }
1759
1760    // Count by file
1761    let mut by_file: HashMap<String, usize> = HashMap::new();
1762    for m in &matches {
1763        *by_file.entry(m.file.clone()).or_insert(0) += 1;
1764    }
1765
1766    Ok(PatternResult {
1767        pattern: pattern.to_string(),
1768        total_matches: matches.len(),
1769        matches,
1770        by_file,
1771    })
1772}
1773
1774// =============================================================================
1775// PHASE 9: SCOPE ANALYSIS
1776// =============================================================================
1777
1778/// Analyze scope at a specific location
1779async fn analyze_scope_cmd(
1780    project: &Project,
1781    location: &str,
1782    filter: &TraceFilter,
1783) -> Result<ScopeResult> {
1784    debug!(location = %location, "analyze_scope");
1785    let _ = filter; // TODO: Apply filter to scope analysis
1786
1787    // Parse file:line format
1788    let parts: Vec<&str> = location.rsplitn(2, ':').collect();
1789    if parts.len() != 2 {
1790        return Err(Error::SearchError {
1791            message: "Location must be in format file:line".to_string(),
1792        });
1793    }
1794
1795    let line: u32 = parts[0].parse().map_err(|_| Error::SearchError {
1796        message: "Invalid line number".to_string(),
1797    })?;
1798    let file_pattern = parts[1];
1799
1800    let index = load_semantic_index(project)?;
1801
1802    // Find the file
1803    let file_id = index
1804        .files
1805        .iter()
1806        .enumerate()
1807        .find(|(_, p)| p.to_string_lossy().contains(file_pattern))
1808        .map(|(id, _)| id as u16);
1809
1810    let file_id = match file_id {
1811        Some(id) => id,
1812        None => {
1813            return Err(Error::SearchError {
1814                message: format!("File not found: {}", file_pattern),
1815            });
1816        }
1817    };
1818
1819    let file_path = index
1820        .file_path(file_id)
1821        .map(|p| p.to_string_lossy().to_string())
1822        .unwrap_or_default();
1823
1824    // Find enclosing scope
1825    let enclosing_scope = find_enclosing_symbol(&index, file_id, line);
1826
1827    // Find local variables (symbols in the same scope that are defined before this line)
1828    let mut local_variables = Vec::new();
1829    let mut parameters = Vec::new();
1830    let mut imports = Vec::new();
1831
1832    for symbol in index.symbols_in_file(file_id) {
1833        let name = index.symbol_name(symbol).unwrap_or("<unknown>");
1834        let kind = symbol_kind_str(symbol.symbol_kind());
1835
1836        // Check if this symbol is in scope at the given line
1837        if symbol.start_line <= line && symbol.end_line >= line {
1838            // This is the enclosing function/class
1839            continue;
1840        }
1841
1842        if symbol.start_line < line && symbol.end_line < line {
1843            // Symbol defined before the line
1844            match symbol.symbol_kind() {
1845                SymbolKind::Variable | SymbolKind::Constant => {
1846                    local_variables.push(ScopeVariable {
1847                        name: name.to_string(),
1848                        kind: kind.to_string(),
1849                        defined_at: symbol.start_line,
1850                    });
1851                }
1852                SymbolKind::Function | SymbolKind::Method => {
1853                    // Could be a parameter if inside a function
1854                    if let Some(ref scope) = enclosing_scope {
1855                        if name != scope {
1856                            parameters.push(ScopeVariable {
1857                                name: name.to_string(),
1858                                kind: kind.to_string(),
1859                                defined_at: symbol.start_line,
1860                            });
1861                        }
1862                    }
1863                }
1864                _ => {}
1865            }
1866        }
1867    }
1868
1869    // Find imports (tokens with Import kind in this file)
1870    for token in index.tokens_in_file(file_id) {
1871        if token.token_kind() == crate::trace::TokenKind::Import {
1872            let name = index.token_name(token).unwrap_or("<unknown>");
1873            imports.push(name.to_string());
1874        }
1875    }
1876
1877    Ok(ScopeResult {
1878        file: file_path,
1879        line,
1880        enclosing_scope,
1881        local_variables,
1882        parameters,
1883        imports,
1884    })
1885}
1886
1887// =============================================================================
1888// PHASE 10: STATISTICS
1889// =============================================================================
1890
1891/// Compute codebase statistics
1892async fn compute_stats_cmd(project: &Project, filter: &TraceFilter) -> Result<StatsResult> {
1893    debug!("compute_stats");
1894
1895    let index = load_semantic_index(project)?;
1896    let stats = index.stats();
1897
1898    // Helper to check if a file passes the filter
1899    let file_passes = |file_id: u16| -> bool {
1900        if let Some(path) = index.file_path(file_id) {
1901            filter.matches_path(&path.to_string_lossy())
1902        } else {
1903            false
1904        }
1905    };
1906
1907    // Helper to check if a symbol passes the filter
1908    let symbol_passes = |symbol: &crate::trace::Symbol| -> bool {
1909        if let Some(path) = index.file_path(symbol.file_id) {
1910            let name = index.symbol_name(symbol).unwrap_or("");
1911            let kind = symbol_kind_str(symbol.symbol_kind());
1912            filter.matches_symbol(name, kind, &path.to_string_lossy())
1913        } else {
1914            false
1915        }
1916    };
1917
1918    // Count symbols by kind (filtered)
1919    let mut symbols_by_kind: HashMap<String, usize> = HashMap::new();
1920    let mut filtered_symbol_count = 0;
1921    for symbol in &index.symbols {
1922        if symbol_passes(symbol) {
1923            filtered_symbol_count += 1;
1924            *symbols_by_kind
1925                .entry(symbol_kind_str(symbol.symbol_kind()).to_string())
1926                .or_insert(0) += 1;
1927        }
1928    }
1929
1930    // Count files by extension (filtered)
1931    let mut files_by_extension: HashMap<String, usize> = HashMap::new();
1932    let mut filtered_file_count = 0;
1933    for (idx, file) in index.files.iter().enumerate() {
1934        if file_passes(idx as u16) {
1935            filtered_file_count += 1;
1936            let ext = file
1937                .extension()
1938                .map(|e| e.to_string_lossy().to_string())
1939                .unwrap_or_else(|| "unknown".to_string());
1940            *files_by_extension.entry(ext).or_insert(0) += 1;
1941        }
1942    }
1943
1944    // Find most referenced symbols (aggregated by name, filtered)
1945    let mut symbol_ref_counts: HashMap<String, usize> = HashMap::new();
1946    for s in &index.symbols {
1947        if symbol_passes(s) {
1948            if let Some(name) = index.symbol_name(s) {
1949                let ref_count = index.references_to(s.id).count();
1950                if ref_count > 0 {
1951                    *symbol_ref_counts.entry(name.to_string()).or_insert(0) += ref_count;
1952                }
1953            }
1954        }
1955    }
1956    let mut sorted_refs: Vec<_> = symbol_ref_counts.into_iter().collect();
1957    sorted_refs.sort_by(|a, b| b.1.cmp(&a.1));
1958    let most_referenced: Vec<_> = sorted_refs.into_iter().take(10).collect();
1959
1960    // Find largest files (by symbol count, filtered)
1961    let mut file_symbol_counts: HashMap<u16, usize> = HashMap::new();
1962    for symbol in &index.symbols {
1963        if file_passes(symbol.file_id) {
1964            *file_symbol_counts.entry(symbol.file_id).or_insert(0) += 1;
1965        }
1966    }
1967    let mut largest_files: Vec<_> = file_symbol_counts
1968        .into_iter()
1969        .filter_map(|(file_id, count)| {
1970            let path = index.file_path(file_id)?;
1971            Some((path.to_string_lossy().to_string(), count))
1972        })
1973        .collect();
1974    largest_files.sort_by(|a, b| b.1.cmp(&a.1));
1975    largest_files.truncate(10);
1976
1977    // Use filtered counts if filter is active, otherwise use global stats
1978    let (total_files, total_symbols) =
1979        if filter.path.is_some() || filter.symbol_type.is_some() || filter.name_pattern.is_some() {
1980            (filtered_file_count, filtered_symbol_count)
1981        } else {
1982            (stats.files, stats.symbols)
1983        };
1984
1985    // Calculate call graph stats
1986    let max_call_depth = calculate_max_call_depth(&index);
1987    let avg_call_depth = calculate_avg_call_depth(&index);
1988
1989    Ok(StatsResult {
1990        total_files,
1991        total_symbols,
1992        total_tokens: stats.tokens, // Not filtered (token-level filtering is expensive)
1993        total_references: stats.references, // Not filtered
1994        total_edges: stats.edges,   // Not filtered
1995        total_entry_points: stats.entry_points,
1996        symbols_by_kind,
1997        files_by_extension,
1998        most_referenced,
1999        largest_files,
2000        max_call_depth,
2001        avg_call_depth,
2002    })
2003}
2004
2005fn calculate_max_call_depth(index: &SemanticIndex) -> usize {
2006    let mut max_depth = 0;
2007
2008    for &entry_id in &index.entry_points {
2009        let depth = calculate_depth_from(index, entry_id, &mut HashSet::new());
2010        max_depth = max_depth.max(depth);
2011    }
2012
2013    max_depth
2014}
2015
2016fn calculate_depth_from(index: &SemanticIndex, sym_id: u32, visited: &mut HashSet<u32>) -> usize {
2017    if visited.contains(&sym_id) {
2018        return 0;
2019    }
2020    visited.insert(sym_id);
2021
2022    let callees = index.callees(sym_id);
2023    if callees.is_empty() {
2024        return 0;
2025    }
2026
2027    let max_child_depth = callees
2028        .iter()
2029        .map(|&callee| calculate_depth_from(index, callee, visited))
2030        .max()
2031        .unwrap_or(0);
2032
2033    max_child_depth + 1
2034}
2035
2036fn calculate_avg_call_depth(index: &SemanticIndex) -> f32 {
2037    if index.entry_points.is_empty() {
2038        return 0.0;
2039    }
2040
2041    let total_depth: usize = index
2042        .entry_points
2043        .iter()
2044        .map(|&id| calculate_depth_from(index, id, &mut HashSet::new()))
2045        .sum();
2046
2047    total_depth as f32 / index.entry_points.len() as f32
2048}
2049
2050// =============================================================================
2051// TRACE OPERATIONS
2052// =============================================================================
2053
2054/// Trace symbol invocation paths
2055async fn trace_symbol_cmd(
2056    project: &Project,
2057    symbol: &str,
2058    max_depth: usize,
2059    direct: bool,
2060    filter: &TraceFilter,
2061) -> Result<TraceResult> {
2062    debug!(symbol = %symbol, max_depth, direct, ?filter, "trace_symbol");
2063
2064    let index = load_semantic_index(project)?;
2065
2066    // Determine symbols to search for
2067    let symbols_to_search = if direct {
2068        vec![symbol.to_string()]
2069    } else {
2070        expand_query_with_ai(symbol).await
2071    };
2072
2073    debug!(symbols = ?symbols_to_search, "Searching for symbols");
2074
2075    // Find and trace all matching symbols
2076    let mut all_trace_results = Vec::new();
2077    for sym_name in &symbols_to_search {
2078        let trace_results = trace_symbol_by_name(&index, sym_name, Some(max_depth));
2079        all_trace_results.extend(trace_results);
2080    }
2081
2082    if all_trace_results.is_empty() {
2083        return Ok(TraceResult {
2084            symbol: symbol.to_string(),
2085            defined_at: None,
2086            kind: "unknown".to_string(),
2087            invocation_paths: Vec::new(),
2088            total_paths: 0,
2089            entry_points: 0,
2090        });
2091    }
2092
2093    // Convert traverse results to output format
2094    let mut invocation_paths = Vec::new();
2095    let mut entry_points_set = std::collections::HashSet::new();
2096
2097    for trace_result in &all_trace_results {
2098        for path in &trace_result.paths {
2099            let entry_symbol = index.symbol(path.entry_point);
2100            let entry_name = entry_symbol
2101                .and_then(|s| index.symbol_name(s))
2102                .unwrap_or("<unknown>");
2103            let entry_file = entry_symbol
2104                .and_then(|s| index.file_path(s.file_id))
2105                .map(|p| p.to_string_lossy().to_string())
2106                .unwrap_or_default();
2107            let entry_kind = entry_symbol
2108                .map(|s| symbol_kind_str(s.symbol_kind()))
2109                .unwrap_or("function");
2110
2111            entry_points_set.insert(path.entry_point);
2112
2113            let chain: Vec<ChainStep> = path
2114                .chain
2115                .iter()
2116                .enumerate()
2117                .filter_map(|(i, &sym_id)| {
2118                    let sym = index.symbol(sym_id)?;
2119                    let name = index.symbol_name(sym)?;
2120                    let file = index
2121                        .file_path(sym.file_id)
2122                        .map(|p| p.to_string_lossy().to_string())
2123                        .unwrap_or_default();
2124                    let line = if i > 0 {
2125                        path.call_lines
2126                            .get(i - 1)
2127                            .copied()
2128                            .unwrap_or(sym.start_line)
2129                    } else {
2130                        sym.start_line
2131                    };
2132
2133                    Some(ChainStep {
2134                        symbol: name.to_string(),
2135                        file,
2136                        line,
2137                        column: None,
2138                        context: None,
2139                    })
2140                })
2141                .collect();
2142
2143            if !chain.is_empty() {
2144                invocation_paths.push(InvocationPath {
2145                    entry_point: format!("{} ({})", entry_name, entry_file),
2146                    entry_kind: entry_kind.to_string(),
2147                    chain,
2148                });
2149            }
2150        }
2151    }
2152
2153    // AI reranking when not in direct mode
2154    if !direct && invocation_paths.len() > 1 {
2155        invocation_paths = rerank_paths_with_ai(symbol, invocation_paths).await;
2156    }
2157
2158    let first_target = all_trace_results.first().map(|r| r.target);
2159    let defined_at = first_target.and_then(|id| {
2160        let sym = index.symbol(id)?;
2161        let file = index.file_path(sym.file_id)?;
2162        Some(format!("{}:{}", file.display(), sym.start_line))
2163    });
2164    let kind = first_target
2165        .and_then(|id| index.symbol(id))
2166        .map(|s| symbol_kind_str(s.symbol_kind()).to_string())
2167        .unwrap_or_else(|| "function".to_string());
2168
2169    Ok(TraceResult {
2170        symbol: symbol.to_string(),
2171        defined_at,
2172        kind,
2173        invocation_paths: invocation_paths.clone(),
2174        total_paths: invocation_paths.len(),
2175        entry_points: entry_points_set.len(),
2176    })
2177}
2178
2179// =============================================================================
2180// AI ENHANCEMENT
2181// =============================================================================
2182
2183/// Expand a query into related symbol names using AI
2184async fn expand_query_with_ai(query: &str) -> Vec<String> {
2185    let providers = auth::get_authenticated_providers();
2186
2187    if providers.is_empty() {
2188        debug!("No AI provider authenticated, skipping query expansion");
2189        return vec![query.to_string()];
2190    }
2191
2192    if !is_natural_language_query(query) && query.len() > 15 {
2193        return vec![query.to_string()];
2194    }
2195
2196    let expanded = if providers.contains(&Provider::Anthropic) {
2197        match auth::get_anthropic_token() {
2198            Ok(token) => {
2199                let client = ClaudeClient::new(token);
2200                match client.expand_query(query).await {
2201                    Ok(symbols) => {
2202                        debug!(count = symbols.len(), "AI expanded query to symbols");
2203                        symbols
2204                    }
2205                    Err(e) => {
2206                        warn!("AI query expansion failed: {}", e);
2207                        vec![query.to_string()]
2208                    }
2209                }
2210            }
2211            Err(e) => {
2212                warn!("Failed to get Anthropic token: {}", e);
2213                vec![query.to_string()]
2214            }
2215        }
2216    } else if providers.contains(&Provider::Google) {
2217        match auth::get_google_token() {
2218            Ok(token) => {
2219                let client = GeminiClient::new(token);
2220                match client.expand_query(query).await {
2221                    Ok(symbols) => {
2222                        debug!(count = symbols.len(), "AI expanded query to symbols");
2223                        symbols
2224                    }
2225                    Err(e) => {
2226                        warn!("AI query expansion failed: {}", e);
2227                        vec![query.to_string()]
2228                    }
2229                }
2230            }
2231            Err(e) => {
2232                warn!("Failed to get Google token: {}", e);
2233                vec![query.to_string()]
2234            }
2235        }
2236    } else {
2237        vec![query.to_string()]
2238    };
2239
2240    let mut result = expanded;
2241    if !result.iter().any(|s| s.eq_ignore_ascii_case(query)) {
2242        result.insert(0, query.to_string());
2243    }
2244    result.truncate(10);
2245    result
2246}
2247
2248/// Rerank invocation paths by relevance using AI
2249async fn rerank_paths_with_ai(query: &str, mut paths: Vec<InvocationPath>) -> Vec<InvocationPath> {
2250    let providers = auth::get_authenticated_providers();
2251
2252    if providers.is_empty() {
2253        debug!("No AI provider authenticated, skipping reranking");
2254        return paths;
2255    }
2256
2257    if paths.len() <= 3 {
2258        return paths;
2259    }
2260
2261    let path_descriptions: Vec<String> = paths
2262        .iter()
2263        .map(|p| {
2264            let chain_str: Vec<String> = p.chain.iter().map(|c| c.symbol.clone()).collect();
2265            format!(
2266                "Entry: {} ({})\nChain: {}",
2267                p.entry_point,
2268                p.entry_kind,
2269                chain_str.join(" -> ")
2270            )
2271        })
2272        .collect();
2273
2274    let indices = if providers.contains(&Provider::Anthropic) {
2275        match auth::get_anthropic_token() {
2276            Ok(token) => {
2277                let client = ClaudeClient::new(token);
2278                match client.rerank_trace(query, &path_descriptions).await {
2279                    Ok(idx) => {
2280                        debug!(order = ?idx, "AI reranked trace paths");
2281                        idx
2282                    }
2283                    Err(e) => {
2284                        warn!("AI reranking failed: {}", e);
2285                        (0..paths.len()).collect()
2286                    }
2287                }
2288            }
2289            Err(e) => {
2290                warn!("Failed to get Anthropic token: {}", e);
2291                (0..paths.len()).collect()
2292            }
2293        }
2294    } else if providers.contains(&Provider::Google) {
2295        match auth::get_google_token() {
2296            Ok(token) => {
2297                let client = GeminiClient::new(token);
2298                match client.rerank_trace(query, &path_descriptions).await {
2299                    Ok(idx) => {
2300                        debug!(order = ?idx, "AI reranked trace paths");
2301                        idx
2302                    }
2303                    Err(e) => {
2304                        warn!("AI reranking failed: {}", e);
2305                        (0..paths.len()).collect()
2306                    }
2307                }
2308            }
2309            Err(e) => {
2310                warn!("Failed to get Google token: {}", e);
2311                (0..paths.len()).collect()
2312            }
2313        }
2314    } else {
2315        (0..paths.len()).collect()
2316    };
2317
2318    let original_paths = std::mem::take(&mut paths);
2319    let mut reranked = Vec::with_capacity(original_paths.len());
2320
2321    for &idx in &indices {
2322        if idx < original_paths.len() {
2323            reranked.push(original_paths[idx].clone());
2324        }
2325    }
2326
2327    for (i, path) in original_paths.into_iter().enumerate() {
2328        if !indices.contains(&i) {
2329            reranked.push(path);
2330        }
2331    }
2332
2333    reranked
2334}
2335
2336// =============================================================================
2337// DEAD CODE
2338// =============================================================================
2339
2340/// Find dead/unused code
2341async fn find_dead_code_cmd(
2342    project: &Project,
2343    limit: Option<usize>,
2344    filter: &TraceFilter,
2345    xref: bool,
2346) -> Result<DeadCodeResult> {
2347    debug!("find_dead_code filter={:?} xref={}", filter, xref);
2348
2349    let index = load_semantic_index(project)?;
2350
2351    let dead_symbols = find_dead_symbols(&index);
2352
2353    let mut symbols = Vec::new();
2354    let mut by_kind: HashMap<String, usize> = HashMap::new();
2355    let mut by_file: HashMap<String, usize> = HashMap::new();
2356
2357    for sym in dead_symbols {
2358        let file = index
2359            .file_path(sym.file_id)
2360            .map(|p| p.to_string_lossy().to_string())
2361            .unwrap_or_else(|| "<unknown>".to_string());
2362
2363        let name = index.symbol_name(sym).unwrap_or("<unknown>").to_string();
2364        let kind = symbol_kind_str(sym.symbol_kind()).to_string();
2365
2366        // Apply universal filter
2367        if !filter.matches_symbol(&name, &kind, &file) {
2368            continue;
2369        }
2370
2371        *by_kind.entry(kind.clone()).or_insert(0) += 1;
2372        *by_file.entry(file.clone()).or_insert(0) += 1;
2373
2374        // Cross-reference: find potential callers if enabled
2375        let potential_callers = if xref {
2376            find_potential_callers(&index, sym, &name)
2377        } else {
2378            Vec::new()
2379        };
2380
2381        symbols.push(DeadSymbol {
2382            name,
2383            kind,
2384            file,
2385            line: sym.start_line,
2386            reason: "No references or calls found".to_string(),
2387            potential_callers,
2388        });
2389    }
2390
2391    // Sort by file and line
2392    symbols.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
2393
2394    // Apply limit
2395    if let Some(limit) = limit {
2396        symbols.truncate(limit);
2397    }
2398
2399    Ok(DeadCodeResult {
2400        total_dead: symbols.len(),
2401        symbols,
2402        by_kind,
2403        by_file,
2404    })
2405}
2406
2407/// Find potential callers for a dead symbol (for cross-referencing)
2408fn find_potential_callers(
2409    index: &SemanticIndex,
2410    dead_sym: &crate::trace::Symbol,
2411    dead_name: &str,
2412) -> Vec<PotentialCaller> {
2413    let mut callers = Vec::new();
2414
2415    // Strategy 1: Find functions in the same file that could call this
2416    let same_file_symbols: Vec<_> = index
2417        .symbols
2418        .iter()
2419        .filter(|s| {
2420            s.file_id == dead_sym.file_id
2421                && s.id != dead_sym.id
2422                && matches!(s.symbol_kind(), SymbolKind::Function | SymbolKind::Method)
2423        })
2424        .collect();
2425
2426    for caller in same_file_symbols.iter().take(3) {
2427        if let Some(caller_name) = index.symbol_name(caller) {
2428            if let Some(path) = index.file_path(caller.file_id) {
2429                callers.push(PotentialCaller {
2430                    name: caller_name.to_string(),
2431                    file: path.to_string_lossy().to_string(),
2432                    line: caller.start_line,
2433                    reason: "Same file - could call this".to_string(),
2434                });
2435            }
2436        }
2437    }
2438
2439    // Strategy 2: Find entry points that could use this
2440    for &entry_id in index.entry_points.iter().take(2) {
2441        if let Some(entry_sym) = index.symbols.iter().find(|s| s.id == entry_id) {
2442            if entry_sym.id != dead_sym.id {
2443                if let Some(entry_name) = index.symbol_name(entry_sym) {
2444                    if let Some(path) = index.file_path(entry_sym.file_id) {
2445                        callers.push(PotentialCaller {
2446                            name: entry_name.to_string(),
2447                            file: path.to_string_lossy().to_string(),
2448                            line: entry_sym.start_line,
2449                            reason: "Entry point - could reach this".to_string(),
2450                        });
2451                    }
2452                }
2453            }
2454        }
2455    }
2456
2457    // Strategy 3: Find token matches (name appears but not as actual reference)
2458    if let Some(token_ids) = index.tokens_by_name(dead_name) {
2459        for &token_id in token_ids.iter().take(2) {
2460            if let Some(token) = index.token(token_id) {
2461                // Skip if it's at the definition location
2462                if token.file_id != dead_sym.file_id || token.line != dead_sym.start_line {
2463                    if let Some(path) = index.file_path(token.file_id) {
2464                        callers.push(PotentialCaller {
2465                            name: dead_name.to_string(),
2466                            file: path.to_string_lossy().to_string(),
2467                            line: token.line,
2468                            reason: "Token match - name appears here".to_string(),
2469                        });
2470                    }
2471                }
2472            }
2473        }
2474    }
2475
2476    callers
2477}
2478
2479/// Run interactive TUI mode
2480async fn run_tui(_args: &TraceArgs, _project: &Project) -> Result<()> {
2481    eprintln!("TUI mode is not yet implemented.");
2482    eprintln!("Use --json or default ASCII output instead.");
2483    Err(Error::SearchError {
2484        message: "TUI mode not implemented".to_string(),
2485    })
2486}
2487
2488#[cfg(test)]
2489mod tests {
2490    use super::*;
2491
2492    #[test]
2493    fn test_args_output_format() {
2494        let args = TraceArgs {
2495            symbol: Some("test".to_string()),
2496            direct: false,
2497            refs: None,
2498            reads: None,
2499            writes: None,
2500            callers: None,
2501            callees: None,
2502            type_name: None,
2503            module: None,
2504            pattern: None,
2505            flow: None,
2506            impact: None,
2507            scope: None,
2508            dead: false,
2509            stats: false,
2510            cycles: false,
2511            kind: None,
2512            r#in: None,
2513            symbol_type: None,
2514            name: None,
2515            group_by: None,
2516            json: true,
2517            plain: false,
2518            csv: false,
2519            dot: false,
2520            markdown: false,
2521            tui: false,
2522            max_depth: 10,
2523            context: 0,
2524            limit: None,
2525            count: false,
2526            summary: false,
2527            xref: false,
2528            project: None,
2529        };
2530        assert_eq!(args.output_format(), OutputFormat::Json);
2531    }
2532
2533    #[test]
2534    fn test_args_operations() {
2535        let args = TraceArgs {
2536            symbol: Some("test".to_string()),
2537            direct: false,
2538            refs: None,
2539            reads: None,
2540            writes: None,
2541            callers: None,
2542            callees: None,
2543            type_name: None,
2544            module: None,
2545            pattern: None,
2546            flow: None,
2547            impact: None,
2548            scope: None,
2549            dead: false,
2550            stats: false,
2551            cycles: false,
2552            kind: None,
2553            r#in: None,
2554            symbol_type: None,
2555            name: None,
2556            group_by: None,
2557            json: false,
2558            plain: false,
2559            csv: false,
2560            dot: false,
2561            markdown: false,
2562            tui: false,
2563            max_depth: 10,
2564            context: 0,
2565            limit: None,
2566            count: false,
2567            summary: false,
2568            xref: false,
2569            project: None,
2570        };
2571
2572        let ops = args.operations();
2573        assert_eq!(ops.len(), 1);
2574        match &ops[0] {
2575            TraceOperation::Trace(sym) => assert_eq!(sym, "test"),
2576            _ => panic!("Expected Trace operation"),
2577        }
2578    }
2579
2580    #[test]
2581    fn test_args_refs_operations() {
2582        let args = TraceArgs {
2583            symbol: None,
2584            direct: false,
2585            refs: Some("userId".to_string()),
2586            reads: None,
2587            writes: None,
2588            callers: None,
2589            callees: None,
2590            type_name: None,
2591            module: None,
2592            pattern: None,
2593            flow: None,
2594            impact: None,
2595            scope: None,
2596            dead: false,
2597            stats: false,
2598            cycles: false,
2599            kind: None,
2600            r#in: None,
2601            symbol_type: None,
2602            name: None,
2603            group_by: None,
2604            json: false,
2605            plain: false,
2606            csv: false,
2607            dot: false,
2608            markdown: false,
2609            tui: false,
2610            max_depth: 10,
2611            context: 0,
2612            limit: None,
2613            count: false,
2614            summary: false,
2615            xref: false,
2616            project: None,
2617        };
2618
2619        let ops = args.operations();
2620        assert_eq!(ops.len(), 1);
2621        match &ops[0] {
2622            TraceOperation::Refs { symbol, kind } => {
2623                assert_eq!(symbol, "userId");
2624                assert!(kind.is_none());
2625            }
2626            _ => panic!("Expected Refs operation"),
2627        }
2628    }
2629
2630    #[test]
2631    fn test_args_composable_operations() {
2632        // Test that multiple flags result in multiple operations
2633        let args = TraceArgs {
2634            symbol: Some("test".to_string()),
2635            direct: false,
2636            refs: Some("other".to_string()),
2637            reads: None,
2638            writes: None,
2639            callers: None,
2640            callees: None,
2641            type_name: None,
2642            module: None,
2643            pattern: None,
2644            flow: None,
2645            impact: None,
2646            scope: None,
2647            dead: true,
2648            stats: true,
2649            cycles: false,
2650            kind: None,
2651            r#in: None,
2652            symbol_type: None,
2653            name: None,
2654            group_by: None,
2655            json: false,
2656            plain: false,
2657            csv: false,
2658            dot: false,
2659            markdown: false,
2660            tui: false,
2661            max_depth: 10,
2662            context: 0,
2663            limit: None,
2664            count: false,
2665            summary: false,
2666            xref: false,
2667            project: None,
2668        };
2669
2670        let ops = args.operations();
2671        // Should have: DeadCode, Stats, Refs, Trace (4 operations)
2672        assert_eq!(ops.len(), 4, "Expected 4 operations, got {:?}", ops);
2673
2674        // Verify all expected operations are present
2675        let has_dead = ops.iter().any(|op| matches!(op, TraceOperation::DeadCode));
2676        let has_stats = ops.iter().any(|op| matches!(op, TraceOperation::Stats));
2677        let has_refs = ops
2678            .iter()
2679            .any(|op| matches!(op, TraceOperation::Refs { .. }));
2680        let has_trace = ops.iter().any(|op| matches!(op, TraceOperation::Trace(_)));
2681
2682        assert!(has_dead, "Missing DeadCode operation");
2683        assert!(has_stats, "Missing Stats operation");
2684        assert!(has_refs, "Missing Refs operation");
2685        assert!(has_trace, "Missing Trace operation");
2686    }
2687}
2688
2689#[allow(dead_code)]
2690pub fn debug_index_stats(project: &Project) -> Result<()> {
2691    let index = load_semantic_index(project)?;
2692
2693    println!("=== INDEX DEBUG ===");
2694    println!("Symbols: {}", index.symbols.len());
2695    println!("Tokens: {}", index.tokens.len());
2696    println!("symbol_by_name entries: {}", index.symbol_by_name.len());
2697    println!("token_by_name entries: {}", index.token_by_name.len());
2698
2699    println!("\nSample token names:");
2700    for (name, ids) in index.token_by_name.iter().take(10) {
2701        println!("  '{}' -> {} occurrences", name, ids.len());
2702    }
2703
2704    if let Some(ids) = index.tokens_by_name("userId") {
2705        println!("\n'userId' found: {} occurrences", ids.len());
2706    } else {
2707        println!("\n'userId' NOT FOUND in token_by_name");
2708        let matches = index.tokens_matching("userId");
2709        println!("Tokens containing 'userId': {}", matches.len());
2710    }
2711
2712    Ok(())
2713}