Skip to main content

codanna/
retrieve.rs

1//! Retrieve command implementations using Envelope schema for JSON output.
2//!
3//! This module uses QueryContext to reduce duplication across retrieve functions.
4
5use crate::Symbol;
6use crate::indexing::facade::IndexFacade;
7use crate::io::{
8    EntityType, ExitCode, OutputFormat, OutputManager, OutputStatus,
9    envelope::{EntityType as EnvelopeEntityType, Envelope, ResultCode},
10    schema::{OutputData, OutputMetadata, UnifiedOutput, UnifiedOutputBuilder},
11};
12use crate::symbol::context::SymbolContext;
13use serde::Serialize;
14use std::borrow::Cow;
15use std::fmt::Display;
16
17// =============================================================================
18// QueryContext - Shared abstraction for retrieve commands
19// =============================================================================
20
21/// Result of symbol resolution.
22pub enum ResolveResult {
23    /// Found exactly one symbol
24    Found(Symbol),
25    /// Symbol not found
26    NotFound,
27    /// Multiple symbols match (ambiguous)
28    Ambiguous(Vec<Symbol>),
29    /// Invalid symbol_id format
30    InvalidId(String),
31}
32
33/// Shared query execution context for retrieve commands.
34///
35/// Reduces code duplication by centralizing:
36/// - Symbol resolution (name or symbol_id:XXX)
37/// - Not-found / ambiguous handling
38/// - Envelope construction for JSON output
39/// - Text formatting for terminal output
40pub struct QueryContext<'a> {
41    indexer: &'a IndexFacade,
42    format: OutputFormat,
43    fields: Option<Vec<String>>,
44    entity_type: EnvelopeEntityType,
45    command_name: &'static str,
46}
47
48impl<'a> QueryContext<'a> {
49    /// Create a new query context.
50    pub fn new(
51        indexer: &'a IndexFacade,
52        format: OutputFormat,
53        fields: Option<Vec<String>>,
54        entity_type: EnvelopeEntityType,
55        command_name: &'static str,
56    ) -> Self {
57        Self {
58            indexer,
59            format,
60            fields,
61            entity_type,
62            command_name,
63        }
64    }
65
66    /// Resolve a symbol by name or symbol_id:XXX format.
67    pub fn resolve_symbol(&self, query: &str, language: Option<&str>) -> ResolveResult {
68        // Check for symbol_id:XXX format
69        if let Some(id_str) = query.strip_prefix("symbol_id:") {
70            match id_str.parse::<u32>() {
71                Ok(id) => match self.indexer.get_symbol(crate::SymbolId(id)) {
72                    Some(sym) => ResolveResult::Found(sym),
73                    None => ResolveResult::NotFound,
74                },
75                Err(_) => ResolveResult::InvalidId(id_str.to_string()),
76            }
77        } else {
78            // Name-based lookup
79            let symbols = self.indexer.find_symbols_by_name(query, language);
80            match symbols.len() {
81                0 => ResolveResult::NotFound,
82                1 => ResolveResult::Found(symbols.into_iter().next().unwrap()),
83                _ => ResolveResult::Ambiguous(symbols),
84            }
85        }
86    }
87
88    /// Handle resolution errors and return appropriate exit code.
89    pub fn handle_resolve_error(&self, result: ResolveResult, query: &str) -> ExitCode {
90        match result {
91            ResolveResult::Found(_) => ExitCode::Success, // Should not happen
92            ResolveResult::NotFound => self.output_not_found(query),
93            ResolveResult::Ambiguous(symbols) => self.output_ambiguous(query, &symbols),
94            ResolveResult::InvalidId(id) => self.output_invalid_id(&id),
95        }
96    }
97
98    /// Output not-found result.
99    pub fn output_not_found(&self, query: &str) -> ExitCode {
100        if self.format == OutputFormat::Json {
101            let envelope: Envelope<()> = Envelope::not_found(format!(
102                "No symbol found for '{query}'"
103            ))
104            .with_entity_type(self.entity_type)
105            .with_query(query)
106            .with_hint(
107                "Use codanna retrieve symbol <name> to search, or try semantic_search_with_context"
108                    .to_string(),
109            );
110
111            println!("{}", envelope.to_json().expect("envelope serialization"));
112            ExitCode::NotFound
113        } else {
114            eprintln!("Not found: '{query}'");
115            ExitCode::NotFound
116        }
117    }
118
119    /// Output ambiguous match result.
120    pub fn output_ambiguous(&self, query: &str, symbols: &[Symbol]) -> ExitCode {
121        if self.format == OutputFormat::Json {
122            // In JSON mode, return an error with suggestions
123            let suggestions: Vec<String> = symbols
124                .iter()
125                .take(10)
126                .map(|s| format!("symbol_id:{}", s.id.value()))
127                .collect();
128
129            let envelope: Envelope<()> = Envelope::error(
130                ResultCode::InvalidQuery,
131                format!(
132                    "Ambiguous: found {} symbol(s) named '{query}'",
133                    symbols.len()
134                ),
135            )
136            .with_entity_type(self.entity_type)
137            .with_query(query)
138            .with_hint(format!(
139                "Use: codanna retrieve {} symbol_id:<id>",
140                self.command_name
141            ));
142
143            // Add context with symbol details
144            let context: Vec<serde_json::Value> = symbols
145                .iter()
146                .take(10)
147                .map(|s| {
148                    serde_json::json!({
149                        "symbol_id": s.id.value(),
150                        "kind": format!("{:?}", s.kind),
151                        "file_path": s.file_path,
152                        "line": s.range.start_line + 1
153                    })
154                })
155                .collect();
156
157            let envelope = envelope.with_error_details(crate::io::envelope::ErrorDetails {
158                suggestions,
159                context: Some(serde_json::json!(context)),
160            });
161
162            println!("{}", envelope.to_json().expect("envelope serialization"));
163            ExitCode::GeneralError
164        } else {
165            // Text mode - print to stderr
166            eprintln!(
167                "Ambiguous: found {} symbol(s) named '{}':",
168                symbols.len(),
169                query
170            );
171            for (i, sym) in symbols.iter().take(10).enumerate() {
172                eprintln!(
173                    "  {}. symbol_id:{} - {:?} at {}:{}",
174                    i + 1,
175                    sym.id.value(),
176                    sym.kind,
177                    sym.file_path,
178                    sym.range.start_line + 1
179                );
180            }
181            if symbols.len() > 10 {
182                eprintln!("  ... and {} more", symbols.len() - 10);
183            }
184            eprintln!(
185                "\nUse: codanna retrieve {} symbol_id:<id>",
186                self.command_name
187            );
188            ExitCode::GeneralError
189        }
190    }
191
192    /// Output invalid symbol_id error.
193    pub fn output_invalid_id(&self, id: &str) -> ExitCode {
194        if self.format == OutputFormat::Json {
195            let envelope: Envelope<()> = Envelope::error(
196                ResultCode::InvalidQuery,
197                format!("Invalid symbol_id format: '{id}'"),
198            )
199            .with_hint("symbol_id must be a positive integer");
200
201            println!("{}", envelope.to_json().expect("envelope serialization"));
202        } else {
203            eprintln!("Invalid symbol_id format: {id}");
204        }
205        ExitCode::GeneralError
206    }
207
208    /// Output success with data items.
209    pub fn output_success<T: Serialize + Display>(
210        &self,
211        data: Vec<T>,
212        query: &str,
213        hint: Option<&str>,
214    ) -> ExitCode {
215        let count = data.len();
216
217        if self.format == OutputFormat::Json {
218            let mut envelope = Envelope::success(data)
219                .with_entity_type(self.entity_type)
220                .with_count(count)
221                .with_query(query)
222                .with_message(format!("Found {count} result(s)"));
223
224            if let Some(h) = hint {
225                envelope = envelope.with_hint(h);
226            }
227
228            let json = if let Some(ref fields) = self.fields {
229                envelope.to_json_with_fields(fields)
230            } else {
231                envelope.to_json()
232            };
233
234            println!("{}", json.expect("envelope serialization"));
235            ExitCode::Success
236        } else {
237            // Text mode - use Display trait
238            for item in &data {
239                println!("{item}");
240            }
241            ExitCode::Success
242        }
243    }
244
245    /// Output empty success (symbol found but no results).
246    pub fn output_empty(&self, query: &str, message: &str) -> ExitCode {
247        if self.format == OutputFormat::Json {
248            let envelope: Envelope<Vec<()>> = Envelope::success(vec![])
249                .with_entity_type(self.entity_type)
250                .with_count(0)
251                .with_query(query)
252                .with_message(message);
253
254            println!("{}", envelope.to_json().expect("envelope serialization"));
255        } else {
256            println!("{message}");
257        }
258        ExitCode::Success
259    }
260}
261
262/// Execute retrieve symbol command
263///
264/// Unlike callers/calls, this returns ALL matching symbols (not ambiguous error).
265pub fn retrieve_symbol(
266    indexer: &IndexFacade,
267    name: &str,
268    language: Option<&str>,
269    format: OutputFormat,
270    fields: Option<Vec<String>>,
271) -> ExitCode {
272    use crate::symbol::context::ContextIncludes;
273
274    // Check if name is a symbol_id (format: "symbol_id:123")
275    let symbols = if let Some(id_str) = name.strip_prefix("symbol_id:") {
276        // Direct symbol_id lookup
277        match id_str.parse::<u32>() {
278            Ok(id) => match indexer.get_symbol(crate::SymbolId(id)) {
279                Some(sym) => vec![sym],
280                None => vec![],
281            },
282            Err(_) => {
283                // Invalid symbol_id format
284                if format == OutputFormat::Json {
285                    let envelope: Envelope<()> = Envelope::error(
286                        ResultCode::InvalidQuery,
287                        format!("Invalid symbol_id format: '{id_str}'"),
288                    )
289                    .with_hint("symbol_id must be a positive integer");
290                    println!("{}", envelope.to_json().expect("envelope serialization"));
291                } else {
292                    eprintln!("Invalid symbol_id format: {id_str}");
293                }
294                return ExitCode::GeneralError;
295            }
296        }
297    } else {
298        // Name-based lookup
299        indexer.find_symbols_by_name(name, language)
300    };
301
302    if symbols.is_empty() {
303        // Not found
304        if format == OutputFormat::Json {
305            let envelope: Envelope<()> = Envelope::not_found(format!("No symbol found for '{name}'"))
306                .with_entity_type(EnvelopeEntityType::Symbol)
307                .with_query(name)
308                .with_hint("Use codanna retrieve search <query> for fuzzy matching, or try semantic_search_with_context");
309            println!("{}", envelope.to_json().expect("envelope serialization"));
310        } else {
311            eprintln!("Not found: '{name}'");
312        }
313        return ExitCode::NotFound;
314    }
315
316    // Transform symbols to SymbolContext with file paths and relationships
317    let symbols_with_context: Vec<SymbolContext> = symbols
318        .into_iter()
319        .filter_map(|symbol| {
320            indexer.get_symbol_context(
321                symbol.id,
322                ContextIncludes::IMPLEMENTATIONS
323                    | ContextIncludes::DEFINITIONS
324                    | ContextIncludes::CALLERS,
325            )
326        })
327        .collect();
328
329    let count = symbols_with_context.len();
330
331    if format == OutputFormat::Json {
332        let mut envelope = Envelope::success(symbols_with_context)
333            .with_entity_type(EnvelopeEntityType::Symbol)
334            .with_count(count)
335            .with_query(name)
336            .with_message(format!("Found {count} symbol(s)"))
337            .with_hint("Use symbol_id for precise lookup in subsequent queries");
338
339        // Include language filter in metadata if specified
340        if let Some(lang) = language {
341            envelope = envelope.with_lang(lang);
342        }
343
344        let json = if let Some(ref f) = fields {
345            envelope.to_json_with_fields(f)
346        } else {
347            envelope.to_json()
348        };
349
350        println!("{}", json.expect("envelope serialization"));
351        ExitCode::Success
352    } else {
353        // Text output
354        for ctx in &symbols_with_context {
355            println!("{ctx}");
356        }
357        ExitCode::Success
358    }
359}
360
361/// Execute retrieve callers command
362///
363/// Uses QueryContext for symbol resolution with ambiguous handling.
364pub fn retrieve_callers(
365    indexer: &IndexFacade,
366    function: &str,
367    language: Option<&str>,
368    format: OutputFormat,
369    fields: Option<Vec<String>>,
370) -> ExitCode {
371    use crate::symbol::context::ContextIncludes;
372
373    // Use QueryContext for symbol resolution
374    let ctx = QueryContext::new(
375        indexer,
376        format,
377        fields.clone(),
378        EnvelopeEntityType::Callers,
379        "callers",
380    );
381
382    // Resolve symbol (handles not-found, ambiguous, invalid id)
383    let symbol = match ctx.resolve_symbol(function, language) {
384        ResolveResult::Found(s) => s,
385        other => return ctx.handle_resolve_error(other, function),
386    };
387
388    // Get callers for this specific symbol
389    let callers = indexer.get_calling_functions_with_metadata(symbol.id);
390
391    // Handle empty results: symbol exists but has no callers
392    if callers.is_empty() {
393        return ctx.output_empty(function, &format!("No functions call '{function}'"));
394    }
395
396    // Transform to SymbolContext with relationships
397    let callers_with_context: Vec<SymbolContext> = callers
398        .into_iter()
399        .filter_map(|(caller, _metadata)| {
400            indexer.get_symbol_context(
401                caller.id,
402                ContextIncludes::CALLS | ContextIncludes::DEFINITIONS,
403            )
404        })
405        .collect();
406
407    let count = callers_with_context.len();
408
409    if format == OutputFormat::Json {
410        let mut envelope = Envelope::success(callers_with_context)
411            .with_entity_type(EnvelopeEntityType::Callers)
412            .with_count(count)
413            .with_query(function)
414            .with_message(format!("Found {count} caller(s)"))
415            .with_hint("Use symbol_id for precise lookup");
416
417        if let Some(lang) = language {
418            envelope = envelope.with_lang(lang);
419        }
420
421        let json = if let Some(ref f) = fields {
422            envelope.to_json_with_fields(f)
423        } else {
424            envelope.to_json()
425        };
426
427        println!("{}", json.expect("envelope serialization"));
428        ExitCode::Success
429    } else {
430        // Text output
431        for ctx in &callers_with_context {
432            println!("{ctx}");
433        }
434        ExitCode::Success
435    }
436}
437
438/// Execute retrieve calls command
439///
440/// Uses QueryContext for symbol resolution with ambiguous handling.
441pub fn retrieve_calls(
442    indexer: &IndexFacade,
443    function: &str,
444    language: Option<&str>,
445    format: OutputFormat,
446    fields: Option<Vec<String>>,
447) -> ExitCode {
448    use crate::symbol::context::ContextIncludes;
449
450    // Use QueryContext for symbol resolution
451    let ctx = QueryContext::new(
452        indexer,
453        format,
454        fields.clone(),
455        EnvelopeEntityType::Calls,
456        "calls",
457    );
458
459    // Resolve symbol (handles not-found, ambiguous, invalid id)
460    let symbol = match ctx.resolve_symbol(function, language) {
461        ResolveResult::Found(s) => s,
462        other => return ctx.handle_resolve_error(other, function),
463    };
464
465    // Get calls for this specific symbol
466    let calls = indexer.get_called_functions_with_metadata(symbol.id);
467
468    // Handle empty results: symbol exists but makes no calls
469    if calls.is_empty() {
470        return ctx.output_empty(function, &format!("'{function}' makes no function calls"));
471    }
472
473    // Transform to SymbolContext with relationships
474    let calls_with_context: Vec<SymbolContext> = calls
475        .into_iter()
476        .filter_map(|(called, _metadata)| {
477            indexer.get_symbol_context(
478                called.id,
479                ContextIncludes::CALLERS | ContextIncludes::DEFINITIONS,
480            )
481        })
482        .collect();
483
484    let count = calls_with_context.len();
485
486    if format == OutputFormat::Json {
487        let mut envelope = Envelope::success(calls_with_context)
488            .with_entity_type(EnvelopeEntityType::Calls)
489            .with_count(count)
490            .with_query(function)
491            .with_message(format!("Found {count} call(s)"))
492            .with_hint("Use symbol_id for precise lookup");
493
494        if let Some(lang) = language {
495            envelope = envelope.with_lang(lang);
496        }
497
498        let json = if let Some(ref f) = fields {
499            envelope.to_json_with_fields(f)
500        } else {
501            envelope.to_json()
502        };
503
504        println!("{}", json.expect("envelope serialization"));
505        ExitCode::Success
506    } else {
507        // Text output
508        for ctx in &calls_with_context {
509            println!("{ctx}");
510        }
511        ExitCode::Success
512    }
513}
514
515/// Execute retrieve implementations command
516///
517/// Uses QueryContext for symbol resolution with ambiguous handling.
518pub fn retrieve_implementations(
519    indexer: &IndexFacade,
520    trait_name: &str,
521    language: Option<&str>,
522    format: OutputFormat,
523    fields: Option<Vec<String>>,
524) -> ExitCode {
525    use crate::symbol::context::ContextIncludes;
526
527    // Use QueryContext for symbol resolution
528    let ctx = QueryContext::new(
529        indexer,
530        format,
531        fields.clone(),
532        EnvelopeEntityType::Symbol, // Implementations are symbols
533        "implementations",
534    );
535
536    // Resolve trait symbol (handles not-found, ambiguous, invalid id)
537    let trait_symbol = match ctx.resolve_symbol(trait_name, language) {
538        ResolveResult::Found(s) => s,
539        other => return ctx.handle_resolve_error(other, trait_name),
540    };
541
542    // Get implementations for this trait
543    let implementations = indexer.get_implementations(trait_symbol.id);
544
545    // Handle empty results
546    if implementations.is_empty() {
547        return ctx.output_empty(
548            trait_name,
549            &format!("No implementations found for '{trait_name}'"),
550        );
551    }
552
553    // Transform to SymbolContext with relationships
554    let impls_with_context: Vec<SymbolContext> = implementations
555        .into_iter()
556        .filter_map(|symbol| {
557            indexer.get_symbol_context(
558                symbol.id,
559                ContextIncludes::DEFINITIONS | ContextIncludes::CALLERS,
560            )
561        })
562        .collect();
563
564    let count = impls_with_context.len();
565
566    if format == OutputFormat::Json {
567        let mut envelope = Envelope::success(impls_with_context)
568            .with_entity_type(EnvelopeEntityType::Symbol)
569            .with_count(count)
570            .with_query(trait_name)
571            .with_message(format!("Found {count} implementation(s)"))
572            .with_hint("Use symbol_id for precise lookup");
573
574        if let Some(lang) = language {
575            envelope = envelope.with_lang(lang);
576        }
577
578        let json = if let Some(ref f) = fields {
579            envelope.to_json_with_fields(f)
580        } else {
581            envelope.to_json()
582        };
583
584        println!("{}", json.expect("envelope serialization"));
585        ExitCode::Success
586    } else {
587        // Text output
588        for ctx in &impls_with_context {
589            println!("{ctx}");
590        }
591        ExitCode::Success
592    }
593}
594
595/// Execute retrieve search command
596///
597/// Full-text search with optional filters. Uses Envelope for JSON output.
598pub fn retrieve_search(
599    indexer: &IndexFacade,
600    query: &str,
601    limit: usize,
602    kind: Option<&str>,
603    module: Option<&str>,
604    language: Option<&str>,
605    format: OutputFormat,
606    fields: Option<Vec<String>>,
607) -> ExitCode {
608    use crate::symbol::context::ContextIncludes;
609
610    // Parse the kind filter if provided
611    let kind_filter = kind.and_then(|k| match k.to_lowercase().as_str() {
612        "function" => Some(crate::SymbolKind::Function),
613        "struct" => Some(crate::SymbolKind::Struct),
614        "trait" => Some(crate::SymbolKind::Trait),
615        "interface" => Some(crate::SymbolKind::Interface),
616        "class" => Some(crate::SymbolKind::Class),
617        "method" => Some(crate::SymbolKind::Method),
618        "field" => Some(crate::SymbolKind::Field),
619        "variable" => Some(crate::SymbolKind::Variable),
620        "constant" => Some(crate::SymbolKind::Constant),
621        "module" => Some(crate::SymbolKind::Module),
622        "typealias" => Some(crate::SymbolKind::TypeAlias),
623        "enum" => Some(crate::SymbolKind::Enum),
624        _ => {
625            eprintln!("Warning: Unknown symbol kind '{k}', ignoring filter");
626            None
627        }
628    });
629
630    let search_results = indexer
631        .search(query, limit, kind_filter, module, language)
632        .unwrap_or_default();
633
634    // Transform search results to SymbolContext with relationships
635    let results_with_context: Vec<SymbolContext> = search_results
636        .into_iter()
637        .filter_map(|result| {
638            indexer.get_symbol_context(
639                result.symbol_id,
640                ContextIncludes::IMPLEMENTATIONS
641                    | ContextIncludes::DEFINITIONS
642                    | ContextIncludes::CALLERS,
643            )
644        })
645        .collect();
646
647    let count = results_with_context.len();
648
649    if format == OutputFormat::Json {
650        // Build envelope
651        let envelope = if results_with_context.is_empty() {
652            Envelope::not_found(format!("No results for '{query}'"))
653                .with_entity_type(EnvelopeEntityType::SearchResult)
654                .with_query(query)
655                .with_hint("Try broader search terms or use semantic_search_with_context")
656        } else {
657            let mut env = Envelope::success(results_with_context)
658                .with_entity_type(EnvelopeEntityType::SearchResult)
659                .with_count(count)
660                .with_query(query)
661                .with_message(format!("Found {count} result(s)"))
662                .with_hint("Use symbol_id for precise lookup");
663
664            if let Some(lang) = language {
665                env = env.with_lang(lang);
666            }
667            env
668        };
669
670        let json = if let Some(ref f) = fields {
671            envelope.to_json_with_fields(f)
672        } else {
673            envelope.to_json()
674        };
675
676        println!("{}", json.expect("envelope serialization"));
677
678        if count == 0 {
679            ExitCode::NotFound
680        } else {
681            ExitCode::Success
682        }
683    } else {
684        // Text output
685        if results_with_context.is_empty() {
686            eprintln!("No results for '{query}'");
687            ExitCode::NotFound
688        } else {
689            for ctx in &results_with_context {
690                println!("{ctx}");
691            }
692            ExitCode::Success
693        }
694    }
695}
696
697/// Execute retrieve impact command
698// DEPRECATED: This function has been disabled.
699// Use MCP semantic_search_with_context or slash commands instead.
700// The impact command had fundamental flaws:
701// - Only worked for functions, not structs/traits/enums
702// - Returned empty results for valid symbols
703// - Conceptually wrong (not all symbols have "impact")
704#[allow(dead_code)]
705pub fn retrieve_impact(
706    indexer: &IndexFacade,
707    symbol_name: &str,
708    max_depth: usize,
709    format: OutputFormat,
710) -> ExitCode {
711    let mut output = OutputManager::new(format);
712    let symbols = indexer.find_symbols_by_name(symbol_name, None);
713
714    if symbols.is_empty() {
715        let unified = UnifiedOutput {
716            status: OutputStatus::NotFound,
717            entity_type: EntityType::Impact,
718            count: 0,
719            data: OutputData::<SymbolContext>::Empty,
720            metadata: Some(OutputMetadata {
721                query: Some(Cow::Borrowed(symbol_name)),
722                tool: None,
723                timing_ms: None,
724                truncated: None,
725                extra: Default::default(),
726            }),
727            guidance: None,
728            exit_code: ExitCode::NotFound,
729        };
730
731        match output.unified(unified) {
732            Ok(code) => code,
733            Err(e) => {
734                eprintln!("Error writing output: {e}");
735                ExitCode::GeneralError
736            }
737        }
738    } else {
739        // Get impact analysis for the first matching symbol
740        let symbol = &symbols[0];
741        let impact_symbol_ids = indexer.get_impact_radius(symbol.id, Some(max_depth));
742
743        // Transform impact symbols to SymbolContext with relationships
744        use crate::symbol::context::ContextIncludes;
745
746        let impact_with_path: Vec<SymbolContext> = impact_symbol_ids
747            .into_iter()
748            .filter_map(|symbol_id| {
749                // Get full context for each impacted symbol
750                indexer.get_symbol_context(
751                    symbol_id,
752                    ContextIncludes::CALLERS | ContextIncludes::CALLS,
753                )
754            })
755            .collect();
756
757        let unified = UnifiedOutputBuilder::items(impact_with_path, EntityType::Impact)
758            .with_metadata(OutputMetadata {
759                query: Some(Cow::Borrowed(symbol_name)),
760                tool: None,
761                timing_ms: None,
762                truncated: None,
763                extra: Default::default(),
764            })
765            .build();
766
767        match output.unified(unified) {
768            Ok(code) => code,
769            Err(e) => {
770                eprintln!("Error writing output: {e}");
771                ExitCode::GeneralError
772            }
773        }
774    }
775}
776
777/// Execute retrieve describe command
778///
779/// Uses QueryContext for symbol resolution with ambiguous handling.
780/// Returns full symbol context with all relationships.
781pub fn retrieve_describe(
782    indexer: &IndexFacade,
783    symbol_name: &str,
784    language: Option<&str>,
785    format: OutputFormat,
786    fields: Option<Vec<String>>,
787) -> ExitCode {
788    // Use QueryContext for symbol resolution
789    let ctx = QueryContext::new(
790        indexer,
791        format,
792        fields.clone(),
793        EnvelopeEntityType::Symbol,
794        "describe",
795    );
796
797    // Resolve symbol (handles not-found, ambiguous, invalid id)
798    let symbol = match ctx.resolve_symbol(symbol_name, language) {
799        ResolveResult::Found(s) => s,
800        other => return ctx.handle_resolve_error(other, symbol_name),
801    };
802
803    // Build rich context with all relationships
804    let file_path = SymbolContext::symbol_location(&symbol);
805
806    let mut context = SymbolContext {
807        symbol: symbol.clone(),
808        file_path,
809        relationships: Default::default(),
810    };
811
812    // Get calls for this specific symbol
813    let calls = indexer.get_called_functions_with_metadata(symbol.id);
814    if !calls.is_empty() {
815        context.relationships.calls = Some(calls);
816    }
817
818    // Get callers for this specific symbol
819    let callers = indexer.get_calling_functions_with_metadata(symbol.id);
820    if !callers.is_empty() {
821        context.relationships.called_by = Some(callers);
822    }
823
824    // Get defines for this specific symbol
825    let deps = indexer.get_dependencies(symbol.id);
826    if let Some(defines) = deps.get(&crate::RelationKind::Defines) {
827        context.relationships.defines = Some(defines.clone());
828    }
829
830    // Load implementations (for traits/interfaces) and implements (for types)
831    use crate::SymbolKind;
832    match symbol.kind {
833        SymbolKind::Trait | SymbolKind::Interface => {
834            let implementations = indexer.get_implementations(symbol.id);
835            if !implementations.is_empty() {
836                context.relationships.implemented_by = Some(implementations);
837            }
838        }
839        SymbolKind::Struct | SymbolKind::Enum | SymbolKind::Class => {
840            // What traits does this type implement?
841            let impls = indexer.get_implemented_traits(symbol.id);
842            if !impls.is_empty() {
843                context.relationships.implements = Some(impls);
844            }
845        }
846        _ => {}
847    }
848
849    // Load extends relationships (for classes)
850    match symbol.kind {
851        SymbolKind::Class | SymbolKind::Struct => {
852            // What does this class extend?
853            let extends = indexer.get_extends(symbol.id);
854            if !extends.is_empty() {
855                context.relationships.extends = Some(extends);
856            }
857
858            // What classes extend this class?
859            let extended_by = indexer.get_extended_by(symbol.id);
860            if !extended_by.is_empty() {
861                context.relationships.extended_by = Some(extended_by);
862            }
863        }
864        _ => {}
865    }
866
867    // Load uses relationships (for all symbols)
868    let uses = indexer.get_uses(symbol.id);
869    if !uses.is_empty() {
870        context.relationships.uses = Some(uses);
871    }
872
873    let used_by = indexer.get_used_by(symbol.id);
874    if !used_by.is_empty() {
875        context.relationships.used_by = Some(used_by);
876    }
877
878    // Output
879    if format == OutputFormat::Json {
880        let mut envelope = Envelope::success(context)
881            .with_entity_type(EnvelopeEntityType::Symbol)
882            .with_count(1)
883            .with_query(symbol_name)
884            .with_message(format!("Symbol '{}' described", symbol.name))
885            .with_hint("Use callers/calls commands to explore relationships further");
886
887        if let Some(lang) = language {
888            envelope = envelope.with_lang(lang);
889        }
890
891        let json = if let Some(ref f) = fields {
892            envelope.to_json_with_fields(f)
893        } else {
894            envelope.to_json()
895        };
896
897        println!("{}", json.expect("envelope serialization"));
898        ExitCode::Success
899    } else {
900        // Text output
901        println!("{context}");
902        ExitCode::Success
903    }
904}