Skip to main content

rag_rat_core/query/
graph.rs

1use std::collections::BTreeSet;
2
3use rusqlite::{Connection, params_from_iter};
4use serde::Serialize;
5
6const CALL_EDGE_KINDS: &[&str] = &["calls_name", "constructs"];
7const MACRO_EDGE_KINDS: &[&str] = &["uses_macro"];
8const REFERENCE_EDGE_KINDS: &[&str] =
9    &["references_type", "imports", "exports", "contains", "implements"];
10const OPTIONAL_EDGE_KINDS: &[&str] = &[
11    "calls_name",
12    "constructs",
13    "uses_macro",
14    "references_type",
15    "imports",
16    "exports",
17    "contains",
18    "implements",
19];
20
21#[derive(Debug, Clone, Default)]
22pub struct GraphTraversalOptions {
23    pub include_references: bool,
24    pub include_unresolved: bool,
25    pub include_macros: bool,
26    pub include_common_methods: bool,
27    pub edge_kinds: Option<Vec<String>>,
28    pub resolution_mode: GraphResolutionMode,
29    pub symbol_id: Option<i64>,
30    pub logical_symbol_id: Option<i64>,
31}
32
33#[derive(Debug, Serialize)]
34pub struct GraphTraversalReport {
35    pub query: GraphTraversalQuery,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub logical_symbol: Option<LogicalSymbol>,
38    #[serde(skip_serializing_if = "Vec::is_empty")]
39    pub variants: Vec<LogicalSymbolVariant>,
40    pub summary: GraphTraversalSummary,
41    pub coverage: GraphCoverage,
42    pub results: Vec<GraphHop>,
43}
44
45#[derive(Debug, Serialize)]
46pub struct GraphTraversalQuery {
47    pub tool: String,
48    pub symbol_id: Option<i64>,
49    pub logical_symbol_id: Option<i64>,
50    pub symbol_path: String,
51    pub resolution: String,
52}
53
54#[derive(Debug, Clone, Serialize)]
55pub struct LogicalSymbol {
56    pub logical_symbol_id: i64,
57    pub qualified_name: String,
58    pub variant_count: u64,
59    pub group_reason: String,
60}
61
62#[derive(Debug, Clone, Serialize)]
63pub struct LogicalSymbolVariant {
64    pub symbol_id: i64,
65    pub cfg_expr: Option<String>,
66    pub signature_hash: Option<String>,
67    pub start_line: i64,
68    pub end_line: i64,
69}
70
71#[derive(Debug, Default, Serialize)]
72pub struct GraphTraversalSummary {
73    pub returned_count: u64,
74    pub total_matching_edges: u64,
75    pub truncated: bool,
76    pub exact_verified: u64,
77    pub syntactic: u64,
78    pub name_only: u64,
79    pub ambiguous: u64,
80    pub unresolved: u64,
81    pub false_positive_risk: String,
82    pub completeness_risk: String,
83}
84
85#[derive(Debug, Default, Serialize)]
86pub struct GraphCoverage {
87    pub indexed_files: u64,
88    pub parser_failures: u64,
89    pub stale_files: u64,
90    pub known_index_gaps: Vec<String>,
91    pub parser_coverage_for_paths: Vec<GraphPathCoverage>,
92}
93
94#[derive(Debug, Serialize)]
95pub struct GraphPathCoverage {
96    pub path: String,
97    pub language: String,
98    pub parser_status: String,
99    pub graph_status: String,
100    pub last_indexed_revision: Option<String>,
101}
102
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
104pub enum GraphResolutionMode {
105    Exact,
106    #[default]
107    Syntactic,
108    Fuzzy,
109}
110
111impl GraphResolutionMode {
112    pub fn parse(value: Option<&str>) -> anyhow::Result<Self> {
113        match value.unwrap_or("syntactic") {
114            "exact" => Ok(Self::Exact),
115            "syntactic" => Ok(Self::Syntactic),
116            "fuzzy" => Ok(Self::Fuzzy),
117            other => anyhow::bail!(
118                "unknown graph resolution mode `{other}`; expected exact, syntactic, or fuzzy"
119            ),
120        }
121    }
122
123    pub fn as_str(self) -> &'static str {
124        match self {
125            Self::Exact => "exact",
126            Self::Syntactic => "syntactic",
127            Self::Fuzzy => "fuzzy",
128        }
129    }
130}
131
132impl GraphTraversalOptions {
133    pub fn callee_edge_kinds(&self) -> anyhow::Result<Vec<String>> {
134        if let Some(edge_kinds) = &self.edge_kinds {
135            validate_edge_kinds(edge_kinds)?;
136            return Ok(edge_kinds.clone());
137        }
138        let mut edge_kinds =
139            CALL_EDGE_KINDS.iter().map(|value| (*value).to_string()).collect::<Vec<_>>();
140        if self.include_macros {
141            edge_kinds.extend(MACRO_EDGE_KINDS.iter().map(|value| (*value).to_string()));
142        }
143        if self.include_references {
144            edge_kinds.extend(REFERENCE_EDGE_KINDS.iter().map(|value| (*value).to_string()));
145        }
146        Ok(edge_kinds)
147    }
148
149    pub fn caller_edge_kinds(&self) -> anyhow::Result<Vec<String>> {
150        self.callee_edge_kinds()
151    }
152}
153
154#[derive(Debug, Serialize)]
155pub struct CompareGraphTextReport {
156    pub query: CompareGraphTextQuery,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub logical_symbol: Option<LogicalSymbol>,
159    #[serde(skip_serializing_if = "Vec::is_empty")]
160    pub variants: Vec<LogicalSymbolVariant>,
161    pub summary: CompareGraphTextSummary,
162    pub coverage: GraphCoverage,
163    pub matched_hits: Vec<MatchedGraphTextHit>,
164    pub text_only_hits: Vec<TextOnlyHit>,
165    pub graph_only_edges: Vec<GraphOnlyEdge>,
166    pub likely_parser_gaps: Vec<TextOnlyHit>,
167    pub likely_false_positives: Vec<GraphOnlyEdge>,
168}
169
170#[derive(Debug, Serialize)]
171pub struct CompareGraphTextQuery {
172    pub symbol_id: Option<i64>,
173    pub logical_symbol_id: Option<i64>,
174    pub symbol_path: String,
175    pub pattern: String,
176    pub resolution: String,
177    pub include_tests: bool,
178}
179
180#[derive(Debug, Default, Serialize)]
181pub struct CompareGraphTextSummary {
182    pub graph_hits: u64,
183    pub graph_edges: u64,
184    pub text_hits: u64,
185    pub matched: u64,
186    pub graph_only: u64,
187    pub text_only: u64,
188    pub text_mentions: u64,
189    pub likely_parser_gaps: u64,
190    pub likely_false_positives: u64,
191    pub likely_index_gaps: u64,
192    pub complete: bool,
193    pub recommended_fallback: String,
194    pub pattern_match_mode: String,
195    pub warnings: Vec<String>,
196}
197
198#[derive(Debug, Serialize)]
199pub struct MatchedGraphTextHit {
200    pub path: String,
201    pub line: i64,
202    pub text: String,
203    pub target: Option<String>,
204    pub edge_kind: String,
205    pub confidence: String,
206    pub resolution: String,
207}
208
209#[derive(Debug, Clone, Serialize)]
210pub struct TextOnlyHit {
211    pub path: String,
212    pub line: i64,
213    pub text: String,
214    pub reason: String,
215    pub likely_gap: String,
216}
217
218#[derive(Debug, Clone, Serialize)]
219pub struct GraphOnlyEdge {
220    pub path: String,
221    pub line: i64,
222    pub target: Option<String>,
223    pub edge_kind: String,
224    pub confidence: String,
225    pub resolution: String,
226    pub evidence: Option<String>,
227    pub reason: String,
228    pub likely_reason: String,
229}
230
231#[derive(Debug, Clone, Serialize)]
232pub struct GraphHop {
233    pub edge_id: i64,
234    pub from_symbol: Option<String>,
235    pub to_symbol: Option<String>,
236    pub edge_kind: String,
237    pub confidence: String,
238    pub edge_confidence: String,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub target: Option<String>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub target_qualified_name: Option<String>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub evidence: Option<String>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub receiver_hint: Option<String>,
247    pub resolution: String,
248    pub verified_target_symbol: bool,
249    pub shown_by_default: bool,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub callsite: Option<Callsite>,
252}
253
254#[derive(Debug, Clone, Serialize)]
255pub struct Callsite {
256    pub path: String,
257    pub line: i64,
258    pub span: [i64; 2],
259}
260
261pub fn traverse(
262    conn: &Connection,
263    symbol: &str,
264    reverse: bool,
265    limit: u32,
266) -> anyhow::Result<Vec<GraphHop>> {
267    traverse_with_options(conn, symbol, reverse, limit, &GraphTraversalOptions::default())
268}
269
270pub fn traverse_with_options(
271    conn: &Connection,
272    symbol: &str,
273    reverse: bool,
274    limit: u32,
275    options: &GraphTraversalOptions,
276) -> anyhow::Result<Vec<GraphHop>> {
277    let edge_kinds =
278        if reverse { options.caller_edge_kinds()? } else { options.callee_edge_kinds()? };
279    let quoted = quoted_placeholders(edge_kinds.len());
280    let unique_short_name = unique_symbol_name(conn, short_name(symbol))?;
281    let mode = options.resolution_mode;
282    let sql = if reverse {
283        let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
284        let tier = reverse_tier(mode);
285        format!(
286            "
287            SELECT COALESCE(from_symbols.qualified_name, edges.from_name) AS from_symbol,
288                   COALESCE(to_symbols.qualified_name, edges.to_name) AS to_symbol,
289                   edges.id AS edge_id,
290                   edges.edge_kind AS edge_kind,
291                   edges.confidence AS confidence,
292                   edges.to_name AS target,
293                   edges.target_qualified_name AS target_qualified_name,
294                   edges.evidence AS evidence,
295                   edges.receiver_hint AS receiver_hint,
296                   edges.resolution AS edge_resolution,
297                   edges.to_symbol_id IS NOT NULL AS verified_target_symbol,
298                   source_files.path AS callsite_path,
299                   COALESCE(NULLIF(edges.source_start_line, 0), 1) AS callsite_start_line,
300                   COALESCE(NULLIF(edges.source_end_line, 0), NULLIF(edges.source_start_line, 0), 1) AS callsite_end_line,
301                   {tier} AS match_tier
302            FROM edges
303            JOIN files source_files ON source_files.id = edges.source_file_id
304            LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
305            LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
306            WHERE edges.edge_kind IN ({quoted})
307              AND ({predicate})
308            ORDER BY match_tier,
309                CASE edges.confidence
310                    WHEN 'Exact' THEN 0
311                    WHEN 'Syntactic' THEN 1
312                    WHEN 'NameOnly' THEN 2
313                    ELSE 3
314                END,
315                edges.edge_kind,
316                edges.from_name
317            LIMIT ?5
318            "
319        )
320    } else {
321        let predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
322        let target_filter = forward_target_filter(mode, options);
323        let visibility_filter = forward_visibility_filter(options);
324        format!(
325            "
326            SELECT COALESCE(from_symbols.qualified_name, edges.from_name) AS from_symbol,
327                   COALESCE(to_symbols.qualified_name, edges.to_name) AS to_symbol,
328                   edges.id AS edge_id,
329                   edges.edge_kind AS edge_kind,
330                   edges.confidence AS confidence,
331                   edges.to_name AS target,
332                   edges.target_qualified_name AS target_qualified_name,
333                   edges.evidence AS evidence,
334                   edges.receiver_hint AS receiver_hint,
335                   edges.resolution AS edge_resolution,
336                   edges.to_symbol_id IS NOT NULL AS verified_target_symbol,
337                   source_files.path AS callsite_path,
338                   COALESCE(NULLIF(edges.source_start_line, 0), 1) AS callsite_start_line,
339                   COALESCE(NULLIF(edges.source_end_line, 0), NULLIF(edges.source_start_line, 0), 1) AS callsite_end_line,
340                   0 AS match_tier
341            FROM edges
342            JOIN files source_files ON source_files.id = edges.source_file_id
343            LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
344            LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
345            WHERE edges.edge_kind IN ({quoted})
346              AND ({predicate})
347              AND ({target_filter})
348              AND ({visibility_filter})
349              AND ?4 IN ('true', 'false')
350            ORDER BY
351                CASE edges.confidence
352                    WHEN 'Exact' THEN 0
353                    WHEN 'Syntactic' THEN 1
354                    WHEN 'NameOnly' THEN 2
355                    ELSE 3
356                END,
357                edges.edge_kind,
358                edges.to_name
359            LIMIT ?5
360            "
361        )
362    };
363    let params = traversal_params(
364        symbol,
365        limit,
366        &edge_kinds,
367        options.symbol_id,
368        options.logical_symbol_id,
369        unique_short_name,
370    );
371    let mut stmt = conn.prepare(&sql)?;
372    let rows = stmt.query_map(params_from_iter(params), |row| {
373        let edge_kind: String = row.get("edge_kind")?;
374        let confidence: String = row.get("confidence")?;
375        let verified_target_symbol = row.get("verified_target_symbol")?;
376        let resolution = resolution_label(
377            mode,
378            row.get::<_, String>("edge_resolution")?,
379            row.get("match_tier")?,
380            verified_target_symbol,
381        );
382        let callsite_path: String = row.get("callsite_path")?;
383        let callsite_start = row.get("callsite_start_line")?;
384        let callsite_end = row.get("callsite_end_line")?;
385        Ok(GraphHop {
386            edge_id: row.get("edge_id")?,
387            from_symbol: row.get("from_symbol")?,
388            to_symbol: row.get("to_symbol")?,
389            edge_kind: edge_kind.clone(),
390            confidence: confidence.clone(),
391            edge_confidence: confidence,
392            target: row.get("target")?,
393            target_qualified_name: row.get("target_qualified_name")?,
394            evidence: row.get("evidence")?,
395            receiver_hint: row.get("receiver_hint")?,
396            resolution,
397            verified_target_symbol,
398            shown_by_default: CALL_EDGE_KINDS.contains(&edge_kind.as_str()),
399            callsite: Some(Callsite {
400                path: callsite_path,
401                line: callsite_start,
402                span: [callsite_start, callsite_end],
403            }),
404        })
405    })?;
406    let mut hops = Vec::new();
407    for row in rows {
408        hops.push(row?);
409    }
410    dedupe_hops(&mut hops);
411    Ok(hops)
412}
413
414fn dedupe_hops(hops: &mut Vec<GraphHop>) {
415    let mut seen = BTreeSet::new();
416    hops.retain(|hop| {
417        let callsite = hop.callsite.as_ref();
418        seen.insert((
419            hop.from_symbol.clone(),
420            hop.to_symbol.clone(),
421            hop.edge_id,
422            hop.edge_kind.clone(),
423            hop.target.clone(),
424            hop.target_qualified_name.clone(),
425            hop.receiver_hint.clone(),
426            callsite.map(|value| value.path.clone()),
427            callsite.map(|value| value.span),
428        ))
429    });
430}
431
432pub fn traversal_summary(
433    conn: &Connection,
434    symbol: &str,
435    reverse: bool,
436    limit: u32,
437    options: &GraphTraversalOptions,
438    returned_count: usize,
439) -> anyhow::Result<GraphTraversalSummary> {
440    let edge_kinds =
441        if reverse { options.caller_edge_kinds()? } else { options.callee_edge_kinds()? };
442    let quoted = quoted_placeholders(edge_kinds.len());
443    let unique_short_name = unique_symbol_name(conn, short_name(symbol))?;
444    let mode = options.resolution_mode;
445    let sql = if reverse {
446        let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
447        format!(
448            "
449            SELECT
450                COUNT(*),
451                SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
452                SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
453                SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
454                SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
455                SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
456            FROM edges
457            LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
458            WHERE edges.edge_kind IN ({quoted})
459              AND ({predicate})
460            "
461        )
462    } else {
463        let predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
464        let target_filter = forward_target_filter(mode, options);
465        let visibility_filter = forward_visibility_filter(options);
466        format!(
467            "
468            SELECT
469                COUNT(*),
470                SUM(CASE WHEN edges.to_symbol_id IS NOT NULL THEN 1 ELSE 0 END),
471                SUM(CASE WHEN edges.confidence = 'Syntactic' THEN 1 ELSE 0 END),
472                SUM(CASE WHEN edges.confidence = 'NameOnly' THEN 1 ELSE 0 END),
473                SUM(CASE WHEN edges.confidence = 'Ambiguous' THEN 1 ELSE 0 END),
474                SUM(CASE WHEN edges.to_symbol_id IS NULL THEN 1 ELSE 0 END)
475            FROM edges
476            LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
477            WHERE edges.edge_kind IN ({quoted})
478              AND ({predicate})
479              AND ({target_filter})
480              AND ({visibility_filter})
481              AND ?4 IN ('true', 'false')
482            "
483        )
484    };
485    let params = traversal_params(
486        symbol,
487        limit,
488        &edge_kinds,
489        options.symbol_id,
490        options.logical_symbol_id,
491        unique_short_name,
492    );
493    let mut summary = conn.query_row(&sql, params_from_iter(params), |row| {
494        Ok(GraphTraversalSummary {
495            returned_count: u64::try_from(returned_count).unwrap_or(u64::MAX),
496            total_matching_edges: count_col(row, 0)?,
497            truncated: false,
498            exact_verified: count_col(row, 1)?,
499            syntactic: count_col(row, 2)?,
500            name_only: count_col(row, 3)?,
501            ambiguous: count_col(row, 4)?,
502            unresolved: count_col(row, 5)?,
503            false_positive_risk: String::new(),
504            completeness_risk: String::new(),
505        })
506    })?;
507    let hidden_unresolved = hidden_unresolved_candidate_count(
508        conn,
509        symbol,
510        reverse,
511        &edge_kinds,
512        options,
513        unique_short_name,
514    )?;
515    summary.total_matching_edges = summary.total_matching_edges.saturating_add(hidden_unresolved);
516    summary.unresolved = summary.unresolved.saturating_add(hidden_unresolved);
517    summary.truncated = summary.total_matching_edges > u64::from(limit);
518    summary.false_positive_risk = false_positive_risk(&summary, mode).to_string();
519    summary.completeness_risk = completeness_risk(&summary).to_string();
520    Ok(summary)
521}
522
523fn count_col(row: &rusqlite::Row<'_>, index: usize) -> rusqlite::Result<u64> {
524    let value = row.get::<_, Option<i64>>(index)?.unwrap_or(0);
525    Ok(u64::try_from(value).unwrap_or(0))
526}
527
528fn false_positive_risk(summary: &GraphTraversalSummary, mode: GraphResolutionMode) -> &'static str {
529    if summary.ambiguous > 0 || mode == GraphResolutionMode::Fuzzy {
530        "high"
531    } else if summary.name_only > 0
532        || summary.unresolved > 0
533        || mode == GraphResolutionMode::Syntactic
534    {
535        "medium"
536    } else {
537        "low"
538    }
539}
540
541fn completeness_risk(summary: &GraphTraversalSummary) -> &'static str {
542    if summary.truncated
543        || summary.unresolved > summary.exact_verified.saturating_add(summary.syntactic)
544    {
545        "high"
546    } else if summary.unresolved > 0 || summary.name_only > 0 || summary.ambiguous > 0 {
547        "medium"
548    } else {
549        "low"
550    }
551}
552
553fn hidden_unresolved_candidate_count(
554    conn: &Connection,
555    symbol: &str,
556    reverse: bool,
557    edge_kinds: &[String],
558    options: &GraphTraversalOptions,
559    unique_short_name: bool,
560) -> anyhow::Result<u64> {
561    let mode = options.resolution_mode;
562    let quoted = quoted_placeholders(edge_kinds.len());
563    let sql = if reverse {
564        let predicate = reverse_predicate(mode, options.logical_symbol_id.is_some());
565        format!(
566            "
567            SELECT COUNT(*)
568            FROM edges
569            LEFT JOIN symbols to_symbols ON to_symbols.id = edges.to_symbol_id
570            WHERE edges.edge_kind IN ({quoted})
571              AND edges.to_symbol_id IS NULL
572              AND NOT ({predicate})
573              AND (
574                edges.target_qualified_name = ?1
575                OR edges.target_qualified_name LIKE ?2
576                OR edges.to_name = ?3
577              )
578            "
579        )
580    } else {
581        let source_predicate = forward_source_predicate(mode, options.logical_symbol_id.is_some());
582        let target_filter = forward_target_filter(mode, options);
583        let visibility_filter = forward_visibility_filter(options);
584        format!(
585            "
586            SELECT COUNT(*)
587            FROM edges
588            LEFT JOIN symbols from_symbols ON from_symbols.id = edges.from_symbol_id
589            WHERE edges.edge_kind IN ({quoted})
590              AND ({source_predicate})
591              AND edges.to_symbol_id IS NULL
592              AND NOT (({target_filter}) AND ({visibility_filter}))
593              AND ?4 IN ('true', 'false')
594            "
595        )
596    };
597    let params = traversal_params(
598        symbol,
599        0,
600        edge_kinds,
601        options.symbol_id,
602        options.logical_symbol_id,
603        unique_short_name,
604    );
605    let count = conn.query_row(&sql, params_from_iter(params), |row| count_col(row, 0))?;
606    Ok(count)
607}
608
609fn validate_edge_kinds(edge_kinds: &[String]) -> anyhow::Result<()> {
610    for edge_kind in edge_kinds {
611        if !OPTIONAL_EDGE_KINDS.contains(&edge_kind.as_str()) {
612            anyhow::bail!("unknown graph edge kind `{edge_kind}`");
613        }
614    }
615    Ok(())
616}
617
618fn traversal_params(
619    symbol: &str,
620    limit: u32,
621    edge_kinds: &[String],
622    symbol_id: Option<i64>,
623    logical_symbol_id: Option<i64>,
624    unique_short_name: bool,
625) -> Vec<String> {
626    let qualified = symbol.to_string();
627    let short = short_name(symbol).to_string();
628    let fuzzy_qualified = format!("%::{qualified}");
629    let allow_name_fallback = (!is_qualified_symbol(symbol)).to_string();
630    let mut params = vec![
631        qualified,
632        fuzzy_qualified,
633        short,
634        allow_name_fallback,
635        limit.to_string(),
636        symbol_id.unwrap_or(-1).to_string(),
637        unique_short_name.to_string(),
638        logical_symbol_id.unwrap_or(-1).to_string(),
639    ];
640    params.extend(edge_kinds.iter().cloned());
641    params
642}
643
644fn quoted_placeholders(count: usize) -> String {
645    (0..count).map(|index| format!("?{}", index + 9)).collect::<Vec<_>>().join(", ")
646}
647
648fn reverse_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
649    if logical {
650        return match mode {
651            GraphResolutionMode::Exact => {
652                "edges.to_symbol_id IS NOT NULL
653                 AND edges.to_symbol_id IN (
654                    SELECT symbol_id
655                    FROM logical_symbol_members
656                    WHERE logical_symbol_id = ?8
657                 )"
658            },
659            GraphResolutionMode::Syntactic => {
660                "(edges.to_symbol_id IN (
661                    SELECT symbol_id
662                    FROM logical_symbol_members
663                    WHERE logical_symbol_id = ?8
664                  )
665                  OR edges.target_qualified_name = ?1)"
666            },
667            GraphResolutionMode::Fuzzy => {
668                "edges.to_symbol_id IN (
669                    SELECT symbol_id
670                    FROM logical_symbol_members
671                    WHERE logical_symbol_id = ?8
672                 )
673                 OR to_symbols.name = ?3
674                 OR to_symbols.qualified_name = ?1
675                 OR to_symbols.qualified_name LIKE ?2
676                 OR edges.target_qualified_name = ?1
677                 OR edges.target_qualified_name LIKE ?2
678                 OR (?4 = 'true' AND edges.to_name = ?3)"
679            },
680        };
681    }
682    match mode {
683        GraphResolutionMode::Exact => {
684            "edges.to_symbol_id IS NOT NULL
685             AND (edges.to_symbol_id = ?6 OR to_symbols.qualified_name = ?1)"
686        },
687        GraphResolutionMode::Syntactic => {
688            "(edges.to_symbol_id = ?6
689              OR to_symbols.qualified_name = ?1
690              OR (?7 = 'true' AND to_symbols.name = ?3)
691              OR edges.target_qualified_name = ?1)"
692        },
693        GraphResolutionMode::Fuzzy => {
694            "to_symbols.name = ?3
695             OR to_symbols.qualified_name = ?1
696             OR to_symbols.qualified_name LIKE ?2
697             OR edges.target_qualified_name = ?1
698             OR edges.target_qualified_name LIKE ?2
699             OR (?4 = 'true' AND edges.to_name = ?3)"
700        },
701    }
702}
703
704fn reverse_tier(mode: GraphResolutionMode) -> &'static str {
705    match mode {
706        GraphResolutionMode::Exact => "0",
707        GraphResolutionMode::Syntactic => {
708            "CASE
709                WHEN edges.to_symbol_id IS NOT NULL THEN 0
710                WHEN edges.target_qualified_name = ?1 THEN 1
711                ELSE 4
712             END"
713        },
714        GraphResolutionMode::Fuzzy => {
715            "CASE
716                WHEN edges.to_symbol_id IS NOT NULL THEN 0
717                WHEN edges.target_qualified_name = ?1 OR edges.target_qualified_name LIKE ?2 THEN 1
718                WHEN ?4 = 'true' AND edges.to_name = ?3 THEN 2
719                ELSE 4
720             END"
721        },
722    }
723}
724
725fn forward_source_predicate(mode: GraphResolutionMode, logical: bool) -> &'static str {
726    if logical {
727        return match mode {
728            GraphResolutionMode::Exact => {
729                "from_symbols.id IS NOT NULL
730                 AND from_symbols.id IN (
731                    SELECT symbol_id
732                    FROM logical_symbol_members
733                    WHERE logical_symbol_id = ?8
734                 )"
735            },
736            GraphResolutionMode::Syntactic => {
737                "from_symbols.id IN (
738                    SELECT symbol_id
739                    FROM logical_symbol_members
740                    WHERE logical_symbol_id = ?8
741                 )
742                 OR edges.from_name = ?1"
743            },
744            GraphResolutionMode::Fuzzy => {
745                "from_symbols.id IN (
746                    SELECT symbol_id
747                    FROM logical_symbol_members
748                    WHERE logical_symbol_id = ?8
749                 )
750                 OR from_symbols.name = ?3
751                 OR from_symbols.qualified_name = ?1
752                 OR from_symbols.qualified_name LIKE ?2
753                 OR edges.from_name = ?1
754                 OR edges.from_name LIKE ?2"
755            },
756        };
757    }
758    match mode {
759        GraphResolutionMode::Exact => {
760            "from_symbols.id IS NOT NULL
761             AND (from_symbols.id = ?6 OR from_symbols.qualified_name = ?1)"
762        },
763        GraphResolutionMode::Syntactic => {
764            "from_symbols.id = ?6
765             OR from_symbols.qualified_name = ?1
766             OR (?7 = 'true' AND from_symbols.name = ?3)
767             OR edges.from_name = ?1"
768        },
769        GraphResolutionMode::Fuzzy => {
770            "from_symbols.name = ?3
771             OR from_symbols.qualified_name = ?1
772             OR from_symbols.qualified_name LIKE ?2
773             OR edges.from_name = ?1
774             OR edges.from_name LIKE ?2"
775        },
776    }
777}
778
779fn forward_target_filter(
780    mode: GraphResolutionMode,
781    options: &GraphTraversalOptions,
782) -> &'static str {
783    match mode {
784        GraphResolutionMode::Exact => "edges.to_symbol_id IS NOT NULL",
785        GraphResolutionMode::Syntactic => {
786            if options.include_unresolved {
787                "1 = 1"
788            } else if options.include_macros {
789                "
790                edges.to_symbol_id IS NOT NULL
791                OR edges.target_qualified_name IS NOT NULL
792                OR edges.edge_kind = 'uses_macro'
793                "
794            } else {
795                "edges.to_symbol_id IS NOT NULL OR edges.target_qualified_name IS NOT NULL"
796            }
797        },
798        GraphResolutionMode::Fuzzy => "1 = 1",
799    }
800}
801
802fn forward_visibility_filter(options: &GraphTraversalOptions) -> &'static str {
803    match (
804        options.include_unresolved,
805        options.include_macros,
806        options.include_common_methods,
807    ) {
808        (true, true, true) => "1 = 1",
809        (true, true, false) => {
810            "
811            (
812                edges.edge_kind != 'calls_name'
813                OR edges.to_name NOT IN (
814                    'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
815                    'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
816                    'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
817                    'expect', 'ok', 'err'
818                )
819                OR edges.to_symbol_id IS NOT NULL
820            )
821            "
822        },
823        (true, false, true) => "edges.edge_kind != 'uses_macro'",
824        (true, false, false) => {
825            "
826            edges.edge_kind != 'uses_macro'
827            AND (
828                edges.edge_kind != 'calls_name'
829                OR edges.to_name NOT IN (
830                    'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
831                    'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
832                    'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
833                    'expect', 'ok', 'err'
834                )
835                OR edges.to_symbol_id IS NOT NULL
836            )
837            "
838        },
839        (false, true, true) => {
840            "
841            (
842                edges.edge_kind = 'calls_name'
843                AND (
844                    edges.to_symbol_id IS NOT NULL
845                    OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
846                )
847            )
848            OR (
849                edges.edge_kind = 'constructs'
850                AND edges.to_symbol_id IS NOT NULL
851            )
852            OR edges.edge_kind = 'uses_macro'
853            OR edges.edge_kind NOT IN ('calls_name', 'constructs')
854            "
855        },
856        (false, true, false) => {
857            "
858            (
859                edges.edge_kind = 'calls_name'
860                AND (
861                    edges.to_symbol_id IS NOT NULL
862                    OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
863                )
864                AND (
865                    edges.to_name NOT IN (
866                        'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
867                        'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
868                        'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
869                        'expect', 'ok', 'err'
870                    )
871                    OR edges.to_symbol_id IS NOT NULL
872                )
873            )
874            OR (
875                edges.edge_kind = 'constructs'
876                AND edges.to_symbol_id IS NOT NULL
877            )
878            OR edges.edge_kind = 'uses_macro'
879            OR edges.edge_kind NOT IN ('calls_name', 'constructs')
880            "
881        },
882        (false, false, true) => {
883            "
884            edges.edge_kind != 'uses_macro'
885            AND (
886                (
887                    edges.edge_kind = 'calls_name'
888                    AND (
889                        edges.to_symbol_id IS NOT NULL
890                        OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
891                    )
892                )
893                OR (
894                    edges.edge_kind = 'constructs'
895                    AND edges.to_symbol_id IS NOT NULL
896                )
897                OR edges.edge_kind NOT IN ('calls_name', 'constructs')
898            )
899            "
900        },
901        (false, false, false) => {
902            "
903            edges.edge_kind != 'uses_macro'
904            AND (
905                (
906                    edges.edge_kind = 'calls_name'
907                    AND (
908                        edges.to_symbol_id IS NOT NULL
909                        OR (edges.confidence = 'Syntactic' AND edges.target_qualified_name IS NOT NULL)
910                    )
911                    AND (
912                        edges.to_name NOT IN (
913                            'clone', 'map', 'map_err', 'and_then', 'unwrap_or', 'unwrap_or_else',
914                            'to_string', 'to_owned', 'as_ref', 'as_mut', 'get', 'insert',
915                            'new', 'default', 'into', 'from', 'iter', 'collect', 'unwrap',
916                            'expect', 'ok', 'err'
917                        )
918                        OR edges.to_symbol_id IS NOT NULL
919                    )
920                )
921                OR (
922                    edges.edge_kind = 'constructs'
923                    AND edges.to_symbol_id IS NOT NULL
924                )
925                OR edges.edge_kind NOT IN ('calls_name', 'constructs')
926            )
927            "
928        },
929    }
930}
931
932fn unique_symbol_name(conn: &Connection, name: &str) -> anyhow::Result<bool> {
933    let count: i64 = conn.query_row(
934        "SELECT COUNT(*) AS symbol_count FROM symbols WHERE name = ?1",
935        [name],
936        |row| row.get("symbol_count"),
937    )?;
938    Ok(count == 1)
939}
940
941fn resolution_label(
942    mode: GraphResolutionMode,
943    stored: String,
944    tier: i64,
945    verified_target_symbol: bool,
946) -> String {
947    if mode == GraphResolutionMode::Exact && verified_target_symbol {
948        return "exact".to_string();
949    }
950    if stored != "unresolved" {
951        return stored;
952    }
953    match tier {
954        1 => "target_qualified_suffix".to_string(),
955        2 => "target_name_fallback".to_string(),
956        _ => stored,
957    }
958}
959
960fn short_name(symbol: &str) -> &str {
961    symbol.rsplit([':', '.', '#', '/']).find(|part| !part.is_empty()).unwrap_or(symbol)
962}
963
964fn is_qualified_symbol(symbol: &str) -> bool {
965    symbol.contains("::")
966        || symbol.contains(".rs:")
967        || symbol.contains(".ts:")
968        || symbol.contains(".tsx:")
969        || symbol.contains(".kt:")
970        || symbol.contains('/')
971}